quchip.engine

Engine pipeline: Chip ResolvedFrame HamiltonianDescription SolveProblem.

The engine is the physics-to-solver layer. It owns no solvers and no backend-specific types; it produces structured, backend-agnostic descriptions that each backend converts to its own optimal form. The single 2π boundary lives in quchip.engine.stage2_assembly and nowhere else.

Pipeline

Public API

Functions

build_hamiltonian_description(chip, ...)

Build a HamiltonianDescription (stages 1-2 only).

build_problem(chip, drive_ops, tlist, *[, ...])

Run stages 1-4 and return a frozen SolveProblem.

simulate(chip, drive_ops, tlist, *[, ...])

Build a SolveProblem, dispatch it, and wrap the solver output.

solve_batch(batch, *[, progress])

Dispatch a SolveBatch through its chip backend.

solve_many(batch_or_problems, *[, progress])

Batch-dispatch typed solve requests that share one chip configuration.

solve_problem(problem, *[, ...])

Dispatch a SolveProblem through its chip backend.

quchip.engine.simulate(chip, drive_ops, tlist, *, solver=None, options=None, e_ops=None, initial_state=None, check_truncation=True, truncation_threshold=0.001, partition=True)[source]

Build a SolveProblem, dispatch it, and wrap the solver output.

Parameters mirror build_problem(). solver is "sesolve" or "mesolve"; None auto-selects mesolve when collapse operators exist. e_ops is dict-form, keyed by device label (or a 2-tuple of labels for two-body observables), and favors object references via resolve_label(). The Hilbert-truncation safety net is inherited from solve_problem(); check_truncation / truncation_threshold are threaded down.

Parameters:
  • chip (Chip) – The chip to simulate.

  • drive_ops (list of DriveOp) – Scheduled drive operations, typically produced by a QuantumSequence.

  • tlist (array_like) – Solver time grid in ns.

  • solver ({"sesolve", "mesolve"}, optional) – Solver selection; None auto-selects mesolve when collapse operators are present, else sesolve.

  • options (dict, optional) – Backend solver options. Must not contain a "backend" key (backend selection is chip-owned).

  • e_ops (dict, optional) – Observables keyed by device label (or a 2-tuple of labels for a two-body observable).

  • initial_state (optional) – Initial state. None defaults to the chip ground state. A Mapping (device label/object -> Fock index, e.g. {"q0": 1}) resolves through Chip.state() — the same dressed-state semantics _resolve_initial_state_spec() uses — on both the joint and the partitioned path. Any other value (a raw backend state) is passed through unchanged.

  • check_truncation (bool, default True) – Screen the result for over-populated top Fock levels.

  • truncation_threshold (float, default 1e-3) – Top-level population above which the truncation check warns.

  • partition (bool, default True) – When the chip splits into independent sub-chips (see Chip.partition()), dispatch one solve per component and combine them into a PartitionedSimulationResult instead of solving the full tensor-product space. Declines back to the joint solve (returning a plain SimulationResult) when the partition is trivial or initial_state is a raw backend state rather than None/a Mapping. Set False to force the joint solve unconditionally. simulate_batch/solve_many never partition in v1 — batched dispatch always solves the full chip.

Returns:

The wrapped solver output.

Return type:

SimulationResult or PartitionedSimulationResult

Raises:
  • ValueError – If solver is neither "sesolve" nor "mesolve", if tlist is not one-dimensional, finite, strictly increasing, and at least two points long, or if any drive_ops entry’s pulse window does not overlap tlist with positive measure (both concrete-only checks; see build_problem()).

  • RuntimeError – If the backend solve fails.

Examples

>>> import numpy as np
>>> from quchip import Chip, DuffingTransmon, ChargeDrive, Gaussian, QuantumSequence
>>> from quchip.engine import simulate
>>> q = DuffingTransmon(freq=5.0, anharmonicity=-0.3, levels=3)
>>> chip = Chip([q], frame="rotating", rwa=True)
>>> ctrl = ChargeDrive(target=q)
>>> chip.wire(ctrl)
>>> seq = QuantumSequence(chip)
>>> _ = seq.schedule(ctrl, envelope=Gaussian(duration=20.0, sigmas=3, amplitude=0.02), freq=chip.freq(q))
>>> tlist = np.linspace(0.0, 20.0, 41)
>>> result = simulate(chip, list(seq.scheduled_ops), tlist, e_ops={q: q.number_operator()})
>>> populations = result.expect(q)
quchip.engine.build_problem(chip, drive_ops, tlist, *, solver=None, options=None, e_ops=None, initial_state=None)[source]

Run stages 1-4 and return a frozen SolveProblem.

Returns an immutable request that can be passed to solve_problem(), batched with solve_many(), or serialized. No solver is invoked.

Parameters:
  • chip (Chip) – The chip whose Hamiltonian, frame, and backend are assembled.

  • drive_ops (list of DriveOp) – Scheduled drive operations, typically produced by a QuantumSequence.

  • tlist (array_like) – Solver time grid in ns.

  • solver ({"sesolve", "mesolve"}, optional) – Solver selection; None auto-selects mesolve when collapse operators are present, else sesolve.

  • options (dict, optional) – Backend solver options. Must not contain a "backend" key (backend selection is chip-owned).

  • e_ops (dict, optional) – Observables keyed by device label (or a 2-tuple of labels for a two-body observable).

  • initial_state (optional) – Initial state; None defaults to the chip ground state.

Returns:

The frozen request handed to a backend.

Return type:

SolveProblem

Raises:

ValueError – If tlist is not one-dimensional, finite, strictly increasing, and at least two points long, or if any drive_ops entry’s pulse window [start_time, start_time + envelope.duration] does not overlap tlist with positive measure. Both checks are concrete-only and skip silently under JAX tracing.

Examples

>>> import numpy as np
>>> from quchip import Chip, DuffingTransmon, ChargeDrive, Gaussian, QuantumSequence
>>> from quchip.engine import build_problem, solve_problem
>>> q = DuffingTransmon(freq=5.0, anharmonicity=-0.3, levels=3)
>>> chip = Chip([q], frame="rotating", rwa=True)
>>> ctrl = ChargeDrive(target=q)
>>> chip.wire(ctrl)
>>> seq = QuantumSequence(chip)
>>> _ = seq.schedule(ctrl, envelope=Gaussian(duration=20.0, sigmas=3, amplitude=0.02), freq=chip.freq(q))
>>> problem = build_problem(chip, list(seq.scheduled_ops), np.linspace(0.0, 20.0, 41))
>>> result = solve_problem(problem)
quchip.engine.solve_problem(problem, *, check_truncation=True, truncation_threshold=0.001)[source]

Dispatch a SolveProblem through its chip backend.

This is the common single-solve chokepoint, so the Hilbert-truncation safety net lives here: unless check_truncation=False, the wrapped result is screened for over-populated top Fock levels (warning above truncation_threshold). Every example-facing single-solve path (chip.solve, seq.simulate) inherits the check by routing through here.

Parameters:
Return type:

SimulationResult

quchip.engine.solve_many(batch_or_problems, *, progress=True)[source]

Batch-dispatch typed solve requests that share one chip configuration.

Accepts a SolveBatch, a ProblemBatch, or a flat list of SolveProblem objects. The batched paths are preferred: backends convert shared operators exactly once and stitch per-element coefficients into one parallel solve.

Parameters:
Return type:

SimulationBatchResult

quchip.engine.solve_batch(batch, *, progress=True)[source]

Dispatch a SolveBatch through its chip backend.

The backend converts each shared operator exactly once and stitches per-element coefficient data before running the parallel solve.

Parameters:
Return type:

SimulationBatchResult

quchip.engine.build_hamiltonian_description(chip, drive_ops, **kwargs)[source]

Build a HamiltonianDescription (stages 1-2 only).

Parameters:
Returns:

Static and dynamic terms plus dropped-term records.

Return type:

HamiltonianDescription

class quchip.engine.BatchedHamiltonianDescription(batch_size, static_terms, dynamic_operators, dynamic_origins, dynamic_tags, dynamic_signals, dims=(), metadata=<factory>, dropped_terms_by_element=())[source]

Bases: object

Batched Hamiltonian IR that splits shared vs per-element structure.

Describes N Hamiltonians with identical operator skeletons (static_terms and per-slot dynamic operators) but independent per-slot ScalarModulation signal programs. Backends convert the shared operators once and stitch per-element coefficient data into one prepared batch.

dynamic_signals is indexed [slot][element]; on sweep axes that do not touch signals every entry on a slot is identity-equal. dropped_terms_by_element is indexed [element]: which bands are dropped is template-structural and shared across the batch, but each record’s frequency depends on that element’s drive frequency, so the records themselves are stored per element and restored on the matching element() call.

Parameters:
batch_size: int
static_terms: tuple[StaticTerm, ...]
dynamic_operators: tuple[CanonicalOperator, ...]
dynamic_origins: tuple[Literal['device', 'coupling', 'drive', 'crosstalk', 'flux'], ...]
dynamic_tags: tuple[str | None, ...]
dynamic_signals: tuple[tuple[ScalarModulation, ...], ...]
dims: tuple[int, ...] = ()
metadata: dict[str, Any]
dropped_terms_by_element: tuple[tuple[DroppedTerm, ...], ...] = ()
element(index)[source]

Reconstruct the single-element description at index.

Parameters:

index (int)

Return type:

HamiltonianDescription

class quchip.engine.CanonicalOperator(layout, values, shape, dims, basis, subsystem_labels, indices=None, indptr=None, offsets=None, tag=None)[source]

Bases: object

Backend-free operator with explicit dense/CSR/DIA payload and subsystem metadata.

For dense the payload is the full 2D matrix; for csr it is the 1D nonzero value array paired with indices/indptr; for dia it is a 2D (n_diags, n_cols) array paired with offsets. dims must multiply to shape[0] and subsystem_labels names each subsystem.

Parameters:
layout: Literal['dense', 'csr', 'dia']
values: Any
shape: tuple[int, int]
dims: tuple[int, ...]
basis: str
subsystem_labels: tuple[str, ...]
indices: Any | None = None
indptr: Any | None = None
offsets: Any | None = None
tag: str | None = None
property is_sparse: bool

True for the csr / dia layouts, False for dense.

classmethod from_dense(values, *, dims, basis, subsystem_labels, tag=None)[source]
Parameters:
Return type:

CanonicalOperator

classmethod from_csr(values, indices, indptr, *, shape, dims, basis, subsystem_labels, tag=None)[source]
Parameters:
Return type:

CanonicalOperator

classmethod from_dia(values, offsets, *, shape, dims, basis, subsystem_labels, tag=None)[source]
Parameters:
Return type:

CanonicalOperator

with_metadata(*, dims=None, basis=None, subsystem_labels=None, tag=None)[source]

Return a metadata-adjusted copy (payload unchanged).

Parameters:
Return type:

CanonicalOperator

to_dense()[source]

Materialize the payload as a dense shape-sized matrix.

Vectorized and array-namespace-preserving (JAX-safe): a traced JAX payload yields a JAX array via .at[].set / .add, a concrete NumPy payload yields a NumPy array. Callers that need a guaranteed concrete NumPy matrix must wrap the result in np.asarray(..., dtype=complex) themselves.

Return type:

Any

fingerprint()[source]

Batching key: value-sensitive, with an automatic tracer-safe fallback.

Two crosstalk-rebuilt operators carrying the same coefficients collapse to the same key so they batch into one slot (stage 4). Under jax.jit the payload is a tracer (possibly hidden inside a backend qarray wrapper, e.g. dynamiqs SparseDIAQArray); contains_tracer() detects that and the key falls back to layout + shape/dtype structure only, so tobytes() is never called on a tracer and two equivalent traced operators in different batch slots still produce identical keys.

Return type:

tuple

class quchip.engine.Carrier(freq, sign=-1)[source]

Bases: SignalNode

Oscillating carrier exp(sign · i · freq · t).

freq is in angular units (rad/ns). The default sign = -1 matches the convention used in rotating-frame decompositions (Scully & Zubairy, Quantum Optics, §5), where a raising-type band on a detuning rotates as exp(−iΔt). Both fields are registered as pytree children (freq may be traced; sign is semantically a static ±1 — do not map over it).

Parameters:
freq: float
sign: Literal[-1, 1] = -1
evaluate(t, *, xp)[source]

Return exp(sign · i · freq · t) at time(s) t (ns).

Parameters:
Return type:

Any

bands()[source]

Return the single sign·freq band with a unit-constant envelope.

Return type:

tuple[CarrierBand, …]

class quchip.engine.DroppedTerm(source, operator, reason, band_weights=None, amplitude=None, frequency=None)[source]

Bases: object

Advisory record for a Hamiltonian term elided by an approximation.

Emitted by physics components (couplings, drives, …) whose local Hamiltonian routines discard terms under an approximation such as the rotating-wave approximation. Stage 2 aggregates these records into HamiltonianDescription.dropped_terms so callers can audit what was silently removed — in particular, compare each dropped band’s amplitude against its oscillation frequency, the smallness ratio that governs RWA validity (leading correction ∼ amplitude²/frequency, the Bloch–Siegert scale).

The string fields are static and value-free. amplitude and frequency hold raw numeric values in GHz ordinary frequency — possibly JAX-traced; they are never formatted or branched on during assembly. band_weights is static structure (excitation-change weights, one per mode the operator acts on) that stage 2 uses to resolve frequency from the frame without the owner knowing frame references.

Parameters:
  • source (str) – Label of the owning component (coupling / drive / …) that dropped the term.

  • operator (str) – Human-readable operator string (e.g. "a_q0 · a_q1").

  • reason (str) – Short reason (e.g. "counter-rotating under RWA").

  • band_weights (tuple[int, ...] | None) – Excitation-change weights of the dropped band, one per endpoint mode in the owner’s declared order (e.g. (-1, -1) for a·b). None when not applicable.

  • amplitude (Any | None) – Static prefactor of the dropped term in GHz (e.g. the coupling g); possibly traced. None when the prefactor is time-dependent (drive envelopes) or unknown.

  • frequency (Any | None) – Oscillation frequency of the dropped band in the assembly frame, GHz, positive; possibly traced. None until resolved (stage 2 fills it from the frame and band_weights).

source: str
operator: str
reason: str
band_weights: tuple[int, ...] | None = None
amplitude: Any = None
frequency: Any = None
class quchip.engine.DynamicTerm(operator, time_dependence, origin='drive', tag=None)[source]

Bases: object

Time-dependent Hamiltonian contribution operator · f(t).

f(t) is wrapped in ScalarModulation, which each backend lowers into its native coefficient representation (QuTiP callback, dynamiqs sampled array, etc.). The operator is 2π-scaled already (see module docstring). tag is an optional human label; it does not participate in physics.

Parameters:
operator: CanonicalOperator
time_dependence: ScalarModulation
origin: Literal['device', 'coupling', 'drive', 'crosstalk', 'flux'] = 'drive'
tag: str | None = None
class quchip.engine.HamiltonianDescription(static_terms, dynamic_terms, dims=(), metadata=<factory>, dropped_terms=())[source]

Bases: object

Backend-agnostic time-dependent Hamiltonian — the stage-2 / backend contract.

Represents

\[H(t) \;=\; \sum_s c_s \, O_s \;+\; \sum_d O_d \, f_d(t)\]

where each static / dynamic operator already carries 2π and each f_d(t) is a ScalarModulation over a SignalProgram AST. metadata carries advisory solver hints (e.g. max_carrier_freq_ghz, max_step_ns); a backend may consult them or apply an equivalent numerical strategy of its own, but remains responsible for resolving finite-support dynamics — a finite-width pulse must not be silently skipped by an adaptive integrator that never samples it. dropped_terms records any terms that owning components elided under an approximation (RWA, etc.) — advisory metadata for auditing, never consumed by backends.

Parameters:
static_terms: tuple[StaticTerm, ...]
dynamic_terms: tuple[DynamicTerm, ...]
dims: tuple[int, ...] = ()
metadata: dict[str, Any]
dropped_terms: tuple[DroppedTerm, ...] = ()
dropped_terms_summary()[source]

Format dropped_terms as a multi-line human-readable string.

Traced amplitude / frequency values print as traced rather than being concretized.

Return type:

str

class quchip.engine.ScalarModulation(signal)[source]

Bases: object

Typed wrapper marking a SignalProgram as a scalar modulation on a DynamicTerm.

Parameters:

signal (SignalNode)

signal: SignalNode
class quchip.engine.SolveBatch(chip, hamiltonian, initial_states, tlist, c_ops=(), e_ops=None, e_ops_meta=None, resolved_frame=None, solver=None, options=<factory>)[source]

Bases: object

Batched counterpart to SolveProblem.

Bundles one BatchedHamiltonianDescription plus shared solver metadata and per-element initial states. Backends solve the N elements in one native batched call (vmap on dynamiqs; shared-operator + stitched coefficient arrays on QuTiP).

Parameters:
chip: Any
hamiltonian: BatchedHamiltonianDescription
initial_states: tuple[Any, ...]
tlist: Any
c_ops: tuple[Any, ...] = ()
e_ops: Any = None
e_ops_meta: Any = None
resolved_frame: Any = None
solver: str | None = None
options: dict[str, Any]
property batch_size: int

Number of elements N in the batch.

element(index)[source]

Reconstruct the single-element SolveProblem at index.

Parameters:

index (int)

Return type:

SolveProblem

class quchip.engine.SolveProblem(chip, hamiltonian, initial_state, tlist, c_ops=(), e_ops=None, e_ops_meta=None, resolved_frame=None, solver=None, options=<factory>)[source]

Bases: object

Immutable simulation request handed from the chip pipeline to a backend.

Bundles the stage-2 HamiltonianDescription, an initial_state, solver time grid, collapse operators, decomposed e_ops + their BandMeta, the ResolvedFrame, and solver options. chip owns backend selection, so options must not contain a "backend" key (enforced in __post_init__). e_ops_meta is the metadata stage 3 uses to recombine flattened band expectations back into dict-keyed observables.

Parameters:
chip: Any
hamiltonian: Any
initial_state: Any
tlist: Any
c_ops: tuple[Any, ...] = ()
e_ops: Any = None
e_ops_meta: Any = None
resolved_frame: Any = None
solver: str | None = None
options: dict[str, Any]
class quchip.engine.StaticTerm(operator, coefficient=1.0, origin='device', metadata=<factory>)[source]

Bases: object

Time-independent Hamiltonian contribution.

The operator payload has already been scaled by 2π at the stage-2 boundary; backends must not re-apply it. coefficient multiplies operator and may be a concrete scalar or a JAX tracer (sweeps over static couplings, detunings, etc.). origin is purely advisory metadata.

Parameters:
operator: CanonicalOperator
coefficient: complex = 1.0
origin: Literal['device', 'coupling', 'drive', 'crosstalk', 'flux'] = 'device'
metadata: dict[str, Any]

Modules

bands

Band decomposition by excitation-change weight.

ir

IR types flowing between engine stages.

partitioned

Partition-aware dispatch for quchip.engine.simulate().

solver_hints

Advisory solver-hint heuristics (post-assembly metadata only).

stage1_frames

Stage 1: resolve a FrameSpec into a ResolvedFrame.

stage2_assembly

Stage 2: assemble a HamiltonianDescription from chip, drive ops, and frame.

stage3_observables

Stage 3: band-decompose dict-form e_ops and demodulate expectations post-solve.

stage4_problem

Stage 4: pack stage outputs + collapse operators into a frozen SolveProblem.