Coverage for qpdk / models / waveguides.py: 100%
89 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
1"""Waveguides."""
3from functools import partial
5import jax
6import jax.numpy as jnp
7import sax
8from gdsfactory.typings import CrossSectionSpec
9from jax.typing import ArrayLike
10from sax.models.rf import (
11 coplanar_waveguide as _sax_coplanar_waveguide,
12 electrical_open,
13 electrical_short,
14 microstrip as _sax_microstrip,
15)
17from qpdk.models.constants import DEFAULT_FREQUENCY, ε_0, π
18from qpdk.models.cpw import get_cpw_dimensions, get_cpw_substrate_params
19from qpdk.models.generic import admittance, short_2_port, tee
20from qpdk.tech import coplanar_waveguide
23def straight(
24 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
25 length: sax.Float = 1000,
26 cross_section: CrossSectionSpec = "cpw",
27) -> sax.SDict:
28 r"""S-parameter model for a straight coplanar waveguide.
30 Wraps :func:`sax.models.rf.coplanar_waveguide`, extracting the physical
31 dimensions from the given *cross_section* and the PDK layer stack.
33 Computes S-parameters analytically using conformal-mapping CPW theory
34 following Simons :cite:`simonsCoplanarWaveguideCircuits2001` (ch. 2)
35 and the Qucs-S CPW model (`Qucs technical documentation`_, §12.4).
36 Conductor thickness corrections use the first-order model of
37 Gupta, Garg, Bahl, and Bhartia :cite:`guptaMicrostripLinesSlotlines1996`.
39 .. _Qucs technical documentation:
40 https://qucs.sourceforge.net/docs/technical/technical.pdf
42 Args:
43 f: Array of frequency points in Hz
44 length: Physical length in µm
45 cross_section: The cross-section of the waveguide.
47 Returns:
48 sax.SDict: S-parameters dictionary
49 """
50 width, gap = get_cpw_dimensions(cross_section)
51 h, t, ep_r = get_cpw_substrate_params()
53 return _sax_coplanar_waveguide(
54 f=f,
55 length=length,
56 width=width,
57 gap=gap,
58 thickness=t,
59 substrate_thickness=h,
60 ep_r=ep_r,
61 )
64def straight_microstrip(
65 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
66 length: sax.Float = 1000,
67 width: sax.Float = 10.0,
68 h: sax.Float = 500.0,
69 t: sax.Float = 0.2,
70 ep_r: sax.Float = 11.45,
71 tand: sax.Float = 0.0,
72) -> sax.SDict:
73 r"""S-parameter model for a straight microstrip transmission line.
75 Wraps :func:`sax.models.rf.microstrip`, mapping the qpdk parameter
76 names to the upstream model.
78 Computes S-parameters analytically using the Hammerstad-Jensen
79 :cite:`hammerstadAccurateModelsMicrostrip1980` closed-form expressions
80 for effective permittivity and characteristic impedance, as described
81 in Pozar :cite:`m.pozarMicrowaveEngineering2012` (ch. 3, §3.8).
82 Conductor thickness corrections follow
83 Gupta et al. :cite:`guptaMicrostripLinesSlotlines1996` (§2.2.4).
85 Args:
86 f: Array of frequency points in Hz.
87 length: Physical length in µm.
88 width: Strip width in µm.
89 h: Substrate height in µm.
90 t: Conductor thickness in µm (default 0.2 µm = 200 nm).
91 ep_r: Relative permittivity of the substrate (default 11.45 for Si).
92 tand: Dielectric loss tangent (default 0 — lossless).
94 Returns:
95 sax.SDict: S-parameters dictionary.
96 """
97 return _sax_microstrip(
98 f=f,
99 length=length,
100 width=width,
101 substrate_thickness=h,
102 thickness=t,
103 ep_r=ep_r,
104 tand=tand,
105 )
108def straight_shorted(
109 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
110 length: sax.Float = 1000,
111 cross_section: CrossSectionSpec = "cpw",
112) -> sax.SDict:
113 """S-parameter model for a straight waveguide with one shorted end.
115 This may be used to model a quarter-wave coplanar waveguide resonator.
117 Note:
118 The port ``o2`` is internally shorted and should not be used.
119 It seems to be a Sax limitation that we need to define at least two ports.
121 Args:
122 f: Array of frequency points in Hz
123 length: Physical length in µm
124 cross_section: The cross-section of the waveguide.
126 Returns:
127 sax.SDict: S-parameters dictionary
128 """
129 instances = {
130 "straight": straight(f=f, length=length, cross_section=cross_section),
131 "short": short_2_port(f=f),
132 }
133 connections = {
134 "straight,o2": "short,o1",
135 }
136 ports = {
137 "o1": "straight,o1",
138 "o2": "short_2_port,o2", # don't use: shorted!
139 }
140 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
143def straight_open(
144 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
145 length: sax.Float = 1000,
146 cross_section: CrossSectionSpec = "cpw",
147) -> sax.SType:
148 """S-parameter model for a straight waveguide with one open end.
150 Note:
151 The port ``o2`` is internally open-circuited and should not be used.
152 It is provided to match the number of ports in the layout component.
154 Args:
155 f: Array of frequency points in Hz
156 length: Physical length in µm
157 cross_section: The cross-section of the waveguide.
159 Returns:
160 sax.SType: S-parameters dictionary
161 """
162 instances = {
163 "straight": straight(f=f, length=length, cross_section=cross_section),
164 "open": electrical_open(f=f, n_ports=2),
165 }
166 connections = {
167 "straight,o2": "open,o1",
168 }
169 ports = {
170 "o1": "straight,o1",
171 "o2": "open,o2", # don't use: opened!
172 }
173 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
176def straight_double_open(
177 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
178 length: sax.Float = 1000,
179 cross_section: CrossSectionSpec = "cpw",
180) -> sax.SType:
181 """S-parameter model for a straight waveguide with open ends.
183 Note:
184 Ports ``o1`` and ``o2`` are internally open-circuited and should not be used.
185 They are provided to match the number of ports in the layout component.
187 Args:
188 f: Array of frequency points in Hz
189 length: Physical length in µm
190 cross_section: The cross-section of the waveguide.
192 Returns:
193 sax.SType: S-parameters dictionary
194 """
195 instances = {
196 "straight": straight(f=f, length=length, cross_section=cross_section),
197 "open1": electrical_open(f=f, n_ports=2),
198 "open2": electrical_open(f=f, n_ports=2),
199 }
200 connections = {
201 "straight,o1": "open1,o1",
202 "straight,o2": "open2,o1",
203 }
204 ports = {
205 "o1": "open1,o2", # don't use: opened!
206 "o2": "open2,o2", # don't use: opened!
207 }
208 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
211@partial(jax.jit, static_argnames=["west", "east", "north", "south"])
212def nxn(
213 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
214 west: int = 1,
215 east: int = 1,
216 north: int = 1,
217 south: int = 1,
218) -> sax.SType:
219 """NxN junction model using tee components.
221 This model creates an N-port divider/combiner by chaining 3-port tee
222 junctions. All ports are connected to a single node.
224 Args:
225 f: Array of frequency points in Hz.
226 west: Number of ports on the west side.
227 east: Number of ports on the east side.
228 north: Number of ports on the north side.
229 south: Number of ports on the south side.
231 Returns:
232 sax.SType: S-parameters dictionary with ports o1, o2, ..., oN.
234 Raises:
235 ValueError: If total number of ports is not positive.
236 """
237 f = jnp.asarray(f)
238 n_ports = west + east + north + south
240 if n_ports <= 0:
241 raise ValueError("Total number of ports must be positive.")
242 if n_ports == 1:
243 return electrical_open(f=f)
244 if n_ports == 2:
245 return electrical_short(f=f, n_ports=2)
247 instances = {f"tee_{i}": tee(f=f) for i in range(n_ports - 2)}
248 connections = {f"tee_{i},o3": f"tee_{i + 1},o1" for i in range(n_ports - 3)}
250 ports = {
251 "o1": "tee_0,o1",
252 "o2": "tee_0,o2",
253 }
254 for i in range(1, n_ports - 2):
255 ports[f"o{i + 2}"] = f"tee_{i},o2"
257 # Last tee's o3 is the last external port
258 ports[f"o{n_ports}"] = f"tee_{n_ports - 3},o3"
260 return sax.evaluate_circuit_fg((connections, ports), instances)
263@partial(jax.jit, inline=True)
264def airbridge(
265 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
266 cpw_width: sax.Float = 10.0,
267 bridge_width: sax.Float = 10.0,
268 airgap_height: sax.Float = 3.0,
269 loss_tangent: sax.Float = 1.2e-8,
270) -> sax.SType:
271 r"""S-parameter model for a superconducting CPW airbridge.
273 The airbridge is modeled as a lumped lossy shunt admittance (accounting for
274 dielectric loss and shunt capacitance) embedded between two sections of
275 transmission line that represent the physical footprint of the bridge.
277 Parallel plate capacitor model is as done in :cite:`chenFabricationCharacterizationAluminum2014`
278 The default value for the loss tangent :math:`\tan\,\delta` is also taken from there.
280 Args:
281 f: Array of frequency points in Hz
282 cpw_width: Width of the CPW center conductor in µm.
283 bridge_width: Width of the airbridge in µm.
284 airgap_height: Height of the airgap in µm.
285 loss_tangent: Dielectric loss tangent of the supporting layer/residues.
287 Returns:
288 sax.SDict: S-parameters dictionary
289 """
290 f = jnp.asarray(f)
291 ω = 2 * π * f
293 # Parallel plate capacitance
294 c_pp = (ε_0 * cpw_width * 1e-6 * bridge_width * 1e-6) / (airgap_height * 1e-6)
296 # Heuristics: fringing capacitance assumed to be 20% of the parallel plate for small bridges.
297 c_bridge = c_pp * 1.2
299 # Admittance of the bridge (Conductance from dielectric loss + Susceptance)
300 Y_bridge = ω * c_bridge * (loss_tangent + 1j)
302 return admittance(f=f, y=Y_bridge)
305def tsv(
306 f: ArrayLike = DEFAULT_FREQUENCY,
307 via_height: float = 1000.0,
308) -> sax.SDict:
309 """S-parameter model for a through-silicon via (TSV), wrapped to :func:`~straight`.
311 TODO: add a constant loss channel for TSVs.
313 Args:
314 f: Array of frequency points in Hz
315 via_height: Physical height (length) of the TSV in µm.
317 Returns:
318 sax.SDict: S-parameters dictionary
319 """
320 return straight(f=f, length=via_height)
323def indium_bump(
324 f: ArrayLike = DEFAULT_FREQUENCY,
325 bump_height: float = 10.0,
326) -> sax.SType:
327 """S-parameter model for an indium bump, wrapped to :func:`~straight`.
329 TODO: add a constant loss channel for indium bumps.
331 Args:
332 f: Array of frequency points in Hz
333 bump_height: Physical height (length) of the indium bump in µm.
335 Returns:
336 sax.SType: S-parameters dictionary
337 """
338 return straight(f=f, length=bump_height)
341def bend_circular(
342 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
343 length: sax.Float = 1000,
344 cross_section: CrossSectionSpec = "cpw",
345) -> sax.SDict:
346 """S-parameter model for a circular bend, wrapped to :func:`~straight`.
348 Args:
349 f: Array of frequency points in Hz
350 length: Physical length in µm
351 cross_section: The cross-section of the waveguide.
353 Returns:
354 sax.SDict: S-parameters dictionary
355 """
356 return straight(f=f, length=length, cross_section=cross_section)
359def bend_euler(
360 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
361 length: sax.Float = 1000,
362 cross_section: CrossSectionSpec = "cpw",
363) -> sax.SDict:
364 """S-parameter model for an Euler bend, wrapped to :func:`~straight`.
366 Args:
367 f: Array of frequency points in Hz
368 length: Physical length in µm
369 cross_section: The cross-section of the waveguide.
371 Returns:
372 sax.SDict: S-parameters dictionary
373 """
374 return straight(f=f, length=length, cross_section=cross_section)
377def bend_s(
378 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
379 length: sax.Float = 1000,
380 cross_section: CrossSectionSpec = "cpw",
381) -> sax.SDict:
382 """S-parameter model for an S-bend, wrapped to :func:`~straight`.
384 Args:
385 f: Array of frequency points in Hz
386 length: Physical length in µm
387 cross_section: The cross-section of the waveguide.
389 Returns:
390 sax.SDict: S-parameters dictionary
391 """
392 return straight(f=f, length=length, cross_section=cross_section)
395def rectangle(
396 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
397 length: sax.Float = 1000,
398 cross_section: CrossSectionSpec = "cpw",
399) -> sax.SDict:
400 """S-parameter model for a rectangular section, wrapped to :func:`~straight`.
402 Args:
403 f: Array of frequency points in Hz
404 length: Physical length in µm
405 cross_section: The cross-section of the waveguide.
407 Returns:
408 sax.SDict: S-parameters dictionary
409 """
410 return straight(f=f, length=length, cross_section=cross_section)
413def taper_cross_section(
414 f: ArrayLike = DEFAULT_FREQUENCY,
415 length: sax.Float = 1000,
416 cross_section_1: CrossSectionSpec = "cpw",
417 cross_section_2: CrossSectionSpec = "cpw",
418 n_points: int = 50,
419) -> sax.SDict:
420 """S-parameter model for a cross-section taper using linear interpolation.
422 Args:
423 f: Array of frequency points in Hz
424 length: Physical length in µm
425 cross_section_1: Cross-section for the start of the taper.
426 cross_section_2: Cross-section for the end of the taper.
427 n_points: Number of segments to divide the taper into for simulation.
429 Returns:
430 sax.SDict: S-parameters dictionary
431 """
432 n_points = int(n_points)
433 w1, g1 = get_cpw_dimensions(cross_section_1)
434 w2, g2 = get_cpw_dimensions(cross_section_2)
436 f = jnp.asarray(f)
437 segment_length = length / n_points
439 ws = jnp.linspace(w1, w2, n_points)
440 gs = jnp.linspace(g1, g2, n_points)
442 instances = {
443 f"straight_{i}": straight(
444 f=f,
445 length=segment_length,
446 cross_section=coplanar_waveguide(width=float(ws[i]), gap=float(gs[i])),
447 )
448 for i in range(n_points)
449 }
450 connections = {
451 f"straight_{i},o2": f"straight_{i + 1},o1" for i in range(n_points - 1)
452 }
453 ports = {
454 "o1": "straight_0,o1",
455 "o2": f"straight_{n_points - 1},o2",
456 }
457 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
460def launcher(
461 f: ArrayLike = DEFAULT_FREQUENCY,
462 straight_length: sax.Float = 200.0,
463 taper_length: sax.Float = 100.0,
464 cross_section_big: CrossSectionSpec | None = None,
465 cross_section_small: CrossSectionSpec = "cpw",
466) -> sax.SDict:
467 """S-parameter model for a launcher, effectively a straight section followed by a taper.
469 Args:
470 f: Array of frequency points in Hz
471 straight_length: Length of the straight section in µm.
472 taper_length: Length of the taper section in µm.
473 cross_section_big: Cross-section for the wide section.
474 cross_section_small: Cross-section for the narrow section.
476 Returns:
477 sax.SDict: S-parameters dictionary
478 """
479 f = jnp.asarray(f)
480 if cross_section_big is None:
481 cross_section_big = coplanar_waveguide(width=200, gap=100)
483 instances = {
484 "straight": straight(
485 f=f,
486 length=straight_length,
487 cross_section=cross_section_big,
488 ),
489 "taper": taper_cross_section(
490 f=f,
491 length=taper_length,
492 cross_section_1=cross_section_big,
493 cross_section_2=cross_section_small,
494 ),
495 }
496 connections = {
497 "straight,o2": "taper,o1",
498 }
499 ports = {
500 "waveport": "straight,o1",
501 "o1": "taper,o2",
502 }
503 return sax.backends.evaluate_circuit_fg((connections, ports), instances)