Hello,
I need to differentiate a quantum master equation over a parameter, using QuantumToolbox.jl. Under the hood it is simply an Ordinary Differential Equation involving SciMLOperators.
I’m able to differentiate the Schrödinger equation with the following code
using QuantumToolbox
using OrdinaryDiffEq
using SciMLSensitivity
using Zygote
using Enzyme
using BenchmarkTools
##
const N = 16
F = 1
γ = 1
const a = destroy(N)
const ψ0 = fock(N, 0)
# coef1(p, t) = p.Δ
coef1(p, t) = p[1]
const H = QobjEvo(a' * a, coef1) + F * (a' + a)
const c_ops = [sqrt(γ) * a]
const L = liouvillian(H, c_ops)
function my_f_sesolve(p)
tlist = range(0, 1.5, 100)
sol = sesolve(H, ψ0, tlist, progress_bar=Val(false), params=p, saveat=[tlist[end]], sensealg = BacksolveAdjoint(autojacvec=EnzymeVJP()))
# return real(abs2(sol.states[end].data[end]))
return real(expect(a' * a, sol.states[end]))
end
Δ = 1.0
params = [Δ]
my_f_sesolve(params) # 1.8585255970284336
##
Zygote.gradient(my_f_sesolve, params) # ([-2.7374779226913737],)
The internal code is probably too complicated to be explained here, but the working principle is basic linear algebra between matrices and vectors.
Current limitations of autodiff for this situation can be found in this issue, especially in the last comment where I summarize everything.
However, if I try to differentiate the master equation it fails. This case should be equivalent to the previous one, where the SciMLOperator involved in the ODE is only bigger than the Schrödinger case. Here the code
function my_f_mesolve(p)
tlist = range(0, 1.5, 100)
sol = mesolve(H, ψ0, tlist, c_ops, progress_bar=Val(false), params=p, saveat=[tlist[end]], sensealg = BacksolveAdjoint(autojacvec=EnzymeVJP()))
# return real(abs2(sol.states[end].data[end]))
return real(expect(a' * a, sol.states[end]))
end
##
Δ = 1.0
params = [Δ]
my_f_mesolve(params) # 0.925041907046927
##
Zygote.gradient(my_f_mesolve, params)
And the Error:
Summary
ERROR: Need an adjoint for constructor QuantumObject{OperatorQuantumObject, Dimensions{1, Tuple{Space}}, Matrix{ComplexF64}}. Gradient is of type Diagonal{Float64, Vector{Float64}}
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:35
[2] (::Zygote.Jnew{QuantumObject{…}, Nothing, false})(Δ::Diagonal{Float64, Vector{…}})
@ Zygote ~/.julia/packages/Zygote/HdT4O/src/lib/lib.jl:324
[3] (::Zygote.var"#2230#back#327"{Zygote.Jnew{QuantumObject{…}, Nothing, false}})(Δ::Diagonal{Float64, Vector{Float64}})
@ Zygote ~/.julia/packages/ZygoteRules/CkVIK/src/adjoint.jl:72
[4] QuantumObject
@ ~/.julia/packages/QuantumToolbox/YmsAS/src/qobj/quantum_object.jl:59 [inlined]
[5] (::Zygote.Pullback{Tuple{…}, Any})(Δ::Diagonal{Float64, Vector{…}})
@ Zygote ~/.julia/packages/Zygote/HdT4O/src/compiler/interface2.jl:0
[6] *
@ ~/.julia/packages/QuantumToolbox/YmsAS/src/qobj/arithmetic_and_attributes.jl:72 [inlined]
[7] (::Zygote.Pullback{Tuple{…}, Tuple{…}})(Δ::Diagonal{Float64, Vector{…}})
@ Zygote ~/.julia/packages/Zygote/HdT4O/src/compiler/interface2.jl:0
[8] expect
@ ~/.julia/packages/QuantumToolbox/YmsAS/src/qobj/functions.jl:58 [inlined]
[9] my_f_mesolve
@ ~/GitHub/Research/Undef/Autodiff QuantumToolbox/autodiff.jl:65 [inlined]
[10] (::Zygote.Pullback{Tuple{…}, Tuple{…}})(Δ::Float64)
@ Zygote ~/.julia/packages/Zygote/HdT4O/src/compiler/interface2.jl:0
[11] (::Zygote.var"#88#89"{Zygote.Pullback{Tuple{…}, Tuple{…}}})(Δ::Float64)
@ Zygote ~/.julia/packages/Zygote/HdT4O/src/compiler/interface.jl:97
[12] gradient(f::Function, args::Vector{Float64})
@ Zygote ~/.julia/packages/Zygote/HdT4O/src/compiler/interface.jl:154
[13] top-level scope
@ ~/GitHub/Research/Undef/Autodiff QuantumToolbox/autodiff.jl:76
Some type information was truncated. Use `show(err)` to see complete types.
The differentiation of the ODE itself seem to work, since if I return real(abs2(sol.states[end].data[end]))
instead of real(expect(a' * a, sol.states[end]))
it works. So, apparently the expect
function doesn’t work in the case of mesolve
, but it works in the case of sesolve
.
To give more context, a QuantumObject
like the a
variable here is a struct
containing three fields
- The
data
field: the actual matrix or vector representing the state or the operator - The
type
anddimensions
fields: some structs to store the information of the quantum system
Conclusions
It seems that I have to define some adjoint for the QuantumObject
struct, whatever it means. If so, I don’t understand why I don’t need it for the Schrödinger equation case, but I need it for the master equation.