Backend and solver options¶
quchip can run the same resolved model through QuTiP or dynamiqs. Both receive the same declared devices, controls, frame, approximation, collapse channels, and observables; each supplies its own numerical implementation.
Select a backend¶
QuTiP is installed with quchip and is the default:
chip = Chip(devices, couplings, backend="qutip")
Install the dynamiqs extra before selecting its backend:
python -m pip install 'quchip[dynamiqs]'
chip = Chip(devices, couplings, backend="dynamiqs")
A sequence can use either backend for one call without rebuilding the chip:
qutip_result = sequence.simulate(tlist=times, backend="qutip")
dynamiqs_result = sequence.simulate(tlist=times, backend="dynamiqs")
Compare the two results when an observable needs a numerical cross-check.
Choose the equation¶
The solver argument selects the equation. Leaving it unset follows the
declared model:
Model |
Automatic solver |
Evolved object |
|---|---|---|
No collapse channels |
|
State vector $ |
One or more collapse channels |
|
Density matrix \(\rho\) under the Lindblad master equation |
Both backends implement both equations. Set solver="mesolve" explicitly when
a density-matrix calculation is needed without collapse channels. Setting
solver="sesolve" explicitly on a model with collapse channels leaves those
channels out of the evolution.
result = sequence.simulate(
tlist=times,
solver="mesolve",
backend="qutip",
)
Pick from the calculation¶
Calculation |
Available route |
|---|---|
One closed- or open-system solve |
QuTiP or dynamiqs |
Long evolution under a small constant generator |
QuTiP’s automatic |
Large sparse constant closed system |
QuTiP’s |
Non-stiff time-dependent equation |
QuTiP adaptive methods or dynamiqs |
Stiff equation |
QuTiP |
Homogeneous parameter or pulse batch |
dynamiqs native vectorized batch or QuTiP process-parallel batch |
Heterogeneous problem list |
QuTiP process-parallel solves |
JAX gradient, |
dynamiqs |
Master-equation integration with a Rouchon scheme |
dynamiqs |
One stationary Lindblad state with sparse solver choices |
QuTiP |
A stationary state or VNA response inside |
dynamiqs constrained direct solve |
Steady-state solvers¶
chip.steadystate() skips time integration and solves the static Lindblad
equation directly. The resolved Hamiltonian must have no dynamic terms, and
the normalized stationary state must be unique.
QuTiP passes these choices to qutip.steadystate:
result = chip.steadystate(
options={"method": "direct", "solver": "spsolve"},
)
|
Solver choices |
Use it for |
|---|---|---|
|
|
The usual stationary solve; choose dense or sparse linear algebra to match the Liouvillian |
|
Sparse or dense eigensolver |
Finding the zero-eigenvalue state directly |
|
Dense SVD |
Small systems where a dense null-space calculation is acceptable |
|
The direct-method linear solvers |
Inverse-power iteration near the zero eigenvalue |
|
Repeated propagator application |
Convergence from an initial density matrix |
The residual is evaluated with QuTiP’s sparse Liouvillian. Nullity and
condition number require dense linear algebra, so quchip computes them only
through total Hilbert dimension 16 by default. Change that threshold with
options={"diagnostic_max_dimension": 24}; above it, both fields are None.
The dynamiqs backend uses method="direct". dynamiqs does not supply a public
steady-state solver in the supported release, so quchip constructs the
Liouvillian, replaces one row with Tr(rho) = 1, and calls jax.numpy.linalg.solve.
That path keeps stationary observables and finite-amplitude VNA response inside
JAX transformations. It reports the residual, nullity, and condition number;
it does not add a regularizer to a singular generator. Outside JAX tracing a
non-unique generator raises. Inside jax.jit, where Python exceptions cannot
depend on traced values, the result state is NaN when the nullity is not one.
The steady-state and microwave-port guide shows the state, scattering, spectrum, and correlation APIs.
QuTiP methods¶
QuTiP receives its method as a string in options:
result = sequence.simulate(
tlist=times,
backend="qutip",
options={
"method": "vern9",
"rtol": 1e-9,
"atol": 1e-11,
"nsteps": 100_000,
"max_step": 0.5,
},
)
rtol and atol control the adaptive error estimate. nsteps caps the number
of internal steps. max_step limits each internal step in ns. The save grid in
tlist controls where results are returned; the method chooses its internal
steps separately.
|
How it works |
What it enables |
|---|---|---|
|
Adaptive multistep method for non-stiff ODEs; QuTiP’s default |
General closed- and open-system evolution |
|
Implicit adaptive multistep method |
Stiff equations with separated time scales |
|
Switches between Adams and BDF |
Problems whose stiffness is not known in advance |
|
Eighth-order explicit Dormand-Prince method |
High-accuracy non-stiff evolution |
|
Seventh- and ninth-order explicit Runge-Kutta methods with dense output |
High-accuracy trajectories with many save times |
|
Fifth-order explicit Runge-Kutta method |
A lower-order adaptive alternative |
|
Diagonalizes a constant Hamiltonian or Liouvillian once, then evaluates the propagator at each save time |
Long constant evolution without stepping across the full interval |
|
Applies an approximate exponential in a Krylov subspace |
Large sparse, constant |
For a constant problem, quchip selects diag automatically when:
the user did not select another
method;the resolved Hamiltonian has no time-dependent terms; and
the total Hilbert dimension is at most 64 for
sesolve, or at most 12 formesolve.
The open-system propagator acts on a \(d^2\)-dimensional vectorized density
matrix, so its dense diagonalization reaches the practical cap sooner. An
explicit non-diag method always takes precedence. diag does not use
adaptive tolerances or step controls, so quchip removes atol, rtol,
nsteps, and max_step and records that choice at INFO level.
For a finite pulse inside a long idle interval, quchip derives a QuTiP
max_step from the narrowest pulse window when the user has not supplied one.
This prevents the adaptive solver from stepping across the pulse without
sampling it.
simulate_batch() uses reusable worker processes for QuTiP batches. Small
batches stay in the calling process; larger batches distribute independent
solves across CPU processes.
dynamiqs methods¶
dynamiqs receives a method object. Put tolerances and the step budget on that object:
import dynamiqs as dq
result = sequence.simulate(
tlist=times,
backend="dynamiqs",
options={
"method": dq.method.Dopri8(
rtol=1e-9,
atol=1e-11,
max_steps=100_000,
),
},
)
Method object |
How it works |
What it enables |
|---|---|---|
|
Fifth-order explicit adaptive Runge-Kutta method; dynamiqs’ default |
General differentiable closed- and open-system evolution |
|
Fifth-order explicit adaptive Dormand-Prince method |
An alternative error estimator for non-stiff equations |
|
Eighth-order explicit adaptive Dormand-Prince method |
High-accuracy non-stiff evolution |
|
Third- or fifth-order implicit adaptive method |
Stiff Hamiltonians and Liouvillians |
|
First-order fixed-step method |
A fixed-grid reference calculation |
|
First-order fixed-step master-equation method |
|
|
Second- or third-order master-equation methods; adaptive by default or fixed-step when |
Higher-order Rouchon integration for open systems |
If no method is supplied, quchip leaves dynamiqs on its Tsit5 default and
derives a max_steps budget. max_steps caps the number of internal steps. A
hard step-size constraint requires a fixed-step method where applicable,
schedule segmentation, or comparison with a refined calculation.
dynamiqs solves run inside JAX. The first call for a new static structure and
array shape compiles; later calls with the same structure can reuse the
compiled solve. simulate_batch() stacks a structurally homogeneous batch and
runs one native vectorized solve. A configured JAX installation determines
whether execution uses CPU, GPU, or another accelerator.
The default gradient mode supports reverse-mode differentiation such as
jax.grad. Forward mode is available for jax.jacfwd and JVP calculations:
result = sequence.simulate(
tlist=times,
backend="dynamiqs",
options={
"method": dq.method.Tsit5(rtol=1e-8, atol=1e-10),
"gradient": dq.gradient.Forward(),
},
)
Keep graph structure, Hilbert dimensions, the number of Hamiltonian terms, and the number of collapse channels fixed across a compiled optimization loop or native batch.
Frames affect both backends¶
Both backends solve the same engine result. A frame change can still change the numerical problem. A rotating frame may remove fast diagonal evolution, while rotating an interaction band can make that band explicitly time-dependent.
resolved = chip.resolve()
print(resolved.resolved_frame)
print(len(resolved.dynamic_terms))
Automatic QuTiP diagonal propagation requires zero dynamic terms. Adaptive methods in either backend can also take fewer internal steps when the selected frame removes fast oscillations.
Check the result¶
Compare the reported observable at two tolerance or step settings, check Hilbert-space truncation, and run both backends when they support the chosen workflow.