"""Model of a lumped resonator made of a high-kinetic-inductance nanowire."""
import jax
import jax.numpy as jnp
import sax
from gdsfactory.typings import CrossSectionSpec
from sax.models.rf import capacitor, electrical_short, inductor, tee
from qpdk.models.capacitor import plate_capacitor_capacitance_analytical
from qpdk.models.constants import DEFAULT_FREQUENCY, π
from qpdk.models.cpw import (
cpw_ep_r_from_cross_section,
cpw_z0_from_cross_section,
get_cpw_dimensions,
)
from qpdk.models.inductor import meander_inductor_inductance_analytical
from qpdk.models.math import capacitance_per_length_conformal
from qpdk.models.waveguides import straight
from qpdk.tech import (
MeanderCornerStyle,
meander_n_squares_drawn,
validate_nanowire_layout,
)
__all__ = [
"grounded_pad_capacitance_analytical",
"nanowire_pad_capacitances_analytical",
"nanowire_resonator",
"nanowire_resonator_parameters",
]
[docs]
def grounded_pad_capacitance_analytical(
pad_width: jax.typing.ArrayLike,
pad_height: jax.typing.ArrayLike,
gap: jax.typing.ArrayLike,
ep_r: jax.typing.ArrayLike,
) -> jax.Array:
r"""Capacitance between a coplanar pad and a ground plane edge.
The pad is a strip of width :math:`W` (``pad_height``) and length
:math:`\ell` (``pad_width``) at distance :math:`s` from the straight edge
of a semi-infinite ground plane in the same plane.
The 2D capacitance of two coplanar electrodes on a line depends only on the
cross ratio of their four edge positions, because a Möbius map of the half
plane preserves both the cross ratio and the capacitance. For edges at
:math:`-\infty, 0, s, s + W` the cross ratio is :math:`\lambda = W/(W + s)`,
and two symmetric pads with edges at :math:`\pm 1, \pm 1/k` have
:math:`\lambda = \bigl((1 - k)/(1 + k)\bigr)^2`. So the grounded pad has the
capacitance of symmetric coplanar pads with modulus
.. math::
k = \frac{1 - \sqrt{\lambda}}{1 + \sqrt{\lambda}},
which :func:`~qpdk.models.capacitor.plate_capacitor_capacitance_analytical`
evaluates :cite:`chenCompactInductorcapacitorResonators2023`. Fringe fields
at the two ends of the pad are neglected.
The cross-ratio mapping above is derived here from conformal invariance,
not quoted from a reference; ``tests/models/test_nanowire.py`` checks it
against the symmetric-pad formula.
Args:
pad_width: Pad length :math:`\ell` along the ground edge in µm.
pad_height: Pad width :math:`W` across the gap in µm.
gap: Gap :math:`s` between the pad and the ground edge in µm.
ep_r: Relative permittivity of the substrate.
Returns:
Capacitance in F.
"""
sqrt_lambda = jnp.sqrt(pad_height / (pad_height + gap))
k = (1 - sqrt_lambda) / (1 + sqrt_lambda)
return plate_capacitor_capacitance_analytical(
length=pad_width, width=(1 - k) / 2, gap=k, ep_r=ep_r
)
def _coplanar_strips_capacitance_per_length(
cross_ratio: jax.typing.ArrayLike, ep_r: jax.typing.ArrayLike
) -> jax.Array:
r"""Capacitance per length in F/m of two coplanar electrodes with cross ratio :math:`\lambda`.
See :func:`grounded_pad_capacitance_analytical` for the mapping to the modulus
:math:`k = (1 - \sqrt{\lambda})/(1 + \sqrt{\lambda})`.
Returns:
Capacitance per unit length in F/m.
"""
sqrt_lambda = jnp.sqrt(cross_ratio)
k = (1 - sqrt_lambda) / (1 + sqrt_lambda)
return capacitance_per_length_conformal(1 - k**2, ep_r)
def _cross_ratio(
a: jax.typing.ArrayLike,
b: jax.typing.ArrayLike,
c: jax.typing.ArrayLike,
d: jax.typing.ArrayLike,
) -> jax.Array:
r"""Cross ratio of the electrodes ``[a, b]`` and ``[c, d]`` on the real line.
``d < a`` means the second electrode wraps through infinity, as for a ground
plane on both sides.
Returns:
The cross ratio :math:`\lambda`.
"""
a, b, c, d = map(jnp.asarray, (a, b, c, d))
return (b - a) * (d - c) / ((c - a) * (d - b))
[docs]
def nanowire_pad_capacitances_analytical(
pad_width: jax.typing.ArrayLike,
pad_height: jax.typing.ArrayLike,
pad_separation: jax.typing.ArrayLike,
pad_ground_gap: jax.typing.ArrayLike,
coupling_gap: jax.typing.ArrayLike,
centre_width: jax.typing.ArrayLike,
centre_gap: jax.typing.ArrayLike,
ep_r: jax.typing.ArrayLike,
) -> tuple[jax.Array, jax.Array]:
r"""Pad-to-ground and pad-to-feedline capacitance of the nanowire resonator.
All conductors are zero-thickness sheets on the substrate surface, so each
2D cross-section is a set of electrodes on a line and its capacitances follow
from conformal mapping with :math:`\varepsilon_\text{eff} = (\varepsilon_\text{r} + 1)/2`.
Two such cross-sections are superposed:
* Across the feedline, along the pad width :math:`\ell`, the line holds the
ground pad (a half line, at ``pad_separation`` :math:`d` from the pad), the
pad of height :math:`h`, the feedline centre conductor of width :math:`w`
at ``coupling_gap`` :math:`g`, and the feedline ground at ``centre_gap``
:math:`s`. With :math:`C_{X}` the capacitance between electrode :math:`X`
and the other two shorted together, the mutual capacitance of pad and
centre conductor is
.. math::
C_\text{c} = \frac{\ell}{2} \bigl(C_\text{pad} + C_\text{centre} - C_\text{ground}\bigr),
an approximation to the three-electrode mutual capacitance, within
the method-of-moments band given below. Each :math:`C_X` is a two-electrode
capacitance with the modulus of
:func:`grounded_pad_capacitance_analytical`.
* The pad-to-ground capacitance is the pad against the ground pad alone,
from :func:`grounded_pad_capacitance_analytical` with gap :math:`d`, plus
the pad against the side grounds at ``pad_ground_gap`` over its height
:math:`h`, as a CPW centre conductor of width :math:`\ell`.
Superposing the two directions neglects the pad corners and ends. Checked
against a method-of-moments extraction of the drawn 3D geometry (pad, ground
pad, side grounds and a 10/6 µm feedline on silicon) at five geometries
with :math:`\ell` = 200–300 µm, :math:`h` = 80–150 µm, :math:`d` = 40–60 µm,
:math:`g` = 4–10 µm and ``pad_ground_gap`` = 20–50 µm,
:math:`C_\text{c}` agrees within −10 % to +2 %, low for larger
:math:`g` because end fringing to the centre conductor is neglected, and the
pad-to-ground capacitance within +3 % to +7 %. The reference values are in
``tests/models/test_nanowire.py``.
Args:
pad_width: Pad size :math:`\ell` along the feedline in µm.
pad_height: Pad size :math:`h` across the feedline in µm.
pad_separation: Gap :math:`d` between the pad and the ground pad in µm.
pad_ground_gap: Gap between the pad and the side grounds in µm.
coupling_gap: Gap :math:`g` between the pad and the centre conductor in µm.
centre_width: Feedline centre conductor width :math:`w` in µm.
centre_gap: Feedline gap :math:`s` in µm.
ep_r: Relative permittivity of the substrate.
Returns:
Tuple of the pad-to-ground capacitance and the coupling capacitance
:math:`C_\text{c}`, both in F.
"""
pad_width, pad_height, pad_separation, pad_ground_gap = map(
jnp.asarray, (pad_width, pad_height, pad_separation, pad_ground_gap)
)
# Electrode edges across the feedline, from the ground pad up.
ground_bottom = -pad_separation
pad_bottom, pad_top = 0.0, pad_height
centre_bottom = pad_top + coupling_gap
centre_top = centre_bottom + centre_width
ground_top = centre_top + centre_gap
c_pad = _coplanar_strips_capacitance_per_length(
_cross_ratio(pad_bottom, pad_top, centre_bottom, ground_bottom), ep_r
)
c_centre = _coplanar_strips_capacitance_per_length(
_cross_ratio(centre_bottom, centre_top, ground_top, pad_top), ep_r
)
c_ground = _coplanar_strips_capacitance_per_length(
_cross_ratio(pad_bottom, centre_top, ground_top, ground_bottom), ep_r
)
coupling = (c_pad + c_centre - c_ground) / 2 * pad_width * 1e-6
k_side = pad_width / (pad_width + 2 * pad_ground_gap)
side = 4 * capacitance_per_length_conformal(k_side**2, ep_r) * pad_height * 1e-6
bottom = grounded_pad_capacitance_analytical(
pad_width, pad_height, pad_separation, ep_r
)
return bottom + side, coupling
def nanowire_resonator_parameters(
wire_width: float = 0.1,
wire_length: float = 40.0,
n_turns: int = 1,
wire_gap: float | None = None,
lead_length: float | None = None,
corner_style: MeanderCornerStyle = "sharp",
radius: float | None = None,
pad_width: float = 300.0,
pad_height: float = 150.0,
pad_ground_gap: float = 50.0,
coupling_gap: float = 4.0,
sheet_inductance: float = 40e-12,
thickness: float = 0.015,
coupling_capacitance: float | None = None,
cross_section: CrossSectionSpec = "cpw",
) -> dict[str, jax.Array | float]:
r"""Circuit parameters of :func:`qpdk.cells.nanowire_resonator`.
The resonator is a linear LC to ground, coupled to the feedline through
:math:`C_\text{c}`:
.. math::
L &= L_\square N_\square + L_\text{geo} \\
\omega_\text{r} &= \frac{1}{\sqrt{L (C + C_\text{c})}} \\
Q_\text{c} &= \frac{2 (C + C_\text{c})}{\omega_\text{r} C_\text{c}^2 Z_0}
:math:`N_\square` is the drawn square count from
:func:`qpdk.tech.meander_n_squares_drawn` plus ``lead_length / wire_width``
for each of the two leads, :math:`L_\text{geo}` the geometric
inductance of the sharp meander from
:func:`~qpdk.models.inductor.meander_inductor_inductance_analytical`, and
:math:`C` and :math:`C_\text{c}` the pad-to-ground and pad-to-feedline
capacitances from :func:`nanowire_pad_capacitances_analytical` for the drawn
pads, with the pad separation ``wire_length + 2 * lead_length``.
:math:`Q_\text{c}` follows from the feedline loading the resonator with
:math:`Z_0/2` through :math:`C_\text{c}`, valid for
:math:`\omega_\text{r} C_\text{c} Z_0 \ll 1`.
Assumptions:
* The kinetic inductance is linear: no current or power dependence.
* :math:`C` and :math:`C_\text{c}` are closed-form approximations that
neglect the pad corners and ends. Against a method-of-moments
extraction they agree within the bands stated in
:func:`nanowire_pad_capacitances_analytical`, which shift
:math:`f_\text{r}` by a few percent. Pass ``coupling_capacitance`` to
use an extracted value instead.
* The wire and its leads add no capacitance.
* The feedline is a lossless CPW of ``cross_section``, and the resonator
couples to it at a single point, its midpoint.
* Losses are not modelled: the internal quality factor is infinite.
Settings the cell rejects raise :class:`ValueError` through
:func:`~qpdk.tech.validate_nanowire_layout`.
Args:
wire_width: Nanowire width in µm.
wire_length: Length of each nanowire run in µm.
n_turns: Number of nanowire runs.
wire_gap: Gap between runs in µm. Defaults to ``wire_width``.
lead_length: Length of each straight lead between the meander and a
pad in µm. Defaults to ``wire_gap``.
corner_style: Corner style of a meandered wire.
radius: Centreline radius for ``"round"`` corners in µm.
pad_width: Pad size along the feedline in µm.
pad_height: Pad size across the feedline in µm.
pad_ground_gap: Gap between the pads and the side grounds in µm.
coupling_gap: Gap between the upper pad and the feedline centre
conductor in µm.
sheet_inductance: Kinetic sheet inductance :math:`L_\square` in H/□.
The PDK has no calibrated NbTiN value. The 40 pH/□ default is the
TiN film of :cite:`joshiStrongKineticinductanceKerr2022`, an example
only; set it from a measurement of the film in use.
thickness: Film thickness in µm, used for :math:`L_\text{geo}`.
coupling_capacitance: Coupling capacitance :math:`C_\text{c}` in F.
``None`` derives it from the layout.
cross_section: Feedline cross-section, used for the substrate
permittivity, the feedline dimensions and :math:`Z_0`.
Returns:
Dictionary with ``n_squares``, ``kinetic_inductance``,
``geometric_inductance``, ``inductance`` (H), ``capacitance`` (F),
``coupling_capacitance`` (F), ``z0`` (Ω), ``resonance_frequency`` (Hz)
and ``coupling_quality_factor``.
"""
wire_gap = wire_width if wire_gap is None else wire_gap
lead_length = wire_gap if lead_length is None else lead_length
validate_nanowire_layout(
wire_width=wire_width,
n_turns=n_turns,
wire_gap=wire_gap,
wire_length=wire_length,
lead_length=lead_length,
pad_width=pad_width,
pad_height=pad_height,
pad_ground_gap=pad_ground_gap,
coupling_gap=coupling_gap,
)
n_squares = (
meander_n_squares_drawn(
n_turns, wire_length, wire_width, wire_gap, corner_style, radius
)
+ 2 * lead_length / wire_width
)
kinetic = sheet_inductance * n_squares
geometric = meander_inductor_inductance_analytical(
n_turns=n_turns,
turn_length=wire_length,
wire_width=wire_width,
wire_gap=wire_gap,
sheet_inductance=0.0,
thickness=thickness,
)
inductance = kinetic + geometric
ep_r = cpw_ep_r_from_cross_section(cross_section)
centre_width, centre_gap = get_cpw_dimensions(cross_section)
capacitance, layout_coupling = nanowire_pad_capacitances_analytical(
pad_width,
pad_height,
wire_length + 2 * lead_length,
pad_ground_gap,
coupling_gap,
centre_width,
centre_gap,
ep_r,
)
c_c = layout_coupling if coupling_capacitance is None else coupling_capacitance
z0 = jnp.real(cpw_z0_from_cross_section(cross_section))
c_total = capacitance + c_c
omega_r = 1 / jnp.sqrt(inductance * c_total)
q_c = 2 * c_total / (omega_r * c_c**2 * z0)
return {
"n_squares": n_squares,
"kinetic_inductance": kinetic,
"geometric_inductance": geometric,
"inductance": inductance,
"capacitance": capacitance,
"coupling_capacitance": c_c,
"z0": z0,
"resonance_frequency": omega_r / (2 * π),
"coupling_quality_factor": q_c,
}
[docs]
def nanowire_resonator(
*,
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
wire_width: float = 0.1,
wire_length: float = 40.0,
n_turns: int = 1,
wire_gap: float | None = None,
lead_length: float | None = None,
corner_style: MeanderCornerStyle = "sharp",
radius: float | None = None,
pad_width: float = 300.0,
pad_height: float = 150.0,
pad_ground_gap: float = 50.0,
coupling_gap: float = 4.0,
sheet_inductance: float = 40e-12,
thickness: float = 0.015,
coupling_capacitance: float | None = None,
feedline_length: float = 500.0,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
r"""SAX model of :func:`qpdk.cells.nanowire_resonator`.
The circuit is built from generic SAX elements
:cite:`m.pozarMicrowaveEngineering2012`. The feedline is two
:func:`~qpdk.models.straight` halves of ``feedline_length / 2``, joined by a
:func:`~sax.models.rf.tee`. The third tee port leads through the coupling
:func:`~sax.models.rf.capacitor` :math:`C_\text{c}` to the resonator node,
where a :func:`~sax.models.rf.capacitor` :math:`C` and an
:func:`~sax.models.rf.inductor` :math:`L` each end in an
:func:`~sax.models.rf.electrical_short` to ground:
.. svgbob::
o1 ─── feed_1 ───┬─── feed_2 ─── o2
│
Cc
│
┌────┴────┐
C L
│ │
GND GND
:math:`|S_{21}|` vanishes at
:math:`\omega_\text{r} = 1/\sqrt{L (C + C_\text{c})}`. Circuit values and
assumptions are those of
:func:`~qpdk.models.nanowire.nanowire_resonator_parameters`.
Design references for the nanowire are
:cite:`joshiStrongKineticinductanceKerr2022,maleevaCircuitQuantumElectrodynamics2018`.
Settings the cell rejects raise :class:`ValueError` through
:func:`~qpdk.tech.validate_nanowire_layout`.
Args:
f: Array of frequency points in Hz.
wire_width: Nanowire width in µm.
wire_length: Length of each nanowire run in µm.
n_turns: Number of nanowire runs.
wire_gap: Gap between runs in µm. Defaults to ``wire_width``.
lead_length: Length of each straight lead between the meander and a
pad in µm. Defaults to ``wire_gap``.
corner_style: Corner style of a meandered wire.
radius: Centreline radius for ``"round"`` corners in µm.
pad_width: Pad size along the feedline in µm.
pad_height: Pad size across the feedline in µm.
pad_ground_gap: Gap between the pads and the side grounds in µm.
coupling_gap: Gap between the upper pad and the feedline in µm.
sheet_inductance: Kinetic sheet inductance in H/□. The default is a TiN
example, not a calibrated NbTiN value.
thickness: Film thickness in µm.
coupling_capacitance: Coupling capacitance in F. ``None`` derives it
from the layout.
feedline_length: Length of the feedline in µm.
cross_section: Feedline cross-section.
Returns:
sax.SDict: S-parameters with ports ``o1`` and ``o2``.
"""
gap = wire_width if wire_gap is None else wire_gap
validate_nanowire_layout(
wire_width=wire_width,
n_turns=n_turns,
wire_gap=gap,
wire_length=wire_length,
lead_length=gap if lead_length is None else lead_length,
pad_width=pad_width,
pad_height=pad_height,
pad_ground_gap=pad_ground_gap,
coupling_gap=coupling_gap,
feedline_length=feedline_length,
)
p = nanowire_resonator_parameters(
wire_width=wire_width,
wire_length=wire_length,
n_turns=n_turns,
wire_gap=wire_gap,
lead_length=lead_length,
corner_style=corner_style,
radius=radius,
pad_width=pad_width,
pad_height=pad_height,
pad_ground_gap=pad_ground_gap,
coupling_gap=coupling_gap,
sheet_inductance=sheet_inductance,
thickness=thickness,
coupling_capacitance=coupling_capacitance,
cross_section=cross_section,
)
f_arr = jnp.asarray(f)
z0 = p["z0"]
instances: dict[str, sax.SType] = {
"feed_1": straight(
f=f_arr, length=feedline_length / 2, cross_section=cross_section
),
"feed_2": straight(
f=f_arr, length=feedline_length / 2, cross_section=cross_section
),
"tee_feed": tee(f=f_arr),
"coupling": capacitor(f=f_arr, capacitance=p["coupling_capacitance"], z0=z0),
"tee_node": tee(f=f_arr),
"pad": capacitor(f=f_arr, capacitance=p["capacitance"], z0=z0),
"wire": inductor(f=f_arr, inductance=p["inductance"], z0=z0),
"ground_pad": electrical_short(f=f_arr),
"ground_wire": electrical_short(f=f_arr),
}
connections = {
"feed_1,o2": "tee_feed,o1",
"tee_feed,o2": "feed_2,o1",
"tee_feed,o3": "coupling,o1",
"coupling,o2": "tee_node,o1",
"tee_node,o2": "pad,o1",
"pad,o2": "ground_pad,o1",
"tee_node,o3": "wire,o1",
"wire,o2": "ground_wire,o1",
}
ports = {"o1": "feed_1,o1", "o2": "feed_2,o2"}
return sax.evaluate_circuit_fg((connections, ports), instances)