Source code for qpdk.models.junction

"""Josephson junction Models."""

import warnings

import jax
import jax.numpy as jnp
import numpy as np
import sax
from gdsfactory.typings import CrossSectionSpec
from sax.models.rf import electrical_short, impedance

from qpdk.models.constants import DEFAULT_FREQUENCY, Φ_0
from qpdk.models.generic import abcd_to_sdict
from qpdk.models.jit import jit
from qpdk.models.waveguides import straight


def _warn_if_overbiased(ib: float, ic: float) -> None:
    """Host callback to warn if bias current exceeds critical current.

    Uses NumPy, not JAX: dispatching JAX operations from inside a host callback
    can deadlock against the computation that invoked it.
    """
    if np.any(np.abs(np.asarray(ib)) >= np.asarray(ic)):
        warnings.warn(
            "DC bias |I_b| >= I_c detected. Linearized RCSJ model is invalid in the voltage state. "
            "Please check your 'ib' and 'ic' ('ic_tot', 'flux', 'asymmetry') inputs.",
            RuntimeWarning,
            stacklevel=0,
        )


def _rcsj_admittance(
    f: sax.FloatArrayLike,
    ic: sax.Float,
    capacitance: sax.Float,
    resistance: sax.Float,
    ib: sax.Float,
) -> jax.Array:
    r"""Admittance of the linearised RCSJ element, warning if ``|ib| >= ic``.

    Returns:
        The admittance :math:`1/R + j\omega C + 1/(j\omega L_J)` in Siemens.
    """
    jax.debug.callback(_warn_if_overbiased, ib, ic)

    ω = 2 * jnp.pi * jnp.asarray(f)

    # Bias-dependent phase factor
    # jnp.clip ensures we don't get NaNs during compilation tracing or slightly overbiased states
    cos_Φ_0 = jnp.sqrt(jnp.clip(1.0 - (ib / ic) ** 2, min=1e-10))

    # Josephson inductance
    Lⱼ = Φ_0 / (2 * jnp.pi * ic * cos_Φ_0)

    # Admittances (parallel RCSJ)
    Y_R = 1 / resistance
    Y_C = 1j * ω * capacitance
    Y_L = 1 / (1j * ω * Lⱼ)

    return Y_R + Y_C + Y_L


[docs] @jit(inline=True) def josephson_junction( *, f: sax.FloatArrayLike = DEFAULT_FREQUENCY, ic: sax.Float = 1e-6, capacitance: sax.Float = 5e-15, resistance: sax.Float = 10e3, ib: sax.Float = 0.0, ) -> sax.SDict: r"""Josephson junction (RCSJ) small-signal Sax model. Linearized RCSJ model consisting of a bias-dependent Josephson inductance in parallel with capacitance and resistance. Valid in the superconducting (zero-voltage) state and for small AC signals. Default capacitance taken from :cite:`shcherbakovaFabricationMeasurementsHybrid2015`. See :cite:`McCumber1968` for details. Args: f: Array of frequency points in Hz ic: Critical current :math:`I_\text{c}` in Amperes capacitance: Junction capacitance :math:`C` in Farads resistance: Shunt resistance :math:`R` in Ohms ib: DC bias current :math:`I_\text{b}` in Amperes (:math:`\|I_\text{b}\| < I_\text{c}`) Returns: sax.SDict: S-parameters dictionary with ports left_wide and right_wide """ Y_JJ = _rcsj_admittance(f, ic, capacitance, resistance, ib) return sax.rename_ports( impedance(f=f, z=1 / Y_JJ), {"o1": "left_wide", "o2": "right_wide"} )
[docs] @jit(inline=True) def squid_junction( *, f: sax.FloatArrayLike = DEFAULT_FREQUENCY, ic_tot: sax.Float = 2e-6, asymmetry: sax.Float = 0.0, capacitance: sax.Float = 10e-15, resistance: sax.Float = 5e3, ib: sax.Float = 0.0, flux: sax.FloatArrayLike = 0.0, ) -> sax.SDict: r"""DC SQUID small-signal Sax model in the zero-screening limit. Treats the DC SQUID as a single effective RCSJ whose critical current is tunable by an external magnetic flux. Assumes negligible loop geometric inductance. See :cite:`kochChargeinsensitiveQubitDesign2007a` and :cite:`tinkhamIntroductionSuperconductivity2015` for details on asymmetric SQUIDs and effective Josephson inductance. Args: f: Array of frequency points in Hz ic_tot: Total critical current sum :math:`I_{\text{c}1} + I_{\text{c}2}` in Amperes asymmetry: Junction asymmetry :math:`(I_{\text{c}1} - I_{\text{c}2}) / I_{\text{c,tot}}` capacitance: Total SQUID capacitance :math:`C_1 + C_2` in Farads resistance: Total SQUID shunt resistance :math:`R_1 || R_2` in Ohms ib: DC bias current :math:`I_\text{b}` in Amperes flux: External magnetic flux :math:`\Phi_{\text{ext}}` in Webers Returns: sax.SDict: S-parameters dictionary """ ω = 2 * jnp.pi * jnp.asarray(f) # Normalized flux phi_reduced = jnp.pi * flux / Φ_0 # Flux-tunable critical current for an asymmetric SQUID ic_squid = ic_tot * jnp.sqrt( jnp.cos(phi_reduced) ** 2 + (asymmetry**2) * jnp.sin(phi_reduced) ** 2 ) jax.debug.callback(_warn_if_overbiased, ib, ic_squid) # Bias-dependent phase factor # Guard against ic_squid == 0 (e.g. symmetric SQUID at half-flux quantum) ratio_sq = jnp.where( ic_squid > 0, (ib / jnp.where(ic_squid > 0, ic_squid, 1.0)) ** 2, 0.0 ) cos_Φ_0_eff = jnp.sqrt(jnp.clip(1.0 - ratio_sq, min=1e-10)) # Effective Josephson inductance (will go to inf when ic_squid == 0) Lⱼ_eff = Φ_0 / (2 * jnp.pi * jnp.where(ic_squid > 0, ic_squid, 1e-20) * cos_Φ_0_eff) # Admittances (parallel RCSJ) Y_R = 1 / resistance Y_C = 1j * ω * capacitance # Guard against division by zero in Y_L if L_j_eff somehow goes to zero or Y_L -> 0 if L_j_eff is inf Y_L = jnp.where(ic_squid > 0, 1 / (1j * ω * Lⱼ_eff), 0j) # Total SQUID admittance Y_SQUID = Y_R + Y_C + Y_L return impedance(f=f, z=1 / Y_SQUID)
def _abcd_matmul(a: jax.Array, b: jax.Array) -> jax.Array: """Multiply two batches of 2×2 ABCD matrices with shape ``(..., 2, 2)``.""" return jnp.einsum("...ij,...jk->...ik", a, b) _ABCD_POWER_BITS = 24 """Bits of the chain length handled by :func:`_abcd_power` (:math:`N < 2^{24}`).""" def _abcd_power(t: jax.Array, n: jax.Array) -> jax.Array: r"""Raise a batch of ABCD matrices to a traced integer power by squaring. Uses a fixed :math:`2 \times 24` matrix products, so ``n`` may be a traced array (as in a SAX circuit) without recompiling for every value. Returns: The batch of matrices :math:`T^n`. """ result = jnp.broadcast_to(jnp.eye(2, dtype=t.dtype), t.shape) base = t for bit in range(_ABCD_POWER_BITS): take = ((n >> bit) & 1).astype(bool) result = jnp.where(take, _abcd_matmul(result, base), result) base = _abcd_matmul(base, base) return result @jit(inline=True) def _josephson_junction_array( *, f: sax.FloatArrayLike, n: jax.Array, ic: sax.Float, capacitance: sax.Float, ground_capacitance: sax.Float, resistance: sax.Float, ib: sax.Float, z0: sax.Float, ) -> sax.SDict: """Jitted body of :func:`josephson_junction_array` with an integer ``n``. Returns: The S-parameter dictionary, NaN where ``n`` is out of range. """ in_range = (n >= 1) & (n < 2**_ABCD_POWER_BITS) Z_JJ = 1 / _rcsj_admittance(f, ic, capacitance, resistance, ib) Y_g = 2j * jnp.pi * jnp.asarray(f) * ground_capacitance * jnp.ones_like(Z_JJ) one = jnp.ones_like(Z_JJ) zero = jnp.zeros_like(Z_JJ) t_junction = jnp.stack( [jnp.stack([one, Z_JJ], axis=-1), jnp.stack([zero, one], axis=-1)], axis=-2 ) t_cell = jnp.stack( [ jnp.stack([one + Z_JJ * Y_g, Z_JJ], axis=-1), jnp.stack([Y_g, one], axis=-1), ], axis=-2, ) abcd = _abcd_matmul(_abcd_power(t_cell, n - 1), t_junction) abcd = jnp.where(in_range, abcd, jnp.nan) sdict = abcd_to_sdict(f=f, abcd=abcd, z0=z0) # Every cell has AD - BC = 1, so the chain is reciprocal. Setting S12 = S21 # keeps the round-off of a long cascade out of the determinant. sdict["o1", "o2"] = sdict["o2", "o1"] return sdict
[docs] def josephson_junction_array( *, f: sax.FloatArrayLike = DEFAULT_FREQUENCY, n_junctions: int = 43, ic: sax.Float = Φ_0 / (2 * np.pi * 1.9e-9), capacitance: sax.Float = 40e-15, ground_capacitance: sax.Float = 0.04e-15, resistance: sax.Float = np.inf, ib: sax.Float = 0.0, z0: sax.Float = 50.0, ) -> sax.SDict: r"""Josephson junction array (junction chain) small-signal Sax model. A chain of :math:`N` identical linearised junctions in series between ports ``o1`` and ``o2``. Each junction is the parallel RCSJ element of :func:`josephson_junction`, with Josephson inductance :math:`L_\text{J} = \Phi_0 / (2 \pi I_\text{c} \cos\varphi_0)`, junction capacitance :math:`C_\text{J}` and resistance :math:`R`. Each of the :math:`N - 1` interior islands has a capacitance :math:`C_\text{g}` to ground: .. svgbob:: o1 ──[J]──┬──[J]──┬── ... ──┬──[J]── o2 │ │ │ C_g C_g C_g │ │ │ GND GND GND The two-port is evaluated as the ABCD cascade .. math:: \mathbf{T} = \left(\mathbf{T}_\text{J} \mathbf{T}_\text{g}\right)^{N-1} \mathbf{T}_\text{J}, \qquad \mathbf{T}_\text{J} = \begin{pmatrix} 1 & Z_\text{J} \\ 0 & 1 \end{pmatrix}, \qquad \mathbf{T}_\text{g} = \begin{pmatrix} 1 & 0 \\ \mathrm{j} \omega C_\text{g} & 1 \end{pmatrix}, with :math:`Z_\text{J} = (1/R + \mathrm{j}\omega C_\text{J} + 1/(\mathrm{j}\omega L_\text{J}))^{-1}`, and converted to S-parameters referenced to :math:`Z_0` :cite:`m.pozarMicrowaveEngineering2012`. The power is taken by repeated squaring, so the cost does not grow with :math:`N` and ``n_junctions`` may be traced (for example set from a SAX netlist). For a lossless chain with both ends grounded, the cascade resonates at the normal modes of the array :cite:`maslukMicrowaveCharacterizationJosephson2012`: .. math:: \omega_k = \omega_\text{p} \sqrt{\frac{1 - \cos(\pi k / N)} {C_\text{g} / (2 C_\text{J}) + 1 - \cos(\pi k / N)}}, \qquad k = 1, \dots, N - 1, where :math:`\omega_\text{p} = 1 / \sqrt{L_\text{J} C_\text{J}}` is the plasma frequency that bounds the spectrum from above. Assumptions: - Small-signal, zero-voltage state: each junction is a linear inductor. - Identical junctions and uniform :math:`C_\text{g}` on the interior islands. The ground capacitance of the two end islands belongs to whatever the ports connect to, so :math:`N = 1` is a single junction. - Nearest-neighbour coupling only: no long-range island-to-island capacitance. Default per-junction values are the fit to the 80-junction array of :cite:`maslukMicrowaveCharacterizationJosephson2012` (:math:`L_\text{J} = 1.9\,\text{nH}`, :math:`C_\text{J} = 40\,\text{fF}`, :math:`C_\text{g} = 0.04\,\text{fF}`). The default :math:`N = 43` matches the :func:`~qpdk.cells.josephson_junction_array` cell, whose default junction count and target :math:`E_\text{L}` follow :cite:`manucharyan_fluxonium_2009`. The default junctions of the two differ (:math:`43 \times 1.9\,\text{nH} = 82\,\text{nH}` here against the cell's estimate of :math:`314\,\text{nH}`): to simulate a laid out array, pass ``ic`` from the cell's ``info["critical_current_nA"]`` (converted to A) together with a junction capacitance for that process. Args: f: Array of frequency points in Hz. n_junctions: Number of junctions :math:`N` in the chain, :math:`1 \le N < 2^{24}`. Non-integer values are truncated. ic: Critical current :math:`I_\text{c}` of each junction in Amperes. capacitance: Capacitance :math:`C_\text{J}` of each junction in Farads. ground_capacitance: Capacitance :math:`C_\text{g}` of each interior island to ground in Farads. resistance: Shunt resistance :math:`R` of each junction in Ohms. ib: DC bias current :math:`I_\text{b}` through the chain in Amperes (:math:`|I_\text{b}| < I_\text{c}`). z0: Reference impedance :math:`Z_0` of both ports in Ohms. Returns: sax.SDict: S-parameters dictionary with ports ``o1`` and ``o2``. Raises: ValueError: If a concrete ``n_junctions`` is outside :math:`1 \le N < 2^{24}`. A traced out-of-range value returns NaN. """ n = jnp.asarray(n_junctions).astype(jnp.int32) if not isinstance(n, jax.core.Tracer) and not 1 <= int(n) < 2**_ABCD_POWER_BITS: raise ValueError( f"n_junctions must satisfy 1 <= n_junctions < 2**{_ABCD_POWER_BITS}, " f"got {n_junctions}" ) return _josephson_junction_array( f=f, n=n, ic=ic, capacitance=capacitance, ground_capacitance=ground_capacitance, resistance=resistance, ib=ib, z0=z0, )
[docs] def squid_termination( *, f: sax.FloatArrayLike = DEFAULT_FREQUENCY, length: sax.Float = 10.0, cross_section: CrossSectionSpec = "cpw", ic_tot: sax.Float = 4.8e-6, asymmetry: sax.Float = 0.0, capacitance: sax.Float = 10e-15, resistance: sax.Float = 5e3, flux: sax.FloatArrayLike = 0.0, ) -> sax.SDict: r"""One-port Sax model of a CPW lead shorted to ground through a DC SQUID. This is the termination of a flux-driven :math:`\lambda/4` Josephson parametric amplifier (JPA): a CPW lead of ``length``, then the :func:`~qpdk.models.junction.squid_junction` admittance in series with an ideal short. It models the :func:`~qpdk.cells.squid_termination` cell. The flux-tunable SQUID inductance :math:`L_\text{s}(\Phi) = \Phi_0 / (2 \pi I_\text{c,tot} |\cos(\pi \Phi / \Phi_0)|)` pulls the resonance below :math:`\omega_{\lambda/4}` :cite:`yamamotoFluxdrivenJosephsonParametric2008`. The model neglects the loop geometric inductance, which is valid for :math:`\beta_\text{L} = 2 L_\text{loop} I_\text{c} / \Phi_0 \lesssim 0.1`. The default :math:`I_\text{c,tot} = 2 \times 2.4\,\text{µA}` follows the low-:math:`\beta_\text{L}` WMI devices :cite:`pogorzalekHystereticFluxResponse2017`. It is an assumption, not a value extracted from the layout. Args: f: Array of frequency points in Hz length: Length of the CPW lead in µm cross_section: Cross-section of the CPW lead ic_tot: Total critical current :math:`I_{\text{c}1} + I_{\text{c}2}` in Amperes asymmetry: Junction asymmetry :math:`(I_{\text{c}1} - I_{\text{c}2}) / I_{\text{c,tot}}` capacitance: Total SQUID capacitance :math:`C_1 + C_2` in Farads resistance: Total SQUID shunt resistance :math:`R_1 || R_2` in Ohms flux: External magnetic flux :math:`\Phi_{\text{ext}}` in Webers Returns: sax.SDict: S-parameters dictionary with the single port ``o1`` """ instances: dict[str, sax.SType] = { "lead": straight(f=f, length=length, cross_section=cross_section), "squid": squid_junction( f=f, ic_tot=ic_tot, asymmetry=asymmetry, capacitance=capacitance, resistance=resistance, flux=flux, ), "short": electrical_short(f=f), } connections = {"lead,o2": "squid,o1", "squid,o2": "short,o1"} ports = {"o1": "lead,o1"} return sax.evaluate_circuit_fg((connections, ports), instances)
if __name__ == "__main__": import matplotlib.pyplot as plt sax.set_port_naming_strategy("optical") f = jnp.linspace(1e9, 10e9, 500) s = josephson_junction(f=f, ic=1e-6, capacitance=50e-15, resistance=10e3, ib=0.5e-6) plt.figure() plt.plot(f / 1e9, jnp.abs(s["left_wide", "left_wide"]), label="|S11|") plt.plot(f / 1e9, jnp.abs(s["left_wide", "right_wide"]), label="|S12|") plt.plot(f / 1e9, jnp.abs(s["right_wide", "right_wide"]), label="|S22|") plt.xlabel("Frequency [GHz]") plt.ylabel("Magnitude") plt.legend() plt.title("Josephson Junction S-parameters") plt.show()