Coverage for qpdk / models / cpw.py: 96%
48 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
1r"""Coplanar waveguide (CPW) and microstrip electromagnetic analysis.
3This module provides JAX-jittable functions for computing the characteristic
4impedance, effective permittivity, and propagation constant of coplanar
5waveguides and microstrip lines. All results are obtained analytically so
6the functions compose freely with JAX transformations (``jit``, ``grad``,
7``vmap``, …).
9The electromagnetic core functions are provided by :mod:`sax.models.rf` and
10re-exported here for convenience. This module adds layout-to-model helpers
11that extract physical dimensions from the qpdk layer stack and cross-section
12specifications.
14CPW Theory
15----------
16The quasi-static CPW analysis follows the conformal-mapping approach
17described by Simons :cite:`simonsCoplanarWaveguideCircuits2001` (ch. 2) and
18Ghione & Naldi :cite:`ghioneAnalyticalFormulasCoplanar1984`.
19Conductor thickness corrections use the first-order formulae of
20Gupta, Garg, Bahl & Bhartia :cite:`guptaMicrostripLinesSlotlines1996`
21(§7.3, Eqs. 7.98-7.100).
23Microstrip Theory
24-----------------
25The microstrip analysis uses the Hammerstad-Jensen
26:cite:`hammerstadAccurateModelsMicrostrip1980` closed-form expressions for
27effective permittivity and characteristic impedance, as presented in
28Pozar :cite:`m.pozarMicrowaveEngineering2012` (ch. 3, §3.8).
30General
31-------
32The ABCD-to-S-parameter conversion is the standard microwave-network
33relation from Pozar :cite:`m.pozarMicrowaveEngineering2012` (ch. 4).
35The implementation was cross-checked against the Qucs-S model
36(see `Qucs technical documentation`_, §12 for CPW, §11 for microstrip).
38.. _Qucs technical documentation:
39 https://qucs.sourceforge.net/docs/technical/technical.pdf
41Functions
42---------
43All geometry parameters are in **SI base units** (metres, etc.) unless
44noted otherwise. Frequency is in **Hz**.
45"""
47from functools import cache
48from typing import cast
50import gdsfactory as gf
51import jax.numpy as jnp
52from gdsfactory.typings import CrossSectionSpec
53from jax.typing import ArrayLike
54from sax.models.rf import (
55 cpw_epsilon_eff,
56 cpw_thickness_correction,
57 cpw_z0,
58 microstrip_epsilon_eff,
59 microstrip_thickness_correction,
60 microstrip_z0,
61 propagation_constant,
62 transmission_line_s_params,
63)
65from qpdk.tech import LAYER_STACK, get_etch_section, material_properties
67__all__ = [
68 "cpw_ep_r_from_cross_section",
69 "cpw_epsilon_eff",
70 "cpw_parameters",
71 "cpw_thickness_correction",
72 "cpw_z0",
73 "cpw_z0_from_cross_section",
74 "get_cpw_dimensions",
75 "get_cpw_substrate_params",
76 "microstrip_epsilon_eff",
77 "microstrip_thickness_correction",
78 "microstrip_z0",
79 "propagation_constant",
80 "transmission_line_s_params",
81]
84# ===================================================================
85# Layout-to-Model Helpers
86# ===================================================================
89@cache
90def get_cpw_substrate_params() -> tuple[float, float, float]:
91 """Extract substrate parameters from the PDK layer stack.
93 Returns:
94 ``(h, t, ep_r)`` — substrate height (µm), conductor thickness (µm),
95 and relative permittivity.
96 """
97 h = LAYER_STACK.layers["Substrate"].thickness # µm
98 t = LAYER_STACK.layers["M1"].thickness # µm
99 ep_r = material_properties[cast(str, LAYER_STACK.layers["Substrate"].material)][
100 "relative_permittivity"
101 ]
102 return float(h), float(t), float(ep_r)
105def get_cpw_dimensions(
106 cross_section: CrossSectionSpec, **kwargs
107) -> tuple[float, float]:
108 """Extracts CPW width and gap from a cross-section specification.
110 Args:
111 cross_section: A gdsfactory cross-section specification.
112 **kwargs: Additional keyword arguments passed to `gf.get_cross_section`.
114 Returns:
115 tuple[float, float]: Width and gap of the CPW.
116 """
117 # Make sure a PDK is activated
118 from qpdk import PDK # noqa: PLC0415
120 PDK.activate()
121 xs = gf.get_cross_section(cross_section, **kwargs)
123 width = xs.width
124 etch_section = get_etch_section(xs)
125 return width, etch_section.width
128@cache
129def cpw_parameters(
130 width: float,
131 gap: float,
132) -> tuple[float, float]:
133 r"""Compute effective permittivity and characteristic impedance for a CPW.
135 Uses the JAX-jittable functions from :mod:`sax.models.rf` with the
136 PDK layer stack (substrate height, conductor thickness, material
137 permittivity).
139 Conductor thickness corrections follow
140 Gupta, Garg, Bahl & Bhartia :cite:`guptaMicrostripLinesSlotlines1996`
141 (§7.3, Eqs. 7.98-7.100).
143 Args:
144 width: Centre-conductor width in µm.
145 gap: Gap between centre conductor and ground plane in µm.
147 Returns:
148 ``(ep_eff, z0)`` — effective permittivity (dimensionless) and
149 characteristic impedance (Ω).
150 """
151 width = float(width)
152 gap = float(gap)
154 h_um, t_um, ep_r = get_cpw_substrate_params()
156 # Convert to SI (metres)
157 w_m = width * 1e-6
158 s_m = gap * 1e-6
159 h_m = h_um * 1e-6
160 t_m = t_um * 1e-6
162 # Base (zero-thickness) quantities
163 ep_eff = cpw_epsilon_eff(w_m, s_m, h_m, ep_r)
165 if t_um > 0:
166 ep_eff_t, z0_val = cpw_thickness_correction(w_m, s_m, t_m, ep_eff)
167 return float(ep_eff_t), float(z0_val)
169 z0_val = cpw_z0(w_m, s_m, ep_eff)
170 return float(ep_eff), float(z0_val)
173def cpw_z0_from_cross_section(
174 cross_section: CrossSectionSpec,
175 f: ArrayLike | None = None,
176) -> jnp.ndarray:
177 """Characteristic impedance of a CPW defined by a layout cross-section.
179 Args:
180 cross_section: A gdsfactory cross-section specification.
181 f: Frequency array (Hz). Used only to determine the output shape;
182 the impedance is frequency-independent in the quasi-static model.
184 Returns:
185 Characteristic impedance broadcast to the shape of *f* (Ω).
186 """
187 width, gap = get_cpw_dimensions(cross_section)
188 _ep_eff, z0_val = cpw_parameters(width, gap)
189 z0 = jnp.asarray(z0_val)
190 if f is not None:
191 f = jnp.asarray(f)
192 z0 = jnp.broadcast_to(z0, f.shape)
193 return z0
196def cpw_ep_r_from_cross_section(
197 cross_section: CrossSectionSpec, # noqa: ARG001
198) -> float:
199 r"""Substrate relative permittivity for a given cross-section.
201 .. note::
202 The substrate permittivity is determined by the PDK layer stack
203 (``LAYER_STACK["Substrate"]``), not by the cross-section geometry.
204 All CPW cross-sections on the same substrate share the same
205 :math:`\varepsilon_r`. The *cross_section* parameter is accepted
206 for API symmetry with :func:`cpw_z0_from_cross_section`.
208 Args:
209 cross_section: A gdsfactory cross-section specification.
211 Returns:
212 Relative permittivity of the substrate.
213 """
214 _h, _t, ep_r = get_cpw_substrate_params()
215 return ep_r