"""Inductor and lumped-element resonator models."""
from functools import partial
import jax
import jax.numpy as jnp
import sax
from gdsfactory.typings import CrossSectionSpec
from qpdk.models.capacitor import interdigital_capacitor_capacitance_analytical
from qpdk.models.constants import DEFAULT_FREQUENCY, μ_0, π
from qpdk.models.cpw import (
cpw_ep_r_from_cross_section,
get_cpw_dimensions,
get_cpw_substrate_params,
)
from qpdk.models.generic import inductor, lc_resonator
@partial(jax.jit, inline=True)
def self_inductance_strip(length: float, width: float, thickness: float) -> jax.Array:
r"""Analytical formula for the self-inductance of a rectangular metal strip.
Uses the formula from :cite:`chenCompactInductorcapacitorResonators2023`:
.. math::
L_s = \frac{\mu_0 l}{2\pi} \left[ \ln\left(\frac{2l}{w+t}\right) + 0.5 + \frac{w+t}{3l} \right]
Args:
length: Length of the strip in m.
width: Width of the strip in m.
thickness: Thickness of the strip in m.
Returns:
Self-inductance in Henries.
"""
return (μ_0 * length / (2 * π)) * (
jnp.log(2 * length / (width + thickness))
+ 0.5
+ (width + thickness) / (3 * length)
)
@partial(jax.jit, inline=True)
def mutual_inductance_parallel_strips(length: float, d: float) -> jax.Array:
r"""Analytical formula for the mutual inductance between two parallel metal strips.
Uses the formula from :cite:`chenCompactInductorcapacitorResonators2023`:
.. math::
L_m(d) = \frac{\mu_0 l}{2\pi} \left[ \ln \left( \frac{l}{d} + \sqrt{1 + \frac{l^2}{d^2}} \right) - \sqrt{1 + \frac{d^2}{l^2}} + \frac{d}{l} \right]
Args:
length: Length of the strips in m.
d: Center-to-center distance between the strips in m.
Returns:
Mutual inductance in Henries.
"""
return (μ_0 * length / (2 * π)) * (
jnp.log(length / d + jnp.sqrt(1 + (length / d) ** 2))
- jnp.sqrt(1 + (d / length) ** 2)
+ d / length
)
[docs]
@partial(jax.jit, inline=True)
def meander_inductor_inductance_analytical(
n_turns: int,
turn_length: float,
wire_width: float,
wire_gap: float,
sheet_inductance: float,
thickness: float | None = None,
) -> jax.Array:
r"""Analytical formula for meander inductor inductance.
The total inductance is the sum of geometric and kinetic contributions:
.. math::
L_{\text{total}} = L_g + L_k
The geometric inductance :math:`L_g` is calculated by summing the
self-inductances of all horizontal segments and the mutual inductances
between all pairs of parallel segments, following
:cite:`chenCompactInductorcapacitorResonators2023`:
.. math::
L_g = N L_s + 2 \sum_{k=1}^{N-1} (N-k) (-1)^k L_m(k p)
where :math:`N` is the number of turns and :math:`p` is the pitch.
The kinetic inductance :math:`L_k` is calculated from the sheet
inductance :math:`L_\square`:
.. math::
L_k = L_\square \cdot \frac{\ell_{\text{total}}}{w}
Args:
n_turns: Number of horizontal meander runs.
turn_length: Length of each horizontal run in µm.
wire_width: Width of the meander wire in µm.
wire_gap: Gap between adjacent meander runs in µm.
sheet_inductance: Sheet inductance per square in H/□.
thickness: Thickness of the metal film in µm. If None, it is
fetched from the PDK technology parameters.
Returns:
Total inductance in Henries.
"""
if thickness is None:
_h, thickness, _ep_r = get_cpw_substrate_params()
# Convert to SI (meters)
l_m = turn_length * 1e-6
w_m = wire_width * 1e-6
g_m = wire_gap * 1e-6
t_m = thickness * 1e-6
p_m = w_m + g_m # Pitch (center-to-center)
# 1. Geometric Inductance
# Self-inductance of horizontal segments (turns)
L_s_horiz = self_inductance_strip(l_m, w_m, t_m)
# Self-inductance of vertical connection segments
L_s_vert = self_inductance_strip(p_m, w_m, t_m)
# Mutual inductance sum between horizontal segments
# Formula: L_g = sum(L_s_i) + sum_{i!=j} M_ij
# Current directions alternate: sign(i, j) = (-1)**(i-j)
# L_g_horiz = N*L_s_horiz + 2 * sum_{i=0 to N-2} sum_{j=i+1 to N-1} (-1)**(j-i) * L_m(abs(j-i)*p)
# This simplifies to the sum used in Chen et al. (2023):
# L_g_horiz = N*L_s_horiz + 2 * sum_{k=1 to N-1} (N-k) * (-1)**k * L_m(k*p)
offsets = jnp.arange(1, 501)
mask = offsets < n_turns
L_m_sum = jnp.sum(
jnp.where(
mask,
(n_turns - offsets)
* ((-1.0) ** offsets)
* mutual_inductance_parallel_strips(l_m, offsets * p_m),
0.0,
)
)
# Ensure L_g_horiz calculation is accurate. The negative mutual inductance
# should be outweighed by the self-inductance for physically valid meanders.
L_g_horiz = n_turns * L_s_horiz + 2 * L_m_sum
L_g = L_g_horiz + (n_turns - 1) * L_s_vert
# 2. Kinetic Inductance
# Total wire length in µm (horizontal runs + vertical connections)
total_length_um = n_turns * turn_length + jnp.maximum(0, n_turns - 1) * wire_gap
n_squares = total_length_um / wire_width
L_k = sheet_inductance * n_squares
return L_g + L_k
[docs]
def meander_inductor(
*,
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
n_turns: int = 5,
turn_length: float = 200.0,
cross_section: CrossSectionSpec = "meander_inductor_cross_section",
sheet_inductance: float = 0.4e-12,
) -> sax.SDict:
r"""Meander inductor SAX model.
Computes the inductance from the meander geometry and returns
S-parameters of an equivalent lumped inductor.
The model extracts the center conductor width and gap from the provided
cross-section. To ensure the etched regions of adjacent meander runs
do not overlap and interfere with the characteristic impedance of each other,
the vertical pitch is calculated as:
.. math::
p = w + 2 \cdot g
where :math:`w` is the center conductor width and :math:`g` is the gap
width. This corresponds to a metal-to-metal spacing of :math:`2g`.
Args:
f: Array of frequency points in Hz.
n_turns: Number of horizontal meander runs.
turn_length: Length of each horizontal run in µm.
cross_section: Cross-section specification for the meander wire.
Used to determine the wire width and the gap between runs.
sheet_inductance: Sheet inductance per square in H/□.
Returns:
sax.SDict: S-parameters dictionary.
"""
f_arr = jnp.asarray(f)
wire_width, wire_gap_half = get_cpw_dimensions(cross_section)
wire_gap = 2 * wire_gap_half
inductance = meander_inductor_inductance_analytical(
n_turns=n_turns,
turn_length=turn_length,
wire_width=wire_width,
wire_gap=wire_gap,
sheet_inductance=sheet_inductance,
)
return inductor(f=f_arr, inductance=inductance)
[docs]
def lumped_element_resonator(
*,
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
fingers: int = 20,
finger_length: float = 20.0,
finger_gap: float = 2.0,
finger_thickness: float = 5.0,
n_turns: int = 5,
sheet_inductance: float = 0.4e-12,
cross_section: CrossSectionSpec = "meander_inductor_cross_section",
grounded: bool = False,
) -> sax.SDict:
r"""Lumped-element LC resonator SAX model.
Combines an interdigital capacitor and a meander inductor in parallel
to form an LC resonator. The resonance frequency is:
.. math::
f_r = \frac{1}{2\pi\sqrt{LC}}
where :math:`C` is computed from the interdigital capacitor geometry
using :func:`~qpdk.models.capacitor.interdigital_capacitor_capacitance_analytical`
and :math:`L` is computed from the meander inductor geometry using
:func:`meander_inductor_inductance_analytical`.
The inductor section uses the width and gap derived from the
`cross_section` to ensure consistent RF behavior across the meander.
The vertical spacing between meander runs is set to twice the etch gap
to prevent overlap of the etched regions.
See :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`.
Args:
f: Array of frequency points in Hz.
fingers: Number of interdigital capacitor fingers.
finger_length: Length of each capacitor finger in µm.
finger_gap: Gap between adjacent capacitor fingers in µm.
finger_thickness: Width of each capacitor finger in µm.
n_turns: Number of horizontal meander inductor runs (must be odd to
match the cell geometry where the path spans left-to-right bus bars).
sheet_inductance: Sheet inductance per square in H/□.
cross_section: Cross-section specification. Used for substrate
permittivity and to determine inductor wire width and gap.
grounded: If True, one port of the resonator is grounded.
Returns:
sax.SDict: S-parameters dictionary with ports o1 and o2.
"""
f_arr = jnp.asarray(f)
ep_r = cpw_ep_r_from_cross_section(cross_section)
capacitance = interdigital_capacitor_capacitance_analytical(
fingers=fingers,
finger_length=finger_length,
finger_gap=finger_gap,
thickness=finger_thickness,
ep_r=ep_r,
)
wire_width, wire_gap_half = get_cpw_dimensions(cross_section)
wire_gap = 2 * wire_gap_half
cap_width = 2 * finger_thickness + finger_length + finger_gap
meander_turn_length = cap_width - 4 * wire_width
inductance = meander_inductor_inductance_analytical(
n_turns=n_turns,
turn_length=meander_turn_length,
wire_width=wire_width,
wire_gap=wire_gap,
sheet_inductance=sheet_inductance,
)
return lc_resonator(
f=f_arr,
capacitance=capacitance,
inductance=inductance,
grounded=grounded,
)