quchip.engine.ir¶
IR types flowing between engine stages.
This module is the contract between the engine and its backends. It defines four families of immutable, JAX-pytree-friendly types:
Signal Program AST — subclasses of
SignalNode(Constant,EnvelopeRef,Window,Shift,Scale,PolarScale,Add,Multiply,Conjugate,RealPart,Carrier). A pure functional description of a time-dependent scalar coefficientf(t) : ℝ → ℂ. Every leaf that a user may sweep (envelope parameters, amplitudes, phases, carrier frequencies) is a pytree leaf so the whole program is differentiable through JAX.CanonicalOperator— backend-free operator storage in dense / CSR / DIA layouts plus subsystem metadata. Backends convert to and from this format.Hamiltonian terms —
StaticTerm,DynamicTerm, and the per-stage containerHamiltonianDescriptionplus the batchedBatchedHamiltonianDescription.Solve requests —
SolveProblemandSolveBatch, the frozen hand-offs to backends.backendselection is chip-owned and is explicitly forbidden fromoptions.
A note on 2π: every operator here has already been scaled by 2π at the stage-2 boundary. Carrier frequencies are stored in angular units (rad/ns). IR consumers (backends, analyses) must not re-apply 2π.
Functions
|
Normalize a user-supplied modulation input to a |
|
Rewrite signal into |
|
Evaluate a signal program at time(s) t (ns); xp defaults to NumPy. |
|
Return the |
|
Recursively simplify a signal program by canceling exact opposing carrier pairs. |
Classes
|
|
|
Batched Hamiltonian IR that splits shared vs per-element structure. |
|
Backend-free operator with explicit dense/CSR/DIA payload and subsystem metadata. |
|
Oscillating carrier |
|
One band of a carrier-normalized signal: |
|
|
|
|
|
Drive operation scheduled on a device or a modulable coupling. |
|
Advisory record for a Hamiltonian term elided by an approximation. |
|
Time-dependent Hamiltonian contribution |
|
Reference to a pulse envelope; its |
|
Backend-agnostic time-dependent Hamiltonian — the stage-2 / backend contract. |
|
Chip-topology-invariant Hamiltonian skeleton. |
|
|
|
Scale child by |
|
|
|
Per-device frame information produced by stage 1. |
|
Typed wrapper marking a |
|
Multiply child by a complex scalar |
|
Time-shift child by |
Base class for signal-program AST nodes. |
|
|
Batched counterpart to |
|
Immutable simulation request handed from the chip pipeline to a backend. |
|
Time-independent Hamiltonian contribution. |
|
Gate child to |
- class quchip.engine.ir.SignalNode[source]¶
Bases:
objectBase class for signal-program AST nodes.
A node describes a time-dependent scalar
f(t) : ℝ → ℂ. Subclasses:are
@dataclass(frozen=True); every dataclass field is a JAX pytree child (registration happens automatically on subclass definition), so any field a user may sweep is differentiable;name the fields that hold child nodes (or tuples of child nodes) in
_signal_child_fields, which powers generic traversal (signal_children()) and rewriting (rebuild_children());implement
evaluate()— the node’s pointwise semantics;override
bands()when (and only when) the node interacts withCarrierleaves: the default treats any carrier-free subtree as a single zero-frequency band, which is exact for every envelope-like node.
- signal_children()[source]¶
Return this node’s child nodes (flattening tuple-valued fields).
- Return type:
tuple[SignalNode, …]
- rebuild_children(transform)[source]¶
Reconstruct this node with transform applied to each child.
Non-child fields are preserved; nodes without children pass through untouched.
- Parameters:
transform (Any)
- Return type:
- bands()[source]¶
Rewrite this subtree into carrier-normalized bands.
Default: a carrier-free subtree is exactly one zero-frequency band whose envelope is the subtree itself. Nodes whose subtrees may contain
Carrierleaves must override this with their carrier algebra (seedecompose_carrier_bands()).- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.Constant(value: 'complex')[source]¶
Bases:
SignalNode- Parameters:
value (complex)
- bands()[source]¶
Return the single zero-frequency band carrying this constant.
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.EnvelopeRef(envelope)[source]¶
Bases:
SignalNodeReference to a pulse envelope; its
waveform(t)is called at evaluation time.- Parameters:
envelope (BaseEnvelope)
- envelope: BaseEnvelope¶
- bands()[source]¶
Return the single zero-frequency band carrying this envelope.
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.Window(child, start, stop)[source]¶
Bases:
SignalNodeGate child to
[start, stop]; zero outside.- Parameters:
child (SignalNode)
start (float)
stop (float)
- child: SignalNode¶
- bands()[source]¶
Return the child bands with the same time gate applied to each envelope.
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.Shift(child, delta_t)[source]¶
Bases:
SignalNodeTime-shift child by
delta_t:child(t - delta_t).- Parameters:
child (SignalNode)
delta_t (float)
- child: SignalNode¶
- bands()[source]¶
Return the child bands, each carrying the shift’s carrier phase.
A time shift distributes over bands and contributes the constant carrier phase
exp(-i·freq·Δt)per band.- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.Scale(child, factor)[source]¶
Bases:
SignalNodeMultiply child by a complex scalar
factor.- Parameters:
child (SignalNode)
factor (complex)
- child: SignalNode¶
- bands()[source]¶
Return the child bands with
factorfolded into each envelope.- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.PolarScale(child, amplitude, theta)[source]¶
Bases:
SignalNodeScale child by
amplitude * exp(i * theta)(both are pytree leaves).- Parameters:
child (SignalNode)
amplitude (float)
theta (float)
- child: SignalNode¶
- bands()[source]¶
Return the child bands with the polar scale folded into each envelope.
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.Add(children: 'tuple[SignalNode, ...]')[source]¶
Bases:
SignalNode- Parameters:
children (tuple[SignalNode, ...])
- children: tuple[SignalNode, ...]¶
- bands()[source]¶
Return the concatenation of every child’s bands.
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.Multiply(children: 'tuple[SignalNode, ...]')[source]¶
Bases:
SignalNode- Parameters:
children (tuple[SignalNode, ...])
- children: tuple[SignalNode, ...]¶
- bands()[source]¶
Return the frequency convolution: the Cartesian product of child bands.
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.Conjugate(child: 'SignalNode')[source]¶
Bases:
SignalNode- Parameters:
child (SignalNode)
- child: SignalNode¶
- bands()[source]¶
Return the child bands with each envelope conjugated and its frequency negated.
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.RealPart(child: 'SignalNode')[source]¶
Bases:
SignalNode- Parameters:
child (SignalNode)
- child: SignalNode¶
- bands()[source]¶
Return each band split into
±freqhalves viaRe z = (z + z̄) / 2.- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.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, …]
- quchip.engine.ir.SignalProgram¶
alias of
SignalNode
- class quchip.engine.ir.ScalarModulation(signal)[source]¶
Bases:
objectTyped wrapper marking a
SignalProgramas a scalar modulation on aDynamicTerm.- Parameters:
signal (SignalNode)
- signal: SignalNode¶
- quchip.engine.ir.as_scalar_modulation(modulation, *, owner)[source]¶
Normalize a user-supplied modulation input to a
ScalarModulation.Accepts a
BaseEnvelope(wrapped asScalarModulation(EnvelopeRef(env))) or an existingScalarModulation. Shared by tunable devices and couplings so the coercion rules live in one place.- Parameters:
- Return type:
- quchip.engine.ir.signal_children(node)[source]¶
Return the
SignalProgramchild nodes of node.Dispatches to
SignalNode.signal_children(); aScalarModulationwrapper contributes itssignal.EnvelopeRef.envelopeis aBaseEnvelope, not aSignalProgramchild, and so is not returned here.
- quchip.engine.ir.evaluate_signal_program(signal, t, *, xp=None)[source]¶
Evaluate a signal program at time(s) t (ns); xp defaults to NumPy.
- Parameters:
signal (SignalNode)
t (Any)
xp (Any | None)
- Return type:
- quchip.engine.ir.simplify_signal(signal)[source]¶
Recursively simplify a signal program by canceling exact opposing carrier pairs.
- Parameters:
signal (SignalNode)
- Return type:
- class quchip.engine.ir.CarrierBand(envelope, freq)[source]¶
Bases:
objectOne band of a carrier-normalized signal:
envelope(t) · exp(i · freq · t).decompose_carrier_bands()rewrites anySignalPrograminto a sum of these bands, whereenvelopeis guaranteed carrier-free (noCarrierleaves) and therefore slow, andfreqis the angular band frequency (rad/ns, sign folded in, JAX-traceable). Backends use this to keep the fast oscillation analytic while sampling only the slow envelope — exact regardless of how resonant the carrier is, unlike pre-sampling the whole product.- Parameters:
envelope (SignalNode)
freq (Any)
- envelope: SignalNode¶
- quchip.engine.ir.decompose_carrier_bands(signal)[source]¶
Rewrite signal into
Σ_k envelope_k(t) · exp(i · freq_k · t)with carrier-free envelopes.This is the scalar-coefficient analogue of the operator band decomposition in
quchip.engine.bands: everyCarrierleaf is pulled out into a band frequency, leaving a slow, carrier-freeenvelopeper band. The rewrite is exact and follows the carrier algebra, implemented node-locally in eachSignalNode.bands():Carrier(freq, sign)→ one band(1, sign·freq).Conjugate→ conjugate the envelope, flip the band frequency.RealPart→ split each band into±freq(Re z = (z+z̄)/2).Multiply→ frequency convolution (Cartesian product of bands).Add→ concatenate bands.Scale/PolarScale/Window/Shift→ distribute over bands (Shiftalso contributes the constant phaseexp(-i·freq·Δt)).
All frequency arithmetic stays in JAX-traceable terms (no
float(), no branching on traced values).- Parameters:
signal (SignalNode)
- Return type:
tuple[CarrierBand, …]
- class quchip.engine.ir.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.ir.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¶
- class quchip.engine.ir.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.ir.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.ir.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.ir.HamiltonianTemplate(resolved_frame, dims, static_terms=(), invariant_dynamic_terms=(), drive_terms=(), reference_drive_ops=(), dropped_terms=(), weight_zero_drops=(), static_spectral_bound_ghz=None)[source]¶
Bases:
objectChip-topology-invariant Hamiltonian skeleton.
Contains:
static_terms— already assembledH₀and any static (same-frame) coupling folds.invariant_dynamic_terms— dynamic terms whose signal programs do not depend on drive variants (e.g. band-decomposed couplings), already simplified at template-compile time.drive_terms— pre-embedded, 2π-scaled drive bands (CompiledDriveTerm) ready for per-variant reinstantiation.reference_drive_ops— the structural yardstick used byinstantiate_hamiltonian_description()to reject drive-ops that change the template’s skeleton (device, drive, envelope type, or drive type).
Sweep leaves (envelope parameters, drive frequencies, phases, frame scalars) are not in the template; they rebuild on every instantiation.
- Parameters:
- weight_zero_drops: tuple[Any, ...] = ()¶
SINGLE_TONE weight-0 bands dropped structurally under RWA at compile time (
_compile_drive_terms()). The drop decision needs no drive frequency; resolving each entry into aDroppedTermdoes, so this stays a pointer (tuple[stage2._StructuralDrop, ...]) until instantiation.
- static_spectral_bound_ghz: float | None = None¶
Advisory spectral-bound hint (ordinary GHz) for the static terms. Computed once at template compile — the static terms are invariant across a sweep, so re-materializing their dense diagonal on every instantiation is wasted work.
Nonewhen empty, oversized, or not fully concrete (a traced coefficient stays dynamic). Only the variant-specific carrier-frequency hint is recomputed per instantiation.
- class quchip.engine.ir.ResolvedFrame(frequencies, demod_freqs, mode)[source]¶
Bases:
objectPer-device frame information produced by stage 1.
Describes the rotating-frame transformation applied uniformly to the chip:
frequencies[label]— the per-device integration-frame frequencyω_framein GHz. The static Hamiltonian gets the counter-term−Σᵢ ω_frame,ᵢ nᵢ.demod_freqs[label] = reference_freq − ω_frame— the demodulation frequency used post-solve by stage 3 to rotate expectations back into the user’s control frame.reference_freqis the device attribute (seereference_freq); it merely defaults to the dressed drive frequency when not set explicitly.mode— one of"lab"/"rotating"/"float"/"dict".
- class quchip.engine.ir.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.ir.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.ir.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.ir.DriveOp(target_label, envelope, freq=None, start_time=0.0, phase_offset=0.0, drive_label='')[source]¶
Bases:
objectDrive operation scheduled on a device or a modulable coupling.
freqis in GHz;Noneselects flux drive (or baseband edge pump).start_timeandphase_offsetapply in the control frame.drive_labelresolves the drive in the chip’s control equipment (e.g."charge_0").target_labelresolves in the chip’s device or coupling label space.The pulse window
[start_time, start_time + envelope.duration]must overlap the solvetlistwith positive measure — a window that only touches atlistendpoint contributes no evolution and is rejected (prepare_solve_problem_context()).- Parameters:
- envelope: BaseEnvelope¶