Source code for qpdk.models.waveguides

"""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)