Schrödinger Equation Solver
Unitary evolution
The dynamics of a closed (pure) quantum system is governed by the Schrödinger equation
\[i\hbar\frac{\partial}{\partial t}\Psi(\vec{x}, t) = \hat{H}\Psi(\vec{x}, t),\]
where $\Psi(\vec{x}, t)$ is the wave function, $\hat{H}$ is the Hamiltonian, and $\hbar$ is reduced Planck constant. In general, the Schrödinger equation is a partial differential equation (PDE) where both $\Psi$ and $\hat{H}$ are functions of space $\vec{x}$ and time $t$. For computational purposes it is useful to expand the PDE in a set of basis functions that span the Hilbert space of the Hamiltonian, and to write the equation in matrix and vector form, namely
\[i\hbar\frac{d}{dt}|\psi(t)\rangle = \hat{H}|\psi(t)\rangle,\]
where $|\psi(t)\rangle$ is the state vector, and the Hamiltonian $\hat{H}$ is now under matrix representation. This matrix equation can, in principle, be solved by diagonalizing the Hamiltonian matrix $\hat{H}$. In practice, however, it is difficult to perform this diagonalization unless the size of the Hilbert space (dimension of the matrix $\hat{H}$) is small. Analytically, it is a formidable task to calculate the dynamics for systems with more than two states. If, in addition, we consider dissipation due to the inevitable interaction with a surrounding environment, the computational complexity grows even larger, and we have to resort to numerical calculations in all realistic situations. This illustrates the importance of numerical calculations in describing the dynamics of open quantum systems, and the need for efficient and accessible tools for this task.
The Schrödinger equation, which governs the time-evolution of closed quantum systems, is defined by its Hamiltonian and state vector. In the previous sections, Manipulating States and Operators and Tensor Products and Partial Traces, we showed how Hamiltonians and state vectors are constructed in QuantumToolbox.jl
. Given a Hamiltonian, we can calculate the unitary (non-dissipative) time-evolution of an arbitrary initial state vector $|\psi(0)\rangle$ using the QuantumToolbox
time evolution problem sesolveProblem
or directly call the function sesolve
. It evolves the state vector $|\psi(t)\rangle$ and evaluates the expectation values for a set of operators e_ops
at each given time points, using an ordinary differential equation solver provided by the powerful julia package DifferentialEquation.jl
.
Example: Spin dynamics
For example, the time evolution of a quantum spin-$\frac{1}{2}$ system (initialized in spin-$\uparrow$) with tunneling rate $0.1$ is calculated, and the expectation values of the Pauli-Z operator $\hat{\sigma}_z$ is also evaluated, with the following code
H = 2 * π * 0.1 * sigmax()
ψ0 = basis(2, 0) # spin-up
tlist = LinRange(0.0, 10.0, 20)
prob = sesolveProblem(H, ψ0, tlist, e_ops = [sigmaz()])
sol = sesolve(prob)
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
Here, we generate the time evolution problem by sesolveProblem
first and then put it into the function sesolve
. One can also directly call sesolve
, which we also demonstrates in below.
The function returns TimeEvolutionSol
, as described in the previous section Time Evolution Solutions. The attribute expect
in sol
ution is a list of expectation values for the operator(s) that are passed to the e_ops
keyword argument.
sol.expect
1×20 Matrix{ComplexF64}:
1.0+0.0im 0.789141+0.0im 0.245485+0.0im … 0.789141+0.0im 1.0+0.0im
Passing multiple operators to e_ops
as a Vector
results in the expectation values for each operators at each time points.
tlist = LinRange(0.0, 10.0, 100)
sol = sesolve(H, ψ0, tlist, e_ops = [sigmaz(), sigmay()])
Solution of time evolution
(return code: Success)
--------------------------
num_states = 1
num_expect = 2
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
Here, we call sesolve
directly instead of pre-defining sesolveProblem
first (as shown previously).
times = sol.times
print(size(times))
(100,)
expt = sol.expect
print(size(expt))
(2, 100)
We can therefore plot the expectation values:
using CairoMakie
CairoMakie.enable_only_mime!(MIME"image/svg+xml"())
expt_z = real(expt[1,:])
expt_y = real(expt[2,:])
fig = Figure(size = (500, 350))
ax = Axis(fig[1, 1], xlabel = "Time", ylabel = "Expectation values")
lines!(ax, times, expt_z, label = L"\langle\hat{\sigma}_z\rangle", linestyle = :solid)
lines!(ax, times, expt_y, label = L"\langle\hat{\sigma}_y\rangle", linestyle = :dash)
axislegend(ax, position = :rb)
fig
If the keyword argument e_ops
is not specified (or given as an empty Vector
), the sesolve
and functions return a TimeEvolutionSol
that contains a list of state vectors which corresponds to the time points specified in tlist
:
tlist = [0, 10]
sol = sesolve(H, ψ0, tlist) # or specify: e_ops = []
sol.states
2-element Vector{QuantumObject{Vector{ComplexF64}, KetQuantumObject, 1}}:
Quantum Object: type=Ket dims=[2] size=(2,)
2-element Vector{ComplexF64}:
1.0 + 0.0im
0.0 + 0.0im
Quantum Object: type=Ket dims=[2] size=(2,)
2-element Vector{ComplexF64}:
0.9999999543220176 + 0.0im
0.0 - 5.637738564909251e-8im
This is because the states
will be saved depend on the keyword argument saveat
in kwargs
. If e_ops
is empty, the default value of saveat=tlist
(saving the states corresponding to tlist
), otherwise, saveat=[tlist[end]]
(only save the final state).
One can also specify e_ops
and saveat
separately:
tlist = [0, 5, 10]
sol = sesolve(H, ψ0, tlist, e_ops = [sigmay()], saveat = tlist)
Solution of time evolution
(return code: Success)
--------------------------
num_states = 3
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
sol.expect
1×3 Matrix{ComplexF64}:
0.0+0.0im -3.85604e-8+0.0im -8.84188e-8+0.0im
sol.states
3-element Vector{QuantumObject{Vector{ComplexF64}, KetQuantumObject, 1}}:
Quantum Object: type=Ket dims=[2] size=(2,)
2-element Vector{ComplexF64}:
1.0 + 0.0im
0.0 + 0.0im
Quantum Object: type=Ket dims=[2] size=(2,)
2-element Vector{ComplexF64}:
-0.9999999835117493 + 0.0im
0.0 + 1.9280206725638072e-8im
Quantum Object: type=Ket dims=[2] size=(2,)
2-element Vector{ComplexF64}:
0.9999999627203573 + 0.0im
0.0 - 4.42094083968742e-8im