Skip to content

Commit 46d6333

Browse files
Introduce sesolve_map and mesolve_map (#554)
Co-authored-by: Alberto Mercurio <[email protected]>
1 parent 5a4f8a2 commit 46d6333

File tree

14 files changed

+470
-105
lines changed

14 files changed

+470
-105
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
- Fix `cite()` bibtex output. ([#552])
11+
- Implement `sesolve_map` and `mesolve_map` for solving multiple initial states and parameter sets in parallel. ([#554])
1112
- Add `qeye_like` and `qzero_like`, which are synonyms of `one` and `zero`. ([#555])
1213
- Add steadystate and DSF benchmarks. The `SteadyStateODESOlver` tolerances are lowered to `terminate_reltol=1e-4` and `terminate_abstol=1e-6` to improve speed at the cost of accuracy. ([#557])
1314

@@ -328,5 +329,6 @@ Release date: 2024-11-13
328329
[#544]: https://github.com/qutip/QuantumToolbox.jl/issues/544
329330
[#546]: https://github.com/qutip/QuantumToolbox.jl/issues/546
330331
[#552]: https://github.com/qutip/QuantumToolbox.jl/issues/552
332+
[#554]: https://github.com/qutip/QuantumToolbox.jl/issues/554
331333
[#555]: https://github.com/qutip/QuantumToolbox.jl/issues/555
332334
[#557]: https://github.com/qutip/QuantumToolbox.jl/issues/557

docs/src/resources/api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ mesolve
215215
mcsolve
216216
ssesolve
217217
smesolve
218+
sesolve_map
219+
mesolve_map
218220
dfd_mesolve
219221
liouvillian
220222
liouvillian_generalized

docs/src/users_guide/cluster.md

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -193,72 +193,44 @@ flush(stdout)
193193
end
194194

195195
@everywhere begin
196-
const Nc = 20
197-
const ωc = 1.0
198-
const g = 0.05
199-
const γ = 0.01
200-
const F = 0.01
196+
Nc = 20
197+
ωc = 1.0
198+
g = 0.05
199+
γ = 0.01
200+
F = 0.01
201201

202-
const a = tensor(destroy(Nc), qeye(2))
202+
a = tensor(destroy(Nc), qeye(2))
203203

204-
const σm = tensor(qeye(Nc), sigmam())
205-
const σp = tensor(qeye(Nc), sigmap())
204+
σm = tensor(qeye(Nc), sigmam())
205+
σp = tensor(qeye(Nc), sigmap())
206+
σz = tensor(qeye(Nc), sigmaz())
206207

207-
H(ωq) = ωc * a' * a + ωq * tensor(num(Nc), qeye(2)) + g * (a' * σm + a * σp)
208+
ωq_fun(p, t) = p[1] # ωq
209+
drive_fun(p, t) = p[3] * cos(p[2] * t) # F cos(ωd t)
208210

209-
coef(p, t) = p.F * cos(p.ωd * t) # coefficient for the time-dependent term
211+
H = ωc * a' * a + QobjEvo(σz / 2, ωq_fun) + g * (a' * σm + a * σp) + QobjEvo(a + a', drive_fun)
210212

211-
const c_ops = [sqrt(γ) * a, sqrt(γ) * σm]
212-
const e_ops = [a' * a]
213+
c_ops = [sqrt(γ) * a, sqrt(γ) * σm]
214+
e_ops = [a' * a]
213215
end
214216

215-
# Define the ODE problem and the EnsembleProblem generation function
217+
ωq_list = range(ωc - 3*g, ωc + 3*g, 100)
218+
ωd_list = range(ωc - 3*g, ωc + 3*g, 100)
219+
F_list = [F]
216220

217-
@everywhere begin
218-
ωq_list = range(ωc - 3*g, ωc + 3*g, 100)
219-
ωd_list = range(ωc - 3*g, ωc + 3*g, 100)
220-
221-
const iter = collect(Iterators.product(ωq_list, ωd_list))
222-
223-
function my_prob_func(prob, i, repeat, channel)
224-
ωq, ωd = iter[i]
225-
H_i = H(ωq)
226-
H_d_i = H_i + QobjEvo(a + a', coef) # Hamiltonian with a driving term
227-
228-
L = liouvillian(H_d_i, c_ops).data # Make the Liouvillian
229-
230-
put!(channel, true) # Update the progress bar channel
231-
232-
remake(prob, f=L, p=(F = F, ωd = ωd))
233-
end
234-
end
235-
236-
ωq, ωd = iter[1]
237-
H0 = H(ωq) + QobjEvo(a + a', coef)
238-
ψ0 = tensor(fock(Nc, 0), basis(2, 1)) # Ground State
221+
ψ_list = [
222+
tensor(fock(Nc, 0), basis(2, 0)),
223+
tensor(fock(Nc, 0), basis(2, 1))
224+
]
239225
tlist = range(0, 20 / γ, 1000)
240226

241-
prob = mesolveProblem(H0, ψ0, tlist, c_ops, e_ops=e_ops, progress_bar=Val(false), params=(F = F, ωd = ωd))
242-
243-
### Just to print the progress bar
244-
progr = ProgressBar(length(iter))
245-
progr_channel::RemoteChannel{Channel{Bool}} = RemoteChannel(() -> Channel{Bool}(1))
246-
###
247-
ens_prob = EnsembleProblem(prob.prob, prob_func=(prob, i, repeat) -> my_prob_func(prob, i, repeat, progr_channel))
248-
249-
250-
@sync begin
251-
@async while take!(progr_channel)
252-
next!(progr)
253-
end
254-
255-
@async begin
256-
sol = solve(ens_prob, Tsit5(), EnsembleSplitThreads(), trajectories = length(iter))
257-
put!(progr_channel, false)
258-
end
259-
end
227+
sol = mesolve_map(H, ψ_list, tlist, c_ops; e_ops = e_ops, params = (ωq_list, ωd_list, F_list), ensemblealg = EnsembleSplitThreads())
260228
```
261229

262-
We are using the [`mesolveProblem`](@ref) function to define the master equation problem. We added some code to manage the progress bar, which is updated through a `RemoteChannel`. The `prob_func` argument of the `EnsembleProblem` function is used to define the function that generates the problem for each iteration. The `iter` variable contains the product of the `ωq_list` and `ωd_list` lists, which are used to sweep over the parameters. The `sol = solve(ens_prob, Tsit5(), EnsembleDistributed(), trajectories=length(iter))` command is used to solve the problem with the distributed ensemble method. The output of the script will be printed in the `output.out` file, which contains an output similar to the previous example.
230+
Notice that we are using the [`mesolve_map`](@ref) function, which internally uses the `SciMLBase.EnsembleProblem` function to parallelize the computation of [`mesolveProblem`](@ref). The result is an `Array` of [`TimeEvolutionSol`](@ref), where each element corresponds to a specific combination of initial states and parameters. One can access the solution for a specific combination of initial states and parameters using indexing. For example,
263231

232+
```julia
233+
sol[i,j,k,l].expect
234+
```
264235

236+
this will give the expectation values for the case of `ψ_list[i]`, `ωq_list[j]`, `ωd_list[k]`, and `F_list[l]`.

src/qobj/superoperators.jl

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,5 @@ liouvillian(H::AbstractQuantumObject{Operator}, Id_cache::Diagonal = I(prod(H.di
180180
liouvillian(H::AbstractQuantumObject{SuperOperator}, Id_cache::Diagonal) = H
181181

182182
function _sum_lindblad_dissipators(c_ops, Id_cache::Diagonal)
183-
D = 0
184-
# sum all the (time-independent) c_ops first
185-
c_ops_ti = filter(op -> isa(op, QuantumObject), c_ops)
186-
if !isempty(c_ops_ti)
187-
D += mapreduce(op -> lindblad_dissipator(op, Id_cache), +, c_ops_ti)
188-
end
189-
190-
# sum rest of the QobjEvo together
191-
c_ops_td = filter(op -> isa(op, QuantumObjectEvolution), c_ops)
192-
if !isempty(c_ops_td)
193-
D += mapreduce(op -> lindblad_dissipator(op, Id_cache), +, c_ops_td)
194-
end
195-
196-
return D
183+
return sum(op -> lindblad_dissipator(op, Id_cache), c_ops; init = zero(spre(first(c_ops), Id_cache)))
197184
end

src/time_evolution/brmesolve.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export bloch_redfield_tensor, brterm, brmesolve
33
@doc raw"""
44
bloch_redfield_tensor(
55
H::QuantumObject{Operator},
6-
a_ops::Union{AbstractVector, Tuple},
6+
a_ops::Union{AbstractVector, Tuple, Nothing},
77
c_ops::Union{AbstractVector, Tuple, Nothing}=nothing;
88
sec_cutoff::Real=0.1,
99
fock_basis::Union{Val,Bool}=Val(false)
@@ -28,7 +28,7 @@ The return depends on `fock_basis`.
2828
"""
2929
function bloch_redfield_tensor(
3030
H::QuantumObject{Operator},
31-
a_ops::Union{AbstractVector,Tuple},
31+
a_ops::Union{AbstractVector,Tuple,Nothing},
3232
c_ops::Union{AbstractVector,Tuple,Nothing} = nothing;
3333
sec_cutoff::Real = 0.1,
3434
fock_basis::Union{Val,Bool} = Val(false),
@@ -44,7 +44,9 @@ function bloch_redfield_tensor(
4444
# Check whether we can rotate the terms to the eigenbasis directly in the Hamiltonian space
4545
fock_basis_hamiltonian = getVal(fock_basis) && sec_cutoff == -1
4646

47-
R = isempty(a_ops) ? 0 : sum(x -> _brterm(rst, x[1], x[2], sec_cutoff, fock_basis_hamiltonian), a_ops)
47+
R =
48+
(isnothing(a_ops) || isempty(a_ops)) ? 0 :
49+
sum(x -> _brterm(rst, x[1], x[2], sec_cutoff, fock_basis_hamiltonian), a_ops)
4850

4951
# If in fock basis, we need to transform the terms back to the fock basis
5052
# Note: we can transform the terms in the Hamiltonian space only if sec_cutoff is -1

src/time_evolution/callback_helpers/callback_helpers.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ Return the Callback that is responsible for saving the expectation values of the
139139
=#
140140
function _get_save_callback(sol::AbstractODESolution, method::Type{SF}) where {SF<:AbstractSaveFunc}
141141
kwargs = NamedTuple(sol.prob.kwargs) # Convert to NamedTuple to support Zygote.jl
142-
if hasproperty(kwargs, :callback)
142+
if hasproperty(kwargs, :callback) && !isnothing(kwargs.callback)
143143
return _get_save_callback(kwargs.callback, method)
144144
else
145145
return nothing

src/time_evolution/mesolve.jl

Lines changed: 156 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
1-
export mesolveProblem, mesolve
1+
export mesolveProblem, mesolve, mesolve_map
22

33
_mesolve_make_L_QobjEvo(H::Union{QuantumObject,Nothing}, c_ops) = QobjEvo(liouvillian(H, c_ops); type = SuperOperator())
44
_mesolve_make_L_QobjEvo(H::Union{QuantumObjectEvolution,Tuple}, c_ops) = liouvillian(QobjEvo(H), c_ops)
55
_mesolve_make_L_QobjEvo(H::Nothing, c_ops::Nothing) = throw(ArgumentError("Both H and
66
c_ops are Nothing. You are probably running the wrong function."))
77

8+
function _gen_mesolve_solution(sol, times, dimensions, isoperket::Val)
9+
if getVal(isoperket)
10+
ρt = map-> QuantumObject(ϕ, type = OperatorKet(), dims = dimensions), sol.u)
11+
else
12+
ρt = map-> QuantumObject(vec2mat(ϕ), type = Operator(), dims = dimensions), sol.u)
13+
end
14+
15+
kwargs = NamedTuple(sol.prob.kwargs) # Convert to NamedTuple for Zygote.jl compatibility
16+
17+
return TimeEvolutionSol(
18+
times,
19+
sol.t,
20+
ρt,
21+
_get_expvals(sol, SaveFuncMESolve),
22+
sol.retcode,
23+
sol.alg,
24+
kwargs.abstol,
25+
kwargs.reltol,
26+
)
27+
end
28+
829
@doc raw"""
930
mesolveProblem(
1031
H::Union{AbstractQuantumObject,Tuple},
@@ -207,23 +228,143 @@ end
207228
function mesolve(prob::TimeEvolutionProblem, alg::OrdinaryDiffEqAlgorithm = Tsit5(); kwargs...)
208229
sol = solve(prob.prob, alg; kwargs...)
209230

210-
# No type instabilities since `isoperket` is a Val, and so it is known at compile time
211-
if getVal(prob.kwargs.isoperket)
212-
ρt = map-> QuantumObject(ϕ, type = OperatorKet(), dims = prob.dimensions), sol.u)
213-
else
214-
ρt = map-> QuantumObject(vec2mat(ϕ), type = Operator(), dims = prob.dimensions), sol.u)
231+
return _gen_mesolve_solution(sol, prob.times, prob.dimensions, prob.kwargs.isoperket)
232+
end
233+
234+
@doc raw"""
235+
mesolve_map(
236+
H::Union{AbstractQuantumObject,Tuple},
237+
ψ0::Union{QuantumObject,AbstractVector{<:QuantumObject}},
238+
tlist::AbstractVector,
239+
c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
240+
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
241+
ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
242+
e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
243+
params::Union{NullParameters,Tuple} = NullParameters(),
244+
progress_bar::Union{Val,Bool} = Val(true),
245+
kwargs...,
246+
)
247+
248+
Solve the master equation for multiple initial states and parameter sets using ensemble simulation.
249+
250+
This function computes the time evolution for all combinations (Cartesian product) of initial states and parameter sets, solving the Lindblad master equation (see [`mesolve`](@ref)):
251+
252+
```math
253+
\frac{\partial \hat{\rho}(t)}{\partial t} = -i[\hat{H}, \hat{\rho}(t)] + \sum_n \mathcal{D}(\hat{C}_n) [\hat{\rho}(t)]
254+
```
255+
256+
where
257+
258+
```math
259+
\mathcal{D}(\hat{C}_n) [\hat{\rho}(t)] = \hat{C}_n \hat{\rho}(t) \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n \hat{\rho}(t) - \frac{1}{2} \hat{\rho}(t) \hat{C}_n^\dagger \hat{C}_n
260+
```
261+
262+
for each combination in the ensemble.
263+
264+
# Arguments
265+
266+
- `H`: Hamiltonian of the system ``\hat{H}``. It can be either a [`QuantumObject`](@ref), a [`QuantumObjectEvolution`](@ref), or a `Tuple` of operator-function pairs.
267+
- `ψ0`: Initial state(s) of the system. Can be a single [`QuantumObject`](@ref) or a `Vector` of initial states. It can be either a [`Ket`](@ref), [`Operator`](@ref) or [`OperatorKet`](@ref).
268+
- `tlist`: List of time points at which to save either the state or the expectation values of the system.
269+
- `c_ops`: List of collapse operators ``\{\hat{C}_n\}_n``. It can be either a `Vector` or a `Tuple`.
270+
- `alg`: The algorithm for the ODE solver. The default is `Tsit5()`.
271+
- `ensemblealg`: Ensemble algorithm to use for parallel computation. Default is `EnsembleThreads()`.
272+
- `e_ops`: List of operators for which to calculate expectation values. It can be either a `Vector` or a `Tuple`.
273+
- `params`: A `Tuple` of parameter sets. Each element should be an `AbstractVector` representing the sweep range for that parameter. The function will solve for all combinations of initial states and parameter sets.
274+
- `progress_bar`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities.
275+
- `kwargs`: The keyword arguments for the ODEProblem.
276+
277+
# Notes
278+
279+
- The function returns an array of solutions with dimensions matching the Cartesian product of initial states and parameter sets.
280+
- If `ψ0` is a vector of `m` states and `params = (p1, p2, ...)` where `p1` has length `n1`, `p2` has length `n2`, etc., the output will be of size `(m, n1, n2, ...)`.
281+
- If `H` is an [`Operator`](@ref), `ψ0` is a [`Ket`](@ref) and `c_ops` is `Nothing`, the function will call [`sesolve_map`](@ref) instead.
282+
- See [`mesolve`](@ref) for more details.
283+
284+
# Returns
285+
286+
- An array of [`TimeEvolutionSol`](@ref) objects with dimensions `(length(ψ0), length(params[1]), length(params[2]), ...)`.
287+
"""
288+
function mesolve_map(
289+
H::Union{AbstractQuantumObject{HOpType},Tuple},
290+
ψ0::AbstractVector{<:QuantumObject{StateOpType}},
291+
tlist::AbstractVector,
292+
c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
293+
alg::OrdinaryDiffEqAlgorithm = Tsit5(),
294+
ensemblealg::EnsembleAlgorithm = EnsembleThreads(),
295+
e_ops::Union{Nothing,AbstractVector,Tuple} = nothing,
296+
params::Union{NullParameters,Tuple} = NullParameters(),
297+
progress_bar::Union{Val,Bool} = Val(true),
298+
kwargs...,
299+
) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator,OperatorKet}}
300+
(isoper(H) && all(isket, ψ0) && isnothing(c_ops)) && return sesolve_map(
301+
H,
302+
ψ0,
303+
tlist;
304+
alg = alg,
305+
ensemblealg = ensemblealg,
306+
e_ops = e_ops,
307+
params = params,
308+
progress_bar = progress_bar,
309+
kwargs...,
310+
)
311+
312+
# mapping initial states and parameters
313+
# Convert to appropriate format based on state type
314+
ψ0_iter = map(ψ0) do state
315+
T = _complex_float_type(eltype(state))
316+
if isoperket(state)
317+
to_dense(T, copy(state.data))
318+
else
319+
to_dense(T, mat2vec(ket2dm(state).data))
320+
end
215321
end
322+
iter =
323+
params isa NullParameters ? collect(Iterators.product(ψ0_iter, [params])) :
324+
collect(Iterators.product(ψ0_iter, params...))
325+
ntraj = length(iter)
216326

217-
kwargs = NamedTuple(sol.prob.kwargs) # Convert to NamedTuple for Zygote.jl compatibility
327+
# we disable the progress bar of the mesolveProblem because we use a global progress bar for all the trajectories
328+
prob = mesolveProblem(
329+
H,
330+
first(ψ0),
331+
tlist,
332+
c_ops;
333+
e_ops = e_ops,
334+
params = first(iter)[2:end],
335+
progress_bar = Val(false),
336+
kwargs...,
337+
)
218338

219-
return TimeEvolutionSol(
339+
# generate and solve ensemble problem
340+
_output_func = _ensemble_dispatch_output_func(ensemblealg, progress_bar, ntraj, _standard_output_func) # setup global progress bar
341+
ens_prob = TimeEvolutionProblem(
342+
EnsembleProblem(
343+
prob.prob,
344+
prob_func = (prob, i, repeat) -> _se_me_map_prob_func(prob, i, repeat, iter),
345+
output_func = _output_func[1],
346+
safetycopy = false,
347+
),
220348
prob.times,
221-
sol.t,
222-
ρt,
223-
_get_expvals(sol, SaveFuncMESolve),
224-
sol.retcode,
225-
sol.alg,
226-
kwargs.abstol,
227-
kwargs.reltol,
349+
prob.dimensions,
350+
(progr = _output_func[2], channel = _output_func[3], isoperket = prob.kwargs.isoperket),
228351
)
352+
sol = _ensemble_dispatch_solve(ens_prob, alg, ensemblealg, ntraj)
353+
354+
# handle solution and make it become an Array of TimeEvolutionSol
355+
sol_vec =
356+
[_gen_mesolve_solution(sol[:, i], prob.times, prob.dimensions, prob.kwargs.isoperket) for i in eachindex(sol)] # map is type unstable
357+
if params isa NullParameters # if no parameters specified, just return a Vector
358+
return sol_vec
359+
else
360+
return reshape(sol_vec, size(iter))
361+
end
229362
end
363+
mesolve_map(
364+
H::Union{AbstractQuantumObject{HOpType},Tuple},
365+
ψ0::QuantumObject{StateOpType},
366+
tlist::AbstractVector,
367+
c_ops::Union{Nothing,AbstractVector,Tuple} = nothing;
368+
kwargs...,
369+
) where {HOpType<:Union{Operator,SuperOperator},StateOpType<:Union{Ket,Operator,OperatorKet}} =
370+
mesolve_map(H, [ψ0], tlist, c_ops; kwargs...)

0 commit comments

Comments
 (0)