quchip.control.sequence

Declarative pulse programming for a Chip.

QuantumSequence is the user-facing scheduling API. It tracks per-(device, drive) timing cursors, per-device virtual-Z phase frames, and cross-device barriers, then materializes the schedule into DriveOp objects for the engine.

Conventions

  • Times are ns, frequencies GHz (ordinary, not angular).

  • Virtual-Z shifts accumulate into later microwave pulses on the same device (charge and phase drives); baseband flux pulses are unaffected, matching the lab-frame semantics of software-Z (McKay et al., PRA 96, 022330 (2017)).

  • All sweep axes stay JAX-traceable: pulse parameters, delays, and envelope fields flow into SolveProblem without Python-side concretization.

Examples

>>> from quchip import (
...     DuffingTransmon, ChargeDrive, Chip, QuantumSequence, Gaussian
... )
>>> q = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3)
>>> drive = ChargeDrive(target=q)
>>> chip = Chip([q])
>>> seq = QuantumSequence(chip)
>>> _ = seq.schedule(q, envelope=Gaussian(duration=20.0, amplitude=0.05))
>>> seq.total_duration
20.0

Classes

QuantumSequence(chip)

Declarative pulse sequence builder for a Chip.

class quchip.control.sequence.QuantumSequence(chip)[source]

Bases: object

Declarative pulse sequence builder for a Chip.

Tracks per-(device, drive) timing cursors and per-device virtual-Z phase frames. Append pulses with schedule() (or the conveniences charge(), phase(), flux()); synchronize channels with barrier() and delay(). The schedule is materialized lazily into DriveOp objects by build_problem() and build_batch().

Wherever a device/drive is expected, either the object itself or its string label works, and examples should prefer object references.

Simulation runs through one consistent verb — simulate — across three tiers, from most ergonomic to most explicit:

  1. simulate() / simulate_batch() — schedule and solve in one call (the example-facing path).

  2. solve() / solve_many() — solve a SolveProblem / batch you already hold.

  3. The module-level simulate() / solve_problem() / solve_many() — the low-level “I already have drive_ops / a SolveProblem” tier.

Parameters:

chip (Chip) – Chip this sequence schedules against. Supplies the device and coupling maps used to resolve scheduling targets, the wired ControlEquipment lines, and the frame/backend settings used by build_problem() and simulate().

Examples

>>> from quchip import (
...     DuffingTransmon, ChargeDrive, Chip, QuantumSequence, Gaussian
... )
>>> q = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3)
>>> _ = ChargeDrive(target=q)
>>> chip = Chip([q])
>>> seq = QuantumSequence(chip)
>>> _ = seq.charge(q, envelope=Gaussian(duration=20.0, amplitude=0.05))
>>> seq.vz(q, angle=0.5)
>>> _ = seq.charge(q, envelope=Gaussian(duration=10.0, amplitude=0.03))
schedule(target, *, envelope, freq=None, start_time=None, phase=0.0)[source]

Schedule a pulse on target.

Parameters:
  • target (str | BaseDrive | BaseDevice | BaseCoupling) –

    Accepted forms, resolved in this order:

    • BaseDrive — scheduled directly on that drive (a ParametricDrive pumps its bound coupling).

    • BaseDevice — uses the device’s first connected drive; pass the drive object explicitly when a device has multiple drives.

    • BaseCoupling — uses the unique ParametricDrive line pumping that coupling; pass the drive object explicitly when a coupling has multiple pump lines.

    • str — a label, resolved in order: a device label; then, only when absent from the device map, a coupling label (the two label spaces are disjoint); then, only when absent from both, a control-equipment line label, scheduling directly on that line. This third fallback is what lets a caller schedule by a drive’s own label after its device or coupling target has been eliminated — see eliminate()’s retarget registry, which preserves a converted line’s label. A control-line label that collides with a device/coupling label is shadowed by the device/coupling resolution.

  • envelope (BaseEnvelope) – Pulse envelope.

  • freq (float, optional) – Carrier frequency in GHz for microwave drives. Defaults to the target device’s drive_freq for ChargeDrive; always required (no device-frequency fallback) for PhaseDrive and TwoPhotonDrive; ignored for FluxDrive and baseband ParametricDrive pumps.

  • start_time (float, optional) – Pulse start time in ns. Defaults to the current cursor; earlier times are rejected.

  • phase (float) – Per-pulse phase offset, composed with any accumulated virtual-Z.

Return type:

PulseHandle

charge(target, *, envelope, freq=None, phase=0.0)[source]

Schedule a ChargeDrive pulse; freq defaults to device.drive_freq.

Parameters:
Return type:

PulseHandle

phase(target, *, envelope, freq, phase=0.0)[source]

Schedule a PhaseDrive pulse; freq is required.

Parameters:
Return type:

PulseHandle

flux(target, *, envelope)[source]

Schedule a FluxDrive pulse (no carrier frequency).

Parameters:
Return type:

PulseHandle

pump(coupling, *, envelope, freq=None, start_time=None, phase=0.0)[source]

Schedule an edge pump: baseband δ(t)=A(t) when freq is None, else A(t)·cos(2πft-φ).

Parameters:
Return type:

PulseHandle

flux_to(target, *, target_freq, envelope)[source]

Schedule a flux-drive frequency-shift pulse to target_freq.

This is not an inverse-SQUID flux calibration. It computes δω = target_freq chip.freq(device) and schedules a FluxDrive pulse whose envelope amplitude is that frequency shift in GHz. The resulting Hamiltonian contribution is the linear detuning term δω(t) . envelope should be passed with amplitude=None (or any placeholder) — this method replaces the amplitude with the computed δω.

target_freq may be a JAX tracer (e.g. chip.freq(other_device)).

Parameters:
  • target (str | BaseDevice) – Device to flux-pulse.

  • target_freq (float) – Target 0 1 frequency in GHz.

  • envelope (BaseEnvelope) – Envelope template with all timing parameters set. Its amplitude attribute is replaced by the computed δω; any value passed as amplitude is ignored.

Returns:

Handle to the scheduled entry, usable with .vary().

Return type:

PulseHandle

vz(target, angle)[source]

Apply a virtual-Z frame shift of angle rad on target.

The shift is free (no pulse is emitted) and accumulates into every subsequent microwave pulse on the device via its phase_offset. This is the standard software-Z trick for transmons (McKay et al., PRA 96, 022330 (2017)). Baseband flux pulses are unaffected.

Parameters:
Return type:

None

delay(scope, duration)[source]

Insert an idle delay on all drive channels of scope.

Parameters:
Return type:

DelayHandle

barrier(*labels)[source]

Synchronize channel cursors.

With no arguments, every (device, drive) cursor on the chip is advanced to the current maximum. With explicit device targets, only those devices’ cursors are aligned. Use this to guarantee that pulses scheduled after the barrier start no earlier than any pulse scheduled before it.

Parameters:

labels (str | BaseDevice)

Return type:

None

property total_duration: Any

Total sequence duration in ns (maximum across all cursors).

property scheduled_ops: tuple[DriveOp, ...]

Materialize and return the scheduled DriveOp tuple.

property channel_cursors: dict[tuple[str, str], Any]

Current time cursors keyed by (target_label, drive_label).

build_problem(tlist=None, solver=None, options=None, e_ops=None, initial_state=None)[source]

Build a single SolveProblem from this sequence.

Parameters:
  • tlist (array-like, optional) – Save/output time grid in ns. Defaults to the grid built by _resolve_tlist() from total_duration (10 points/ns, 100-point floor).

  • solver (str, optional) – Backend solver name. Defaults to the backend’s own default solver when omitted.

  • options (dict, optional) – Solver options, merged on top of the defaults {"store_states": True, "store_final_state": True}.

  • e_ops (dict, optional) – Expectation operators keyed by device label (or a 2-tuple of device labels for a two-body operator), mapping to a local operator (or a pair of local operators). Decomposed into per-band terms before reaching the solver.

  • initial_state (Any, optional) – A ket, density matrix, or mapping for state(). Defaults to the chip’s default initial state when omitted.

Returns:

Frozen problem — chip, compiled Hamiltonian description, initial state, tlist, collapse operators, and expectation operators — ready for solve() or solve_problem().

Return type:

SolveProblem

vary(field, values, *, name=None)[source]

Create a BatchAxis over a sequence-level field.

Parameters:
  • field (str) – Sequence-level field to sweep. Only "initial_state" is supported; per-pulse fields (freq, phase, amplitude, …) are swept via PulseHandle.vary() on the handle returned by schedule() instead.

  • values (array-like) – Sequence of values for field, one per batch point.

  • name (str, optional) – Axis name recorded in ProblemBatch.axes and ProblemBatch.params_at(). Defaults to field.

Returns:

Sequence-level axis, consumable by build_batch() or zip().

Return type:

BatchAxis

zip(*axes)[source]

Zip axes into one pairwise dimension.

Parameters:

*axes (BatchAxis) – Two or more axes to zip, each created by this sequence (vary(), PulseHandle.vary(), or DelayHandle.vary()). Every axis must have the same length.

Returns:

Combined axis that build_batch() treats as a single dimension: point i supplies point i from every zipped axis simultaneously, rather than the outer product.

Return type:

ZippedBatchAxis

build_batch(*axes, tlist=None, solver=None, options=None, e_ops=None, initial_state=None)[source]

Build a batched solve request from sweep axes without solving.

The returned ProblemBatch wraps a SolveBatch: shared static/dynamic operators are collected exactly once and only per-element scalar modulations (and per-element initial states, when varied) change across the batch.

Parameters:
Return type:

ProblemBatch

simulate(tlist=None, solver=None, options=None, e_ops=None, initial_state=None, *, backend=None, check_truncation=True, truncation_threshold=0.001, partition=True)[source]

Build and solve a single simulation, routed through simulate().

backend — name ("qutip"/"dynamiqs") or instance — scopes this one call, outranking the chip’s and the process default (so a gradient solve can run on dynamiqs while everything around it stays on QuTiP). Foreign-native initial states are coerced at the solve boundary. Caveat: Python evaluates arguments before the scope opens, so an initial_state=chip.state(...) expression inline in the call is built under the surrounding backend — fine for concrete chips (the state is coerced), but a chip carrying JAX tracers needs a JAX-capable surrounding backend (chip-level or process default) for that construction itself.

Inherits the Hilbert-truncation safety net from solve_problem(); pass check_truncation=False to opt out or retune truncation_threshold.

partition, default True, forwards to simulate(): when the chip splits into independent sub-chips (Chip.partition()), each component is dispatched separately and combined into a PartitionedSimulationResult. This only engages when initial_state is None or a Mapping — a string shorthand or a concrete state is resolved via _resolve_initial_state_spec() first and always takes the joint path. Pass partition=False to force the joint solve unconditionally.

Parameters:
  • tlist (Any | None)

  • solver (str | None)

  • options (dict | None)

  • e_ops (dict | None)

  • initial_state (Any | None)

  • backend (Any | None)

  • check_truncation (bool)

  • truncation_threshold (float)

  • partition (bool)

Return type:

SimulationResult

simulate_batch(*axes, tlist=None, solver=None, options=None, e_ops=None, initial_state=None, backend=None, progress=True, check_truncation=True, truncation_threshold=0.001)[source]

Build and solve a batched sweep. Equivalent to chip.solve_many(seq.build_batch(...)).

backend scopes this one call exactly as in simulate().

The batched solve does not pass through solve_problem(), so the Hilbert-truncation safety net is applied per batch element here (default on); pass check_truncation=False to opt out or retune truncation_threshold.

Parameters:
Return type:

SimulationBatchResult

active_patch(*, hops=1, method='sw')[source]

Reduce the chip to this schedule’s active patch (spectators eliminated).

Convenience for quchip.chip.transformations.active_patch(); see it for the activity rule, elimination order, and validity reporting.

Parameters:
Return type:

ActivePatchResult

clone()[source]

Return a deep copy.

_entries is the single source of truth, so only it is deep-copied; the chip is shared, matching the previous shallow-copy behavior.

Return type:

QuantumSequence

describe()[source]

Plain-text timeline of the scheduled pulses.

One row per pulse — resolved time window, drive → device, envelope with its declared parameters, carrier frequency — plus a trailing count of delays, barriers, and virtual-Z entries. Returns a string; print(seq.describe()).

Return type:

str