quchip.backend.protocol¶
Backend protocol and backend-agnostic solver-result containers.
quchip separates physics description from solver conversion: the engine
(quchip.engine) emits structured IR (HamiltonianDescription,
SolveProblem, BatchedHamiltonianDescription, SolveBatch) that
carries ordinary-GHz frequencies and backend-free operator payloads
(CanonicalOperator). Each concrete backend is free to translate that IR
into whatever native form its solver likes best — this module only fixes the
contract (see Backend), not the storage.
Unit convention¶
All frequencies crossing the backend boundary are ordinary (not angular)
GHz, with time in ns. The single 2π conversion lives at the engine
boundary (stage2_assembly.py) — backends never rescale.
Aliases¶
Operator and State are both typing.Any. Concrete backends bind
them to their native type: qutip.Qobj for QuTiPBackend,
dynamiqs.QArray for DynamiqsBackend.
References
Johansson, Nation, Nori — QuTiP 2, Comput. Phys. Commun. 183, 1760 (2012)
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)
Breuer & Petruccione — The Theory of Open Quantum Systems (OUP, 2002)
Classes
|
Abstract contract implemented by every quchip operator backend. |
- class quchip.backend.protocol.Backend[source]¶
Bases:
ABCAbstract contract implemented by every quchip operator backend.
A backend wraps a quantum-dynamics library (QuTiP, dynamiqs, …) and exposes three orthogonal surfaces:
Operator algebra —
destroy/create/number/identity,tensor,embed,dag,matmul, eigensystem helpers. Concrete operators support native Python arithmetic (+ - * @); quchip never introducesscale/addwrappers.State algebra —
basis/coherent,overlap,ptrace,expect, ket ↔ density-matrix conversion.IR lowering & solve dispatch —
prepare_hamiltonian/prepare_batchconvert engine IR into native RHS form;sesolve/mesolverun the solver;solve_problem/solve_batchdrive the full engine-to-result pipeline.
Defaults provided here are NumPy-based and correct for any backend, so concrete subclasses only override where they can do better (e.g. QuTiP reuses
Qobj.eigenstates, dynamiqs reusesdq.expect).- abstract property array_module: Any¶
Return the array module used by backend-aware numeric code.
Returns
numpyfor CPU-only backends andjax.numpyfor JAX-traceable backends. Engine code that must stay differentiable (signal evaluation, frame assembly) routes array ops through this module so the dynamiqs backend preserves JAX traceability end-to-end.
- abstractmethod to_array(op)[source]¶
Return a dense array (native to
array_module) for op.
- norm(state_or_op)[source]¶
Return the Frobenius / ℓ²-norm of a state or operator.
A concrete
floatfor CPU-only backends; a JAX-traceable backend may return a native 0-d array instead so a traced amplitude stays differentiable — callers that need concreteness route throughmaybe_concrete_scalar().
- from_array(data, dims=None)[source]¶
Construct a native operator from a dense matrix.
dims accepts quchip’s row/col layout (
[[row_dims], [col_dims]]) or a flat list;Nonemeans a single subsystem of sizedata.shape[0].
- diag(values, dims=None)[source]¶
Construct a backend-native diagonal operator from main-diagonal values.
Backends that distinguish layouts (e.g. dynamiqs sparse-DIA) should return their sparse representation so element-wise composition with other diagonal operators stays sparse. Default falls through to
from_array()after building a densenp.diag.
- abstractmethod to_canonical_operator(op)[source]¶
Serialize a native operator into the backend-agnostic canonical IR.
- Parameters:
op (Operator)
- Return type:
- abstractmethod from_canonical_operator(canonical)[source]¶
Reconstruct a native operator from the canonical IR payload.
- Parameters:
canonical (CanonicalOperator)
- Return type:
- coerce_operator(op)[source]¶
Coerce an array-like local operator into backend-native form.
The operator-side mirror of
coerce_state(). Device-side physical-coupling accessors (e.g.charge_coupling_operator()) return dense, trace-safe arrays rather than backend-native operators; composition entry points (tensor(),dag()) route their operands through this hook so both forms compose freely. Backends whose native operators are already arrays override with a trace-safe passthrough.
- eigensystem_data(op)[source]¶
Return ascending eigenvalues, stacked eigenvector matrix, and lazy eigenstates.
- Parameters:
op (Any)
- Return type:
- expect(op, state)[source]¶
Return the expectation value ⟨state|op|state⟩ — accepts ket or density matrix.
- ptrace(state, keep, dims)[source]¶
Reduce a composite state onto subsystem(s) keep via partial trace.
- permute_state(state, dims, order)[source]¶
Reorder a composite state’s subsystems.
dimsare the subsystem levels in state’s current tensor order.orderfollowsnumpy.transposeconvention:order[i]is the current-order subsystem index that becomes positioniafter permuting (soorder == list(range(len(dims)))is a no-op).Default: densify, reshape to one axis per subsystem,
transpose, reshape back — correct for any backend. A ket reshapes/transposes once; a density matrix applies the same subsystem permutation to both the row and column index groups. Concrete backends may override this with a native reorder (e.g. QuTiP’sQobj.permute) that stays sparse-friendly and keeps dims metadata.Every array op routes through
array_module(never barenumpy), so a traced state stays traced end-to-end under the dynamiqs backend.dims/orderare always concrete Python ints (subsystem structure, never a physics parameter), so their product is taken withmath.prod()rather thanarray_module.prod— routing a shape value through the array module would turn it into a tracer underjitand make the subsequentreshapefail.
- stack_states(states)[source]¶
Stack a trajectory’s saved states into a single
(T, …)array.Default densifies each state and ``np.stack``s along a new leading time axis. Backends whose solver already returns a stacked native state (dynamiqs) override this to a no-op pass-through.
- expect_over_time(op, stacked_states)[source]¶
Return ⟨op⟩(t) for every save point, as one
(T,)array inarray_module.Accepts a ket stack
(T, n, 1)(returns ⟨ψ|op|ψ⟩) or a density-matrix stack(T, n, n)(returnsTr(op·ρ)). One einsum replaces the per-pointexpectloop. Every intermediate stays inarray_module, so the JAX backend keeps the trace differentiable.
- overlap_over_time(target, stacked_states)[source]¶
Return ⟨target|ψ(t)⟩ for every save point, as one
(T,)array.For a ket stack this is the phase-sensitive complex amplitude ⟨target|ψ(t)⟩ (never
|·|²— phase-dependent gradients flow through it). For a density-matrix stack there is no single phase, so it returns the target-state population ⟨target|ρ(t)|target⟩. Stays inarray_modulefor JAX traceability.
- populations_over_time(stacked_states)[source]¶
Return full-chip diagonal populations
(T, ∏dims)(real) for every save point.For a ket stack this is
|ψ(t)|²along the level axis (the density matrix is never built); for a DM stack it is the real diagonal. Stays inarray_modulefor JAX traceability.
- ptrace_over_time(stacked_states, keep, dims)[source]¶
Reduce onto subsystem(s) keep at every save point, returning a
(T, k, k)DM stack.Reduces a ket or DM trajectory onto subsystem(s) keep without a per-point Python loop.
- embed(op, device_index, dims)[source]¶
Embed a single-device operator at device_index into the full tensor space.
Validation is shared; the identity factors and tensor product dispatch through the backend’s own
identity()/tensor(), so each backend keeps its native-optimal layout.
- abstractmethod 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.
Handles subsystem SWAP when
index_a > index_band identity-padding for non-adjacent devices, without materializing the dense full matrix when the backend supports sparse layouts.
- coherent(n, alpha)[source]¶
Build the coherent state \(|\alpha\rangle\) truncated to n Fock levels.
Built from the analytic series \(|\alpha\rangle = e^{-|\alpha|^2/2} \sum_k \alpha^k/\sqrt{\Gamma(k+1)}\,|k\rangle\).
- as_density_matrix(state)[source]¶
Promote a ket to its density matrix; pass a density matrix through unchanged.
- abstractmethod tensor(*operators)[source]¶
Return the tensor product of operators, preserving subsystem dims metadata.
- abstractmethod sesolve(H, psi0, tlist, e_ops=None, options=None)[source]¶
Solve the Schrödinger equation \(i\hbar \partial_t |\psi\rangle = H |\psi\rangle\).
- Parameters:
H (Any) – Native Hamiltonian (
Qobj/QobjEvoor dynamiqsTimeQArray).psi0 (Any) – Initial ket.
tlist (Any) – 1D array of save times in ns.
e_ops (list[Any] | None) – Optional observables to accumulate ⟨ψ|Oᵢ|ψ⟩ along the trajectory.
options (dict[str, Any] | None) – Backend-specific options dict. See each backend’s
resolve_solver_optionsfor supported keys.
- Return type:
- abstractmethod mesolve(H, rho0, tlist, c_ops=None, e_ops=None, options=None)[source]¶
Solve the Lindblad master equation (Lindblad 1976; Breuer & Petruccione 2002).
Integrates \(\dot\rho = -i[H,\rho] + \sum_k \mathcal D[L_k]\rho\) with \(\mathcal D[L]\rho = L\rho L^\dagger - \tfrac12\{L^\dagger L, \rho\}\).
- batched_sesolve(problems, *, n_jobs=-1, progress=True)[source]¶
Solve multiple sesolve problems. Default: sequential dispatch.
Backends that can vectorize (dynamiqs
vmap) or parallelize (QuTiP via loky) override this method. Each problem dict carries the same kwargs assesolve().
- batched_mesolve(problems, *, n_jobs=-1, progress=True)[source]¶
Solve multiple mesolve problems. Default: sequential dispatch.
- prepare_hamiltonian(description, tlist)[source]¶
Convert a
HamiltonianDescriptioninto a native solver RHS.The engine passes frequencies already converted by
2π(angular rad/ns); backends must not rescale. Concrete backends override this method to choose their native time-dependence representation.- Parameters:
description (HamiltonianDescription)
tlist (Any)
- Return type:
- resolve_solver_options(options, *, metadata, tlist)[source]¶
Merge user options with backend-side defaults and metadata heuristics.
Default is a shallow copy; concrete backends use metadata (e.g.
spectral_bound_ghz) to fill in integrator step budgets when the user did not specify one.
- coerce_state(state, dims=None)[source]¶
Convert a foreign-native state into this backend’s native form.
Called at the solve boundary so a per-call
backend=override (seeQuantumSequence.simulate()) accepts initial states that were built under another backend — e.g. a QuTiPQobjfromchip.state(...)handed to a dynamiqs gradient solve. Default: pass through unchanged.dimsare the chip’s subsystem levels, for backends whose state type carries tensor structure.
- solve_problem(problem)[source]¶
Lower and solve a
SolveProblem— the single-element entry point.Picks
mesolvewhen collapse operators are present (open system) orsesolveotherwise, unlessproblem.solverforces a choice.- Parameters:
problem (SolveProblem)
- Return type:
- prepare_batch(description, tlist)[source]¶
Lower a
BatchedHamiltonianDescriptioninto a prepared batch.The return type declares the batching strategy:
EagerBatch(one RHS per element),VmappedBatch(one natively batched RHS — dynamiqs), orDeferredBatch(backend-private payload consumed by an overriddensolve_batch()— QuTiP). Default: lowers each element independently viaprepare_hamiltonian()into anEagerBatch.- Parameters:
description (BatchedHamiltonianDescription)
tlist (Any)
- Return type:
- solve_batch(batch, *, progress=True)[source]¶
Lower and solve a
SolveBatch.Default: prepares the batch then dispatches each element’s RHS through
batched_sesolve()/batched_mesolve(). Native backends override to avoid the per-element unpack.- Parameters:
batch (SolveBatch)
progress (bool)
- Return type: