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 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

Backend()

Abstract contract implemented by every quchip operator backend.

class quchip.backend.protocol.Backend[source]

Bases: ABC

Abstract contract implemented by every quchip operator backend.

A backend wraps a quantum-dynamics library (QuTiP, dynamiqs, …) and exposes three orthogonal surfaces:

  • Operator algebradestroy/create/number/identity, tensor, embed, dag, matmul, eigensystem helpers. Concrete operators support native Python arithmetic (+ - * @); quchip never introduces scale/add wrappers.

  • State algebrabasis/coherent, overlap, ptrace, expect, ket ↔ density-matrix conversion.

  • IR lowering & solve dispatchprepare_hamiltonian / prepare_batch convert engine IR into native RHS form; sesolve / mesolve run the solver; solve_problem / solve_batch drive 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 reuses dq.expect).

abstract property array_module: Any

Return the array module used by backend-aware numeric code.

Returns numpy for CPU-only backends and jax.numpy for 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.

Parameters:

op (Any)

Return type:

Any

overlap(a, b)[source]

Return the scalar inner product ⟨a|b⟩ for two kets.

Parameters:
Return type:

complex

norm(state_or_op)[source]

Return the Frobenius / ℓ²-norm of a state or operator.

A concrete float for 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 through maybe_concrete_scalar().

Parameters:

state_or_op (Any)

Return type:

Any

trace(op)[source]

Return the scalar trace Tr(op).

Parameters:

op (Any)

Return type:

complex

destroy(n)[source]

Build the annihilation operator \(\hat a\) for an n-level Fock space.

Parameters:

n (int)

Return type:

Any

create(n)[source]

Build the creation operator \(\hat a^\dagger\) for an n-level Fock space.

Parameters:

n (int)

Return type:

Any

number(n)[source]

Build the number operator \(\hat n = \hat a^\dagger \hat a\).

Parameters:

n (int)

Return type:

Any

identity(n)[source]

Build the identity operator for an n-level space.

Parameters:

n (int)

Return type:

Any

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; None means a single subsystem of size data.shape[0].

Parameters:
Return type:

Any

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 dense np.diag.

Parameters:
Return type:

Any

abstractmethod to_canonical_operator(op)[source]

Serialize a native operator into the backend-agnostic canonical IR.

Parameters:

op (Operator)

Return type:

CanonicalOperator

abstractmethod from_canonical_operator(canonical)[source]

Reconstruct a native operator from the canonical IR payload.

Parameters:

canonical (CanonicalOperator)

Return type:

Operator

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.

Parameters:

op (Any)

Return type:

Any

dag(op)[source]

Return the Hermitian conjugate op†.

Parameters:

op (Any)

Return type:

Any

matmul(a, b)[source]

Return the matrix product a @ b.

Parameters:
Return type:

Any

eigenenergies(op)[source]

Return the ascending eigenvalues of a Hermitian operator.

Parameters:

op (Any)

Return type:

Any

eigenstates(op)[source]

Return (eigenvalues, eigenstates) of a Hermitian operator, ascending.

Parameters:

op (Any)

Return type:

tuple[Any, Any]

eigensystem_data(op)[source]

Return ascending eigenvalues, stacked eigenvector matrix, and lazy eigenstates.

Parameters:

op (Any)

Return type:

EigensystemData

expect(op, state)[source]

Return the expectation value ⟨state|op|state⟩ — accepts ket or density matrix.

Parameters:
Return type:

complex

ptrace(state, keep, dims)[source]

Reduce a composite state onto subsystem(s) keep via partial trace.

Parameters:
  • state (Any) – Composite ket or density matrix over the subsystems in dims.

  • keep (int | list[int]) – Subsystem index (or indices) to retain; all others are traced out.

  • dims (list[int]) – Subsystem dimensions of the full composite space, in order.

Return type:

Any

permute_state(state, dims, order)[source]

Reorder a composite state’s subsystems.

dims are the subsystem levels in state’s current tensor order. order follows numpy.transpose convention: order[i] is the current-order subsystem index that becomes position i after permuting (so order == 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’s Qobj.permute) that stays sparse-friendly and keeps dims metadata.

Every array op routes through array_module (never bare numpy), so a traced state stays traced end-to-end under the dynamiqs backend. dims/order are always concrete Python ints (subsystem structure, never a physics parameter), so their product is taken with math.prod() rather than array_module.prod — routing a shape value through the array module would turn it into a tracer under jit and make the subsequent reshape fail.

Parameters:
Return type:

Any

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.

Parameters:

states (Any)

Return type:

Any

expect_over_time(op, stacked_states)[source]

Return ⟨op⟩(t) for every save point, as one (T,) array in array_module.

Accepts a ket stack (T, n, 1) (returns ⟨ψ|op|ψ⟩) or a density-matrix stack (T, n, n) (returns Tr(op·ρ)). One einsum replaces the per-point expect loop. Every intermediate stays in array_module, so the JAX backend keeps the trace differentiable.

Parameters:
  • op (Any)

  • stacked_states (Any)

Return type:

Any

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 in array_module for JAX traceability.

Parameters:
  • target (Any)

  • stacked_states (Any)

Return type:

Any

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 in array_module for JAX traceability.

Parameters:

stacked_states (Any)

Return type:

Any

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.

Parameters:
Return type:

Any

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.

Parameters:
  • op (Any) – Single-device operator whose dimension matches dims[device_index].

  • device_index (int) – Position of op within the tensor product.

  • dims (Sequence[int]) – Subsystem dimensions of the full space, in order.

Return type:

Any

abstractmethod embed_two_body(op_ab, index_a, index_b, dims)[source]

Embed a two-body operator on devices index_aindex_b into the full space.

Handles subsystem SWAP when index_a > index_b and identity-padding for non-adjacent devices, without materializing the dense full matrix when the backend supports sparse layouts.

Parameters:
Return type:

Any

basis(n, k)[source]

Build the Fock basis ket \(|k\rangle\) in an n-level space.

Parameters:
Return type:

Any

tensor_states(*states)[source]

Return the tensor product of states (defaults to tensor()).

Parameters:

states (Any)

Return type:

Any

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\).

Parameters:
Return type:

Any

state_to_dm(state)[source]

Return a density matrix; pass through if state is already one.

Parameters:

state (Any)

Return type:

Any

is_ket(state)[source]

Return whether state is a column vector (ket) rather than a density matrix.

Parameters:

state (Any)

Return type:

bool

as_density_matrix(state)[source]

Promote a ket to its density matrix; pass a density matrix through unchanged.

Parameters:

state (Any)

Return type:

Any

abstractmethod tensor(*operators)[source]

Return the tensor product of operators, preserving subsystem dims metadata.

Parameters:

operators (Any)

Return type:

Any

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/QobjEvo or dynamiqs TimeQArray).

  • 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_options for supported keys.

Return type:

SolverResult

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\}\).

Parameters:
Return type:

SolverResult

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 as sesolve().

Parameters:
Return type:

list[SolverResult]

batched_mesolve(problems, *, n_jobs=-1, progress=True)[source]

Solve multiple mesolve problems. Default: sequential dispatch.

Parameters:
Return type:

list[SolverResult]

prepare_hamiltonian(description, tlist)[source]

Convert a HamiltonianDescription into a native solver RHS.

The engine passes frequencies already converted by (angular rad/ns); backends must not rescale. Concrete backends override this method to choose their native time-dependence representation.

Parameters:
Return type:

PreparedHamiltonian

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.

Parameters:
Return type:

dict[str, Any]

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 (see QuantumSequence.simulate()) accepts initial states that were built under another backend — e.g. a QuTiP Qobj from chip.state(...) handed to a dynamiqs gradient solve. Default: pass through unchanged. dims are the chip’s subsystem levels, for backends whose state type carries tensor structure.

Parameters:
Return type:

Any

solve_problem(problem)[source]

Lower and solve a SolveProblem — the single-element entry point.

Picks mesolve when collapse operators are present (open system) or sesolve otherwise, unless problem.solver forces a choice.

Parameters:

problem (SolveProblem)

Return type:

SolverResult

prepare_batch(description, tlist)[source]

Lower a BatchedHamiltonianDescription into a prepared batch.

The return type declares the batching strategy: EagerBatch (one RHS per element), VmappedBatch (one natively batched RHS — dynamiqs), or DeferredBatch (backend-private payload consumed by an overridden solve_batch() — QuTiP). Default: lowers each element independently via prepare_hamiltonian() into an EagerBatch.

Parameters:
Return type:

PreparedBatch

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:
Return type:

list[SolverResult]