Solving Problems with Time-dependent Hamiltonians
Generate QobjEvo
In the previous examples of solving time evolution, we assumed that the systems under consideration were described by time-independent Hamiltonians. However, many systems have explicit time dependence in either the Hamiltonian, or the collapse operators (c_ops
) describing coupling to the environment, and sometimes both components might depend on time. The time evolution solvers such as sesolve
, mesolve
, etc., are all capable of handling time-dependent Hamiltonians and collapse operators.
QuantumToolbox.jl
uses QuantumObjectEvolution
(or the abbreviated synonym: QobjEvo
) to represent time-dependent quantum operators, and its data
field (attribute) takes advantage of SciMLOperators.jl
.
To generate a time-dependent operator QobjEvo
, one needs to specify a time-independent Qobj
together with a time-dependent coefficient function with the form coef(p, t)
, namely
coef(p, t) = sin(t)
H_t = QobjEvo(sigmax(), coef)
Quantum Object Evo.: type=Operator dims=[2] size=(2, 2) ishermitian=true isconstant=false
ScalarOperator(0.0 + 0.0im) * MatrixOperator(2 × 2)
The inputs of coefficient function
Please note that although we didn't use the argument p
in the definition of coef
, we still need to put a dummy input p
(in front of t
) in the declaration of coef
. We will describe how to use the parameter p
in the section Using parameters.
The QobjEvo
can also be generated by specifying many pairs of time-independent Qobj
and time-dependent coefficient function. For instance, we will look at a case with the total Hamiltonian
The following code sets up this problem by using a Tuple
which contains many time-dependent pairs (H_j, f_j)
, namely
H0 = qeye(2)
H1 = sigmax()
f1(p, t) = sin(t)
H2 = sigmay()
f2(p, t) = cos(t)
H3 = sigmaz()
f3(p, t) = 9 * exp(-(t / 5)^2)
H_tuple = (
H0,
(H1, f1),
(H2, f2),
(H3, f3),
)
H_t = QobjEvo(H_tuple)
Quantum Object Evo.: type=Operator dims=[2] size=(2, 2) ishermitian=true isconstant=false
(DiagonalOperator(2 × 2) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(2 × 2) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(2 × 2) + ScalarOperator(0.0 + 0.0im) * MatrixOperator(2 × 2))
This is equivalent by generating the QobjEvo
separately and +
(or sum
) them up:
H_t == H0 + QobjEvo(H1, f1) + QobjEvo(H2, f2) + QobjEvo(H3, f3)
true
Most solvers will accept any format that could be made into a QobjEvo
for the Hamiltonian. All of the followings are equivalent:
sol = mesolve(H_t, ...)
sol = mesolve(H_tuple, ...)
Collapse operators c_ops
only accept a list where each element is either a Qobj
or a QobjEvo
. For example, in the following call:
γ1 = sqrt(0.1)
γ2(p, t) = sqrt(sin(t))
c_ops = (
γ1 * sigmaz(),
QobjEvo(sigmax() + sigmay(), γ2)
)
sol = mesolve(H_t, ..., c_ops, ...)
mesolve
will see 2
collapse operators:
As an example, we will look at a case with a time-dependent Hamiltonian of the form
N = 2 # Set where to truncate Fock state for cavity
# basis states for Atom
u = basis(3, 0) # u state
e = basis(3, 1) # excited state
g = basis(3, 2) # ground state
# operators
g_e = tensor(qeye(N), g * e') # |g><e|
u_e = tensor(qeye(N), u * e') # |u><e|
u_u = tensor(qeye(N), u * u') # |u><u|
g_g = tensor(qeye(N), g * g') # |g><g|
a = tensor(destroy(N), qeye(3))
# Hamiltonian
g = 5 # coupling strength
H0 = -g * (g_e' * a + a' * g_e)
H1 = u_e' + u_e
f1(p, t) = 9 * exp(-(t / 5)^2)
H_t = QobjEvo(
(
H0,
(H1, f1)
)
)
# Build collapse operators
κ = 1.5 # Cavity decay rate
γ0 = 6 # Atomic decay rate
c_ops = [
sqrt(κ) * a,
sqrt(4 * γ0 / 9) * g_e, # Use Rb branching ratio of 4/9 e -> g
sqrt(5 * γ0 / 9) * u_e # 5/9 e -> u
]
tlist = LinRange(0, 4, 200) # Define time vector
ψ0 = tensor(basis(N, 0), u) # Define initial state
# Build observables
e_ops = [
a' * a,
u_u,
g_g
]
# solve dynamics
exp_me = mesolve(H_t, ψ0, tlist, c_ops; e_ops = e_ops, progress_bar = Val(false)).expect
exp_mc = mcsolve(H_t, ψ0, tlist, c_ops; e_ops = e_ops, ntraj = 100, progress_bar = Val(false)).expect
# plot by CairoMakie.jl
fig = Figure(size = (500, 350))
ax = Axis(fig[1, 1], xlabel = L"Time $t$", ylabel = "expectation value")
lines!(ax, tlist, real(exp_me[1,:]), label = L"$\hat{a}^\dagger \hat{a}$ (mesolve)", color = :red, linestyle = :solid)
lines!(ax, tlist, real(exp_mc[1,:]), label = L"$\hat{a}^\dagger \hat{a}$ (mcsolve)", color = :red, linestyle = :dash)
lines!(ax, tlist, real(exp_me[2,:]), label = L"$|u \rangle\langle u|$ (mesolve)", color = :green, linestyle = :solid)
lines!(ax, tlist, real(exp_mc[2,:]), label = L"$|u \rangle\langle u|$ (mcsolve)", color = :green, linestyle = :dash)
lines!(ax, tlist, real(exp_me[3,:]), label = L"$|g \rangle\langle g|$ (mesolve)", color = :blue, linestyle = :solid)
lines!(ax, tlist, real(exp_mc[3,:]), label = L"$|g \rangle\langle g|$ (mcsolve)", color = :blue, linestyle = :dash)
axislegend(ax, position = :rc)
fig
The result from mesolve
is identical to that shown in the examples, the mcsolve
however will be noticeably off, suggesting we should increase the number of trajectories ntraj = 100
for this example.
In addition, we can also consider the decay of a simple Harmonic oscillator with time-varying decay rate
N = 10 # number of basis states
a = destroy(N)
H = a' * a
ψ0 = basis(N, 9) # initial state
γ0 = 0.5
γ1(p, t) = sqrt(γ0 * exp(-t))
c_ops_ti = [sqrt(γ0) * a] # time-independent collapse term
c_ops_td = [QobjEvo(a, γ1)] # time-dependent collapse term
e_ops = [a' * a]
tlist = LinRange(0, 10, 100)
exp_ti = mesolve(H, ψ0, tlist, c_ops_ti, e_ops = e_ops; progress_bar = Val(false)).expect[1,:]
exp_td = mesolve(H, ψ0, tlist, c_ops_td, e_ops = e_ops; progress_bar = Val(false)).expect[1,:]
# plot by CairoMakie.jl
fig = Figure(size = (500, 350))
ax = Axis(fig[1, 1], xlabel = L"Time $t$", ylabel = L"\langle \hat{a}^\dagger \hat{a} \rangle")
lines!(ax, tlist, real(exp_ti), label = L"\gamma_0", linestyle = :solid)
lines!(ax, tlist, real(exp_td), label = L"\gamma_1(t) = \gamma_0 e^{-t}", linestyle = :dash)
axislegend(ax, position = :rt)
fig
QobjEvo fields (attributes)
QuantumObjectEvolution
as a time-dependent quantum system, as its main functionality creates a QuantumObject
at a specified time
coef(p, t) = sin(π * t)
Ht = QobjEvo(sigmaz(), coef)
Ht(0.25) # t = 0.25
Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
0.707107+0.0im ⋅
⋅ -0.707107+0.0im
QuantumObjectEvolution
shares a lot of properties with the QuantumObject
:
Ht.data
ScalarOperator(0.7071067811865475 + 0.0im) * MatrixOperator(2 × 2)
Ht.type
Operator
Ht.dims
1-element SVector{1, Int64} with indices SOneTo(1):
2
The properties of a QuantumObjectEvolution
can also be retrieved using several functions with inputting QuantumObjectEvolution
:
size(Ht)
(2, 2)
shape(Ht) # synonym of size(Ht)
(2, 2)
length(Ht)
4
eltype(Ht) # element type
ComplexF64 (alias for Complex{Float64})
println(isket(Ht)) # ket
println(isbra(Ht)) # bra
println(isoper(Ht)) # operator
println(isoperket(Ht)) # operator-ket
println(isoperbra(Ht)) # operator-bra
println(issuper(Ht)) # super operator
println(isconstant(Ht)) # time-independent or not
println(ishermitian(Ht)) # Hermitian
println(isherm(Ht)) # synonym of ishermitian(Ht)
println(issymmetric(Ht)) # symmetric
println(isposdef(Ht)) # positive definite (and Hermitian)
false
false
true
false
false
false
false
true
true
true
false
QobjEvo
follow the same mathematical operations rules as Qobj
. They can be added, subtracted and multiplied with scalar, Qobj
and QobjEvo
. They also support adjoint
, transpose
, and conj
methods, and can be used for SuperOperator
transformation:
coef(p, t) = sin(π * t)
Ht = QobjEvo(sigmaz(), coef)
coef2(p, t) = exp(-t)
c_op = QobjEvo(destroy(2), coef2)
L1 = -1im * (spre(Ht) - spost(Ht'))
L1 += lindblad_dissipator(c_op)
Quantum Object Evo.: type=SuperOperator dims=[2] size=(4, 4) ishermitian=true isconstant=false
((ScalarOperator(0 - 1im) * ScalarOperator(0.0 + 0.0im)) * MatrixOperator(4 × 4) + (ScalarOperator(0 - 1im) * (ScalarOperator(-1) * ScalarOperator(0.0 - 0.0im))) * MatrixOperator(4 × 4) + (ScalarOperator(0.0 - 0.0im) * ScalarOperator(0.0 + 0.0im)) * MatrixOperator(4 × 4))
Or equivalently:
L2 = liouvillian(Ht, [c_op])
Quantum Object Evo.: type=SuperOperator dims=[2] size=(4, 4) ishermitian=true isconstant=false
(ScalarOperator(0.0 + 0.0im) * MatrixOperator(4 × 4) + (ScalarOperator(0.0 - 0.0im) * ScalarOperator(0.0 + 0.0im)) * MatrixOperator(4 × 4))
t = rand()
L1(t) == L2(t)
true
Optimization for superoperator transformation
Although the value of L1
and L2
here is equivalent, one can observe that the structure of L2.data
is much simpler than L1.data
, which means that it will be more efficient to solve the time evolution using L2
instead of L1
. Therefore, we recommend to use liouvillian
or lindblad_dissipator
to generate time-dependent SuperOperator
because these functions have been optimized.
Using parameters
Until now, the coefficients were only functions of time t
. In the definition of QuantumToolbox.jl
allows you to accomplish this by adding extra parameters p
to coefficient functions that make the QobjEvo
. For instance, instead of explicitly writing 9
for 5
for p
as a Vector
or NamedTuple
:
# specify p as a Vector
f1_v(p, t) = p[1] * exp(-(t / p[2])^2)
H_t_v = QobjEvo(sigmaz(), f1_v)
p_v = [9, 5] # 1st element represents A; 2nd element represents σ
t = 1
H_t_v(p_v, t)
Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
8.6471+0.0im ⋅
⋅ -8.6471+0.0im
# specify p as a NamedTuple
f1_n(p, t) = p.A * exp(-(t / p.σ)^2)
H_t_n = QobjEvo(sigmaz(), f1_n)
p_n = (A = 9, σ = 5)
t = 1
H_t_n(p_n, t)
Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
8.6471+0.0im ⋅
⋅ -8.6471+0.0im
t = rand()
H_t_v(p_v, t) == H_t_n(p_n, t)
true
Custom structure of parameter
For more advanced usage, any custom struct
of parameter p
can be used.
When solving time evolutions, the solvers take an single keyword argument params
, which will be directly passed to input p
of the coefficient functions to build the time dependent Hamiltonian and collapse operators. For example, with p::Vector
:
f1(p, t) = p[1] * cos(p[2] * t)
f2(p, t) = p[3] * sin(p[4] * t)
γ(p, t) = sqrt(p[5] * exp(-p[6] * t))
p_total = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
H_t = sigmaz() + QobjEvo(sigmax(), f1) + QobjEvo(sigmay(), f2)
c_ops = [
QobjEvo(destroy(2), γ)
]
e_ops = [sigmaz()]
ψ0 = basis(2, 0)
tlist = LinRange(0, 10, 20)
sol1 = mesolve(H_t, ψ0, tlist, c_ops, params = p_total, e_ops = e_ops; progress_bar = Val(false))
Solution of time evolution
(return code: Success)
--------------------------
num_states = 1
num_expect = 1
ODE alg.: OrdinaryDiffEqTsit5.Tsit5{typeof(OrdinaryDiffEqCore.trivial_limiter!), typeof(OrdinaryDiffEqCore.trivial_limiter!), Static.False}(OrdinaryDiffEqCore.trivial_limiter!, OrdinaryDiffEqCore.trivial_limiter!, static(false))
abstol = 1.0e-8
reltol = 1.0e-6
Similarly, with p::NamedTuple
:
f1(p, t) = p.A1 * cos(p.ω1 * t)
f2(p, t) = p.A2 * sin(p.ω2 * t)
γ(p, t) = sqrt(p.A3 * exp(-p.γ0 * t))
p_total = (
A1 = 0.1,
ω1 = 0.2,
A2 = 0.3,
ω2 = 0.4,
A3 = 0.5,
γ0 = 0.6
)
H_t = sigmaz() + QobjEvo(sigmax(), f1) + QobjEvo(sigmay(), f2)
c_ops = [
QobjEvo(destroy(2), γ)
]
e_ops = [sigmaz()]
ψ0 = basis(2, 0)
tlist = LinRange(0, 10, 20)
sol2 = mesolve(H_t, ψ0, tlist, c_ops, params = p_total, e_ops = e_ops; progress_bar = Val(false))
Solution of time evolution
(return code: Success)
--------------------------
num_states = 1
num_expect = 1
ODE alg.: OrdinaryDiffEqTsit5.Tsit5{typeof(OrdinaryDiffEqCore.trivial_limiter!), typeof(OrdinaryDiffEqCore.trivial_limiter!), Static.False}(OrdinaryDiffEqCore.trivial_limiter!, OrdinaryDiffEqCore.trivial_limiter!, static(false))
abstol = 1.0e-8
reltol = 1.0e-6
sol1.expect == sol2.expect
true