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; 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 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
|
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,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
unresolved_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:
dissipation()— append channels beyondT1/T2.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").
- dressed_fit_target_fields: ClassVar[tuple[tuple[str, str], ...]] = ()¶
(dressed_observable, declared_field)pairs used when this device appears in the desired-chip form offit_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 fromtunable_param_names: a model may expose parameters for sweeps without claiming that inverse design can identify all of them from its default dressed observables.
- 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_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).
- 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.
- default_fit_parameters()[source]¶
Return the conservative bare-parameter selection for default targets.
- 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).
- set_parameter_value(name, value)[source]¶
Apply one validated local parameter value on an isolated device copy.
- 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.
- 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. 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 unresolved_hamiltonian()[source]¶
Return the authored local Hamiltonian before engine policies.
- Return type:
- 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
frameoverrides 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:
- 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.
- resolved_basis(chip_basis='native')[source]¶
Return the device override or inherited chip basis policy.
- resolved_dimension(chip_basis='native')[source]¶
Return the local dimension delivered to the solver.
- energy_level_operator()[source]¶
Return the energy-level index expressed in the authored local basis.
- Return type:
- 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.
- 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 authored local 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 authored local space.
- transition(lower, upper)[source]¶
Hermitian transition between isolated energy states.
The operator
|lower><upper| + |upper><lower|is returned in the authored local basis.
- transition_frequency(lower, upper)[source]¶
Return the isolated
E_upper - E_lowertransition in GHz.
- 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, andthermal_population; subclasses append channels indissipation().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.
- collapse_channels(basis=None)[source]¶
Return normalized local collapse channels.
- Parameters:
basis (BasisRecord | None)
- Return type:
tuple[CollapseChannel, …]
- 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 the common thermal-emission 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(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 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