quchip.devices.base¶
Base device model for quchip.
A device is a truncated-Hilbert-space quantum system owned by a chip.
Subclasses declare their local Hamiltonian on a Fock basis of size
levels.
Contract¶
Hamiltonian ownership. A device owns its local Hamiltonian only; couplings and drives own theirs.
hamiltonian()must return an operator acting on this device’s truncated Hilbert space.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; usequchip.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:
Resonatorstates “non-interacting harmonic mode”;DuffingTransmonstates “Duffing expansion valid in the transmon regimeE_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) andraising_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 whenlevelschanges).
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 is switched on automatically exactly once
after the outermost __init__ returns, so subclasses no longer call
_finish_init by hand.
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
|
Abstract truncated-Hilbert-space quantum device. |
|
Declarative spec for one family of Lindblad collapse channels. |
- class quchip.devices.base.NoiseChannel(name, params, build)[source]¶
Bases:
objectDeclarative spec for one family of Lindblad collapse channels.
A device class composes its dissipation from a tuple of these in
_noise_channels;BaseDevice.collapse_operators()concatenates each channel’sbuild(device)in declaration order. Adding a noise type to a device is therefore one declaration — a parameter field plus a channel entry — with no method override and no change outside the device’s own class.- Parameters:
name (str) – Human-readable channel-family name (diagnostics only).
params (tuple[str, ...]) – Device attribute names this channel consumes — declarative metadata; the build reads the attributes directly.
build (callable) –
build(device) -> list[Operator]: the channel’s collapse operators, empty when its parameters are unset.
- class quchip.devices.base.BaseDevice(levels, label=None, *, T1=None, T2=None, thermal_population=None)[source]¶
Bases:
StateVersioned,Registrable,ABCAbstract truncated-Hilbert-space quantum device.
Concrete subclasses must:
Set
_type_prefix(used for auto-labeling).Expose a
freqattribute — the bare0 -> 1transition frequency in GHz. Any JAX-traceable scalar is fine.Implement
hamiltonian()returning an operator on the truncated Fock basis.
Noise parameters (all optional;
Nonemeans the channel is absent):T1— relaxation time (ns); emission channel at rate1/T1.T2— total 0-1 coherence time (ns, requiresT2 <= 2*T1); adds pure dephasing atgamma_phi = 1/T2 - 1/(2*T1).thermal_population— unitless bath occupationn̄; adds thermal absorption and enhances emission.
They are ordinary attributes: set them at construction or at any time after — the next
simulate/solverebuilds 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 toNoneremoves its channel.Mutation tracking is enabled automatically once construction finishes (see
StateVersioned); subclasses do not call_finish_initthemselves.Optional overrides:
_noise_channels— declare channels beyondT1/T2(append aNoiseChannel;collapse_operators()composes the declared channels automatically).to_dict()/from_dict()— for extra parameters.computational—Trueif the device represents a computational qubit (defaultFalse).
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_dresswalks 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
DeviceModellineage — the default is derived: every declaredparameter()field, in declaration order (seeDeviceModel.__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)BaseDevicesubclass there is no derivation; the default stays empty unless the subclass declares its own tuple — e.g.Fluxoniumuses("E_C", "E_J", "E_L", "phi_ext").
- tunable_params()[source]¶
Return
{name: current_value}for every bare parameter the device exposes for fitting / sweeping.The default implementation walks
tunable_param_namesand reads each attribute. Subclasses with derived bare parameters (e.g. circuit-level devices whosefreqis computed fromE_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 gainphi_extonly at certain operating points).
- 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.
- 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
ValueErrorfor 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).
- property connected_drives: list['BaseDrive']¶
Drives wired to this device, as a fresh list (mutation-safe copy).
- property dressed_freq: float | None¶
Chip-derived dressed 0→1 transition frequency in GHz, or
Nonewithout 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
freqbecause 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.expectis expressed in this frame in every integration frame, and in the default rotating frameresult.statesare 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 atomega - reference_freqwhen 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 — bit-identical to the prior behavior. 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. AssignNoneto restore the default.
- abstractmethod hamiltonian()[source]¶
Return the device Hamiltonian on the truncated Hilbert space.
- Return type:
- 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 everyBaseDevicehas some form of Hilbert-space truncation. A pure-dephasing note is added whenT2is set, since the number-operator dephasing model carries non-obvious assumptions. Subclassessuper().physics_notes()and append their own model-specific notes; no registry / engine-side dispatch is needed.
- dynamic_terms()[source]¶
Time-dependent device terms on the local Hilbert space (default: none).
Subclasses modelling tunable / parametrically modulated devices (tunable transmons, flux-biased fluxonium, parametrically driven modes, …) override this to emit
(local_operator, modulation)pairs. Eachlocal_operatoracts on the device’s own truncated Fock space and is in ordinary GHz; the engine embeds it, applies the single 2π factor, and attaches the modulation as aDynamicTerm.modulationmust be aScalarModulationwrapping a JAX-traceable signal program.
- 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_NAMESand delegating tosuper().local_operator(name)for the base set — and the chip’s observable surface (Chip.observable(),Chip.e_ops()) gains the operator without any engine orChipchange.
- declarative_ops()[source]¶
Map declarative
(label, op-name)keys to backend operators.This is the single lookup the declarative layer consumes when compiling a
PhysicsExpr: aDeviceModeluses it for its local Hamiltonian, and a coupling merges the maps of both endpoints. Keys mirror the operator handles exposed byLocalOps(a,adag,n,I, and the computational-subspacesigma_*family, so spin-like models are first-class citizens of the declarative surface).
- plot_energy_levels(*, ax=None, **kwargs)[source]¶
Plot the device’s bare energy-level ladder (delegates to
quchip.viz).
- plot_wavefunction(n, *, ax=None, **kwargs)[source]¶
Plot the
n-th eigenstate wavefunction (delegates toquchip.viz).
- projector(i, j)[source]¶
|i><j|on the Fock basis.Use
projector(i, i)for the population projector|i><i|andprojector(i, j)for|i><j|. No subspace approximation — the operator acts on the full truncated Hilbert space.
- transition(i, j)[source]¶
Transition operator
|i><j| + |j><i|between Fock levelsiandj.Acts like
sigma_xon the two-level subspace{|i>, |j>}. Useful for qudit work and erasure-protected subspaces where the computational pair is not{|0>, |1>}.
- collapse_operators()[source]¶
Lindblad collapse operators composed from the class’s declared noise channels.
Concatenates each
NoiseChannelin_noise_channelsin declaration order. The built-in channels coverT1/T2/thermal_population(see_thermal_emission_channel()and_pure_dephasing_channel()for the physics and normalization conventions); subclasses add channels by extending the tuple.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.
- classmethod noise_parameter_names()[source]¶
Names of this class’s dissipation parameters, in declaration order.
The deduplicated union of each declared
NoiseChannel’sparams— exactly the attributesset_noise()may touch. Hamiltonian parameters are never included.
- intrinsic_decay_rate()[source]¶
Total lowering-channel (downward) Lindblad rate, in 1/ns, or
Nonewith no decay channel.Reports the actual sum of squared amplitudes of the lowering-operator collapse channel(s)
collapse_operators()builds from_thermal_emission_channel(), matching that construction exactly rather than approximating it:T1set (thermal_populationset or not):(n̄+1)/T1— thesqrt(gamma*(n̄+1))·achannel’s rate,gamma = 1/T1;n̄defaults to0whenthermal_populationis unset, so this reduces to plain1/T1.T1unset,thermal_populationset:n̄+1— the same channel withgamma = 1(_thermal_emission_channel()’s 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 alongsideT1) 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 readsthermal_populationdirectly. Whether a channel exists, and which formula applies, is a static decision (isT1/thermal_populationset?), 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.wirealready 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