"""Waveguides."""
import jax
import jax.numpy as jnp
import sax
from gdsfactory.typings import CrossSectionSpec, Size
from sax.models.rf import (
coplanar_waveguide as _sax_coplanar_waveguide,
electrical_open,
electrical_short,
microstrip as _sax_microstrip,
)
from qpdk.models.constants import DEFAULT_FREQUENCY, ε_0, π
from qpdk.models.cpw import (
cpw_parameters,
get_cpw_dimensions,
get_cpw_substrate_params,
propagation_constant,
transmission_line_s_params,
)
from qpdk.models.generic import shunt_admittance, tee
from qpdk.models.jit import jit
from qpdk.tech import CHIP_LAYERS, Chip, coplanar_waveguide, launcher_cross_section_big
[docs]
def straight(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.FloatArrayLike = 10.0,
cross_section: CrossSectionSpec = "cpw",
conductor_backed: bool = False,
) -> sax.SDict:
r"""S-parameter model for a straight coplanar waveguide.
Wraps :func:`sax.models.rf.coplanar_waveguide`, extracting the physical
dimensions from the given *cross_section* and the PDK layer stack.
With ``conductor_backed=True`` the line is modelled as a conductor-backed
CPW instead, see :func:`~qpdk.models.cbcpw_parameters`.
Computes S-parameters analytically using conformal-mapping CPW theory
following Simons :cite:`simonsCoplanarWaveguideCircuits2001` (ch. 2)
and the Qucs-S CPW model (`Qucs technical documentation`_, §12.4).
Conductor thickness corrections use the first-order model of
Gupta, Garg, Bahl, and Bhartia :cite:`guptaMicrostripLinesSlotlines1996`.
.. _Qucs technical documentation:
https://qucs.sourceforge.net/docs/technical/technical.pdf
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
conductor_backed: If True, the substrate backside is metallised, see
:data:`~qpdk.tech.LAYER_STACK_BACKSIDE`.
Returns:
sax.SDict: S-parameters dictionary
"""
width, gap = get_cpw_dimensions(cross_section)
h, t, ep_r, tand = get_cpw_substrate_params()
s_cpw = _sax_coplanar_waveguide(
f=f,
length=length,
width=width,
gap=gap,
thickness=t,
substrate_thickness=h,
ep_r=ep_r,
tand=tand,
)
# sax may pass ``conductor_backed`` as a traced array inside jitted circuits,
# so select between both results instead of branching in Python.
f = jnp.asarray(f)
ep_eff, z0 = cpw_parameters(width, gap, tand=0.0, conductor_backed=True)
gamma = propagation_constant(f.ravel(), ep_eff.real, tand=tand, ep_r=ep_r)
s11, s21 = transmission_line_s_params(gamma, z0, jnp.asarray(length) * 1e-6)
s_cb = sax.reciprocal({
("o1", "o1"): s11.reshape(f.shape),
("o1", "o2"): s21.reshape(f.shape),
("o2", "o2"): s11.reshape(f.shape),
})
backed = jnp.asarray(conductor_backed, dtype=bool)
return {k: jnp.where(backed, s_cb[k], s_cpw[k]) for k in s_cpw}
[docs]
def straight_all_angle(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 10.0,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
r"""S-parameter model for a straight coplanar waveguide.
Same as :func:`straight`.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
return straight(f=f, length=length, cross_section=cross_section)
[docs]
def straight_microstrip(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 1000,
width: sax.Float = 10.0,
h: sax.Float = 500.0,
t: sax.Float = 0.2,
ep_r: sax.Float = 11.45,
tand: sax.Float = 0.0,
) -> sax.SDict:
r"""S-parameter model for a straight microstrip transmission line.
Wraps :func:`sax.models.rf.microstrip`, mapping the qpdk parameter
names to the upstream model.
Computes S-parameters analytically using the Hammerstad-Jensen
:cite:`hammerstadAccurateModelsMicrostrip1980` closed-form expressions
for effective permittivity and characteristic impedance, as described
in Pozar :cite:`m.pozarMicrowaveEngineering2012` (ch. 3, §3.8).
Conductor thickness corrections follow
Gupta et al. :cite:`guptaMicrostripLinesSlotlines1996` (§2.2.4).
Args:
f: Array of frequency points in Hz.
length: Physical length in µm.
width: Strip width in µm.
h: Substrate height in µm.
t: Conductor thickness in µm (default 0.2 µm = 200 nm).
ep_r: Relative permittivity of the substrate (default 11.45 for Si).
tand: Dielectric loss tangent (default 0 — lossless).
Returns:
sax.SDict: S-parameters dictionary.
"""
return _sax_microstrip(
f=f,
length=length,
width=width,
substrate_thickness=h,
thickness=t,
ep_r=ep_r,
tand=tand,
)
[docs]
def straight_shorted(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 10.0,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for a straight waveguide with one shorted end.
This may be used to model a quarter-wave coplanar waveguide resonator.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
instances: dict[str, sax.SType] = {
"straight": straight(f=f, length=length, cross_section=cross_section),
"short": electrical_short(f=f),
}
connections = {
"straight,o2": "short,o1",
}
ports = {
"o1": "straight,o1",
}
return sax.evaluate_circuit_fg((connections, ports), instances)
[docs]
def straight_open(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 10.0,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for a straight waveguide with one open end.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
instances: dict[str, sax.SType] = {
"straight": straight(f=f, length=length, cross_section=cross_section),
"open": electrical_open(f=f),
}
connections = {
"straight,o2": "open,o1",
}
ports = {
"o1": "straight,o1",
}
return sax.evaluate_circuit_fg((connections, ports), instances)
[docs]
def straight_double_open(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 10.0,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for a straight waveguide with open ends.
Note:
Ports ``o1`` and ``o2`` are internally open-circuited and should not be used.
They are provided to match the number of ports in the layout component.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
instances: dict[str, sax.SType] = {
"straight": straight(f=f, length=length, cross_section=cross_section),
"open1": electrical_open(f=f, n_ports=2),
"open2": electrical_open(f=f, n_ports=2),
}
connections = {
"straight,o1": "open1,o1",
"straight,o2": "open2,o1",
}
ports = {
"o1": "open1,o2", # don't use: opened!
"o2": "open2,o2", # don't use: opened!
}
return sax.backends.evaluate_circuit_fg((connections, ports), instances)
[docs]
@jit(static_argnames=["west", "east", "north", "south"])
def nxn(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
west: int = 1,
east: int = 1,
north: int = 1,
south: int = 1,
) -> sax.SDict:
"""NxN junction model using tee components.
This model creates an N-port divider/combiner by chaining 3-port tee
junctions. All ports are connected to a single node.
Args:
f: Array of frequency points in Hz.
west: Number of ports on the west side.
east: Number of ports on the east side.
north: Number of ports on the north side.
south: Number of ports on the south side.
Returns:
sax.SDict: S-parameters dictionary with ports o1, o2, ..., oN.
Raises:
ValueError: If total number of ports is not positive.
"""
f = jnp.asarray(f)
n_ports = west + east + north + south
if n_ports <= 0:
raise ValueError("Total number of ports must be positive.")
if n_ports == 1:
return electrical_open(f=f)
if n_ports == 2:
return electrical_short(f=f, n_ports=2)
instances: dict[str, sax.SType] = {f"tee_{i}": tee(f=f) for i in range(n_ports - 2)}
connections = {f"tee_{i},o3": f"tee_{i + 1},o1" for i in range(n_ports - 3)}
ports = {
"o1": "tee_0,o1",
"o2": "tee_0,o2",
}
for i in range(1, n_ports - 2):
ports[f"o{i + 2}"] = f"tee_{i},o2"
# Last tee's o3 is the last external port
ports[f"o{n_ports}"] = f"tee_{n_ports - 3},o3"
return sax.evaluate_circuit_fg((connections, ports), instances)
[docs]
@jit(inline=True)
def airbridge(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
cpw_width: sax.Float = 10.0,
bridge_width: sax.Float = 8.0,
airgap_height: sax.Float = 3.0,
loss_tangent: sax.Float = 1.2e-8,
) -> sax.SDict:
r"""S-parameter model for a superconducting CPW airbridge.
The airbridge is modeled as a lumped lossy shunt admittance to ground
(shunt capacitance plus dielectric loss) across the CPW at the bridge
location.
Ports ``o1`` and ``o2`` are the CPW reference planes immediately either
side of the bridge, both referenced to :math:`Z_0 = 50\,\Omega`. The
bridge footprint adds no transmission line length, so the two reference
planes coincide. The landing pads of the layout cell (``e1`` and ``e2``,
on the ``AB_VIA`` layer) contact the ground plane and are *not* RF ports
of this model.
Parallel plate capacitor model is as done in :cite:`chenFabricationCharacterizationAluminum2014`
The default value for the loss tangent :math:`\tan\,\delta` is also taken from there.
Args:
f: Array of frequency points in Hz
cpw_width: Width of the CPW center conductor in µm.
bridge_width: Width of the airbridge in µm.
airgap_height: Height of the airgap in µm.
loss_tangent: Dielectric loss tangent of the supporting layer/residues.
Returns:
sax.SDict: S-parameters dictionary
"""
f = jnp.asarray(f)
ω = 2 * π * f
# Parallel plate capacitance
c_pp = (ε_0 * cpw_width * 1e-6 * bridge_width * 1e-6) / (airgap_height * 1e-6)
# Heuristics: fringing capacitance assumed to be 20% of the parallel plate for small bridges.
c_bridge = c_pp * 1.2
# Admittance of the bridge (Conductance from dielectric loss + Susceptance)
Y_bridge = ω * c_bridge * (loss_tangent + 1j)
return shunt_admittance(f=f, y=Y_bridge)
def _two_port_with_insertion_loss(
s: sax.SDict, loss_db: sax.FloatArrayLike
) -> sax.SDict:
r"""Scale the transmission terms of a two-port by a constant insertion loss.
Multiplies :math:`S_{21}` and :math:`S_{12}` by :math:`10^{-\text{IL}/20}`
and leaves the reflections unchanged, so the element absorbs a fraction
:math:`1 - 10^{-\text{IL}/10}` of the transmitted power independent of
frequency.
Args:
s: Two-port S-parameters dictionary.
loss_db: Insertion loss in dB.
Returns:
sax.SDict: S-parameters dictionary with the scaled transmission.
"""
a = 10 ** (-jnp.asarray(loss_db) / 20)
return {k: v * a if k[0] != k[1] else v for k, v in s.items()}
[docs]
def tsv(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
via_height: float = 1000.0,
loss_db: sax.FloatArrayLike = 4e-5,
) -> sax.SDict:
r"""S-parameter model for a through-silicon via (TSV), wrapped to :func:`~straight`.
The via is a :func:`~straight` of length ``via_height`` followed by a
constant, frequency-independent insertion loss ``loss_db`` per signal
transition.
No cryogenic :math:`S_{21}` measurement of a single superconducting TSV
has been published, so the default is an upper bound derived from
resonators interrupted by TSV transitions at single-photon power and mK
temperatures. Mallek *et al.* :cite:`mallekFabricationSuperconductingThroughsilicon2021`
find no dependence of :math:`Q_\text{i}` (about :math:`10^5` to
:math:`2.4 \times 10^5`, Fig. 7) on where the transition sits along a
:math:`\lambda/4` resonator. Taking the spread as the TSV contribution to
the inverse internal quality factor bounds it at
:math:`\delta_\text{TSV} \lesssim 1/10^5 - 1/(2.4 \times 10^5) \approx 6 \times 10^{-6}`.
The best TSV-terminated resonators of Grigoras *et al.*
:cite:`grigorasQubitcompatibleSubstratesSuperconducting2022`
(:math:`Q_\text{i} \approx 2 \times 10^5`) agree with this, and TSVs used as
capacitors reach :math:`Q_\text{diel} \approx 7.5 \times 10^5`
:cite:`hazardCharacterizationSuperconductingThroughsilicon2023`.
A lumped element adding :math:`\delta` to :math:`1/Q_\text{i}` of a
:math:`\lambda/4` resonator absorbs a power fraction
:math:`a = \delta\,\beta\ell = \delta\,\pi/2` per pass, i.e.
:math:`a \lesssim 9 \times 10^{-6}` and
:math:`\text{IL} = 10 \log_{10}(e)\,a \approx 4 \times 10^{-5}\,\text{dB}`.
Weak spots in the TSV liner drop some resonators to
:math:`Q_\text{i} \approx 10^4` :cite:`grigorasQubitcompatibleSubstratesSuperconducting2022`,
about :math:`7 \times 10^{-4}\,\text{dB}`, which is a reasonable pessimistic corner.
A single signal TSV is also a significant impedance discontinuity
:cite:`mallekFabricationSuperconductingThroughsilicon2021`, which this
model does not include.
Args:
f: Array of frequency points in Hz
via_height: Physical height (length) of the TSV in µm.
loss_db: Insertion loss per TSV transition in dB. Must be non-negative,
a negative value gives gain.
Returns:
sax.SDict: S-parameters dictionary
"""
return _two_port_with_insertion_loss(straight(f=f, length=via_height), loss_db)
[docs]
def indium_bump(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
bump_height: float = 10.0,
loss_db: sax.FloatArrayLike = 1e-6,
) -> sax.SDict:
r"""S-parameter model for an indium bump, wrapped to :func:`~straight`.
The bump is a :func:`~straight` of length ``bump_height`` followed by a
constant, frequency-independent insertion loss ``loss_db`` per signal
transition.
The default comes from Richard *et al.* :cite:`richardAccurateGapControl2026`,
who put 1 to 6 indium transitions in series in the centre conductor of
:math:`\lambda/4` CPW resonators (low photon number, 15 mK) and bound the
contribution of each transition to :math:`1/Q_\text{i}` at
:math:`\delta_\text{In} < 10^{-7}`. A lumped element adding :math:`\delta`
to :math:`1/Q_\text{i}` absorbs a power fraction :math:`a = \delta\,\pi/2`
per pass, i.e. :math:`a < 1.6 \times 10^{-7}` and
:math:`\text{IL} = 10 \log_{10}(e)\,a < 7 \times 10^{-7}\,\text{dB}`,
rounded up to :math:`10^{-6}\,\text{dB}`. Indium bump seams also leave
stripline cavities at single-photon :math:`Q > 3 \times 10^8`
:cite:`leiHighCoherenceSuperconducting2020`.
Args:
f: Array of frequency points in Hz
bump_height: Physical height (length) of the indium bump in µm.
loss_db: Insertion loss per indium bump transition in dB. Must be
non-negative, a negative value gives gain.
Returns:
sax.SDict: S-parameters dictionary
"""
return _two_port_with_insertion_loss(straight(f=f, length=bump_height), loss_db)
[docs]
def bend_circular(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 1000,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for a circular bend, wrapped to :func:`~straight`.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
return straight(f=f, length=length, cross_section=cross_section)
[docs]
def bend_circular_all_angle(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 1000,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for a circular bend, wrapped to :func:`~straight`.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
return straight(f=f, length=length, cross_section=cross_section)
[docs]
def bend_euler(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 1000,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for an Euler bend, wrapped to :func:`~straight`.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
return straight(f=f, length=length, cross_section=cross_section)
[docs]
def bend_euler_all_angle(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 1000,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for an Euler bend, wrapped to :func:`~straight`.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
return straight(f=f, length=length, cross_section=cross_section)
@jit(static_argnames=["npoints"], inline=True)
def _bend_s_length(size: Size, npoints: int = 99) -> jax.Array:
"""Return the layout S-bend length with JAX-compatible operations.
gdsfactory's path-length helper uses NumPy and cannot be traced by JAX.
"""
dx, dy = size
coordinate_dtype = jnp.result_type(dx, dy, jnp.asarray(0.0))
dx = jnp.asarray(dx, dtype=coordinate_dtype)
dy = jnp.asarray(dy, dtype=coordinate_dtype)
t = jnp.linspace(0, 1, npoints, dtype=coordinate_dtype)
one_minus_t = 1 - t
x = 3 * one_minus_t**2 * t * dx / 2 + 3 * one_minus_t * t**2 * dx / 2 + t**3 * dx
y = 3 * one_minus_t * t**2 * dy + t**3 * dy
points = jnp.stack((x, y), axis=1)
polyline_length = jnp.linalg.norm(jnp.diff(points, axis=0), axis=1).sum()
return jnp.where(dy == 0, jnp.abs(dx), polyline_length)
[docs]
def bend_s(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float | None = None,
size: Size = (20.0, 3.0),
npoints: int | jax.Array = 99,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for an S-bend, wrapped to :func:`~straight`.
Args:
f: Array of frequency points in Hz
length: Physical length in µm. When omitted, it is derived from ``size``.
size: Layout S-bend extent in µm.
npoints: Number of points used to discretize the layout Bézier curve;
a scalar array (as gdsfactoryplus model binding emits) is accepted.
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
npoints = int(npoints)
physical_length = (
_bend_s_length(size, npoints=npoints) if length is None else length
)
return straight(f=f, length=physical_length, cross_section=cross_section)
[docs]
def rectangle(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 1000,
cross_section: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for a rectangular section, wrapped to :func:`~straight`.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section: The cross-section of the waveguide.
Returns:
sax.SDict: S-parameters dictionary
"""
return straight(f=f, length=length, cross_section=cross_section)
def _static_linspace(start: float, stop: float, npoints: int) -> list[float]:
"""Return ``jnp.linspace`` values as plain scalars matching its endpoints.
Cross-section dimensions are static geometry, and gdsfactory cross-sections
cannot receive JAX tracers.
"""
if npoints < 2:
return [start]
step = (stop - start) / (npoints - 1)
return [start + i * step for i in range(npoints - 1)] + [stop]
[docs]
def taper_cross_section(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 10,
cross_section1: CrossSectionSpec = "cpw",
cross_section2: CrossSectionSpec = "cpw",
npoints: int | jax.Array = 100,
) -> sax.SDict:
"""S-parameter model for a cross-section taper using linear interpolation.
Args:
f: Array of frequency points in Hz
length: Physical length in µm
cross_section1: Cross-section for the start of the taper.
cross_section2: Cross-section for the end of the taper.
npoints: Number of segments to divide the taper into for simulation.
Clamped to a minimum of one segment. Ignored when both
cross-sections have identical dimensions.
Returns:
sax.SDict: S-parameters dictionary
"""
# SAX passes numeric instance settings as arrays; the segment count is static.
npoints = max(int(npoints), 1)
w1, g1 = get_cpw_dimensions(cross_section1)
w2, g2 = get_cpw_dimensions(cross_section2)
if (w1, g1) == (w2, g2):
return straight(f=f, length=length, cross_section=cross_section1)
f = jnp.asarray(f)
segment_length = length / npoints
widths = _static_linspace(w1, w2, npoints)
gaps = _static_linspace(g1, g2, npoints)
instances: dict[str, sax.SType] = {
f"straight_{i}": straight(
f=f,
length=segment_length,
cross_section=coplanar_waveguide(width=widths[i], gap=gaps[i]),
)
for i in range(npoints)
}
connections = {
f"straight_{i},o2": f"straight_{i + 1},o1" for i in range(npoints - 1)
}
ports = {
"o1": "straight_0,o1",
"o2": f"straight_{npoints - 1},o2",
}
return sax.backends.evaluate_circuit_fg((connections, ports), instances)
[docs]
def launcher(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
straight_length: sax.Float = 200.0,
taper_length: sax.Float = 100.0,
cross_section_big: CrossSectionSpec = launcher_cross_section_big,
cross_section_small: CrossSectionSpec = "cpw",
) -> sax.SDict:
"""S-parameter model for a launcher, effectively a straight section followed by a taper.
Args:
f: Array of frequency points in Hz
straight_length: Length of the straight section in µm.
taper_length: Length of the taper section in µm.
cross_section_big: Cross-section for the wide section.
cross_section_small: Cross-section for the narrow section.
Returns:
sax.SDict: S-parameters dictionary
"""
f = jnp.asarray(f)
instances: dict[str, sax.SType] = {
"straight": straight(
f=f,
length=straight_length,
cross_section=cross_section_big,
),
"taper": taper_cross_section(
f=f,
length=taper_length,
cross_section1=cross_section_big,
cross_section2=cross_section_small,
),
}
connections = {
"straight,o2": "taper,o1",
}
ports = {
"waveport": "straight,o1",
"o1": "taper,o2",
}
return sax.backends.evaluate_circuit_fg((connections, ports), instances)
_PAD_TAPER_SEGMENTS = 20
def _cpw_section(
f: jax.Array,
length: sax.FloatLike,
width: sax.FloatArrayLike,
gap: sax.FloatArrayLike,
) -> sax.SDict:
"""Straight CPW from its dimensions, which may be traced under JIT."""
h, t, ep_r, tand = get_cpw_substrate_params()
return _sax_coplanar_waveguide(
f=f,
length=length,
width=width,
gap=gap,
thickness=t,
substrate_thickness=h,
ep_r=ep_r,
tand=tand,
)
def _cpw_taper(
f: jax.Array,
length: sax.Float,
start: tuple[sax.Float, sax.Float],
stop: tuple[sax.Float, sax.Float],
) -> sax.SDict:
"""Linear taper between two ``(width, gap)`` pairs, which may be traced."""
n = _PAD_TAPER_SEGMENTS
widths = jnp.linspace(start[0], stop[0], n)
gaps = jnp.linspace(start[1], stop[1], n)
instances: dict[str, sax.SType] = {
f"s{i}": _cpw_section(f, length / n, widths[i], gaps[i]) for i in range(n)
}
connections = {f"s{i},o2": f"s{i + 1},o1" for i in range(n - 1)}
ports = {"o1": "s0,o1", "o2": f"s{n - 1},o2"}
return sax.backends.evaluate_circuit_fg((connections, ports), instances)
def _pad_via_pad(
f: sax.FloatArrayLike,
cross_section1: CrossSectionSpec,
cross_section2: CrossSectionSpec,
taper_length1: sax.Float,
taper_length2: sax.Float,
pad_width: sax.Float,
pad_gap: sax.Float,
pad_length: sax.Float,
via: sax.SDict,
) -> sax.SDict:
"""Cascade taper, half pad, vertical interconnect, half pad and taper.
The layout pads are joined only through the vias or bumps, so their netlist
has no path between the two ports; this gives the transitions one. The pad
sections are built from their dimensions rather than a cross-section, so
the numeric settings may be JAX tracers.
Returns:
sax.SDict: S-parameters between the two outer ports.
"""
f = jnp.asarray(f)
pad = (pad_width, pad_gap)
instances: dict[str, sax.SType] = {
"taper1": _cpw_taper(f, taper_length1, get_cpw_dimensions(cross_section1), pad),
"pad1": _cpw_section(f, pad_length / 2, *pad),
"via": via,
"pad2": _cpw_section(f, pad_length / 2, *pad),
"taper2": _cpw_taper(f, taper_length2, pad, get_cpw_dimensions(cross_section2)),
}
connections = {
"taper1,o2": "pad1,o1",
"pad1,o2": "via,o1",
"via,o2": "pad2,o1",
"pad2,o2": "taper2,o1",
}
ports = {"o1": "taper1,o1", "o2": "taper2,o2"}
return sax.backends.evaluate_circuit_fg((connections, ports), instances)
[docs]
def tsv_transition_double_sided(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
cross_section: CrossSectionSpec | None = None,
cross_section_backside: CrossSectionSpec | None = None,
taper_length: sax.Float = 100.0,
taper_length_backside: sax.Float = 100.0,
pad_width: sax.Float = 40.0,
pad_gap: sax.Float = 24.0,
pad_length: sax.Float = 30.0,
chip: Chip = "bottom",
via_height: float = 1000.0,
) -> sax.SDict:
"""S-parameter model for a CPW transition through the substrate.
Port ``o1`` is on the front face and ``o2`` on the backside. The via array
is the :func:`~tsv` model, so this inherits its approximations.
Args:
f: Array of frequency points in Hz
cross_section: Front-face CPW. Defaults to the chip's front cross-section.
cross_section_backside: Backside CPW. Defaults to the chip's backside
cross-section.
taper_length: Length of the front-face taper in µm.
taper_length_backside: Length of the backside taper in µm.
pad_width: Signal width of the pads in µm.
pad_gap: Gap of the pads in µm.
pad_length: Length of each pad in µm.
chip: Chip the transition sits on.
via_height: Height of the TSVs in µm, passed to :func:`~tsv`.
Returns:
sax.SDict: S-parameters dictionary
"""
layers = CHIP_LAYERS[chip]
return _pad_via_pad(
f,
cross_section or layers.front_cross_section,
cross_section_backside or layers.back_cross_section,
taper_length,
taper_length_backside,
pad_width,
pad_gap,
pad_length,
tsv(f=f, via_height=via_height),
)
[docs]
def bump_transition(
f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
cross_section: CrossSectionSpec = "cpw",
cross_section_top: CrossSectionSpec = "cpw_top",
taper_length: sax.Float = 10.0,
taper_length_top: sax.Float = 10.0,
pad_width: sax.Float = 30.0,
pad_gap: sax.Float = 6.0,
pad_length: sax.Float = 30.0,
bump_height: float = 10.0,
) -> sax.SDict:
"""S-parameter model for a CPW transition through a single indium bump.
Port ``o1`` is on the bottom chip and ``o2`` on the flip-chip top chip, see
:func:`~qpdk.cells.bump.bump_transition` for the geometry. The bump is the
:func:`~indium_bump` model, so this inherits its approximations.
Args:
f: Array of frequency points in Hz
cross_section: Bottom-chip CPW.
cross_section_top: Top-chip CPW.
taper_length: Length of the bottom-chip jog into the pad in µm.
taper_length_top: Length of the top-chip jog out of the pad in µm.
pad_width: Signal width of the pads in µm.
pad_gap: Gap of the pads in µm.
pad_length: Length of each pad in µm.
bump_height: Height of the bump in µm, passed to :func:`~indium_bump`.
Returns:
sax.SDict: S-parameters dictionary
"""
return _pad_via_pad(
f,
cross_section,
cross_section_top,
taper_length,
taper_length_top,
pad_width,
pad_gap,
pad_length,
indium_bump(f=f, bump_height=bump_height),
)