Source code for quchip.devices.transmon.flux_tunable

"""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