"""Linear-resonator device model.
Hamiltonian:
.. math:: H = \\omega \\, \\hat{n}
where :math:`\\hat{n} = a^\\dagger a` is the Fock-basis number operator
and :math:`\\omega` is the bare cavity frequency.
Approximation
-------------
Strictly harmonic / non-interacting: no Kerr, no cross-Kerr, no drive
backaction beyond what couplings/drives themselves introduce. This is
the ideal cavity / transmission-line-resonator mode — suitable for
readout cavities, filter modes, photonic oscillators, and cavity-QED
benchmarks where anharmonicity is either absent or modelled
separately. For Kerr / anharmonic cavities, use a device that owns an
explicit ``(K/2) n(n-1)`` term (see ``examples/kerr_cat_qubit.py``).
Optional dissipation
--------------------
Passing ``internal_quality_factor = Q`` adds a single unobserved photon-loss collapse
operator ``sqrt(kappa) a`` with :math:`\\kappa = 2\\pi\\,f/Q`
(angular decay rate, rad/ns).
**Quality-factor convention (physics, not a unit conversion).**
``internal_quality_factor`` is defined against the *ordinary* frequency
``freq`` (GHz) carried by this class. The resulting decay rate is
:math:`\\kappa = 2\\pi\\,f/Q` (angular, rad/ns). The :math:`2\\pi`
here is intrinsic to the physical definition of Q — not an
ordinary→angular units conversion bolted on at the engine boundary.
Concretely, Q counts cycles of the *ordinary* oscillation per
e-folding of energy, so energy decays as
:math:`e^{-t/\\tau} = e^{-\\kappa t}` with
:math:`\\kappa = \\omega/Q = 2\\pi f/Q`. For this reason the
:math:`2\\pi` lives in the resonator's photon-loss noise channel and
must not be moved to the units boundary in ``assembly.py``.
Noise hooks inherited from :class:`~quchip.devices.base.BaseDevice`
(``T1``, ``T2``, ``thermal_population``) produce the Lindblad
channels described in that base class. For circuit-QED conventions
see Krantz et al., *Applied Physics Reviews* **6**, 021318 (2019), §V.
References
----------
* Walls & Milburn, *Quantum Optics*, 2nd ed. (Springer, 2008), Ch. 7.
* Blais, Grimsmo, Girvin & Wallraff, *Circuit quantum electrodynamics*,
*Reviews of Modern Physics* **93**, 025005 (2021).
Example
-------
>>> from quchip.chip import Chip
>>> from quchip.devices import Resonator
>>> r = Resonator(freq=7.2, levels=6, label="readout")
>>> chip = Chip(devices=[r])
>>> r.freq, r.levels
(7.2, 6)
"""
from __future__ import annotations
from typing import Any, ClassVar
import numpy as np
from quchip.declarative.expr import PhysicsExpr
from quchip.declarative.dissipation import CollapseChannel
from quchip.declarative.ops import LocalOps
from quchip.declarative.parameters import UNBOUND, Scalar, parameter
from quchip.devices.fock import FockDevice
[docs]
class Resonator(FockDevice):
"""Linear microwave / photonic resonator — pure harmonic oscillator.
Parameters
----------
freq : float
Bare cavity frequency ω in GHz. Must be positive. May be a JAX
tracer for sweeps / gradients.
internal_quality_factor : float | None, optional
Internal Q referenced to the ordinary frequency ``freq`` in GHz.
When set, adds a photon-loss Lindblad channel
``sqrt(2*pi*freq/Q) a`` with angular decay rate
``kappa = 2*pi*freq/Q`` in rad/ns. Must be positive. Like every
noise parameter, it may be set after construction or cleared with
``None``; the next simulation reflects the current value.
levels : int, default 10
Fock-space truncation. Choose comfortably above the maximum
expected photon occupation.
label : str | None, default None
If omitted, auto-generated as ``resonator_{idx}`` via the shared
labeling counter.
**noise_kwargs
Forwarded verbatim to :class:`BaseDevice` — ``T1``, ``T2``,
``thermal_population``.
Example
-------
>>> from quchip.devices import Resonator
>>> r = Resonator(freq=7.2, internal_quality_factor=10_000, levels=8)
>>> len(r.collapse_operators()) >= 1
True
"""
_type_prefix: ClassVar[str] = "resonator"
_default_levels: ClassVar[int] = 10
tunable_param_names = ("freq",)
dressed_fit_target_fields = (("freq", "freq"),)
dressed_fit_param_names = ("freq",)
freq: Scalar = parameter(default=UNBOUND, positive=True, unit="GHz", symbol=r"\omega")
internal_quality_factor: Scalar = parameter(default=None, positive=True, noise=True, kw_only=True)
approximation = "Linear harmonic oscillator with no Kerr or cross-Kerr self-interaction."
[docs]
def local_hamiltonian(self, op: LocalOps, p: Any) -> PhysicsExpr:
"""Return the harmonic oscillator Hamiltonian ``H = freq * n``."""
return p.freq * op.n
[docs]
def dissipation(self, op: LocalOps, p: Any) -> tuple[CollapseChannel, ...]:
channels = super().dissipation(op, p)
if self.internal_quality_factor is None:
return channels
return channels + (
CollapseChannel(op.a, 2 * np.pi * p.freq / p.internal_quality_factor, "internal_photon_loss"),
)
[docs]
def physics_notes(self) -> list[str]:
"""Return declared harmonic-oscillator and dissipation assumptions."""
notes = super().physics_notes()
notes.append("Linear harmonic oscillator (no Kerr, no cross-Kerr self-interaction)")
if self.internal_quality_factor is not None:
notes.append("Internal dissipation: photon loss at rate κ_internal = 2π·ω/Q_internal")
return notes
[docs]
def intrinsic_decay_rate(self) -> Any | None:
"""Combined lowering-channel rate: ``κ = 2π·freq/Q`` photon loss plus the thermal-emission rate.
Both :attr:`internal_quality_factor` and ``T1``/``thermal_population`` build
independent lowering-operator collapse channels on this device (the
``internal_photon_loss`` channel, a pure loss channel unaffected by
``thermal_population``, and the inherited
thermal-emission channel — see
:meth:`~quchip.devices.base.BaseDevice.intrinsic_decay_rate` for its
``(n̄+1)/T1`` / ``n̄+1`` formulas); this hook reports their summed
rate rather than either alone, so a caller reading one scalar decay
rate (e.g. an adiabatic-elimination Purcell fold) does not
under-count decay when both are set. ``None`` only when neither is
set.
"""
kappa = (
None
if self.internal_quality_factor is None
else 2 * np.pi * self.freq / self.internal_quality_factor
)
thermal_rate = super().intrinsic_decay_rate()
if kappa is None and thermal_rate is None:
return None
if kappa is None:
return thermal_rate
if thermal_rate is None:
return kappa
return kappa + thermal_rate