quchip.engine

Engine pipeline: Chip ResolvedFrame EngineResult 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.assembly and nowhere else.

Responsibilities

Public API

Functions

build_engine_result(chip, drive_ops, **kwargs)

Assemble a EngineResult for a resolved frame.

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

Resolve, assemble, and package a frozen SolveProblem.

build_steadystate_problem(chip, **kwargs)

Build a frozen static Lindblad steady-state request.

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.

steadystate(chip, **kwargs)

Solve a chip's unique static Lindblad steady state.

steadystate_batch(chip, *axes, **kwargs)

Solve static Lindblad steady states over parameter axes.

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, approximation=None)[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 -> energy level, e.g. {"q0": 1}) becomes a product state in the engine’s resolved local bases on both the joint and partitioned paths. An authored full-space ket is projected into that same solver space.

  • 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 always solve the full chip without partitioning.

  • approximation (Any | None)

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 RWA, 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", approximation=RWA())
>>> 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.steadystate(chip, **kwargs)[source]

Solve a chip’s unique static Lindblad steady state.

Parameters:
Return type:

Any

quchip.engine.steadystate_batch(chip, *axes, **kwargs)[source]

Solve static Lindblad steady states over parameter axes.

Parameters:
Return type:

Any

quchip.engine.build_problem(chip, drive_ops, tlist, *, solver=None, options=None, e_ops=None, initial_state=None, approximation=None)[source]

Resolve, assemble, and package 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.

  • approximation (Any | None)

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 RWA, 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", approximation=RWA())
>>> 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.build_steadystate_problem(chip, **kwargs)[source]

Build a frozen static Lindblad steady-state request.

Parameters:
Return type:

SteadyStateProblem

quchip.engine.solve_problem(problem, *, check_truncation=True, truncation_threshold=0.001)[source]

Dispatch a SolveProblem through its chip backend.

All single-solve paths call this function. Unless check_truncation=False, it screens the wrapped result for over-populated top Fock levels and warns above truncation_threshold.

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 or a flat list of SolveProblem objects. The batched path is 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_engine_result(chip, drive_ops, **kwargs)[source]

Assemble a EngineResult for a resolved frame.

Parameters:
Returns:

Static and dynamic terms plus dropped-term records.

Return type:

EngineResult

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

diagonal()[source]

Return the main diagonal without materializing a sparse matrix.

Return type:

Any

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 solve slot. 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.CollapseTerm(operator, rate, source, channel, parameter_paths=())[source]

Bases: object

Backend-neutral Lindblad operator and its separate rate.

Parameters:
operator: CanonicalOperator
rate: Any
source: str
channel: str
parameter_paths: tuple[str, ...] = ()
latex()[source]

Render this collapse channel as an opaque named operator.

Return type:

str

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. Assembly aggregates these records into EngineResult.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 assembly 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 (assembly 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', 'port'] = 'drive'
tag: str | None = None
class quchip.engine.EngineResult(static_terms, dynamic_terms, dims=(), metadata=<factory>, dropped_terms=(), collapse_terms=(), port_terms=(), bases=<factory>, authored=None, resolved_frame=None, approximation=None)[source]

Bases: object

Backend-agnostic time-dependent Hamiltonian passed to backends.

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, ...] = ()
collapse_terms: tuple[CollapseTerm, ...] = ()
port_terms: tuple[PortTerm, ...] = ()
bases: Mapping[str, Any]
authored: Any = None
resolved_frame: Any = None
approximation: Any = None
hamiltonian()[source]

Return the exact canonical Hamiltonian as an inspectable expression.

This view is derived from the same terms backends receive. Matrix leaves remain opaque, while each dynamic coefficient renders as a named function of time.

Return type:

PhysicsExpr

latex()[source]

Render the canonical Hamiltonian with named time functions.

Return type:

str

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.PortTerm(operator, rate, phase, frame_frequency, label, parameter_paths=())[source]

Bases: object

Resolved input-output channel before the sqrt(rate) scaling.

Parameters:
operator: CanonicalOperator
rate: Any
phase: Any
frame_frequency: Any
label: str
parameter_paths: tuple[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, problems, params=None, shape=(), axes=())[source]

Bases: object

Explicit solve problems sharing one dispatch owner and sweep shape.

Parameters:
chip: Any
problems: tuple[SolveProblem, ...]
params: Any = None
shape: tuple[int, ...] = ()
axes: tuple[tuple[str, Any], ...] = ()
property batch_size: int
property initial_states: tuple[Any, ...]
property tlist: Any
signals_for(slot)[source]

Return one dynamic slot across all batch points.

Parameters:

slot (int)

Return type:

tuple[ScalarModulation, …]

params_at(point)[source]

Return sweep values at one grid coordinate.

Parameters:

point (int | tuple[int, ...])

Return type:

dict[str, Any]

element(index)[source]
Parameters:

index (int)

Return type:

SolveProblem

class quchip.engine.SolveProblem(chip, engine_result, initial_state, tlist, 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 EngineResult (Hamiltonian and collapse terms), an initial_state, solver time grid, 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 observable reconstruction uses to recombine flattened band expectations back into dict-keyed observables.

Parameters:
chip: Any
engine_result: Any
initial_state: Any
tlist: 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.SteadyStateProblem(chip, engine_result, e_ops=None, e_ops_meta=None, resolved_frame=None, options=<factory>)[source]

Bases: object

Immutable static Lindblad request handed from a chip to its backend.

Parameters:
chip: Any
engine_result: EngineResult
e_ops: Any = None
e_ops_meta: Any = None
resolved_frame: Any = 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π during engine assembly; 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', 'port'] = 'device'
metadata: dict[str, Any]
class quchip.engine.BasisRecord(kind, vectors, energies, energy_vectors, native_dim, resolved_dim)[source]

Bases: object

One device’s fixed authored-to-solver transformation.

Parameters:
  • kind (Literal['native', 'eigen'])

  • vectors (Any)

  • energies (Any)

  • energy_vectors (Any)

  • native_dim (int)

  • resolved_dim (int)

kind: Literal['native', 'eigen']
vectors: Any
energies: Any
energy_vectors: Any
native_dim: int
resolved_dim: int
property projector: Any

Projector onto the retained authored subspace.

transform_operator(operator)[source]

Apply the recorded authored-to-solver transformation to an operator.

Parameters:

operator (Any)

Return type:

Any

level_operator()[source]

Return the energy-level index operator in the resolved solver basis.

Return type:

Any

Modules

approximations

Engine-side operations owned by explicit approximation strategies.

assembly

Assemble an EngineResult from chip, drive operations, and frame.

bands

Band decomposition by excitation-change weight.

basis

Local energy-basis resolution and explicit transformation records.

frames

Resolve a FrameSpec into a ResolvedFrame.

input_output

Engine-owned input-output assembly for stationary port calculations.

ir

IR types shared by engine responsibilities and backends.

observables

Band-decompose dict-form e_ops and demodulate expectations post-solve.

partitioned

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

problem

Package engine physics and solve inputs into frozen solve requests.

solver_hints

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

steady_state

Build and solve backend-neutral stationary Lindblad problems.