quchip.devices.base

Base device model for quchip.

A device is a finite local quantum system owned by a chip. Subclasses declare their Hamiltonian on an explicit authored LocalSpace; the engine may retain that basis or project it into local energy order.

Contract

  • Hamiltonian ownership. A device owns its local Hamiltonian only; couplings and drives own theirs. unresolved_hamiltonian() returns that authored operator; hamiltonian() returns the engine-resolved local view after basis and frame policies.

  • JAX traceability. Every parameter passed to a subclass’s __init__ (frequency, anharmonicity, T1/T2, thermal population, …) may be a JAX tracer. Validation routines must never force concretization on a traced value; use quchip.utils.jax_utils.maybe_concrete_scalar() to peek at concrete scalars only.

  • Approximation transparency. Each concrete model must document its approximation level and cite a reference. Examples: Resonator states “non-interacting harmonic mode”; DuffingTransmon states “Duffing expansion valid in the transmon regime E_J >> E_C”.

Channels offered to drives

Drives sit on top of a device and emit local Hamiltonians built from standard bosonic / projection operators the device exposes:

  • lowering_operator() (a) and raising_operator() (a_dag) — used by charge / coupling-type drives.

  • number_operator() (n_hat = a_dag @ a) — used by number-coupled (dispersive) drives.

  • sigma_x, sigma_y, sigma_z — the qubit subspace projections onto |0>, |1> (cached; invalidated automatically when levels changes).

State versioning

The engine caches assembled Hamiltonians keyed on state_version. Once construction finishes, every public mutation (anything not prefixed with _ and not label) increments _state_version so caches are invalidated deterministically. This machinery — the seed, the __setattr__ tracking hook, state_version, and _finish_init — is owned by the shared StateVersioned mixin; BaseDevice only contributes its untracked-name set (label) and the levels cache-invalidation hook (_on_attr_set()). Tracking switches on automatically exactly once after the outermost __init__ returns; subclasses do not call _finish_init directly.

Auto-labeling

Subclasses set _type_prefix (e.g. "duffing", "resonator") and a shared counter in quchip.utils.labeling yields labels like "duffing_0", "resonator_0". Reset between tests via quchip.utils.labeling.reset_label_counters().

Serialization

to_dict() writes a JSON-safe snapshot (type fully-qualified name, levels, label, concrete noise parameters). Deserialization dispatch to the registered concrete subclass is owned by the shared Registrable mixin — the registry is populated automatically at subclass-definition time, with no manual registration step.

Units (immutable)

  • Frequencies: GHz, ordinary (not angular).

  • Times: ns.

  • Temperature: mK.

  • Energies: GHz (with hbar = 1).

Example

>>> from quchip.devices import DuffingTransmon, Resonator
>>> from quchip.chip import Chip
>>> q = DuffingTransmon(freq=5.0, anharmonicity=-0.25, levels=3, label="q")
>>> r = Resonator(freq=7.0, levels=6, label="r")
>>> chip = Chip(devices=[q, r])
>>> float((q.freq * q.number_operator()).norm())

Classes

BaseDevice(levels[, label, T1, T2, ...])

Abstract truncated-Hilbert-space quantum device.

class quchip.devices.base.BaseDevice(levels, label=None, *, T1=None, T2=None, thermal_population=None)[source]

Bases: StateVersioned, Registrable, ABC

Abstract truncated-Hilbert-space quantum device.

Concrete subclasses must:

  1. Set _type_prefix (used for auto-labeling).

  2. Expose a freq attribute — the bare 0 -> 1 transition frequency in GHz. Any JAX-traceable scalar is fine.

  3. Implement unresolved_hamiltonian() returning an operator on the truncated Fock basis.

Noise parameters (all optional; None means the channel is absent):

  • T1 — relaxation time (ns); emission channel at rate 1/T1.

  • T2 — total 0-1 coherence time (ns, requires T2 <= 2*T1); adds pure dephasing at gamma_phi = 1/T2 - 1/(2*T1).

  • thermal_population — unitless bath occupation ; adds thermal absorption and enhances emission.

They are ordinary attributes: set them at construction or at any time after — the next simulate/solve rebuilds collapse operators from current values (no rebuild, no cache poking), and post-construction writes get the same validation as the constructor. Setting a parameter back to None removes its channel.

Mutation tracking is enabled automatically once construction finishes (see StateVersioned); subclasses do not call _finish_init themselves.

Optional overrides:

  • dissipation() — append channels beyond T1/T2.

  • to_dict() / from_dict() — for extra parameters.

  • computationalTrue if the device represents a computational qubit (default False).

See module docstring for the full contract.

Parameters:
tunable_param_names: ClassVar[tuple[str, ...]] = ()

Bare parameters this device exposes as differentiable / tunable scalars. fit_a_dress walks this tuple to discover what it is allowed to optimize on each device, decoupling the inverse-design surface from any specific device model. Three states, keyed on whether the value is explicitly declared:

  • No explicit declaration anywhere in the DeviceModel lineage — the default is derived: every declared parameter() field, in declaration order (see DeviceModel.__init_subclass__).

  • Explicit tuple on the class or an ancestor — exact curation, validated at class-definition time; authoritative and inherited until a subclass explicitly replaces it.

  • Explicit empty tuple — deliberately freezes the device (and its subclasses, until one replaces it) out of inverse design.

On a plain (non-DeviceModel) BaseDevice subclass there is no derivation; the default stays empty unless the subclass declares its own tuple — e.g. Fluxonium uses ("E_C", "E_J", "E_L", "phi_ext").

dressed_fit_target_fields: ClassVar[tuple[tuple[str, str], ...]] = ()

(dressed_observable, declared_field) pairs used when this device appears in the desired-chip form of fit_a_dress. Empty means that the model makes no automatic dressed-target claim; circuit-level models can remain fixed until the user supplies explicit constraints.

dressed_fit_param_names: ClassVar[tuple[str, ...]] = ()

Bare parameters normally varied to reproduce dressed_fit_target_fields. This remains separate from tunable_param_names: a model may expose parameters for sweeps without claiming that inverse design can identify all of them from its default dressed observables.

basis: Literal['native', 'eigen'] | None = None
projection_levels: int | None = None
requires_projection_levels: ClassVar[bool] = False
T1: Any = Parameter(default=None, positive=True, nonnegative=False, serialize=True, unit='ns', symbol=None, noise=True, kw_only=True, required=False)
T2: Any = Parameter(default=None, positive=True, nonnegative=False, serialize=True, unit='ns', symbol=None, noise=True, kw_only=True, required=False)
thermal_population: Any = Parameter(default=None, positive=False, nonnegative=True, serialize=True, unit=None, symbol=None, noise=True, kw_only=True, required=False)
tunable_params()[source]

Return {name: current_value} for every bare parameter the device exposes for fitting / sweeping.

The default implementation walks tunable_param_names and reads each attribute. Subclasses with derived bare parameters (e.g. circuit-level devices whose freq is computed from E_C/E_J/E_L) should override the class attribute rather than this method — overrides are the right hook only when the list itself is not static (e.g. flux-tunable devices that gain phi_ext only at certain operating points).

Return type:

dict[str, Any]

default_dressed_targets()[source]

Return declared numbers interpreted as dressed-fit targets.

This hook reads component fields and never diagonalizes a chip. A device class opts in by declaring dressed_fit_target_fields.

Return type:

dict[str, Any]

default_fit_parameters()[source]

Return the conservative bare-parameter selection for default targets.

Return type:

tuple[str, …]

set_tunable_param(name, value)[source]

Update a bare parameter named in tunable_params().

Default implementation uses setattr() so any direct attribute (freq, anharmonicity, E_C, …) works without ceremony. Subclasses with derived properties that need to back-propagate to private state should override this.

Parameters:
Return type:

None

tunable_param_bounds(name, value)[source]

Return (lower, upper) bounds for a tunable parameter at a seed value.

These bounds are consumed by the inverse-design optimizer to keep searches physical. The default uses well-named conventions that cover the common circuit-QED parameters:

  • freq, E_C, E_J, E_L: positive, [0.5·s, 1.5·s] around a positive seed (s).

  • anharmonicity: sign-preserving — negative seeds bound in (2·s, -ε), positive seeds in (ε, 2·s).

  • phi_ext: in [-0.5, 0.5] (one full flux period symmetric around the integer-flux point).

Subclasses override for parameters with other physical constraints. Raises ValueError for unknown names rather than silently optimizing over an unbounded axis, and for a value that is not a concrete real scalar (bounds for a JAX tracer are undefined — the optimizer needs a concrete numeric seed).

Parameters:
Return type:

tuple[float, float]

property connected_drives: list['BaseDrive']

Drives wired to this device, as a fresh list (mutation-safe copy).

copy()[source]

Structural copy detached from drive wiring (used by sweep cloning).

Return type:

BaseDevice

parameter_values()[source]

Return this device’s active bindable values by local field name.

Return type:

dict[str, Any]

set_parameter_value(name, value)[source]

Apply one validated local parameter value on an isolated device copy.

Parameters:
Return type:

None

set_parameter_values(values)[source]

Apply a group of local parameter values on an isolated device copy.

The default is equivalent to repeated set_parameter_value() calls. Devices with coupled parameter semantics may override this hook so the result does not depend on mapping order.

Parameters:

values (Mapping[str, Any])

Return type:

None

property dressed_freq: float | None

Chip-derived dressed 0→1 transition frequency in GHz, or None without a chip context.

property drive_freq: float

Operational 0→1 drive frequency in GHz.

When the device belongs to exactly one chip this is the chip-derived dressed frequency. Standalone devices fall back to their bare freq because no chip Hamiltonian exists to dress against. Returned values may be JAX tracers during traced / differentiated flows.

property reference_freq: Any

Readout / rotating-frame reference frequency in GHz — the device’s LO.

This is the frequency the default (frame="rotating") frame co-rotates at and the reference the readout is reported in: result.expect is expressed in this frame in every integration frame, and in the default rotating frame result.states are too. So transverse observables (<a>, <sigma_x>) come back as the slow demodulated envelope a lab readout produces — non-oscillatory when the device sits at its reference, and turning at omega - reference_freq when detuned (idle Ramsey). Diagonal observables (populations, <n>) are frame-invariant and unaffected either way.

Defaults to drive_freq (the dressed 0->1 frequency), so an unset device co-rotates at its own transition. Set it to model a control/LO reference that differs from the qubit frequency (a calibration detuning). It is a frame / readout reference only: it does not detune drives — the drive carrier is a separate choice, so a real LO error must also set the drive frequency. May be a JAX tracer in traced / differentiated / swept flows. Assign None to restore the default.

to_dict()[source]

JSON-safe serialization; subclasses extend with their own parameters.

Return type:

dict[str, Any]

abstractmethod unresolved_hamiltonian()[source]

Return the authored local Hamiltonian before engine policies.

Return type:

Any

hamiltonian()[source]

Return the local Hamiltonian after basis and frame policies.

Return type:

Any

resolve(*, frame=None)[source]

Resolve this device through the same engine path used by solves.

An owned device inherits its chip’s basis and frame policy unless frame overrides this snapshot. The local result remains a one-device snapshot; couplings to the rest of the chip are intentionally outside a device Hamiltonian’s boundary.

Parameters:

frame (FrameSpec | None)

Return type:

EngineResult

physics_notes()[source]

Return human-readable declarations of this device’s approximations.

Each entry names a non-obvious assumption, approximation, or truncation that a user of this device should be aware of — e.g. “Hilbert truncation: 3 levels”, a model regime (Duffing), or a noise-channel selection (charge- vs flux-coupled T1).

The baseline entry is _truncation_note(), since every BaseDevice has some form of Hilbert-space truncation. A pure-dephasing note is added when T2 is set, since the number-operator dephasing model carries non-obvious assumptions. Subclasses super().physics_notes() and append their own model-specific notes; no registry / engine-side dispatch is needed.

Return type:

list[str]

local_space()[source]

Return this device’s authored local operator space.

Return type:

LocalSpace

resolved_basis(chip_basis='native')[source]

Return the device override or inherited chip basis policy.

Parameters:

chip_basis (Literal['native', 'eigen'])

Return type:

Literal[‘native’, ‘eigen’]

resolved_dimension(chip_basis='native')[source]

Return the local dimension delivered to the solver.

Parameters:

chip_basis (Literal['native', 'eigen'])

Return type:

int

lowering_operator()[source]

Bosonic lowering operator a on the truncated Fock basis.

Return type:

Any

raising_operator()[source]

Bosonic raising operator a† on the truncated Fock basis.

Return type:

Any

number_operator()[source]

Number operator = a†a on the truncated Fock basis.

Return type:

Any

energy_level_operator()[source]

Return the energy-level index expressed in the authored local basis.

Return type:

Any

identity()[source]

Identity operator on the truncated Fock basis.

Return type:

Any

local_operator(name)[source]

Map an operator-name string to this device’s own local operator.

Recognized names: "X" / "Y" / "Z" (Pauli projections on the computational |0>, |1> subspace), "n" (number), "a" (lowering), "a_dag" (raising), "I" (identity). The device owns this vocabulary, so a subclass exposing extra named operators overrides this method — extending _LOCAL_OPERATOR_NAMES and delegating to super().local_operator(name) for the base set — and the chip’s observable surface (Chip.observable(), Chip.e_ops()) gains the operator without any engine or Chip change.

Parameters:

name (str)

Return type:

Any

basis_state(n)[source]

Fock basis state |n> on the truncated Hilbert space.

Parameters:

n (int)

Return type:

Any

coherent_state(alpha)[source]

Coherent state |alpha> on the truncated Fock basis.

Parameters:

alpha (complex)

Return type:

Any

plot_energy_levels(*, ax=None, **kwargs)[source]

Plot the device’s bare energy-level ladder (delegates to quchip.viz).

Parameters:
Return type:

Any

plot_wavefunction(n, *, ax=None, **kwargs)[source]

Plot the n-th eigenstate wavefunction (delegates to quchip.viz).

Parameters:
Return type:

Any

property sigma_x: Any

|0><1| + |1><0| on the computational |0>, |1> subspace.

property sigma_y: Any

-i|0><1| + i|1><0| on the computational |0>, |1> subspace.

property sigma_z: Any

|0><0| - |1><1| on the computational |0>, |1> subspace.

property sigma_plus: Any

|1><0|.

Type:

Raising operator on the computational |0>, |1> subspace

property sigma_minus: Any

|0><1|.

Type:

Lowering operator on the computational |0>, |1> subspace

projector(i, j)[source]

|i><j| on the authored local basis.

Use projector(i, i) for the population projector |i><i| and projector(i, j) for |i><j|. No subspace approximation: the operator acts on the full authored local space.

Parameters:
Return type:

Any

transition(lower, upper)[source]

Hermitian transition between isolated energy states.

The operator |lower><upper| + |upper><lower| is returned in the authored local basis.

Parameters:
Return type:

Any

transition_frequency(lower, upper)[source]

Return the isolated E_upper - E_lower transition in GHz.

Parameters:
Return type:

Any

property computational: bool

Whether this device is a computational qubit. Override in subclasses.

dissipation(op, p)[source]

Return the common T1, T2, and thermal device channels.

Parameters:
Return type:

tuple[CollapseChannel, …]

collapse_operators()[source]

Materialize the device’s authored Lindblad collapse operators.

The built-in channels cover T1, T2, and thermal_population; subclasses append channels in dissipation().

References

Breuer & Petruccione, Theory of Open Quantum Systems (Oxford, 2002), Ch. 3. For circuit-QED conventions see Krantz et al., Applied Physics Reviews 6, 021318 (2019), §V.

Return type:

list[Any]

collapse_channels(basis=None)[source]

Return normalized local collapse channels.

Parameters:

basis (BasisRecord | None)

Return type:

tuple[CollapseChannel, …]

classmethod noise_parameter_names()[source]

Declared fields that Chip.set_noise() may configure.

Return type:

tuple[str, …]

intrinsic_decay_rate()[source]

Total lowering-channel (downward) Lindblad rate, in 1/ns, or None with no decay channel.

Reports the actual sum of squared amplitudes of the lowering-operator collapse channel(s) collapse_operators() builds from the common thermal-emission construction exactly rather than approximating it:

  • T1 set (thermal_population set or not): (n̄+1)/T1 — the sqrt(gamma*(n̄+1))·a channel’s rate, gamma = 1/T1; defaults to 0 when thermal_population is unset, so this reduces to plain 1/T1.

  • T1 unset, thermal_population set: n̄+1 — the same channel with gamma = 1 (the unitless-bath-occupation branch).

  • Neither set: None — no lowering channel.

Subclasses whose collapse_operators() combine several lowering-operator channels (e.g. Resonator’s Q-derived photon loss alongside T1) override this to report the summed rate, so a caller reading a single scalar decay rate (e.g. quchip.chip.transformations.eliminate_device’s Purcell fold) does not have to special-case per-device channel structure.

This is the downward rate only — the sqrt(gamma*n̄)·a† upward (thermal-absorption) channel is not represented; a caller that needs to know whether that channel is present reads thermal_population directly. Whether a channel exists, and which formula applies, is a static decision (is T1/thermal_population set?), never a traced-zero comparison on the resulting rate, which would concretize a traced value and break differentiability.

Return type:

Any | None

connect(drive)[source]

Register a drive as connected: idempotent on identity, replace-on-relabel.

A drive’s label is its stable identity as a control line (chip.wire already rejects duplicate labels within one equipment). Clone-and-rewire flows — chip.clone(), eliminate()’s equipment reattachment, chip.partition() — build a fresh drive object bound to the same label when re-wiring a device that already carries a connected drive, so a same-label, different-object entry marks a stale copy of the same line rather than a second physical line. That stale entry is replaced in place (position preserved); a drive with a distinct label is always appended as an independent line.

Parameters:

drive (BaseDrive)

Return type:

None