quchip.backend.dynamiqs¶
Dynamiqs backend — JAX-native solver for differentiable, vmappable simulation.
This backend wraps dynamiqs (which builds on JAX) to provide:
Full JAX traceability. Every operator, state, and signal program evaluates inside JAX, so gradients flow through frame resolution, carrier phases, envelope parameters, crosstalk mixing, and dissipator strengths.
Native batched solves. A typed
SolveBatchis stacked along a batch axis and integrated with a singledq.sesolve/dq.mesolvecall (vmapunder the hood) insolve_batch(). Structurally heterogeneous batches fail loudly — no silent sequential fallback — so the caller can regroup them viaquchip.engine.solve_many().
References
Guilmin et al. — dynamiqs: an open-source Python library for GPU-accelerated and differentiable simulation of quantum systems (2024)
Bradbury et al. — JAX: composable transformations of Python+NumPy programs (2018)
Lindblad — Commun. Math. Phys. 48, 119 (1976)
Classes
Concrete backend backed by dynamiqs + JAX. |
- class quchip.backend.dynamiqs.DynamiqsBackend[source]¶
Bases:
BackendConcrete backend backed by dynamiqs + JAX. Operators/states are
QArray.Example
>>> from quchip.backend.dynamiqs import DynamiqsBackend >>> backend = DynamiqsBackend() >>> a = backend.destroy(3) >>> n = backend.matmul(backend.dag(a), a) >>> float(backend.to_array(n)[2, 2].real) 2.0
- overlap(a, b)[source]¶
Return the complex inner product ⟨a|b⟩ for two kets, via
dynamiqs.braket.dynamiqs.overlapreturns the real-valued \(|\langle a|b \rangle|^2\) (or a density-matrix fidelity), not the protocol’s phase-sensitive complex inner product —dq.braketis the matching primitive.
- norm(state_or_op)[source]¶
Return the norm of a state or operator via
dynamiqs.norm.Returns the backend-native (possibly traced) scalar rather than a concrete
float— the protocol allows a 0-d array here so a traced amplitude stays traceable underjax.jit/grad.
- diag(values, dims=None)[source]¶
Build a backend-native sparse-DIA diagonal operator from main-diagonal values.
Overrides the protocol default to keep the operator in sparse-DIA layout even when values is a JAX tracer — the offsets
(0,)are concrete, so the SparseDIAQArray constructor accepts the traced values directly. Without this override, dynamiqs’sfrom_canonical_operatordensifies any traced DIA payload, which forces a denseH₀for circuit-style devices and triggers the sparse→dense warning during static-Hamiltonian assembly.
- from_array(data, dims=None)[source]¶
Construct a native
QArrayfrom a dense matrix (row/col or flat dims).
- from_canonical_operator(canonical)[source]¶
Reconstruct a
QArrayfrom the canonical IR payload (dense/DIA/COO).
- coerce_operator(op)[source]¶
Pass op through unchanged (trace-safe): native operators are (JAX) arrays already.
- eigenenergies(op)[source]¶
Return the ascending eigenvalues of a Hermitian operator (
jax.numpy.linalg.eigvalsh).
- eigensystem_data(op)[source]¶
Return ascending eigenvalues, eigenvector matrix, and lazily built eigenstate kets.
- Parameters:
op (Any)
- Return type:
- expect(op, state)[source]¶
Return the expectation value ⟨op⟩ for a ket or density matrix via
dynamiqs.expect.
- ptrace(state, keep, dims)[source]¶
Reduce onto subsystem(s) keep via
dynamiqs.ptrace(partial trace).
- expect_over_time(op, stacked_states)[source]¶
Return ⟨op⟩(t) at every save point via a single batched
dynamiqs.expectcall.
- ptrace_over_time(stacked_states, keep, dims)[source]¶
Reduce onto subsystem(s) keep at every save point (partial trace, batched
dynamiqs.ptrace).
- tensor(*operators)[source]¶
Return the tensor product of operators via
dynamiqs.tensor(pass-through for one factor).
- embed_two_body(op_ab, index_a, index_b, dims)[source]¶
Embed a two-body operator on devices index_a ⊗ index_b into the full space.
Keeps a sparse operator sparse; a dense operator is reordered, padded with identities, and permuted back to the original subsystem order.
- tensor_states(*states)[source]¶
Return the tensor product of states via
dynamiqs.tensor(pass-through for one state).
- coherent(n, alpha)[source]¶
Return the coherent state |α⟩ truncated to n Fock levels (
dynamiqs.coherent).
- resolve_solver_options(options, *, metadata, tlist)[source]¶
Normalize quchip aliases and fill in a
max_stepsbudget.The quchip option aliases (
progress_bar→progress_meter,nsteps→max_steps) are mapped onto dynamiqs’ canonical names by_normalize_dq_options(). Whenmax_stepsis still unset, it is derived bydefault_solver_steps()(see that function’s docstring for the heuristic) — the same heuristic the QuTiP backend uses.max_stepsis an abort ceiling on the integrator’s total internal step count, not a bound on individual step size. Unlike the QuTiP backend, this backend does not consumemetadata['max_step_ns']: dynamiqs’ deterministic adaptive methods (Tsit5,Dopri5,Dopri8,Kvaerno3/Kvaerno5) expose no step-size-bound parameter analogous to QuTiP’smax_step(verified against dynamiqs 0.3.3 — none of those methods accept adtmax-style argument).dq.method.Eventdoes acceptdtmax, but it governs only the no-click evolution insidedq.jssesolve’s jump-SSE integration, not the deterministicsesolve/mesolvepath this backend uses; wrapping a deterministic solve inEventto borrow itsdtmaxwould fail before integration even starts.Consequence: a finite-support pulse embedded in a long idle span can be silently skipped by dynamiqs’ adaptive step selection, the same failure mode
quchip.backend.qutip.QuTiPBackend.resolve_solver_options()fixes viamax_step. The engine’smax_step_nshint is present in metadata but has no landing site on this backend today. Options for closing the gap — segmented integration around each pulse window, or an upstream request for adtmaxknob on dynamiqs’ deterministic methods — are unimplemented future work.
- coerce_state(state, dims=None)[source]¶
Convert a QuTiP
Qobjstate into a dimensioned dynamiqs qarray.Used when a per-call
backend="dynamiqs"override consumes initial states built under the QuTiP backend (chip.state(...)before the call). Composite subsystem dimensions must survive the conversion: dynamiqs checks them when multiplying the Hamiltonian by the state.
- sesolve(H, psi0, tlist, e_ops=None, options=None)[source]¶
Solve the Schrödinger equation via
dynamiqs.sesolve(JAX-traced).
- mesolve(H, rho0, tlist, c_ops=None, e_ops=None, options=None)[source]¶
Solve the Lindblad master equation via
dynamiqs.mesolve(JAX-traced).
- solve_problem(problem)[source]¶
Lower and solve a single
SolveProblemvia a cached jitted solve.Falls back to the protocol default (one-shot
prepare_hamiltonian+sesolve/mesolve) whenever the description contains a dynamic term that is not aScalarModulation(the cached path can only rebuildScalarModulationsignals).- Parameters:
problem (Any)
- Return type:
- prepare_hamiltonian(description, tlist=None)[source]¶
Convert a
HamiltonianDescriptioninto a dynamiqs native RHS.Static terms are summed as qarrays; dynamic terms with
ScalarModulationtime-dependence are wrapped viadynamiqs.modulatedwith a JAX-traceable_SignalCallable.tlistis passed through in metadata but not used for sampling — dynamiqs evaluates callables on the integrator’s adaptive grid.- Parameters:
- Return type:
- prepare_batch(description, tlist)[source]¶
Lower a
BatchedHamiltonianDescriptioninto a single vmapped RHS.Shared operators (static + per-slot dynamic) are converted exactly once via an id-keyed cache. For each dynamic slot, the per-element
ScalarModulationsignals are stacked leaf-by-leaf along a leading batch axis (jnp.stackon matching pytree leaves) and wrapped in a single_BatchedSignalQArrayCallablesodq.timecallablecan vmap the solve.- Raises:
ValueError – If a slot contains heterogeneous pytree structures (cannot be stacked) or a non-
ScalarModulationtime dependence.- Parameters:
- Return type:
- solve_batch(batch, *, progress=True)[source]¶
Solve a
SolveBatchvia a single native dynamiqs vmap.Raises
RuntimeErrorwhen the batch is not structurally homogeneous — no silent fallback. Callers with heterogeneous inputs should regroup throughquchip.engine.solve_many().- Parameters:
- Return type: