Skip to content

Commit 12d149a

Browse files
committed
add keyword argument assume_hermitian
1 parent 341000a commit 12d149a

File tree

7 files changed

+119
-41
lines changed

7 files changed

+119
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +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-
- Generalize the definition of `liouvillian`. It no longer expects the Hamiltonian to be Hermitian. ([#581])
11+
- Add keyword argument `assume_hermitian` to `liouvillian`. This allows users to disable the assumption that the Hamiltonian is Hermitian. ([#581])
1212

1313
## [v0.38.1]
1414
Release date: 2025-10-27

src/QuantumToolbox.jl

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,4 @@ include("visualization.jl")
134134
# deprecated functions
135135
include("deprecated.jl")
136136

137-
# module initialization
138-
function __init__()
139-
# this will be called immediately after the module is loaded (e.g., by import or using) at runtime for the first time.
140-
# useful for announcement
141-
@warn "The definition of `liouvillian` L for a given Hamiltonian H is changed to L[⋅] = -i( H[⋅] - [⋅]H' ), with ' representing complex conjugation. " *
142-
"QuantumToolbox.jl no longer expects H to be Hermitian."
143-
return nothing
144-
end
145-
146137
end

src/qobj/superoperators.jl

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,38 @@ function _spost(B::AbstractSciMLOperator, Id::AbstractMatrix)
3939
return kron(B_T, Id)
4040
end
4141

42-
## intrinsic liouvillian
43-
function _liouvillian(H::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}}
42+
## intrinsic liouvillian
43+
_liouvillian(
44+
H::MT,
45+
Id::AbstractMatrix,
46+
assume_hermitian::Val{true},
47+
) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}} = _liouvillian_Herm(H, Id)
48+
_liouvillian(
49+
H::MT,
50+
Id::AbstractMatrix,
51+
assume_hermitian::Val{false},
52+
) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}} = _liouvillian_NonHerm(H, Id)
53+
54+
## intrinsic liouvillian (assume H is Hermitian)
55+
_liouvillian_Herm(H::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}} =
56+
-1im * (_spre(H, Id) - _spost(H, Id))
57+
_liouvillian_Herm(H::MatrixOperator, Id::AbstractMatrix) = MatrixOperator(_liouvillian_Herm(H.A, Id))
58+
_liouvillian_Herm(H::ScaledOperator, Id::AbstractMatrix) = ScaledOperator(H.λ, _liouvillian_Herm(H.L, Id))
59+
_liouvillian_Herm(H::AddedOperator, Id::AbstractMatrix) = mapreduce(op -> _liouvillian_Herm(op, Id), +, H.ops) # use mapreduce to sum (+) all ops
60+
61+
## intrinsic liouvillian (assume H is Non-Hermitian)
62+
function _liouvillian_NonHerm(H::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}}
4463
CFType = _complex_float_type(H)
4564
return CFType(-1.0im) * _spre(H, Id) + CFType(1.0im) * _spost(H', Id)
4665
end
47-
_liouvillian(H::MatrixOperator, Id::AbstractMatrix) = MatrixOperator(_liouvillian(H.A, Id))
48-
function _liouvillian(H::ScaledOperator, Id::AbstractMatrix)
66+
_liouvillian_NonHerm(H::MatrixOperator, Id::AbstractMatrix) = MatrixOperator(_liouvillian_NonHerm(H.A, Id))
67+
function _liouvillian_NonHerm(H::ScaledOperator, Id::AbstractMatrix)
4968
CFType = _complex_float_type(H)
5069
return CFType(-1.0im) * ScaledOperator(H.λ, _spre(H.L, Id)) +
5170
CFType(1.0im) * ScaledOperator(conj(H.λ), _spost(H.L', Id))
5271
end
53-
_liouvillian(H::AddedOperator, Id::AbstractMatrix) = AddedOperator(map(op -> _liouvillian(op, Id), H.ops))
72+
_liouvillian_NonHerm(H::AddedOperator, Id::AbstractMatrix) =
73+
mapreduce(op -> _liouvillian_NonHerm(op, Id), +, H.ops) # use mapreduce to sum (+) all ops
5474

5575
# intrinsic lindblad_dissipator
5676
function _lindblad_dissipator(O::MT, Id::AbstractMatrix) where {MT<:Union{AbstractMatrix,AbstractSciMLOperator}}
@@ -145,12 +165,25 @@ lindblad_dissipator(O::AbstractQuantumObject{Operator}, Id_cache = I(size(O, 1))
145165
lindblad_dissipator(O::AbstractQuantumObject{SuperOperator}, Id_cache = nothing) = O
146166

147167
@doc raw"""
148-
liouvillian(H::AbstractQuantumObject, c_ops::Union{Nothing,AbstractVector,Tuple}=nothing, Id_cache=I(prod(H.dimensions)))
168+
liouvillian(
169+
H::AbstractQuantumObject,
170+
c_ops::Union{Nothing,AbstractVector,Tuple}=nothing,
171+
Id_cache=I(prod(H.dimensions));
172+
assume_hermitian::Union{Bool,Val} = Val(true),
173+
)
149174
150-
Construct the Liouvillian [`SuperOperator`](@ref) for a system Hamiltonian ``\hat{H}`` and a set of collapse operators ``\{\hat{C}_n\}_n``:
175+
Construct the Liouvillian [`SuperOperator`](@ref) for a system Hamiltonian ``\hat{H}`` and a set of collapse operators ``\{\hat{C}_n\}_n``.
176+
177+
By default, when the Hamiltonian `H` is assumed to be Hermitian [`assume_hermitian = Val(true)` or `true`], the Liouvillian [`SuperOperator`](@ref) is defined as :
178+
179+
```math
180+
\mathcal{L} [\cdot] = -i\left(\hat{H}[\cdot] - [\cdot]\hat{H}\right) + \sum_n \mathcal{D}(\hat{C}_n) [\cdot],
181+
```
182+
183+
otherwise [`assume_hermitian = Val(false)` or `false`],
151184
152185
```math
153-
\mathcal{L} [\cdot] = -i\left(\hat{H}[\cdot] - [\cdot]\hat{H}^\dagger\right) + \sum_n \mathcal{D}(\hat{C}_n) [\cdot]
186+
\mathcal{L} [\cdot] = -i\left(\hat{H}[\cdot] - [\cdot]\hat{H}^\dagger\right) + \sum_n \mathcal{D}(\hat{C}_n) [\cdot],
154187
```
155188
156189
where
@@ -162,27 +195,42 @@ where
162195
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.
163196
164197
See also [`spre`](@ref), [`spost`](@ref), and [`lindblad_dissipator`](@ref).
198+
199+
!!! warning "Beware of type-stability!"
200+
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.
165201
"""
166202
function liouvillian(
167203
H::AbstractQuantumObject{OpType},
168204
c_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
169-
Id_cache::Diagonal = I(prod(H.dimensions)),
205+
Id_cache::Diagonal = I(prod(H.dimensions));
206+
assume_hermitian::Union{Bool,Val} = Val(true),
170207
) where {OpType<:Union{Operator,SuperOperator}}
171-
L = liouvillian(H, Id_cache)
208+
L = liouvillian(H, Id_cache; assume_hermitian = assume_hermitian)
172209
if !(c_ops isa Nothing)
173210
L += _sum_lindblad_dissipators(c_ops, Id_cache)
174211
end
175212
return L
176213
end
177214

178-
liouvillian(H::Nothing, c_ops::Union{AbstractVector,Tuple}, Id_cache::Diagonal = I(prod(c_ops[1].dims))) =
179-
_sum_lindblad_dissipators(c_ops, Id_cache)
180-
181-
liouvillian(H::Nothing, c_ops::Nothing) = 0
182-
183-
liouvillian(H::AbstractQuantumObject{Operator}, Id_cache::Diagonal = I(prod(H.dimensions))) =
184-
get_typename_wrapper(H)(_liouvillian(H.data, Id_cache), SuperOperator(), H.dimensions)
185-
186-
liouvillian(H::AbstractQuantumObject{SuperOperator}, Id_cache::Diagonal) = H
215+
liouvillian(
216+
H::Nothing,
217+
c_ops::Union{AbstractVector,Tuple},
218+
Id_cache::Diagonal = I(prod(c_ops[1].dims));
219+
assume_hermitian::Union{Bool,Val} = Val(true),
220+
) = _sum_lindblad_dissipators(c_ops, Id_cache)
221+
222+
liouvillian(H::Nothing, c_ops::Nothing; assume_hermitian::Union{Bool,Val} = Val(true)) = 0
223+
224+
liouvillian(
225+
H::AbstractQuantumObject{Operator},
226+
Id_cache::Diagonal = I(prod(H.dimensions));
227+
assume_hermitian::Union{Bool,Val} = Val(true),
228+
) = get_typename_wrapper(H)(_liouvillian(H.data, Id_cache, makeVal(assume_hermitian)), SuperOperator(), H.dimensions)
229+
230+
liouvillian(
231+
H::AbstractQuantumObject{SuperOperator},
232+
Id_cache::Diagonal;
233+
assume_hermitian::Union{Bool,Val} = Val(true),
234+
) = H
187235

188236
_sum_lindblad_dissipators(c_ops, Id_cache::Diagonal) = sum(op -> lindblad_dissipator(op, Id_cache), c_ops)

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

test/core-test/quantum_objects_evo.jl

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,15 @@
182182
@testset "Time Dependent Operators and SuperOperators" begin
183183
N = 10
184184
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)
185+
coef1(p, t) = exp(-p.γ * t)
186+
coef2(p, t) = cos(p.ω1 * t)
187+
coef3(p, t) = sin(p.ω2 * t)
188188
t = rand()
189-
p = (ω1 = rand(), ω2 = rand(), ω3 = rand())
189+
p = (γ = rand(), ω1 = rand(), ω2 = rand())
190190

191191
# Operator
192-
H_td = QobjEvo(((a, coef1), a' * a, (a', coef2)))
193-
H_ti = coef1(p, t) * a + a' * a + coef2(p, t) * a'
192+
H_td = QobjEvo(((a + a', coef1), a' * a, (-1im * (a - a'), coef2))) # a Hermitian Hamiltonian
193+
H_ti = coef1(p, t) * (a + a') + a' * a - 1im * coef2(p, t) * (a - a')
194194
ψ = rand_ket(N)
195195
@test H_td(p, t) H_ti
196196
@test iscached(H_td) == true
@@ -225,6 +225,13 @@
225225
@test isconstant(L_td) == false
226226
@test issuper(L_td) == true
227227

228+
# test number of lazy operators and assume_hermitian = Val(false)
229+
L_td_assume_herm = liouvillian(H_td)
230+
L_td_assume_not_herm = liouvillian(H_td, assume_hermitian = Val(false))
231+
@test length(L_td_assume_herm.data.ops) == 3 # 1-time-indep. + 2-time-dep.
232+
@test length(L_td_assume_not_herm.data.ops) == 5 # 1-time-indep. + 2 x 2-time-dep.
233+
@test L_td_assume_herm(p, t) L_td_assume_not_herm(p, t) # check the matrix since H_td is itself Hermitian
234+
228235
coef_wrong1(t) = nothing
229236
coef_wrong2(p, t::ComplexF64) = nothing
230237
@test_logs (:warn,) (:warn,) liouvillian(H_td * H_td) # warnings from lazy tensor
@@ -245,6 +252,7 @@
245252
@inferred liouvillian(H_td, c_ops2)
246253
@inferred liouvillian(H_td2, c_ops1)
247254
@inferred liouvillian(H_td2, c_ops2)
255+
@inferred liouvillian(H_td2, c_ops2, assume_hermitian = Val(false))
248256
end
249257
end
250258
end

test/core-test/time_evolution.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,11 @@ end
10461046
@test sol_me.expect sol_me_td2.expect atol = 1e-6 * length(tlist)
10471047
@test sol_mc.expect sol_mc_td2.expect atol = 1e-2 * length(tlist)
10481048
# @test sol_sse.expect ≈ sol_sse_td2.expect atol = 1e-2 * length(tlist)
1049+
1050+
# test assume_hermitian = Val(false)
1051+
L_td_non_herm = liouvillian(H_td2, c_ops, assume_hermitian = Val(false))
1052+
sol_me_td3 = mesolve(L_td_non_herm, ψ0, tlist, e_ops = e_ops, progress_bar = Val(false), params = p)
1053+
@test sol_me.expect sol_me_td3.expect atol = 1e-6 * length(tlist)
10491054
end
10501055

10511056
@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)