Skip to content

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

julia
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 H^(t) can be separate into time-independent part (H^0) and a summation of many time-dependent operators, which takes the form:

H^(t)=H^0+jfj(t)H^j.

The following code sets up this problem by using a Tuple which contains many time-dependent pairs (H_j, f_j), namely

julia
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:

julia
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:

julia
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:

julia
γ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: 0.1σ^z and sin(t)(σ^x+σ^y).

As an example, we will look at a case with a time-dependent Hamiltonian of the form H^(t)=H^0+f1(t)H^1, where f1(t)=Aexp[(t/σ)2] is the time-dependent driving strength. The following code sets up the problem

julia
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 γ1(t)

julia
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 t:

julia
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:

julia
Ht.data
ScalarOperator(0.7071067811865475 + 0.0im) * MatrixOperator(2 × 2)
julia
Ht.type
Operator
julia
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:

julia
size(Ht)
(2, 2)
julia
shape(Ht) # synonym of size(Ht)
(2, 2)
julia
length(Ht)
4
julia
eltype(Ht) # element type
ComplexF64 (alias for Complex{Float64})
julia
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:

julia
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:

julia
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))
julia
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 f1(t)=Aexp[(t/σ)2] in the previous example, the driving amplitude A and width of the gaussian driving term σ were hardcoded with their numerical values. This is fine for problems that are specialized, or that we only want to run once. However, in many cases, we would like to study the same problem with a range of parameters and don't have to worry about manually changing the values on each run. 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 A and 5 for σ, we can either specify them with p as a Vector or NamedTuple:

julia
# 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
julia
# 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
julia
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:

julia
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:

julia
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
julia
sol1.expect == sol2.expect
true