"""Model of a planar LC resonator for inductive-detection electron spin resonance."""
import jax
import jax.numpy as jnp
import sax
from gdsfactory.typings import CrossSectionSpec
from jax.typing import ArrayLike
from sax.models.rf import capacitor, tee
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
from qpdk.models.generic import (
lc_resonator,
open,
)
from qpdk.models.inductor import self_inductance_strip
from qpdk.models.jit import jit
__all__ = [
"esr_lc_resonator",
"esr_lc_resonator_parameters",
"esr_mode_volume",
"thin_strip_magnetic_field",
]
# Reference impedance of the generic models composed in esr_lc_resonator.
_Z0 = 50.0
[docs]
def thin_strip_magnetic_field(
current: ArrayLike,
width: ArrayLike,
height: ArrayLike,
x: ArrayLike = 0.0,
) -> tuple[jax.Array, jax.Array]:
r"""Magnetic field of an infinite thin strip carrying a uniform current.
The strip lies in the plane :math:`y = 0` between :math:`x = \pm w/2` and
carries the current :math:`I` along :math:`+z`, spread uniformly over its
width. Integrating the field of a line current, :math:`\mu_0 I/(2\pi r)`,
across the strip with the Biot–Savart law
:cite:`griffithsIntroductionElectrodynamics2017` gives, for :math:`y > 0`,
.. math::
B_x &= -\frac{\mu_0 I}{2\pi w}\left[
\arctan\frac{x + w/2}{y} - \arctan\frac{x - w/2}{y}\right] \\
B_y &= \frac{\mu_0 I}{4\pi w}
\ln\frac{(x + w/2)^2 + y^2}{(x - w/2)^2 + y^2}
Above the centre of the strip, :math:`B_x = -\mu_0 I \arctan(w/2y)/(\pi w)`,
which tends to :math:`-\mu_0 I/(2w)` at the surface and to the line-current
field far away. By mirror symmetry the field at depth :math:`y` below the
strip, where the spins of an ESR sample sit, has :math:`B_x` reversed and
:math:`B_y` unchanged.
Uniform-current approximation: a superconducting strip carries most of its
current near the edges :cite:`bienfaitReachingQuantumLimit2016`, so close
to the strip the real field is larger near the edges and smaller above the
centre than this formula gives. The strip thickness is neglected.
Args:
current: Current :math:`I` in A.
width: Strip width :math:`w` in µm.
height: Position :math:`y` relative to the strip plane in µm, nonzero.
Negative values are below the strip.
x: Lateral position measured from the strip centre in µm.
Returns:
Tuple ``(B_x, B_y)`` in T.
"""
w = jnp.asarray(width) * 1e-6
y_signed = jnp.asarray(height) * 1e-6
# Evaluate at |y| and mirror B_x, so both half-spaces use the same branch.
y = jnp.abs(y_signed)
x_m = jnp.asarray(x) * 1e-6
prefactor = μ_0 * jnp.asarray(current) / (2 * π * w)
b_x = (
-jnp.sign(y_signed)
* prefactor
* (jnp.arctan2(x_m + w / 2, y) - jnp.arctan2(x_m - w / 2, y))
)
b_y = (
prefactor
/ 2
* jnp.log(((x_m + w / 2) ** 2 + y**2) / ((x_m - w / 2) ** 2 + y**2))
)
return b_x, b_y
[docs]
def esr_mode_volume(wire_width: ArrayLike, inductance: ArrayLike) -> jax.Array:
r"""Magnetic mode volume of an LC resonator with a thin-strip inductor.
The magnetic mode volume is the field energy over the energy density at the
spins, :math:`V_\text{m} = \int |\mathbf{B}|^2\,\mathrm{d}V / |\delta B_1|^2`.
The field energy of a current :math:`\delta i` in the inductance :math:`L` is
:math:`\int |\mathbf{B}|^2\,\mathrm{d}V = \mu_0 L\,\delta i^2`. Taking
:math:`\delta B_1` at the wire surface from
:func:`thin_strip_magnetic_field`, :math:`\delta B_1 = \mu_0 \delta i/(2w)`,
gives
.. math::
V_\text{m} = \frac{\mu_0 L\,\delta i^2}{\delta B_1^2} = \frac{4 w^2 L}{\mu_0},
independent of :math:`\delta i`. With the single-photon current
:math:`\delta i = \omega_0\sqrt{\hbar/2Z_\text{c}}` of
:cite:`ranjanElectronSpinResonance2020` this is the volume in
:math:`\delta B_1 = \sqrt{\mu_0 \hbar \omega_0 / 2 V_\text{m}}`.
Neither design reference states a closed-form mode volume; both quote
volumes from EM field simulations
:cite:`bienfaitReachingQuantumLimit2016,ranjanElectronSpinResonance2020`.
With their own :math:`L = Z_\text{c}/\omega_0` this estimate gives 0.077 nL
against "~0.02 nl" and 10.5 fL against "~6 fL": the same order of
magnitude, not a reproduction. The derivation counts :math:`L` as
magnetic-field energy, so pass the geometric inductance: kinetic inductance
stores energy in the electrons, not the field. The comparison with the
papers' :math:`Z_\text{c}/\omega_0` therefore overstates the volume where
kinetic inductance contributes, and for Ranjan's 10 µm wire most of that
:math:`L` is stray inductance of the capacitor rather than of the wire.
The uniform-current approximation of :func:`thin_strip_magnetic_field`
applies.
Args:
wire_width: Wire width :math:`w` in µm.
inductance: Inductance :math:`L` in H whose field energy is counted.
Returns:
Mode volume in µm³ (1 µm³ = 1 fL).
"""
return 4 * jnp.asarray(wire_width) ** 2 * jnp.asarray(inductance) / μ_0 * 1e6
[docs]
def esr_lc_resonator_parameters(
wire_width: float = 5.0,
wire_length: float = 730.0,
fingers: int = 12,
finger_length: float = 800.0,
finger_gap: float = 50.0,
finger_thickness: float = 50.0,
film_thickness: float = 0.05,
sheet_inductance: float = 0.4e-12,
coupling_capacitance: float = 8.5e-15,
ground_capacitance: float = 100e-15,
cross_section: CrossSectionSpec = "cpw",
) -> dict[str, jax.Array | float]:
r"""Circuit parameters of :func:`qpdk.cells.esr_lc_resonator`.
The resonator floats: the wire :math:`L` bridges the two electrodes of the
interdigital capacitor :math:`C`, as in
:cite:`ranjanElectronSpinResonance2020`. Each electrode has a capacitance
:math:`C_\text{g}` to the ground plane, and the electrode next to the
feedline couples to it through :math:`C_\text{c}`:
.. svgbob::
feedline ──┬──
│
Cc
│
A ──┬── L ──┬── B
│ └── C ──┘ │
Cg Cg
│ │
GND GND
The differential mode across :math:`L` sees :math:`C` in parallel with the
series combination of the two :math:`C_\text{g}` branches, one of them
loaded by :math:`C_\text{c}`:
.. math::
L &= L_\text{geo} + L_\square N_\square \\
C_\text{eff} &= C + \frac{C_\text{g} (C_\text{g} + C_\text{c})}
{2 C_\text{g} + C_\text{c}} \\
\omega_\text{r} &= \frac{1}{\sqrt{L C_\text{eff}}} \\
\alpha &= \frac{C_\text{g}}{2 C_\text{g} + C_\text{c}} \\
Q_\text{c} &= \frac{2 C_\text{eff}}
{\omega_\text{r} C_\text{c}^2 Z_0 \alpha^2}
:math:`L_\text{geo}` is the self-inductance of the wire as a rectangular
strip from :func:`~qpdk.models.inductor.self_inductance_strip`,
:math:`N_\square = \ell/w` its square count, and :math:`C` the interdigital
capacitance from
:func:`~qpdk.models.capacitor.interdigital_capacitor_capacitance_analytical`.
:math:`\alpha` is the fraction of the resonator voltage that appears on
electrode A, so only :math:`\alpha V` drives the coupling capacitor and
:math:`Q_\text{c}` grows as :math:`1/\alpha^2`. With :math:`C_\text{g} = 0`
the resonator has no return path and decouples from the feedline.
:math:`Q_\text{c}` follows from the feedline loading electrode A with
:math:`Z_0/2` through :math:`C_\text{c}`, valid for
:math:`\omega_\text{r} C_\text{c} Z_0 \ll 1`.
For the wire of :cite:`bienfaitReachingQuantumLimit2016` (5 µm × 730 µm,
50 nm film) :math:`L_\text{geo} = 0.90` nH, against the
:math:`Z_0/\omega_0 = 0.97` nH that the paper's 44 Ω and 7.26 GHz imply.
The defaults give :math:`f_\text{r} \approx 6.9` GHz and
:math:`Q_\text{c} \approx 3 \times 10^4`.
Assumptions:
* The resonator is linear and lossless: no kinetic-inductance
nonlinearity, and the internal quality factor is infinite.
* :math:`L` counts the wire only. The leads joining it to the bus bars and
the stray inductance of the electrodes are neglected. For the bowtie
capacitor of :cite:`ranjanElectronSpinResonance2020`,
:math:`Z_\text{c} \approx 15` Ω at 7.25 GHz implies
:math:`L \approx 0.33` nH, far above the ~11 pH geometric inductance of
its 10 µm wire, so this model underestimates :math:`L` for such short
wires.
* The resonator is coupled to an on-chip feedline. Both references instead
place it in a 3D cavity driven by antennas.
* The feedline and the circuit are referenced to :math:`Z_0 = 50` Ω, the
reference impedance of the generic models used in
:func:`esr_lc_resonator`; the default ``cpw`` cross-section is 49.3 Ω.
* :math:`C_\text{c}` and :math:`C_\text{g}` are parameters, not derived
from ``coupling_gap`` and ``ground_gap``, and both electrodes share one
:math:`C_\text{g}`. The 100 fF default for :math:`C_\text{g}` is an
order-of-magnitude coplanar estimate for ~2 mm of electrode edge 50 µm
from the ground plane. The 8.5 fF default for :math:`C_\text{c}` is chosen
to give :math:`Q_\text{c} \approx 3 \times 10^4`, the external quality
factor of :cite:`ranjanElectronSpinResonance2020`; it does not describe
the default cell, whose 1.15 mm bus bar 50 µm from the feedline is of
order 0.1 pF. Set both from a capacitance extraction of the layout.
* The 0.4 pH/□ sheet inductance is the PDK default for the base metal; the
references do not state one for their 50 nm Al films.
Args:
wire_width: Wire width in µm.
wire_length: Wire length in µm.
fingers: Number of 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.
film_thickness: Film thickness in µm, used for :math:`L_\text{geo}`.
sheet_inductance: Kinetic sheet inductance :math:`L_\square` in H/□.
coupling_capacitance: Coupling capacitance :math:`C_\text{c}` in F.
ground_capacitance: Capacitance :math:`C_\text{g}` of each electrode to
ground in F.
cross_section: Feedline cross-section, used for the substrate
permittivity.
Returns:
Dictionary with ``wire_squares``, ``geometric_inductance``,
``kinetic_inductance``, ``inductance`` (H), ``capacitance`` (F),
``coupling_capacitance`` (F), ``ground_capacitance`` (F),
``effective_capacitance`` (F), ``z0`` (Ω), ``resonance_frequency`` (Hz)
and ``coupling_quality_factor``.
"""
wire_squares = wire_length / wire_width
geometric = self_inductance_strip(
wire_length * 1e-6, wire_width * 1e-6, film_thickness * 1e-6
)
kinetic = sheet_inductance * wire_squares
inductance = geometric + kinetic
ep_r = cpw_ep_r_from_cross_section(cross_section)
capacitance = interdigital_capacitor_capacitance_analytical(
fingers, finger_length, finger_gap, finger_thickness, ep_r
)
c_c, c_g = coupling_capacitance, ground_capacitance
c_eff = capacitance + c_g * (c_g + c_c) / (2 * c_g + c_c)
alpha = c_g / (2 * c_g + c_c)
omega_r = 1 / jnp.sqrt(inductance * c_eff)
q_c = 2 * c_eff / (omega_r * c_c**2 * _Z0 * alpha**2)
return {
"wire_squares": wire_squares,
"geometric_inductance": geometric,
"kinetic_inductance": kinetic,
"inductance": inductance,
"capacitance": capacitance,
"coupling_capacitance": c_c,
"ground_capacitance": c_g,
"effective_capacitance": c_eff,
"z0": _Z0,
"resonance_frequency": omega_r / (2 * π),
"coupling_quality_factor": q_c,
}
[docs]
@jit(static_argnames=["cross_section"])
def esr_lc_resonator(
*,
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
wire_width: float = 5.0,
wire_length: float = 730.0,
fingers: int = 12,
finger_length: float = 800.0,
finger_gap: float = 50.0,
finger_thickness: float = 50.0,
film_thickness: float = 0.05,
sheet_inductance: float = 0.4e-12,
coupling_capacitance: float = 8.5e-15,
ground_capacitance: float = 100e-15,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
r"""SAX model of :func:`qpdk.cells.esr_lc_resonator`.
The circuit of :func:`~qpdk.models.esr.esr_lc_resonator_parameters` is
assembled from generic models: a :func:`sax.models.rf.tee` on the
feedline, the coupling :func:`sax.models.rf.capacitor`, and the floating
:func:`~qpdk.models.generic.lc_resonator` with its ``ground_capacitance``,
whose far port is left open with :func:`~qpdk.models.generic.open`
:cite:`m.pozarMicrowaveEngineering2012`. :math:`|S_{21}|` vanishes at
:math:`\omega_\text{r}`. The feedline is treated as lossless and of zero
length. At DC or with :math:`C_\text{c} = 0` the branch is open and the
feedline transmits fully.
Design references are
:cite:`bienfaitReachingQuantumLimit2016,ranjanElectronSpinResonance2020`.
Args:
f: Array of frequency points in Hz.
wire_width: Wire width in µm.
wire_length: Wire length in µm.
fingers: Number of 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.
film_thickness: Film thickness in µm.
sheet_inductance: Kinetic sheet inductance in H/□.
coupling_capacitance: Coupling capacitance in F.
ground_capacitance: Capacitance of each electrode to ground in F.
cross_section: Feedline cross-section.
Returns:
sax.SDict: S-parameters with ports ``o1`` and ``o2``.
"""
p = esr_lc_resonator_parameters(
wire_width=wire_width,
wire_length=wire_length,
fingers=fingers,
finger_length=finger_length,
finger_gap=finger_gap,
finger_thickness=finger_thickness,
film_thickness=film_thickness,
sheet_inductance=sheet_inductance,
coupling_capacitance=coupling_capacitance,
ground_capacitance=ground_capacitance,
cross_section=cross_section,
)
# At DC or with C_c = 0 the open-ended resonator is a lossless island the
# circuit solve cannot resolve; the feedline then sees an open branch. Feed
# the solve safe values there so neither the result nor its gradient is NaN.
f = jnp.asarray(f)
c_c = p["coupling_capacitance"]
isolated = (f == 0) | (c_c == 0)
f_safe = jnp.where(isolated, 1.0, f)
c_c_safe = jnp.where(c_c == 0, 1e-15, c_c)
instances: dict[str, sax.SType] = {
"tee": tee(f=f_safe),
"cc": capacitor(f=f_safe, capacitance=c_c_safe, z0=_Z0),
"lc": lc_resonator(
f=f_safe,
capacitance=p["capacitance"],
inductance=p["inductance"],
ground_capacitance=p["ground_capacitance"],
),
"end": open(f=f_safe),
}
connections = {
"tee,o3": "cc,o1",
"cc,o2": "lc,o1",
"lc,o2": "end,o1",
}
ports = {"o1": "tee,o1", "o2": "tee,o2"}
s = sax.evaluate_circuit_fg((connections, ports), instances)
return {
(a, b): jnp.where(isolated, float(a != b), s[a, b])
for a in ("o1", "o2")
for b in ("o1", "o2")
}