Skip to content

Commit 402c27a

Browse files
Add keyword argument assume_hermitian to liouvillian (#581)
Co-authored-by: Alberto Mercurio <[email protected]> Co-authored-by: Alberto Mercurio <[email protected]>
1 parent 65403ba commit 402c27a

File tree

9 files changed

+139
-42
lines changed

9 files changed

+139
-42
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased](https://github.com/qutip/QuantumToolbox.jl/tree/main)
99

1010
- Change default solver detection in `eigensolve` when using `sigma` keyword argument (shift-inverse algorithm). If the operator is a `SparseMatrixCSC`, the default solver is `UMFPACKFactorization`, otherwise it is automatically chosen by LinearSolve.jl, depending on the type of the operator. ([#580])
11+
- Add keyword argument `assume_hermitian` to `liouvillian`. This allows users to disable the assumption that the Hamiltonian is Hermitian. ([#581])
1112

1213
## [v0.38.1]
1314
Release date: 2025-10-27
@@ -360,3 +361,4 @@ Release date: 2024-11-13
360361
[#576]: https://github.com/qutip/QuantumToolbox.jl/issues/576
361362
[#579]: https://github.com/qutip/QuantumToolbox.jl/issues/579
362363
[#580]: https://github.com/qutip/QuantumToolbox.jl/issues/580
364+
[#581]: https://github.com/qutip/QuantumToolbox.jl/issues/581

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Pkg = "1"
6464
ProgressMeter = "1.11.0"
6565
Random = "1"
6666
SciMLBase = "2.105"
67-
SciMLOperators = "1.4"
67+
SciMLOperators = "1.11"
6868
SparseArrays = "1"
6969
SpecialFunctions = "2"
7070
StaticArraysCore = "1"

src/qobj/superoperators.jl

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,24 @@ function _spost(B::AbstractSciMLOperator, Id::AbstractMatrix)
4040
end
4141

4242
## intrinsic liouvillian
43-
_liouvillian(H::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}} =
44-
-1im * (_spre(H, Id) - _spost(H, Id))
45-
_liouvillian(H::MatrixOperator, Id::AbstractMatrix) = MatrixOperator(_liouvillian(H.A, Id))
46-
_liouvillian(H::ScaledOperator, Id::AbstractMatrix) = ScaledOperator(H.λ, _liouvillian(H.L, Id))
47-
_liouvillian(H::AddedOperator, Id::AbstractMatrix) = AddedOperator(map(op -> _liouvillian(op, Id), H.ops))
43+
function _liouvillian(
44+
H::MT,
45+
Id::AbstractMatrix,
46+
::Val{assume_hermitian},
47+
) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator},assume_hermitian}
48+
H_spre = _spre(H, Id)
49+
H_spost = assume_hermitian ? _spost(H, Id) : _spost(H', Id)
50+
return -1im * (H_spre - H_spost)
51+
end
52+
function _liouvillian(H::MatrixOperator, Id::AbstractMatrix, assume_hermitian::Val)
53+
isconstant(H) ||
54+
throw(ArgumentError("The given Hamiltonian for constructing Liouvillian must be constant MatrixOperator."))
55+
return MatrixOperator(_liouvillian(H.A, Id, assume_hermitian))
56+
end
57+
_liouvillian(H::ScaledOperator, Id::AbstractMatrix, assume_hermitian::Val{true}) =
58+
ScaledOperator(H.λ, _liouvillian(H.L, Id, assume_hermitian))
59+
_liouvillian(H::AddedOperator, Id::AbstractMatrix, assume_hermitian::Val) =
60+
AddedOperator(map(op -> _liouvillian(op, Id, assume_hermitian), H.ops))
4861

4962
# intrinsic lindblad_dissipator
5063
function _lindblad_dissipator(O::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}}
@@ -139,12 +152,25 @@ lindblad_dissipator(O::AbstractQuantumObject{Operator}, Id_cache = I(size(O, 1))
139152
lindblad_dissipator(O::AbstractQuantumObject{SuperOperator}, Id_cache = nothing) = O
140153

141154
@doc raw"""
142-
liouvillian(H::AbstractQuantumObject, c_ops::Union{Nothing,AbstractVector,Tuple}=nothing, Id_cache=I(prod(H.dimensions)))
155+
liouvillian(
156+
H::AbstractQuantumObject,
157+
c_ops::Union{Nothing,AbstractVector,Tuple}=nothing,
158+
Id_cache=I(prod(H.dimensions));
159+
assume_hermitian::Union{Bool,Val} = Val(true),
160+
)
161+
162+
Construct the Liouvillian [`SuperOperator`](@ref) for a system Hamiltonian ``\hat{H}`` and a set of collapse operators ``\{\hat{C}_n\}_n``.
163+
164+
By default, when the Hamiltonian `H` is assumed to be Hermitian [`assume_hermitian = Val(true)` or `true`], the Liouvillian [`SuperOperator`](@ref) is defined as :
165+
166+
```math
167+
\mathcal{L} [\cdot] = -i\left(\hat{H}[\cdot] - [\cdot]\hat{H}\right) + \sum_n \mathcal{D}(\hat{C}_n) [\cdot],
168+
```
143169
144-
Construct the Liouvillian [`SuperOperator`](@ref) for a system Hamiltonian ``\hat{H}`` and a set of collapse operators ``\{\hat{C}_n\}_n``:
170+
otherwise [`assume_hermitian = Val(false)` or `false`],
145171
146172
```math
147-
\mathcal{L} [\cdot] = -i[\hat{H}, \cdot] + \sum_n \mathcal{D}(\hat{C}_n) [\cdot]
173+
\mathcal{L} [\cdot] = -i\left(\hat{H}[\cdot] - [\cdot]\hat{H}^\dagger\right) + \sum_n \mathcal{D}(\hat{C}_n) [\cdot],
148174
```
149175
150176
where
@@ -156,27 +182,51 @@ where
156182
The optional argument `Id_cache` can be used to pass a precomputed identity matrix. This can be useful when the same function is applied multiple times with a known Hilbert space dimension.
157183
158184
See also [`spre`](@ref), [`spost`](@ref), and [`lindblad_dissipator`](@ref).
185+
186+
!!! warning "Beware of type-stability!"
187+
If you want to keep type stability, it is recommended to use `assume_hermitian = Val(true)` instead of `assume_hermitian = true`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details.
159188
"""
160189
function liouvillian(
161190
H::AbstractQuantumObject{OpType},
162191
c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
163-
Id_cache::Diagonal = I(prod(H.dimensions)),
192+
Id_cache::Diagonal = I(prod(H.dimensions));
193+
assume_hermitian::Union{Bool,Val} = Val(true),
164194
) where {OpType<:Union{Operator,SuperOperator}}
165-
L = liouvillian(H, Id_cache)
166-
if !(c_ops isa Nothing)
167-
L += _sum_lindblad_dissipators(c_ops, Id_cache)
195+
L = liouvillian(H, Id_cache; assume_hermitian = assume_hermitian)
196+
if !isnothing(c_ops)
197+
return L + _sum_lindblad_dissipators(c_ops, Id_cache)
168198
end
169199
return L
170200
end
171201

172-
liouvillian(H::Nothing, c_ops::Union{AbstractVector,Tuple}, Id_cache::Diagonal = I(prod(c_ops[1].dims))) =
202+
liouvillian(H::Nothing, c_ops::Union{AbstractVector,Tuple}, Id_cache::Diagonal = I(prod(c_ops[1].dims)); kwargs...) =
173203
_sum_lindblad_dissipators(c_ops, Id_cache)
174204

175-
liouvillian(H::Nothing, c_ops::Nothing) = 0
205+
liouvillian(H::Nothing, c_ops::Nothing; kwargs...) = 0
206+
207+
liouvillian(
208+
H::AbstractQuantumObject{Operator},
209+
Id_cache::Diagonal = I(prod(H.dimensions));
210+
assume_hermitian::Union{Bool,Val} = Val(true),
211+
) = get_typename_wrapper(H)(_liouvillian(H.data, Id_cache, makeVal(assume_hermitian)), SuperOperator(), H.dimensions)
176212

177-
liouvillian(H::AbstractQuantumObject{Operator}, Id_cache::Diagonal = I(prod(H.dimensions))) =
178-
get_typename_wrapper(H)(_liouvillian(H.data, Id_cache), SuperOperator(), H.dimensions)
213+
liouvillian(H::AbstractQuantumObject{SuperOperator}, Id_cache::Diagonal; kwargs...) = H
179214

180-
liouvillian(H::AbstractQuantumObject{SuperOperator}, Id_cache::Diagonal) = H
215+
_sum_lindblad_dissipators(c_ops::Nothing, Id_cache::Diagonal) = 0
216+
217+
function _sum_lindblad_dissipators(c_ops::AbstractVector, Id_cache::Diagonal)
218+
return sum(op -> lindblad_dissipator(op, Id_cache), c_ops; init = 0)
219+
end
181220

182-
_sum_lindblad_dissipators(c_ops, Id_cache::Diagonal) = sum(op -> lindblad_dissipator(op, Id_cache), c_ops)
221+
# Help the compiler to unroll the sum at compile time
222+
@generated function _sum_lindblad_dissipators(c_ops::Tuple, Id_cache::Diagonal)
223+
N = length(c_ops.parameters)
224+
if N == 0
225+
return :(0)
226+
end
227+
ex = :(lindblad_dissipator(c_ops[1], Id_cache))
228+
for i in 2:N
229+
ex = :($ex + lindblad_dissipator(c_ops[$i], Id_cache))
230+
end
231+
return ex
232+
end

src/settings.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function Base.show(io::IO, s::Settings)
1515
end
1616

1717
@doc raw"""
18-
QuantumToolbox.settings
18+
QuantumToolbox.settings
1919
2020
Contains all the default global settings of QuantumToolbox.jl.
2121

src/time_evolution/mesolve.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export mesolveProblem, mesolve, mesolve_map
22

3-
_mesolve_make_L_QobjEvo(H::Union{QuantumObject,Nothing}, c_ops) = QobjEvo(liouvillian(H, c_ops); type = SuperOperator())
3+
_mesolve_make_L_QobjEvo(H::Union{QuantumObject,Nothing}, c_ops) =
4+
QuantumObjectEvolution(liouvillian(H, c_ops), type = SuperOperator())
45
_mesolve_make_L_QobjEvo(H::Union{QuantumObjectEvolution,Tuple}, c_ops) = liouvillian(QobjEvo(H), c_ops)
56
_mesolve_make_L_QobjEvo(H::Nothing, c_ops::Nothing) = throw(ArgumentError("Both H and
67
c_ops are Nothing. You are probably running the wrong function."))

src/time_evolution/time_evolution_dynamical.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,9 @@ function _DSF_mcsolve_Affect!(integrator)
571571
c_ops0_herm .= map(op -> op' * op, c_ops0)
572572

573573
H_nh = convert(eltype(ψt), 0.5im) * sum(c_ops0_herm)
574-
# By doing this, we are assuming that the system is time-independent and f is a MatrixOperator
575-
copyto!(integrator.f.f.A, lmul!(-1im, H(op_l2, dsf_params).data - H_nh))
574+
# By doing this, we are assuming that the system is time-independent and f is a ScaledOperator
575+
# of the form -1im * (H - H_nh)
576+
copyto!(integrator.f.f.L, H(op_l2, dsf_params).data - H_nh)
576577
return u_modified!(integrator, true)
577578
end
578579

test/core-test/quantum_objects_evo.jl

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using SparseArrays
44
using StaticArraysCore
55
using SciMLOperators
6+
import SciMLOperators: AddedOperator
67

78
# DomainError: incompatible between size of array and type
89
@testset "Thrown Errors" begin
@@ -182,15 +183,15 @@
182183
@testset "Time Dependent Operators and SuperOperators" begin
183184
N = 10
184185
a = destroy(N)
185-
coef1(p, t) = exp(-1im * p.ω1 * t)
186-
coef2(p, t) = sin(p.ω2 * t)
187-
coef3(p, t) = sin(p.ω3 * t)
186+
coef1(p, t) = exp(-p.γ * t)
187+
coef2(p, t) = cos(p.ω1 * t)
188+
coef3(p, t) = sin(p.ω2 * t)
188189
t = rand()
189-
p = (ω1 = rand(), ω2 = rand(), ω3 = rand())
190+
p = (γ = rand(), ω1 = rand(), ω2 = rand())
190191

191192
# Operator
192-
H_td = QobjEvo(((a, coef1), a' * a, (a', coef2)))
193-
H_ti = coef1(p, t) * a + a' * a + coef2(p, t) * a'
193+
H_td = QobjEvo(((a + a', coef1), a' * a, (-1im * (a - a'), coef2))) # a Hermitian Hamiltonian
194+
H_ti = coef1(p, t) * (a + a') + a' * a - 1im * coef2(p, t) * (a - a')
194195
ψ = rand_ket(N)
195196
@test H_td(p, t) H_ti
196197
@test iscached(H_td) == true
@@ -207,7 +208,7 @@
207208
X = a * a'
208209
c_op1 = QobjEvo(a', coef1)
209210
c_op2 = QobjEvo(((a, coef2), (X, coef3)))
210-
c_ops = [c_op1, c_op2]
211+
c_ops = (c_op1, c_op2)
211212
D1_ti = abs2(coef1(p, t)) * lindblad_dissipator(a')
212213
D2_ti =
213214
abs2(coef2(p, t)) * lindblad_dissipator(a) + # normal dissipator for first element in c_op2
@@ -225,6 +226,15 @@
225226
@test isconstant(L_td) == false
226227
@test issuper(L_td) == true
227228

229+
# test number of lazy operators and assume_hermitian = Val(false)
230+
L_td_assume_herm = liouvillian(H_td)
231+
L_td_assume_not_herm = liouvillian(H_td, assume_hermitian = Val(false))
232+
@test L_td_assume_herm.data isa AddedOperator
233+
@test L_td_assume_not_herm.data isa AddedOperator
234+
@test length(L_td_assume_herm.data.ops) == 3 # 1-time-indep. + 2-time-dep.
235+
@test length(L_td_assume_not_herm.data.ops) == 5 # 1-time-indep. + 2 x 2-time-dep.
236+
@test L_td_assume_herm(p, t) L_td_assume_not_herm(p, t) # check the matrix since H_td is itself Hermitian
237+
228238
coef_wrong1(t) = nothing
229239
coef_wrong2(p, t::ComplexF64) = nothing
230240
@test_logs (:warn,) (:warn,) liouvillian(H_td * H_td) # warnings from lazy tensor
@@ -237,15 +247,15 @@
237247
@test_throws ArgumentError cache_operator(L_td, ψ)
238248

239249
@testset "Type Inference" begin
240-
# we use destroy and create here because they somehow causes type instability before
241-
H_td2 = H_td + QobjEvo(destroy(N) + create(N), coef3)
242-
c_ops1 = (destroy(N), create(N))
243-
c_ops2 = (destroy(N), QobjEvo(create(N), coef1))
250+
H_td2 = H_td + QobjEvo(a + a', coef3)
251+
c_ops1 = (a, a')
252+
c_ops2 = (a, QobjEvo(a', coef1))
244253

245254
@inferred liouvillian(H_td, c_ops1)
246255
@inferred liouvillian(H_td, c_ops2)
247256
@inferred liouvillian(H_td2, c_ops1)
248257
@inferred liouvillian(H_td2, c_ops2)
258+
@inferred liouvillian(H_td2, c_ops2, assume_hermitian = Val(false))
249259
end
250260
end
251261
end

test/core-test/time_evolution.jl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ end
6262

6363
@testitem "sesolve" setup=[TESetup] begin
6464
using SciMLOperators
65+
import SciMLOperators: ScaledOperator
6566

6667
# Get parameters from TESetup to simplify the code
6768
H = TESetup.H
@@ -82,7 +83,7 @@ end
8283
amp_rabi = TESetup.g^2 / Ω_rabi^2
8384
##
8485

85-
@test prob.prob.f.f isa MatrixOperator
86+
@test prob.prob.f.f isa ScaledOperator
8687
@test sum(abs.(sol.expect[1, :] .- amp_rabi .* sin.(Ω_rabi * tlist) .^ 2)) / length(tlist) < 0.1
8788
@test length(sol.times) == length(tlist)
8889
@test length(sol.times_states) == 1
@@ -390,6 +391,7 @@ end
390391

391392
@testitem "mcsolve" setup=[TESetup] begin
392393
using SciMLOperators
394+
import SciMLOperators: ScaledOperator
393395
using Statistics
394396

395397
# Get parameters from TESetup to simplify the code
@@ -432,7 +434,7 @@ end
432434
expect_mc_states_mean = expect.(Ref(e_ops[1]), average_states(sol_mc_states))
433435
expect_mc_states_mean2 = expect.(Ref(e_ops[1]), average_states(sol_mc_states2))
434436

435-
@test prob_mc.prob.f.f isa MatrixOperator
437+
@test prob_mc.prob.f.f isa ScaledOperator
436438
@test sum(abs, sol_mc.expect .- sol_me.expect) / length(tlist) < 0.1
437439
@test sum(abs, sol_mc2.expect .- sol_me.expect) / length(tlist) < 0.1
438440
@test sum(abs, average_expect(sol_mc3) .- sol_me.expect) / length(tlist) < 0.1
@@ -523,7 +525,7 @@ end
523525
progress_bar = Val(false),
524526
keep_runs_results = Val(true),
525527
)
526-
@test allocs_tot < n1 * ntraj + 400 # 150 allocations per trajectory + 500 for initialization
528+
@test allocs_tot < n1 * ntraj + 600 # 150 allocations per trajectory + 600 for initialization
527529

528530
allocs_tot = @allocations mcsolve(
529531
H,
@@ -1046,6 +1048,11 @@ end
10461048
@test sol_me.expect sol_me_td2.expect atol = 1e-6 * length(tlist)
10471049
@test sol_mc.expect sol_mc_td2.expect atol = 1e-2 * length(tlist)
10481050
# @test sol_sse.expect ≈ sol_sse_td2.expect atol = 1e-2 * length(tlist)
1051+
1052+
# test assume_hermitian = Val(false)
1053+
L_td_non_herm = liouvillian(H_td2, c_ops, assume_hermitian = Val(false))
1054+
sol_me_td3 = mesolve(L_td_non_herm, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false), params = p)
1055+
@test sol_me.expect sol_me_td3.expect atol = 1e-6 * length(tlist)
10491056
end
10501057

10511058
@testitem "mcsolve, ssesolve and smesolve reproducibility" setup=[TESetup] begin

test/ext-test/cpu/autodiff/autodiff.jl

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ coef_γ(p, t) = sqrt(p[3])
5353
H = QobjEvo(a' * a, coef_Δ) + QobjEvo(a + a', coef_F)
5454
c_ops = [QobjEvo(a, coef_γ)]
5555
const L = liouvillian(H, c_ops)
56+
const L_assume_non_herm = liouvillian(H, c_ops, assume_hermitian = Val(false))
5657

5758
function my_f_mesolve(p)
5859
sol = mesolve(
@@ -67,6 +68,19 @@ function my_f_mesolve(p)
6768
return real(expect(a' * a, sol.states[end]))
6869
end
6970

71+
function my_f_mesolve_assume_non_herm(p)
72+
sol = mesolve(
73+
L_assume_non_herm,
74+
ψ0_mesolve,
75+
tlist_mesolve,
76+
progress_bar = Val(false),
77+
params = p,
78+
sensealg = BacksolveAdjoint(autojacvec = EnzymeVJP()),
79+
)
80+
81+
return real(expect(a' * a, sol.states[end]))
82+
end
83+
7084
# Analytical solution
7185
n_ss(Δ, F, γ) = abs2(F /+ 1im * γ / 2))
7286

@@ -113,6 +127,7 @@ n_ss(Δ, F, γ) = abs2(F / (Δ + 1im * γ / 2))
113127

114128
my_f_mesolve_direct(params)
115129
my_f_mesolve(params)
130+
my_f_mesolve_assume_non_herm(params)
116131

117132
grad_exact = Zygote.gradient((p) -> n_ss(p[1], p[2], p[3]), params)[1]
118133

@@ -122,20 +137,31 @@ n_ss(Δ, F, γ) = abs2(F / (Δ + 1im * γ / 2))
122137
end
123138

124139
@testset "Zygote.jl" begin
125-
grad_qt = Zygote.gradient(my_f_mesolve, params)[1]
126-
@test grad_qt grad_exact atol=1e-6
140+
grad_qt1 = Zygote.gradient(my_f_mesolve, params)[1]
141+
grad_qt2 = Zygote.gradient(my_f_mesolve_assume_non_herm, params)[1]
142+
@test grad_qt1 grad_exact atol=1e-6
143+
@test grad_qt2 grad_exact atol=1e-6
127144
end
128145

129146
@testset "Enzyme.jl" begin
130-
dparams = Enzyme.make_zero(params)
147+
dparams1 = Enzyme.make_zero(params)
131148
Enzyme.autodiff(
132149
Enzyme.set_runtime_activity(Enzyme.Reverse),
133150
my_f_mesolve,
134151
Active,
135-
Duplicated(params, dparams),
152+
Duplicated(params, dparams1),
136153
)[1]
137154

138-
@test dparams grad_exact atol=1e-6
155+
dparams2 = Enzyme.make_zero(params)
156+
Enzyme.autodiff(
157+
Enzyme.set_runtime_activity(Enzyme.Reverse),
158+
my_f_mesolve_assume_non_herm,
159+
Active,
160+
Duplicated(params, dparams2),
161+
)[1]
162+
163+
@test dparams1 grad_exact atol=1e-6
164+
@test dparams2 grad_exact atol=1e-6
139165
end
140166
end
141167
end

0 commit comments

Comments
 (0)