-
Notifications
You must be signed in to change notification settings - Fork 110
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
Promote state and time-span in schroedinger
to the type from H(t)*psi
#356
Conversation
Codecov Report
@@ Coverage Diff @@
## master #356 +/- ##
==========================================
+ Coverage 98.03% 98.05% +0.02%
==========================================
Files 16 16
Lines 1422 1438 +16
==========================================
+ Hits 1394 1410 +16
Misses 28 28
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
You probably already knew most of this (this being marked as a draft) but:
It would be quite wonderful to have autodiff working with this library. |
For this small example;
In this branch, benchmarking gives run time of 21.429 ms ± 797.528 μs with 289 allocations. I'm guessing this will be true for the general case of working with ComplexF64. |
src/schroedinger.jl
Outdated
# general case is Ts<:Complex, Tt<:Real | ||
Ts = eltype(H) | ||
Tt = real(Ts) | ||
(isconcretetype(Ts) && isconcretetype(Tt)) || @warn "For using `ForwardDiff` on `schroedinger` the element type of `real(H(t,psi)*psi)` must be concrete !!\nGot elements of type $Tt \nTry promoting the Hamiltonian elements based on the parameters you are differentiating by." |
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.
In what situations would this warning message be seen? I feel it might be confusing as it will show up in front of users that have not touched ForwardDiff
or schroedinger
.
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.
I'll remove it.
This was trying to warn about something like
ω0, α = 5.0, -0.2 # not promoting
#...
fun = [fω, fα, fΩ]
H_at_t = LazySum([f(0.0) for f=fun], op)
@show H_at_t.factors # type is abstract !! Complex{Real}
in the definition of get_Ht
.
But It doesn't cover all possible "bad" definitions of get_Ht
.
src/schroedinger.jl
Outdated
Ts = eltype(H) | ||
Tt = real(Ts) | ||
(isconcretetype(Ts) && isconcretetype(Tt)) || @warn "For using `ForwardDiff` on `schroedinger` the element type of `real(H(t,psi)*psi)` must be concrete !!\nGot elements of type $Tt \nTry promoting the Hamiltonian elements based on the parameters you are differentiating by." | ||
tspan = Tt.(tspan) |
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.
I imagine there are situations where no promotion is necessary. Can we shortcircuit those?
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.
I'll declare type Dual
and only promote that case, probably would be safer.
The extra allocation does not seem particularly problematic on first sight (as long as it is just a constant non-growing overhead), but it can probably be removed with a bit of extra logic in the promotion function. But I am a bit confused, why is promotion even necessary. You are not deriving with respect to
|
I guess the answer to my question about why promotion is necessary is because there are in-place operations done by the diff eq solver? How about we just shortcircuit on the |
I am also a bit worried: how does this work when another autodiff tool is used? Is this making Zygote and Enzyme happier or is it breaking things for them. |
I think it's a QO.jl issue, meaning in-place operations on states and operators. Using DiffEq.jl directly with matrix and vector there no need for the user to promote anything. |
I think the correct solution would be to fix that issue in QO.jl instead of working around it. My worry is that workarounds that do not follow the DiffEq.jl style would just become more and more difficult to keep functional as the autodiff capabilities in julia evolve. Any idea why DiffEq does not have this problem, but QO has it? Anything in particular that QO does that makes things difficult? |
Last time I tryed Zygote didn't work with QO, and I didn't check if the promoting changed anything. |
That is true, but similarly to my comment above, this would be a fragile fix around some other underlying issue. I would be hesitant to use a library that has such special cases in its code, and such a library would be more difficult for others to continue developing. Moreover, that approach would need to add ForwardDiff as a dependency. Figuring out why QO does not support something that is supported by the underlying DiffEq would probably lead to simpler code and support for other unrelated autodiff tools. Could you give a minimum example of a working autodiff with DiffEq? |
I think it's the preallocation of DiffEq.jl Also had this issue in the past, meaning the user had to promote time and state for ForwardDiff to work. Not sure what solution they implemented internally. |
I think the fix should be in https://github.com/qojulia/QuantumOptics.jl/blob/master/src/timeevolution_base.jl#L14 https://github.com/qojulia/QuantumOptics.jl/blob/master/src/schroedinger.jl#L57 Maybe we should try to expose some of SciMLSensitivity https://docs.sciml.ai/SciMLSensitivity/stable/ |
This might be of use: https://github.com/SciML/PreallocationTools.jl |
Cheating a bit, because there is an issue with complex numbers... "Dual on Complex is ambiguous".
|
Not sure if there is a nice way to deal with complex number here. If everything is nice and analytic, I think it's safe to define Dual on Complex;
then we only need to promote the time-span, and the above DiffEq.jl example works with complex numbers. I'm not sure how or why this issue do not show up with QO.jl |
I will look into using PreallocationTools.jl to better handle edge cases |
I tried to read through the DiffEq code, and it seems they do the promotions themselves already: And they do the promotion in a way that is very similar to what you have created here: It would be fantastic if we can figure out why the promotion does not work by default for us. That way we would not need to add extra code to QO.jl and we will have it for free for all dynamical equations, not just the |
The DiffEq promotion does not automatically work, because their promotion happens only if there is a dual in the
|
My personal belief is that the "proper" solution is to rework a lot of the internals of QO.jl in order to use DiffEq.jl in a more idiomatic manner. That would significantly simplify the code. However, that is a gigantic amount of work that I do not think should be blocking important features like the one you are currently suggesting. Therefore, I am now in favor of the original approach you had started with, even if it re-implements features that supposedly we should get from DiffEq for free. I would suggest using a check similar to |
This reminds me of how to get ForwardDiff.jl working on an ode with complex numbers;
This bypass the promotion in DiffEqBase.jl using the type from I've opened an issue SciML/DiffEqBase.jl#858 |
test/test_ForwardDiff.jl
Outdated
FDgrad(cost1_with_dt, p0) | ||
### test vs finite difference | ||
#### there somtimes an issue with the gradient at p0, which is alot less accurate | ||
@test all([test_vs_fin_diff(cost1_with_dt, p; atol=1e-6) for p=rp.(range(1e-9, 0.1, tests_repetition))]) |
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.
There is a small unstable region around p0
, where AD and finite diff differ by 1e-2
. Not sure why that is.
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.
Would just removing that test make sense? Is it really testing anything that the previous set of much simpler tests do not? This is about testing the autodiff, not about a study of numerical stability.
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.
Yes.
I didn't check this example vs DiffEq, but given that the other examples I compared showed less than 1e-12 difference I think we can remove this test.
And also the 1'st test?
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.
I am not sure which one you refer to as 1st. Testing both the static and dynamic Hamiltonians seems valuable to me.
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.
Sure
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.
Keeping the two tests you specifically cited below seems reasonable to me. You can also guide yourself by the coverage report - we should aim to have all new lines covered by tests.
test/test_ForwardDiff.jl
Outdated
## static | ||
function get_H(p) | ||
op = create(ba0)+destroy(ba0) | ||
return sin(p)*op |
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.
I mean keep this static test
test/test_ForwardDiff.jl
Outdated
target2 = randstate(ba2) | ||
function cost2(par) | ||
a,b = par | ||
Ht(t,u) = A + a*cos(b*t)*B/10 |
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.
And this dynamic test
Are we happy with this? |
I think the way it is done is quite clean, or at least as clean as possible. I am not the biggest fan of the style of the tests: my personal philosophy is to not include tests that are sensitive to numerical instabilities (if the code being tested is unrelated to numerical instability control). But being practical, I do not think it is necessary to redo the tests or block the merge. Maybe in a couple of years there will be a test failure due to some change to default DiffEq / ForwardDiff algorithms, but that is fine. I am a bit worried though that this adds whole 3 minutes to the test run. I would have expected testing this to take 10-20 seconds. Wouldn't both these issues (instabilities and long test runs) be fixed if we just test fewer examples. Testing fewer examples would not drop the test coverage percentage or really change much. Minor notes:
Anyway, I do not see blocking issues. @david-pl , is there a policy/philosophy about what the tests need to look like. These seem good enough even if unpolished and a bit long. More importantly, thanks for fixing some of the ergonimics with autodiff in this package. This would be much more pleasant to use for me now. |
@AmitRotem thanks for all the work here! I have to admit that my knowledge of AD is a bit limited, so I trust @Krastanov here - thanks for the detailed review! Regarding the tests: they are a bit of a mess throughout the package already. Also, time is a bit of an issue as the CI already takes quite long, but we'll have to look at that in a separate PR. There's no real philosophy in how to do things, except maybe that we have one test per file. So long as there are tests for new code, I'm happy! Any mess we have in the tests can be cleaned up in the future. For that I'd simply stick to the way SciML tests are structured. So all good from my side. @Krastanov just give me one last go ahead once you're happy with the PR and I'll merge it. |
I will do one last pass and rerun the tests tonight and will give that go-ahead then. |
@AmitRotem , if I understand correctly, this depends on a change you contributed to DiffEqBase in version 6.113.0. Could you add this to the Project dependencies and compat? You can also then directly do @david-pl good to merge in my opinion after adding the dependency. Details below. The comments about the tests stand, but as you mentioned that requires an overall cleanup, probably following SafeTestsets as done in SciML. This also introduces a dependence on a private DiffEqBase function (the promotion one), however that function has existed since 2018 and very early versions of DiffEqBase. We can not really rely on semver to detect issues with this, but it seems relatively safe (and testing will detect any possible issues in the future). Amit has added another method to that function upstream to make sure it does not break on complex numbers. As I already mentioned. The actual changes look neat and clean. Once this is merged I will create issues to track the addition of the same features to the rest of the solvers. |
Most of the time is compilation time for ForwardDiff.jl. |
add DiffEqBase to Project and compat
Looks good to merge. I do not actually think there is that much need for the tests against DiffEq. We are using DiffEq internally anyway, so just checking for correctness with a quick finite differences approximation on a numerically stable example would also have been enough. |
If you think this minimal version of the test is enough, I'll remove the longer version. |
Thanks for the test cleanup! Looks good to merge to me (the only changes from the previous time I said that is that tests were streamlined). |
For optimizing
cost
wrtpar
, it is sometimes useful to get the gradient ofcost
wrtpar
.Promoting the state and time-span to the type from
Hamiltonian * state
makes it possible to useForwardDiff.jl
oncost
- if you happen to define the Hamiltonian in a compatible manner toForwardDiff.jl
.For example;