Source code for quchip.devices.transmon.flux_tunable

"""Symmetric/asymmetric-SQUID flux-tunable transmon.

``freq`` and ``anharmonicity`` are the simulated physics: the calibrated
local ``0 -> 1`` transition frequency and anharmonicity of the device *at
the stored* ``flux_bias``. :meth:`FluxTunableTransmon.local_hamiltonian`
builds the Duffing Hamiltonian from ``freq`` and ``anharmonicity`` alone —
it does not reference ``flux_bias``. :meth:`FluxTunableTransmon.frequency_at`
and :meth:`FluxTunableTransmon.flux_for_frequency` answer counterfactual
questions — what frequency the SQUID would reach at a different bias — via
the SQUID dispersion relative to this calibrated anchor. Mutating
``flux_bias`` therefore records a new nominal operating point without
retuning the device: it does not change ``freq``, ``anharmonicity``, or the
Hamiltonian.

**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 TYPE_CHECKING, Any

import jax.numpy as jnp

from quchip.declarative.expr import PhysicsExpr
from quchip.declarative.models import DeviceModel
from quchip.declarative.ops import LocalOps
from quchip.declarative.parameters import Scalar, parameter
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(DeviceModel): """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 Calibration-anchor operating point Φ/Φ₀. 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`). The local Hamiltonian does not reference this value directly — ``freq`` and ``anharmonicity`` already carry it. A pytree leaf, so it is differentiable / sweepable like every other device parameter. 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: str = "fluxtunable" _default_levels: int = 3 tunable_param_names = ("freq", "anharmonicity") computational = True approximation = ( "Duffing-approximated SQUID transmon; adiabatic flux (calibration-anchor, " "no Landau-Zener)." ) freq: Scalar = parameter(positive=True, unit="GHz") anharmonicity: Scalar = parameter(unit="GHz") flux_bias: Scalar = parameter(default=0.0, unit="Phi_0") asymmetry: Scalar = parameter(default=0.0) # --- generated __init__ stub (tools/gen_device_stubs.py); do not edit --- if TYPE_CHECKING: def __init__( self, freq: Scalar, anharmonicity: Scalar, flux_bias: Scalar = 0.0, asymmetry: Scalar = 0.0, *, levels: int = 3, label: str | None = None, T1: float | None = None, T2: float | None = None, thermal_population: float | None = None, ) -> None: ... # --- end generated stub ---
[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)
[docs] def local_hamiltonian(self, op: LocalOps) -> PhysicsExpr: """Return the Duffing Hamiltonian built from the calibrated freq and anharmonicity. ``H = ω n + (α/2) n(n − I)``. Does not reference ``flux_bias``. """ return duffing_expr(op, self.freq, self.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