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`` preserves 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; anharmonicity α ≈ −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.

**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``; the Josephson parameters are derived
internally:

    α = −E_C  →  E_C = |α|
    (ω + E_C)² = 8 E_C E_J(flux_bias)  →  E_J_max via SQUID inversion

The SQUID parameters :attr:`_E_C` / :attr:`_E_J_max` are *derived on read* from
the current ``freq`` / ``anharmonicity`` / ``flux_bias`` / ``asymmetry`` — they
carry no cached state, so :meth:`frequency_at` and :meth:`flux_for_frequency`
always reflect a mutated or swept parameter (no stale SQUID metadata).

References
----------
* Koch et al., PRA 76, 042319 (2007), §II.
* 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
"""

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.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 maybe_concrete_scalar


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. May be a JAX tracer. anharmonicity : float Calibrated local anharmonicity α in GHz, at the stored ``flux_bias``. Must be negative (α ≈ −E_C). May 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 this value alone preserves the inferred SQUID calibration and updates ``freq``. Rebinding it together with ``freq`` defines a new anchor. 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. **noise_kwargs Forwarded to :class:`~quchip.devices.base.BaseDevice` — ``T1``, ``T2``, ``thermal_population``. """ _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)." ) 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: """Range checks on 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.""" remaining = dict(values) if "flux_bias" not in remaining: super().set_parameter_values(remaining) return flux_bias = remaining.pop("flux_bias") if "freq" in remaining: frequency = remaining.pop("freq") super().set_parameter_values(remaining) super().__setattr__("flux_bias", flux_bias) super().__setattr__("freq", frequency) return super().set_parameter_values(remaining) self.flux_bias = flux_bias
[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.""" 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. ``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)
# -- 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) phi = jnp.pi * jnp.asarray(self.flux_bias) d = jnp.asarray(self.asymmetry) dispersion_factor = jnp.sqrt(jnp.cos(phi) ** 2 + d ** 2 * jnp.sin(phi) ** 2) return E_J_at_bias / dispersion_factor
[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. """ phi = jnp.pi * jnp.asarray(flux) d = jnp.asarray(self.asymmetry) E_J = self._E_J_max * jnp.sqrt(jnp.cos(phi) ** 2 + d ** 2 * jnp.sin(phi) ** 2) 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). 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; 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