Emulation of the Quantum Dynamics
After we create the Rydberg Hamiltonian and the register for storing the quantum information, we can simulate the quantum many-body dynamics. The coherent dynamics of the system is governed by the Schrödinger equation. The emulation interface of Bloqade is designed in a define-and-run style. With Bloqade, we have two major types of emulation:
- ODE-solver based emulation for most of the problems.
- Krylov based emulation for piecewise constant problems or QAOA-like problem.
Define the ODE Emulation Problem
ODE solvers are the major backend we use for most of the exact quantum dynamics simulation. The ODE solvers for Bloqade are powered by the DifferentialEquations.jl package.
Bloqade provides a special problem type SchrodingerProblem
that supports most of the integrator interface of DifferentialEquations
, and most of the solver options. Here, we introduce some common use cases of the integrator and solver options. For more advanced usage of the solvers, please refer to the above link.
BloqadeODE.SchrodingerProblem
— Typestruct SchrodingerProblem
SchrodingerProblem(reg, tspan, hamiltonian; kw...)
Define a Schrodinger equation problem that uses ODE solver from OrdinaryDiffEq
to solve the dynamics.
Arguments
register
: required, the evolution problem register, can be aSubspaceArrayReg
or anArrayReg
fromYao
.tspan
: required, a(start, stop)
tuple or a single numbert
, the single value formt
is equivalent to(zero(t), t)
.hamiltonian
: required, the evolution hamiltonian, can be created viarydberg_h
.
Common Keyword Arguments
algo
: optional, algorithm to use, this only works for theemulate!
interface. forsolve
or integrator interface, one will need to specify the algorithm explicitly.progress
: print progress bar or not, this may effect the performance when problem scale is small, default istrue
.progress_steps
: steps to update the progress bar, default is5
.reltol
: relative tolerance, default is 1e-8.abstol
: absolute tolerance, default is 1e-8.
Further References
For more ODE options, please refer to Common Solver Options. The SchrodingerProblem
type supports most of the standard DiffEq problem interface.
Run ODE-based Emulation
To run the emulation, you need to define the exact evolution and solver you would like to run with via BloqadeODE.SchrodingerProblem
, and then feed the corresponding object to the emulate!
function:
BloqadeExpr.emulate!
— Functionemulate!(prob)
Run emulation of a given problem.
For example, we can simulate the quantum dynamics of a time-dependent Hamiltonian with the following:
using Bloqade
atoms = generate_sites(SquareLattice(), 3, 3; scale=5.1);
clocks = [0.0, 0.1, 0.2, 0.3, 0.4];
wf = piecewise_constant(;clocks, values=2π*[1.0, 2.0, 3.0, 4.0]);
h = rydberg_h(atoms; Δ=2π*2.0, Ω=wf); # create the Hamiltonian
reg = zero_state(length(atoms)); # create fullspace register
ev = SchrodingerProblem(reg, 0.3, h) # the second input is the total time
emulate!(ev)
SchrodingerProblem:
register info:
type: ArrayReg{2, ComplexF64, Matrix{ComplexF64}}
storage size: 8 bytes
time span (μs): (0.0, 0.3)
equation:
storage size: 76.016 KiB
expression:
nqubits: 9
+
├─ [+] ∑ 2π ⋅ 8.627e5.0/|x_i-x_j|^6 n_i n_j
├─ [+] Ω(t) ⋅ ∑ σ^x_i
└─ [-] 2π ⋅ 2.0 ⋅ ∑ n_i
algorithm: DP8(; stage_limiter! = trivial_limiter!, step_limiter! = trivial_limiter!, thread = static(false),)
options:
save_everystep: false
save_start: false
save_on: false
dense: false
reltol: 1.0e-10
abstol: 1.0e-10
alias_u0: true
With emulate!
, the quantum state stored in reg
has been updated to the state after the time evolution.
If you want to do operations during the real-time evolution, such as measuring observables, you can instead using the integrator interface with for
loop and with TimeChoiceIterator
on your desired clocks, e.g.:
integrator = init(ev, Vern8())
for _ in TimeChoiceIterator(integrator, [0.1, 0.25])
ev.reg # state at selected time
@show measure(ev.reg)[] # measure the state at each time
end
(measure(ev.reg))[] = 001001010 ₍₂₎
(measure(ev.reg))[] = 000101010 ₍₂₎
You can use any function on the reg
object. For calculating observables, please see the Registers and Observables section.
Remember to make sure your operation does not mutate your state so that it won't affect the evolution itself, since the entire time evolution is simulated by mutating the state vector stored in reg
. Avoid using any function that has a !
in its name (indicating that it mutates its arguments per Julia convention) on the register info.reg
unless you are certain about what you are doing.
Choose an ODE Solver
One of the most powerful tools of the Julia ecosystem is the DifferentialEquations ecosystem that implements many different solvers. These solvers have different advantages and trade-offs. Since simulating a quantum many-body Schrödinger equation has some special properties compared to a general ODE problem, we will discuss some general heuristics in this section on how to choose a good ODE solver and how to check if your simulation converges. Because the stiffness of the many-body Schrödinger equation is unknown, we will opt instead to use non-stiff problem algorithms or auto-switching algorithms.
For most of the cases, one can use the VCABM
solver for a large system simulation. However, this method requires more memory, which can be a bottleneck with GPUs.
The Vern
family is another set of solvers that is good for the many-body Schrödinger equation, such as Vern6
, Vern7
, and Vern8
. They also have relatively good memory usage when utilizing GPUs.
For a more detailed list of solvers, please refer to DifferentialEquations:Full List of Solvers. For more detailed explanation on ODE solvers, please refer to DifferentialEquations:Recommended Methods.
Bloqade uses the Dormand-Prince solver (DP8
) by default for all SchrodingerProblem
instances. For dealing with potentially stiff equations one can take advantage of DifferentialEquations
auto-switching algorithms where a default solver can be supplied but upon encountering stiffness can switch to a specific solver. A recommended combination for Bloqade is AutoVern9(Rodas5)
although other combinations are possible (see DifferentialEquations: Pre-Built Stiffness Detecting and Auto-Switching Algorithms)
If you are familiar with MATLAB or Python, you may wish to compare the same methods that you use in MATLAB or Python; you can find the corresponding solvers in Julia in DifferentialEquations:Translations from MATLAB/Python/R.
Adaptive Steps in ODE Solvers
Our ODE solvers use adaptive steps by default. It provides a significant speedup compared to standard fixed-step methods (see our benchmark here). However, if one expects to retrieve the results during the time evolution, e.g., plotting Rydberg densities with the evolution time, fixed-step methods are sometimes preferred.
More specifically, when the adaptive steps are turned on the time steps can become large. However, if one is interested in measuring some observables in smaller time steps, then the adaptive step method will not produce accurate results for the finer time step, instead outputting results at the specific adaptive steps. In this situation, it's better to use fixed-step methods at the times where the observables are measured.
One can use the code below to turn off the adaptive steps when setting up the SchrodingerProblem
:
atoms = generate_sites(SquareLattice(), 3, 3; scale=5.1);
h = rydberg_h(atoms; Δ=2π*2.0, Ω= 2π*1.0); # create the Hamiltonian
reg = zero_state(length(atoms));
prob = SchrodingerProblem(reg, 3.0, h, adaptive = false, dt=1e-3);
SchrodingerProblem:
register info:
type: ArrayReg{2, ComplexF64, Matrix{ComplexF64}}
storage size: 8 bytes
time span (μs): (0.0, 3.0)
equation:
storage size: 83.992 KiB
expression:
nqubits: 9
+
├─ [+] ∑ 2π ⋅ 8.627e5.0/|x_i-x_j|^6 n_i n_j
├─ [+] 2π ⋅ 0.5 ⋅ ∑ σ^x_i
└─ [-] 2π ⋅ 2.0 ⋅ ∑ n_i
algorithm: DP8(; stage_limiter! = trivial_limiter!, step_limiter! = trivial_limiter!, thread = static(false),)
options:
dt: 0.001
adaptive: false
save_everystep: false
save_start: false
save_on: false
dense: false
reltol: 1.0e-10
abstol: 1.0e-10
alias_u0: true
Here, we've specified the fixed time step as dt = 1e-3
. If one only wants the final state of the evolution or if the intervals between each chosen time are much larger than the maximum step size, then adaptive steps are preferred.
Define the Krylov Emulation Problem
The Krylov-based method expects time-independent Hamiltonians. One can define such a time evolution via a KrylovEvolution
object.
BloqadeKrylov.KrylovEvolution
— Typestruct KrylovEvolution
KrylovEvolution(reg::AbstractRegister, clocks, h; kw...)
Create a KrylovEvolution
object that describes a time evolution using Krylov subspace methods.
Arguments
reg
: a register, should be a subtype ofAbstractRegister
.clocks
: the clocks of this time evolution at each step.h
: a hamiltonian expression.
Keyword Arguments
progress
: show progress bar, default isfalse
.progress_name
: progress bar name, default is"emulating"
.normalize_step
: normalize the state everynormalize_step
.normalize_finally
: wether normalize the state in the end of evolution, default istrue
.tol
: tolerance of the Krylov method, default is1e-7
Examples
The following is the simplest way of using KrylovEvolution
via emulate!
. For more advanced usage, please refer to documentation page Emulation.
julia> using Bloqade
julia> r = zero_state(5)
ArrayReg{2, ComplexF64, Array...}
active qudits: 5/5
nlevel: 2
julia> atoms = [(i, ) for i in 1:5]
5-element Vector{Tuple{Int64}}:
(1,)
(2,)
(3,)
(4,)
(5,)
julia> h = rydberg_h(atoms; Ω=sin)
nqubits: 5
+
├─ [+] ∑ 5.42e6/|x_i-x_j|^6 n_i n_j
└─ [+] Ω(t) ⋅ ∑ σ^x_i
julia> prob = KrylovEvolution(r, 0.0:1e-2:0.1, h);
julia> emulate!(prob); # run the emulation
Run Krylov-based Emulation
We can run the Krylov-based emulation in a similar way using emulate!
:
julia> emulate!(KrylovEvolution(reg, clocks, h))
KrylovEvolution: register info: type: ArrayReg{2, ComplexF64, Matrix{ComplexF64}} storage size: 8 bytes clocks start:0.0μs last:0.4μs Hamiltonian number of dynamic terms: 0 storage size: 40 bytes Options: progress=false progress_step=1 progress_name="emulating" normalize_step=5 normalize_finally=true tol=1.0e-7 expmv_backend=BloqadeKrylov.expmv!
However, as its name suggests, the Krylov-based emulation is not a standard ODE problem that DiffEq supports. Thus, it does not support the ODE problem interface, but is more like a gate-based interface. For example, the object KrylovEvolution
is iterable:
for (step, reg, duration) in KrylovEvolution(reg, clocks, h)
@show step
@show reg
@show duration
println("==========")
end
step = 1
reg = ArrayReg{2, ComplexF64, Array...}
active qubits: 9/9
nlevel: 2
duration = 0.0
==========
step = 2
reg = ArrayReg{2, ComplexF64, Array...}
active qubits: 9/9
nlevel: 2
duration = 0.1
==========
step = 3
reg = ArrayReg{2, ComplexF64, Array...}
active qubits: 9/9
nlevel: 2
duration = 0.2
==========
step = 4
reg = ArrayReg{2, ComplexF64, Array...}
active qubits: 9/9
nlevel: 2
duration = 0.3
==========
step = 5
reg = ArrayReg{2, ComplexF64, Array...}
active qubits: 9/9
nlevel: 2
duration = 0.4
==========
Krylov vs ODE Solvers
The KrylovEvolution
uses the Krylov subspace methods to simulate the time evolution of time-independent operators $\exp(i\Delta t_i H)$, where $\Delta t_i$ is the duration of time-independent Hamiltonian $H$ at time $t$. This method is more efficient when the evolution itself is a discrete evolution, e.g. in QAOA and with piecewise_constant
waveforms. As for other cases, ODE solvers are usually more efficient than KrylovEvolution
.