"""Waveguides."""
from functools import partial
import jax
import jax.numpy as jnp
import sax
from gdsfactory.typings import CrossSectionSpec, Size
from jax.typing import ArrayLike
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 get_cpw_dimensions, get_cpw_substrate_params
from qpdk.models.generic import shunt_admittance, tee
from qpdk.tech import coplanar_waveguide, launcher_cross_section_big
[docs]
def straight(
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.
Wraps :func:`sax.models.rf.coplanar_waveguide`, extracting the physical
dimensions from the given *cross_section* and the PDK layer stack.
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.
Returns:
sax.SDict: S-parameters dictionary
"""
width, gap = get_cpw_dimensions(cross_section)
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,
)
[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 = {
"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 = {
"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.SType:
"""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.SType: S-parameters dictionary
"""
instances = {
"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]
@partial(jax.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.SType:
"""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.SType: 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 = {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]
@partial(jax.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.SType:
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)
[docs]
def tsv(
f: ArrayLike = DEFAULT_FREQUENCY,
via_height: float = 1000.0,
) -> sax.SDict:
"""S-parameter model for a through-silicon via (TSV), wrapped to :func:`~straight`.
TODO: add a constant loss channel for TSVs.
Args:
f: Array of frequency points in Hz
via_height: Physical height (length) of the TSV in µm.
Returns:
sax.SDict: S-parameters dictionary
"""
return straight(f=f, length=via_height)
[docs]
def indium_bump(
f: ArrayLike = DEFAULT_FREQUENCY,
bump_height: float = 10.0,
) -> sax.SType:
"""S-parameter model for an indium bump, wrapped to :func:`~straight`.
TODO: add a constant loss channel for indium bumps.
Args:
f: Array of frequency points in Hz
bump_height: Physical height (length) of the indium bump in µm.
Returns:
sax.SType: S-parameters dictionary
"""
return straight(f=f, length=bump_height)
[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)
@partial(jax.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 = 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.
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: ArrayLike = DEFAULT_FREQUENCY,
length: sax.Float = 10,
cross_section1: CrossSectionSpec = "cpw",
cross_section2: CrossSectionSpec = "cpw",
npoints: int = 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
"""
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 = {
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: ArrayLike = 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 = {
"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)