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¶
Stage 1 (
quchip.engine.stage1_frames) — resolve aFrameSpecintoResolvedFrame(per-device frame frequencies, demodulation frequencies, and the frame mode).Stage 2 (
quchip.engine.stage2_assembly) — assemble aHamiltonianDescriptionwith static terms, dynamic terms, and theirScalarModulationsignal programs. Applies 2π, rotating-frame subtraction, RWA band decomposition (Jaynes & Cummings 1963; Gambetta et al., PRA 74, 042318 (2006)).Stage 3 (
quchip.engine.stage3_observables) — decompose dict-forme_opsinto solver-ready bands; post-solve, demodulate expectations back into the lab/control frame.Stage 4 (
quchip.engine.stage4_problem) — pack everything (including collapse operators) into a frozenSolveProblemorSolveBatch.
Public API¶
simulate()— full pipeline + solve + wrap result.build_problem()— stages 1-4, returns aSolveProblem.solve_problem()— dispatch aSolveProblemthrough the chip’s backend.solve_batch()/solve_many()— batched dispatch.
Functions
|
Build a |
|
Run stages 1-4 and return a frozen |
|
Build a |
|
Dispatch a |
|
Batch-dispatch typed solve requests that share one chip configuration. |
|
Dispatch a |
- 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().solveris"sesolve"or"mesolve";Noneauto-selectsmesolvewhen collapse operators exist.e_opsis dict-form, keyed by device label (or a 2-tuple of labels for two-body observables), and favors object references viaresolve_label(). The Hilbert-truncation safety net is inherited fromsolve_problem();check_truncation/truncation_thresholdare 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;
Noneauto-selectsmesolvewhen collapse operators are present, elsesesolve.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.
Nonedefaults to the chip ground state. AMapping(device label/object -> Fock index, e.g.{"q0": 1}) resolves throughChip.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 aPartitionedSimulationResultinstead of solving the full tensor-product space. Declines back to the joint solve (returning a plainSimulationResult) when the partition is trivial orinitial_stateis a raw backend state rather thanNone/aMapping. SetFalseto force the joint solve unconditionally.simulate_batch/solve_manynever partition in v1 — batched dispatch always solves the full chip.
- Returns:
The wrapped solver output.
- Return type:
- Raises:
ValueError – If
solveris neither"sesolve"nor"mesolve", iftlistis not one-dimensional, finite, strictly increasing, and at least two points long, or if anydrive_opsentry’s pulse window does not overlaptlistwith positive measure (both concrete-only checks; seebuild_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 withsolve_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;
Noneauto-selectsmesolvewhen collapse operators are present, elsesesolve.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;
Nonedefaults to the chip ground state.
- Returns:
The frozen request handed to a backend.
- Return type:
- Raises:
ValueError – If
tlistis not one-dimensional, finite, strictly increasing, and at least two points long, or if anydrive_opsentry’s pulse window[start_time, start_time + envelope.duration]does not overlaptlistwith 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
SolveProblemthrough 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 abovetruncation_threshold). Every example-facing single-solve path (chip.solve,seq.simulate) inherits the check by routing through here.- Parameters:
problem (SolveProblem)
check_truncation (bool)
truncation_threshold (float)
- Return type:
- quchip.engine.solve_many(batch_or_problems, *, progress=True)[source]¶
Batch-dispatch typed solve requests that share one chip configuration.
Accepts a
SolveBatch, aProblemBatch, or a flat list ofSolveProblemobjects. The batched paths are preferred: backends convert shared operators exactly once and stitch per-element coefficients into one parallel solve.- Parameters:
batch_or_problems (ProblemBatch | SolveBatch | list[SolveProblem])
progress (bool)
- Return type:
- quchip.engine.solve_batch(batch, *, progress=True)[source]¶
Dispatch a
SolveBatchthrough its chip backend.The backend converts each shared operator exactly once and stitches per-element coefficient data before running the parallel solve.
- Parameters:
batch (SolveBatch)
progress (bool)
- Return type:
- quchip.engine.build_hamiltonian_description(chip, drive_ops, **kwargs)[source]¶
Build a
HamiltonianDescription(stages 1-2 only).- Parameters:
chip (Chip) – The chip whose device, coupling, and drive Hamiltonians are assembled.
drive_ops (list of DriveOp) – Scheduled drive operations to embed as dynamic terms.
**kwargs – Forwarded to
quchip.engine.stage2_assembly.build_hamiltonian_description()(notablyresolved_frame).
- Returns:
Static and dynamic terms plus dropped-term records.
- Return type:
- 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:
objectBatched Hamiltonian IR that splits shared vs per-element structure.
Describes
NHamiltonians with identical operator skeletons (static_termsand per-slot dynamic operators) but independent per-slotScalarModulationsignal programs. Backends convert the shared operators once and stitch per-element coefficient data into one prepared batch.dynamic_signalsis indexed[slot][element]; on sweep axes that do not touch signals every entry on a slot is identity-equal.dropped_terms_by_elementis indexed[element]: which bands are dropped is template-structural and shared across the batch, but each record’sfrequencydepends on that element’s drive frequency, so the records themselves are stored per element and restored on the matchingelement()call.- Parameters:
batch_size (int)
static_terms (tuple[StaticTerm, ...])
dynamic_operators (tuple[CanonicalOperator, ...])
dynamic_origins (tuple[Literal['device', 'coupling', 'drive', 'crosstalk', 'flux'], ...])
dynamic_signals (tuple[tuple[ScalarModulation, ...], ...])
dropped_terms_by_element (tuple[tuple[DroppedTerm, ...], ...])
- static_terms: tuple[StaticTerm, ...]¶
- dynamic_operators: tuple[CanonicalOperator, ...]¶
- dynamic_signals: tuple[tuple[ScalarModulation, ...], ...]¶
- dropped_terms_by_element: tuple[tuple[DroppedTerm, ...], ...] = ()¶
- class quchip.engine.CanonicalOperator(layout, values, shape, dims, basis, subsystem_labels, indices=None, indptr=None, offsets=None, tag=None)[source]¶
Bases:
objectBackend-free operator with explicit dense/CSR/DIA payload and subsystem metadata.
For
densethe payload is the full 2D matrix; forcsrit is the 1D nonzero value array paired withindices/indptr; fordiait is a 2D(n_diags, n_cols)array paired withoffsets.dimsmust multiply toshape[0]andsubsystem_labelsnames each subsystem.- Parameters:
- classmethod from_csr(values, indices, indptr, *, shape, dims, basis, subsystem_labels, tag=None)[source]¶
- with_metadata(*, dims=None, basis=None, subsystem_labels=None, tag=None)[source]¶
Return a metadata-adjusted copy (payload unchanged).
- 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 innp.asarray(..., dtype=complex)themselves.- Return type:
- 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.jitthe payload is a tracer (possibly hidden inside a backend qarray wrapper, e.g. dynamiqsSparseDIAQArray);contains_tracer()detects that and the key falls back to layout + shape/dtype structure only, sotobytes()is never called on a tracer and two equivalent traced operators in different batch slots still produce identical keys.- Return type:
- class quchip.engine.Carrier(freq, sign=-1)[source]¶
Bases:
SignalNodeOscillating carrier
exp(sign · i · freq · t).freqis in angular units (rad/ns). The defaultsign = -1matches the convention used in rotating-frame decompositions (Scully & Zubairy, Quantum Optics, §5), where a raising-type band on a+Δdetuning rotates asexp(−iΔt). Both fields are registered as pytree children (freqmay be traced;signis semantically a static±1— do not map over it).- bands()[source]¶
Return the single
sign·freqband 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:
objectAdvisory 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_termsso 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.
amplitudeandfrequencyhold raw numeric values in GHz ordinary frequency — possibly JAX-traced; they are never formatted or branched on during assembly.band_weightsis static structure (excitation-change weights, one per mode the operator acts on) that stage 2 uses to resolvefrequencyfrom 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)fora·b).Nonewhen not applicable.amplitude (Any | None) – Static prefactor of the dropped term in GHz (e.g. the coupling
g); possibly traced.Nonewhen 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.
Noneuntil resolved (stage 2 fills it from the frame andband_weights).
- class quchip.engine.DynamicTerm(operator, time_dependence, origin='drive', tag=None)[source]¶
Bases:
objectTime-dependent Hamiltonian contribution
operator · f(t).f(t)is wrapped inScalarModulation, which each backend lowers into its native coefficient representation (QuTiP callback, dynamiqs sampled array, etc.). Theoperatoris 2π-scaled already (see module docstring).tagis an optional human label; it does not participate in physics.- Parameters:
operator (CanonicalOperator)
time_dependence (ScalarModulation)
origin (Literal['device', 'coupling', 'drive', 'crosstalk', 'flux'])
tag (str | None)
- operator: CanonicalOperator¶
- time_dependence: ScalarModulation¶
- class quchip.engine.HamiltonianDescription(static_terms, dynamic_terms, dims=(), metadata=<factory>, dropped_terms=())[source]¶
Bases:
objectBackend-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 aScalarModulationover aSignalProgramAST.metadatacarries 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_termsrecords 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, ...])
dropped_terms (tuple[DroppedTerm, ...])
- static_terms: tuple[StaticTerm, ...]¶
- dynamic_terms: tuple[DynamicTerm, ...]¶
- dropped_terms: tuple[DroppedTerm, ...] = ()¶
- dropped_terms_summary()[source]¶
Format
dropped_termsas a multi-line human-readable string.Traced
amplitude/frequencyvalues print astracedrather than being concretized.- Return type:
- class quchip.engine.ScalarModulation(signal)[source]¶
Bases:
objectTyped wrapper marking a
SignalProgramas a scalar modulation on aDynamicTerm.- 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:
objectBatched counterpart to
SolveProblem.Bundles one
BatchedHamiltonianDescriptionplus shared solver metadata and per-element initial states. Backends solve the N elements in one native batched call (vmapon dynamiqs; shared-operator + stitched coefficient arrays on QuTiP).- Parameters:
- hamiltonian: BatchedHamiltonianDescription¶
- element(index)[source]¶
Reconstruct the single-element
SolveProblemat index.- Parameters:
index (int)
- Return type:
- 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:
objectImmutable simulation request handed from the chip pipeline to a backend.
Bundles the stage-2
HamiltonianDescription, aninitial_state, solver time grid, collapse operators, decomposede_ops+ theirBandMeta, theResolvedFrame, and solver options.chipowns backend selection, sooptionsmust not contain a"backend"key (enforced in__post_init__).e_ops_metais the metadata stage 3 uses to recombine flattened band expectations back into dict-keyed observables.- Parameters:
- class quchip.engine.StaticTerm(operator, coefficient=1.0, origin='device', metadata=<factory>)[source]¶
Bases:
objectTime-independent Hamiltonian contribution.
The
operatorpayload has already been scaled by 2π at the stage-2 boundary; backends must not re-apply it.coefficientmultipliesoperatorand may be a concrete scalar or a JAX tracer (sweeps over static couplings, detunings, etc.).originis purely advisory metadata.- Parameters:
- operator: CanonicalOperator¶
Modules
Band decomposition by excitation-change weight. |
|
IR types flowing between engine stages. |
|
Partition-aware dispatch for |
|
Advisory solver-hint heuristics (post-assembly metadata only). |
|
Stage 1: resolve a |
|
Stage 2: assemble a |
|
Stage 3: band-decompose dict-form |
|
Stage 4: pack stage outputs + collapse operators into a frozen |