-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Function space SBP operators #268
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #268 +/- ##
==========================================
+ Coverage 90.70% 90.97% +0.26%
==========================================
Files 33 35 +2
Lines 5112 5262 +150
==========================================
+ Hits 4637 4787 +150
Misses 475 475
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the PR!
- I think a
MatrixDerivativeOperator
is fine for now. Do you have any specific applications using the basis functions later in mind? We could also document that the return value will implement the general interface (likemul!
etc.) but leave the concrete type of the return value open - or mention that it's an implementation detail that may change in the future. - I'm fine using ForwardDiff.jl for this.
Thanks for the answers!
|
A first working version is ready. I can (almost) reproduce the results from the paper, e.g. (cf. p. 12), julia> using SummationByPartsOperators, Optim
Precompiling SummationByPartsOperatorsOptimForwardDiffExt
1 dependency successfully precompiled in 2 seconds. 120 already precompiled.
julia> D = FunctionSpaceOperator((one, identity, exp), 0.0, 1.0, collect(LinRange(0.0, 1.0, 5)), GlaubitzNordströmÖffner2023())
Matrix-based first-derivative operator {T=Float64} on 5 nodes in [0.0, 1.0]
julia> round.(Matrix(mass_matrix(D)) * D.D, digits = 4)
5×5 Matrix{Float64}:
-0.5 0.653 -0.035 -0.1927 0.0748
-0.653 0.0 0.3198 0.5238 -0.1907
0.035 -0.3198 0.0 0.3215 -0.0367
0.1927 -0.5238 -0.3215 0.0 0.6526
-0.0748 0.1907 0.0367 -0.6526 0.5
julia> round.(mass_matrix(D), digits = 4)
5×5 LinearAlgebra.Diagonal{Float64, Vector{Float64}}:
0.076 ⋅ ⋅ ⋅ ⋅
⋅ 0.3621 ⋅ ⋅ ⋅
⋅ ⋅ 0.1245 ⋅ ⋅
⋅ ⋅ ⋅ 0.3609 ⋅
⋅ ⋅ ⋅ ⋅ 0.0766 Up to now, I have focused on the "make it work" phase by trying to be as close as possible to the mathematical formulation. The "make it fast"(er) phase is yet to come. The optimization process is still taking too long for higher number of nodes, but there is probably a lot of potential for performance improvements. Before I dig deeper into some performance improvement, I would like to see your feedback @ranocha. Does this look ok to you in general or do you think the implementation needs some major alterations? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
Up to now, I have focused on the "make it work" phase by trying to be as close as possible to the mathematical formulation. The "make it fast"(er) phase is yet to come. The optimization process is still taking too long for higher number of nodes, but there is probably a lot of potential for performance improvements. Before I dig deeper into some performance improvement, I would like to see your feedback @ranocha. Does this look ok to you in general or do you think the implementation needs some major alterations?
I think the general approach is fine. I just have some high-level comments right now. The lower-level implementation details could be touched later - we can also discuss this together.
Up to now I have only added a few tests to cover the most basic. I had a brief look in SBP_operators_test.jl, but was not quite sure, which of these tests also make sense in this case. (I also didn't find any tests like @test M * D.D + D.D' * M ≈ B, which I found a bit surprising as this would have been one of the first things I would like to see tested for the SBP operators 😅)
Fair enough - feel free to add tests like this to the other files 😅
I have also noticed that the optimization needs a rather small tolerance for the gradient to be able to produce accurate results. I was also wondering about whether we should provide the user some possibility to influence the optimization, e.g., by allowing an additional kwarg options that is passed as Optim.Options to the optimization (with some reasonable default value), or by allowing to use a custom sigmoid function.
That sounds reasonable.
Thanks a lot for your comments!
Ok, I can add some tests, but will do that in a future PR.
I added |
Since the optimization is doing weird things and does not work in the Julia 1.6 test, we decided to restrict this feature to Julia v1.9, see aa868bd. |
Co-authored-by: Hendrik Ranocha <[email protected]>
It is much faster now, but |
Do you have any other reasonable tests in mind, I could add, @ranocha? |
I would also like to stress that the optimization can be very vulnerable. Consider the following example: julia> using SummationByPartsOperators, Optim
julia> nodes = [0.0, rand(3)..., 1.0]
5-element Vector{Float64}:
0.0
0.05651765804974351
0.29863912793993175
0.657232875727107
1.0
julia> basis_functions = [one, identity, exp]
3-element Vector{Function}:
one (generic function with 45 methods)
identity (generic function with 1 method)
exp (generic function with 21 methods)
julia> D = function_space_operator(basis_functions, first(nodes), last(nodes), nodes, GlaubitzNordströmÖffner2023())
Matrix-based first-derivative operator {T=Float64} on 5 nodes in [0.0, 1.0]
julia> D * ones(5)
5-element Vector{Float64}:
-2038.0570044219494
-1.073365630488432e-6
6.732198513992316e-6
3.664382162149593e-6
-2.1692864620170837e-5 which is very far from * Status: failure (reached maximum number of iterations)
* Candidate solution
Final objective value: 2.677287e-10
* Found with
Algorithm: L-BFGS
* Convergence measures
|x - x'| = 8.05e-04 ≰ 0.0e+00
|x - x'|/|x'| = 3.85e-05 ≰ 0.0e+00
|f(x) - f(x')| = 6.90e-22 ≰ 0.0e+00
|f(x) - f(x')|/|f(x')| = 2.58e-12 ≰ 0.0e+00
|g(x)| = 1.17e-11 ≰ 1.0e-14
* Work counters
Seconds run: 0 (vs limit Inf)
Iterations: 10000
f(x) calls: 26276
∇f(x) calls: 26276 So even with an objective value of ~2.7e-10, which is quite small, the operator is very far from being exact. Increasing the number of iterations significantly and decreasing the tolerance doesn't help: julia> D = function_space_operator(basis_functions, first(nodes), last(nodes), nodes, GlaubitzNordströmÖffner2023(); options = Optim.Options(g_tol = 1e-16, iterations = 200000))
* Status: success
* Candidate solution
Final objective value: 2.677287e-10
* Found with
Algorithm: L-BFGS
* Convergence measures
|x - x'| = 2.01e-07 ≰ 0.0e+00
|x - x'|/|x'| = 5.40e-09 ≰ 0.0e+00
|f(x) - f(x')| = 9.38e-23 ≰ 0.0e+00
|f(x) - f(x')|/|f(x')| = 3.50e-13 ≰ 0.0e+00
|g(x)| = 8.08e-17 ≤ 1.0e-16
* Work counters
Seconds run: 0 (vs limit Inf)
Iterations: 102609
f(x) calls: 270140
∇f(x) calls: 270140
Matrix-based first-derivative operator {T=Float64} on 5 nodes in [0.0, 1.0]
julia> D * ones(5)
5-element Vector{Float64}:
-2.46283614165e10
-1.0733685033015306e-6
6.7321802603159675e-6
3.664360717303694e-6
-2.1692791865035588e-5 I think this is a general issue with the optimization process and thus I think a deeper theoretical insight in the optimization would be useful. |
Yes, that's a very good suggestion 👍 |
It looks like this is actually a question for further research and not only an implementation question. |
Yes, definitely. I was just trying to compute the Hessian of the function to check convexity. |
The tests look reasonable 👍 |
Done in 38081b1. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
FYI @phioeffn @jglaubitz |
This is a very first draft for tackling #132. For the construction of the FSBP an optimization problem needs to be solved. I plan to use Optim.jl for this. Thus, I added a package extension to avoid adding Optim.jl as a direct dependency. In order to discuss some design choices, I already created this PR.
In the current draft, I use the
MatrixDerivativeOperator
to represent the functional space operators, i.e.FunctionSpaceOperator
just returnsMatrixDerivativeOperator
. This is easy, but I'm not sure if this is the best design choice on the long run, e.g., this way we "forget" about thebasis
. A more versatile alternative would be to define a new struct that holds aMatrixDerivativeOperator
and potentially more such as thebasis
for potential later use. In this case, several methods (such asmass_matrix
,integrate
,*
...) could just call the corresponding methods of theMatrixDerivativeOperator
.Another question concerns the derivatives of the basis functions, which are needed for the construction process. From a user perspective it would be most convenient if they would not need to be specified explicitly. Do you think, we could use ForwardDiff.jl (i.e. the package extension consists of ForwardDiff.jl and Optim.jl) to compute the derivatives or would that mean too much overhead?
I would be interested in your opinion on these two points @ranocha and if the current approach looks reasonable to you.