"""Symmetric/asymmetric-SQUID flux-tunable transmon.
``freq`` is the calibrated local ``0 -> 1`` transition at ``flux_bias``.
Together they anchor a SQUID dispersion. Moving ``flux_bias`` keeps that
calibration and updates ``freq``, so the local Duffing Hamiltonian follows the
new operating point. Supplying both ``freq`` and ``flux_bias`` defines a new
anchor instead. :meth:`FluxTunableTransmon.frequency_at` and
:meth:`FluxTunableTransmon.flux_for_frequency` inspect the same dispersion
without changing the device.
**Declared approximations**
* Transmon regime: E_J ≫ E_C (exponential charge-dispersion suppression).
* Duffing truncation: the cosine Josephson potential is expanded to quartic
order. The anharmonicity is α ≈ −E_C.
* Adiabatic flux: the SQUID dispersion underlying :meth:`frequency_at` /
:meth:`flux_for_frequency` is a static calibration-anchor relation with no
Landau–Zener physics. Time-dependent flux tuning during gates is applied
through an external :class:`~quchip.control.drive.FluxDrive` whose
real-baseband envelope carries δω(t) in GHz.
* Harmonic charge scale: the charge and phase operators scale with the
zero-point amplitudes ``E_J^(1/4)`` and ``E_J^(-1/4)`` at fixed E_C.
Capacitive ``g`` and charge-drive amplitudes refer to Φ = 0 (see
:meth:`FluxTunableTransmon.charge_coupling_operator`).
**SQUID dispersion**
.. math::
E_J(\\Phi) = E_{J,\\max}
\\sqrt{\\cos^2(\\pi \\Phi/\\Phi_0) + d^2 \\sin^2(\\pi \\Phi/\\Phi_0)}
\\omega(\\Phi) = \\sqrt{8\\, E_C\\, E_J(\\Phi)} - E_C
where ``d = (E_{J1} − E_{J2}) / (E_{J1} + E_{J2})`` is the junction asymmetry
and Φ/Φ₀ is the reduced flux. The user supplies the calibrated local ``freq``
and ``anharmonicity``, and the Josephson parameters are derived internally:
α = −E_C → E_C = |α| (ω + E_C)² = 8 E_C E_J(flux_bias) → E_J_max through
SQUID inversion
:meth:`frequency_at` and :meth:`flux_for_frequency` always show a mutated or
swept parameter (no stale SQUID metadata).
References
----------
* Koch et al., PRA 76, 042319 (2007), §II and §III.
* Krantz et al., APR 6, 021318 (2019), §II.B and §V.A.
* Renger et al., *A superconducting qubit-resonator quantum processor with
effective all-to-all connectivity* — flux-tunable qubits and MOVE/CZ gates.
Examples
--------
>>> from quchip.devices.transmon import FluxTunableTransmon
>>> q = FluxTunableTransmon(freq=4.47, anharmonicity=-0.2006, levels=3)
>>> round(q.freq, 3)
4.47
>>> round(q.anharmonicity, 4)
-0.2006
>>> round(float(q.frequency_at(0.0)), 3)
4.47
"""
# The SQUID parameters `_E_C` / `_E_J_max` are derived on read from the current
# `freq` / `anharmonicity` / `flux_bias` / `asymmetry` and carry no cached
# state.
from __future__ import annotations
import math
from typing import Any, ClassVar, Mapping
import jax.numpy as jnp
from quchip.declarative.expr import PhysicsExpr
from quchip.declarative.models import _parameter_bindings, _symbolic_parameters
from quchip.declarative.ops import LocalOps
from quchip.declarative.parameters import UNBOUND, Scalar, parameter
from quchip.devices.fock import FockDevice
from quchip.devices.transmon.duffing import duffing_expr
from quchip.utils.jax_utils import concrete_array_module, maybe_concrete_scalar
def _josephson_ratio(flux: Any, asymmetry: Any, xp: Any = jnp) -> Any:
"""Return the SQUID ratio ``E_J(Φ)/E_J,max = sqrt(cos²(πΦ) + d² sin²(πΦ))``."""
phi = xp.pi * xp.asarray(flux)
d = xp.asarray(asymmetry)
return xp.sqrt(xp.cos(phi) ** 2 + d ** 2 * xp.sin(phi) ** 2)
def _charge_scale(flux_bias: Any, asymmetry: Any) -> Any:
"""Return ``s(Φ) = (E_J(Φ)/E_J,max)^(1/4)``, the charge scale relative to Φ = 0."""
return _josephson_ratio(flux_bias, asymmetry, concrete_array_module(flux_bias, asymmetry)) ** 0.25
def _check_anharmonicity(value: Any) -> None:
"""Raise ``ValueError`` if *value* is a concrete non-negative scalar.
``anharmonicity`` must be strictly negative: ``E_C = |anharmonicity|``
feeds a division in :attr:`FluxTunableTransmon._E_J_max`, so zero
divides by zero, and a positive value is unphysical for a transmon
(α ≈ −E_C < 0). Concrete scalars only; traced values pass unchecked.
"""
concrete = maybe_concrete_scalar(value)
if concrete is not None and concrete >= 0.0:
raise ValueError(
f"anharmonicity must be negative (E_C = |anharmonicity| feeds the SQUID "
f"inversion and must be positive), got {value}"
)
def _check_asymmetry(value: Any) -> None:
"""Raise ``ValueError`` if *value* is a concrete scalar outside ``[0, 1)``."""
concrete = maybe_concrete_scalar(value)
if concrete is not None and not (0.0 <= concrete < 1.0):
raise ValueError(f"asymmetry must be in [0, 1), got {value}")
def _check_flux_bias_dispersion(flux_bias: Any, asymmetry: Any) -> None:
"""Raise ``ValueError`` if the SQUID dispersion factor is concretely zero.
``flux_bias`` itself is unrestricted — any real value is a valid
calibration anchor. The dispersion factor
``sqrt(cos²(π·flux_bias) + asymmetry²·sin²(π·flux_bias))`` vanishes only
at the joint condition ``asymmetry == 0`` and ``flux_bias`` a
half-integer (a symmetric SQUID's effective E_J is exactly zero at half
flux quantum), which makes :attr:`FluxTunableTransmon._E_J_max` diverge.
Concrete scalars only; either input being traced skips the check.
"""
fb = maybe_concrete_scalar(flux_bias)
d = maybe_concrete_scalar(asymmetry)
if fb is None or d is None:
return
dispersion_factor = math.sqrt(math.cos(math.pi * fb) ** 2 + d**2 * math.sin(math.pi * fb) ** 2)
if dispersion_factor < 1e-9:
raise ValueError(
f"flux_bias={flux_bias} with asymmetry={asymmetry} drives the SQUID's effective "
"E_J to zero; no finite E_J_max reproduces a positive freq there. Choose a "
"different flux_bias or a nonzero asymmetry."
)
[docs]
class FluxTunableTransmon(FockDevice):
"""SQUID-dispersion flux-tunable transmon.
The constructor takes the calibrated local physical parameters. SQUID
metadata is derived on read and is not part of the public interface.
Parameters
----------
freq : float
Calibrated local ``0 -> 1`` transition frequency ω in GHz, at the
stored ``flux_bias``. Must be positive. Can be a JAX tracer.
anharmonicity : float
Calibrated local anharmonicity α in GHz, at the stored ``flux_bias``.
Must be negative (α ≈ −E_C). Can be a JAX tracer.
flux_bias : float, default 0.0
Current operating point Φ/Φ₀ and calibration-anchor coordinate. Any
real value. The SQUID inversion is undefined only at the
symmetric-SQUID degenerate point (``asymmetry == 0`` and ``flux_bias``
a half-integer, see :meth:`validate`). Rebinding only this value keeps
the inferred SQUID calibration and updates ``freq``, and rebinding it
with ``freq`` defines a new anchor. It also sets the charge scale of
:meth:`charge_coupling_operator`. It is a JAX pytree leaf and can be
differentiated or swept through the public chip API.
asymmetry : float, default 0.0
SQUID junction asymmetry d = (E_{J1}−E_{J2})/(E_{J1}+E_{J2}).
Must be in [0, 1).
levels : int, default 3
Fock-space truncation.
label : str | None, default None
Auto-generated as ``fluxtunable_{idx}`` when omitted.
T1 : float or None, default None
Energy-relaxation time in ns. ``None`` disables T1 relaxation.
T2 : float or None, default None
Total 0-1 coherence time in ns. If both are set, ``T2 <= 2*T1``.
thermal_occupation : float or None, default None
Dimensionless mean bath occupation. ``None`` disables absorption.
"""
_type_prefix: ClassVar[str] = "fluxtunable"
_default_levels: ClassVar[int] = 3
tunable_param_names = ("freq", "anharmonicity", "flux_bias")
dressed_fit_target_fields = (
("freq", "freq"),
("anharmonicity", "anharmonicity"),
)
dressed_fit_param_names = ("freq", "anharmonicity")
computational = True
approximation = (
"Duffing-approximated SQUID transmon; adiabatic flux (calibration-anchor, "
"no Landau-Zener); harmonic charge scale (E_J/E_J,max)^(1/4) at the static flux_bias."
)
freq: Scalar = parameter(default=UNBOUND, positive=True, unit="GHz", symbol=r"\omega")
anharmonicity: Scalar = parameter(default=UNBOUND, unit="GHz", symbol=r"\alpha")
flux_bias: Scalar = parameter(default=0.0, unit="Phi_0", symbol=r"\Phi")
asymmetry: Scalar = parameter(default=0.0, symbol="d")
[docs]
def validate(self) -> None:
"""Check the ranges of concrete scalars only. Traced values pass unchecked."""
_check_anharmonicity(self.anharmonicity)
_check_asymmetry(self.asymmetry)
_check_flux_bias_dispersion(self.flux_bias, self.asymmetry)
def _validate_param_write(self, name: str, value: Any) -> None:
"""Re-run the construction-time SQUID checks on relevant post-construction writes.
Mirrors :meth:`validate`, substituting *value* for the field being
written so e.g. ``q.anharmonicity = 0.0`` after construction fails
exactly as it would at construction.
"""
super()._validate_param_write(name, value)
if name == "anharmonicity":
_check_anharmonicity(value)
elif name == "asymmetry":
_check_asymmetry(value)
_check_flux_bias_dispersion(self.flux_bias, value)
elif name == "flux_bias":
_check_flux_bias_dispersion(value, self.asymmetry)
def __setattr__(self, name: str, value: Any) -> None:
"""Retune the local frequency when the live operating bias changes."""
if name == "flux_bias" and getattr(self, "_tracking_enabled", False):
_check_flux_bias_dispersion(value, self.asymmetry)
frequency = self.frequency_at(value)
super().__setattr__("freq", frequency)
super().__setattr__("flux_bias", value)
return
super().__setattr__(name, value)
[docs]
def set_parameter_values(self, values: Mapping[str, Any]) -> None:
"""Apply flux and calibration overrides without mapping-order effects.
Parameters
----------
values : mapping[str, Any]
Parameter updates. Flux-only updates retune ``freq``.
"""
updates = dict(values)
if "flux_bias" in updates and "freq" not in updates:
calibration = self.copy()
calibration.set_parameter_values({name: value for name, value in updates.items() if name != "flux_bias"})
updates["freq"] = calibration.frequency_at(updates["flux_bias"])
super().set_parameter_values(updates)
[docs]
def tunable_param_bounds(self, name: str, value: float) -> tuple[float, float]:
"""Use one SQUID period as the default bound for explicit flux fitting.
Parameters
----------
name : str
Tunable parameter name.
value : float
Concrete optimizer seed.
"""
if name == "flux_bias":
return (-0.5, 0.5)
return super().tunable_param_bounds(name, value)
[docs]
def local_hamiltonian(self, op: LocalOps, p: Any) -> PhysicsExpr:
"""Return the Duffing Hamiltonian built from the calibrated freq and anharmonicity.
Parameters
----------
op : LocalOps
Fock operator namespace.
p : ParameterNamespace
Symbolic calibrated and flux parameters.
``H = ω n + (α/2) n(n − I)``. Rebinding ``flux_bias`` first updates
the stored ``freq`` through the anchored SQUID dispersion.
"""
return duffing_expr(op, p.freq, p.anharmonicity)
def _symbolic_charge_scale(self) -> PhysicsExpr:
"""Return ``s(Φ)`` as a scalar expression of ``flux_bias`` and ``asymmetry``."""
p = _symbolic_parameters(self)
return PhysicsExpr.from_function(_charge_scale, p.flux_bias, p.asymmetry, labels=(), dims=(), name="s")
[docs]
def charge_coupling_operator(self) -> PhysicsExpr:
"""Return the charge operator ``s(Φ) i(a − a†)``.
Notes
-----
At fixed E_C, the transmon charge zero-point amplitude scales as
``E_J^(1/4)`` (Koch et al. 2007, §III). The factor
``s(Φ) = (E_J(Φ)/E_J,max)^(1/4) = (cos²(πΦ) + d² sin²(πΦ))^(1/8)``
takes it relative to Φ = 0, so ``g`` of a capacitive coupling and a
charge-drive amplitude refer to the sweet spot. ``s`` depends on the
static ``flux_bias``. A ``FluxDrive`` pulse does not change it.
"""
expression = self._symbolic_charge_scale() * super().charge_coupling_operator()
return expression.with_bindings(_parameter_bindings(self))
[docs]
def phase_coupling_operator(self) -> PhysicsExpr:
"""Return the phase operator ``(a + a†)/s(Φ)``.
The phase zero-point amplitude scales as ``E_J^(-1/4)``, the inverse of
the charge scale in :meth:`charge_coupling_operator`.
"""
expression = super().phase_coupling_operator() / self._symbolic_charge_scale()
return expression.with_bindings(_parameter_bindings(self))
# -- Derived-on-read SQUID parameters ----------------------------------
@property
def _E_C(self) -> Any:
"""Charging energy E_C = |α|, recomputed from the current anharmonicity."""
return jnp.abs(jnp.asarray(self.anharmonicity))
@property
def _E_J_max(self) -> Any:
"""Maximum Josephson energy, inverted from ``freq`` at the current bias.
``E_C = |α|``, ``E_J(flux_bias) = (ω + E_C)² / (8 E_C)``, then
``E_J_max = E_J(flux_bias) / sqrt(cos²(πΦ) + d²sin²(πΦ))``.
"""
E_C = self._E_C
E_J_at_bias = (jnp.asarray(self.freq) + E_C) ** 2 / (8.0 * E_C)
return E_J_at_bias / _josephson_ratio(self.flux_bias, self.asymmetry)
[docs]
def frequency_at(self, flux: Any) -> Any:
"""SQUID dispersion ω(Φ/Φ₀) in GHz, using derived E_C and E_J_max.
Parameters
----------
flux : float
Reduced flux Φ/Φ₀. JAX-traceable.
"""
E_J = self._E_J_max * _josephson_ratio(flux, self.asymmetry)
return jnp.sqrt(8.0 * self._E_C * E_J) - self._E_C
[docs]
def flux_for_frequency(self, target_freq: Any) -> Any:
"""Inverse SQUID dispersion on the monotonic lobe Φ/Φ₀ ∈ [0, 0.5).
Parameters
----------
target_freq : float or array-like
Desired transition frequency in GHz.
Notes
-----
Derivation:
ω(Φ) = sqrt(8 E_C E_J_max sqrt(cos²(πΦ) + d²sin²(πΦ))) − E_C
→ let S = (ω + E_C)² / (8 E_C E_J_max)
→ cos²(πΦ)(1 − d²) + d² = S²
→ cos²(πΦ) = (S² − d²) / (1 − d²)
Raises
------
ValueError
If *target_freq* is concrete and lands outside the frequency range
:meth:`frequency_at` reaches over Φ/Φ₀ ∈ [0, 0.5) at the current
calibration anchor. A traced *target_freq* (or a traced anchor)
skips this check and the returned flux clips to the lobe endpoint,
so out-of-domain behavior is undefined for traced inputs.
"""
# Validate the concrete target against the attainable endpoint span
# BEFORE forming S: squaring (omega + E_C) would otherwise map a
# below-minimum target onto an attainable S and silently invert to
# the wrong frequency.
target_concrete = maybe_concrete_scalar(target_freq)
if target_concrete is not None:
lo_c = maybe_concrete_scalar(self.frequency_at(0.5))
hi_c = maybe_concrete_scalar(self.frequency_at(0.0))
if lo_c is not None and hi_c is not None:
lo, hi = sorted((lo_c, hi_c))
if not (lo <= target_concrete <= hi):
raise ValueError(
f"target_freq={target_freq} GHz is unattainable at the current "
f"calibration anchor: frequency_at spans [{lo:.6g}, {hi:.6g}] GHz "
"over Φ/Φ₀ ∈ [0, 0.5]."
)
omega = jnp.asarray(target_freq)
S = (omega + self._E_C) ** 2 / (8.0 * self._E_C * self._E_J_max)
d2 = jnp.asarray(self.asymmetry) ** 2
cos2 = (S ** 2 - d2) / (1.0 - d2)
return jnp.arccos(jnp.sqrt(jnp.clip(cos2, 0.0, 1.0))) / jnp.pi
[docs]
def physics_notes(self) -> list[str]:
"""Return declared SQUID-transmon calibration-anchor assumptions."""
notes = super().physics_notes()
notes.append("Time-varying flux enters via an external FluxDrive (δω n̂ modulation)")
return notes