Coverage for qpdk / models / waveguides.py: 98%
111 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-14 10:27 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-14 10:27 +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 electrical_open, electrical_short
12from qpdk.models.constants import DEFAULT_FREQUENCY, ε_0, π
13from qpdk.models.cpw import (
14 cpw_parameters,
15 get_cpw_dimensions,
16 get_cpw_substrate_params,
17 microstrip_epsilon_eff,
18 microstrip_thickness_correction,
19 propagation_constant,
20 transmission_line_s_params,
21)
22from qpdk.models.generic import admittance, short_2_port
23from qpdk.tech import coplanar_waveguide
26def straight(
27 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
28 length: sax.Float = 1000,
29 cross_section: CrossSectionSpec = "cpw",
30) -> sax.SDict:
31 r"""S-parameter model for a straight coplanar waveguide.
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 The propagation constant and characteristic impedance are evaluated
40 with pure-JAX functions (see :mod:`qpdk.models.cpw`) so the model
41 composes with ``jax.jit``, ``jax.grad``, and ``jax.vmap``.
43 .. _Qucs technical documentation:
44 https://qucs.sourceforge.net/docs/technical/technical.pdf
46 Args:
47 f: Array of frequency points in Hz
48 length: Physical length in µm
49 cross_section: The cross-section of the waveguide.
51 Returns:
52 sax.SDict: S-parameters dictionary
53 """
54 f = jnp.asarray(f)
55 f_flat = f.ravel()
57 # Extract CPW parameters (not JAX-traceable, constant-folded)
58 width, gap = get_cpw_dimensions(cross_section)
59 ep_eff, z0_val = cpw_parameters(width, gap)
60 _h, _t, ep_r = get_cpw_substrate_params()
62 # JAX-traceable computation
63 gamma = propagation_constant(f_flat, ep_eff, tand=0.0, ep_r=ep_r)
64 length_m = jnp.asarray(length) * 1e-6
65 s11, s21 = transmission_line_s_params(gamma, z0_val, length_m)
67 sdict: sax.SDict = {
68 ("o1", "o1"): s11.reshape(f.shape),
69 ("o1", "o2"): s21.reshape(f.shape),
70 ("o2", "o2"): s11.reshape(f.shape),
71 }
72 return sax.reciprocal(sdict)
75def straight_microstrip(
76 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
77 length: sax.Float = 1000,
78 width: sax.Float = 10.0,
79 h: sax.Float = 500.0,
80 t: sax.Float = 0.2,
81 ep_r: sax.Float = 11.45,
82 tand: sax.Float = 0.0,
83) -> sax.SDict:
84 r"""S-parameter model for a straight microstrip transmission line.
86 Computes S-parameters analytically using the Hammerstad-Jensen
87 :cite:`hammerstadAccurateModelsMicrostrip1980` closed-form expressions
88 for effective permittivity and characteristic impedance, as described
89 in Pozar :cite:`m.pozarMicrowaveEngineering2012` (ch. 3, §3.8).
90 Conductor thickness corrections follow
91 Gupta et al. :cite:`guptaMicrostripLinesSlotlines1996` (§2.2.4).
93 All computation is done with pure-JAX functions
94 (see :mod:`qpdk.models.cpw`) so the model composes with ``jax.jit``,
95 ``jax.grad``, and ``jax.vmap``.
97 Args:
98 f: Array of frequency points in Hz.
99 length: Physical length in µm.
100 width: Strip width in µm.
101 h: Substrate height in µm.
102 t: Conductor thickness in µm (default 0.2 µm = 200 nm).
103 ep_r: Relative permittivity of the substrate (default 11.45 for Si).
104 tand: Dielectric loss tangent (default 0 — lossless).
106 Returns:
107 sax.SDict: S-parameters dictionary.
108 """
109 f = jnp.asarray(f)
110 f_flat = f.ravel()
112 # Convert to SI (metres)
113 w_m = width * 1e-6
114 h_m = h * 1e-6
115 t_m = t * 1e-6
116 length_m = jnp.asarray(length) * 1e-6
118 # Effective permittivity (Hammerstad-Jensen)
119 ep_eff = microstrip_epsilon_eff(w_m, h_m, ep_r)
121 # Apply conductor thickness correction if t > 0
122 _w_eff, ep_eff_t, z0_val = microstrip_thickness_correction(
123 w_m, h_m, t_m, ep_r, ep_eff
124 )
126 # Propagation constant & S-parameters
127 gamma = propagation_constant(f_flat, ep_eff_t, tand=tand, ep_r=ep_r)
128 s11, s21 = transmission_line_s_params(gamma, z0_val, length_m)
130 sdict: sax.SDict = {
131 ("o1", "o1"): s11.reshape(f.shape),
132 ("o1", "o2"): s21.reshape(f.shape),
133 ("o2", "o2"): s11.reshape(f.shape),
134 }
135 return sax.reciprocal(sdict)
138def straight_shorted(
139 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
140 length: sax.Float = 1000,
141 cross_section: CrossSectionSpec = "cpw",
142) -> sax.SDict:
143 """S-parameter model for a straight waveguide with one shorted end.
145 This may be used to model a quarter-wave coplanar waveguide resonator.
147 Note:
148 The port ``o2`` is internally shorted and should not be used.
149 It seems to be a Sax limitation that we need to define at least two ports.
151 Args:
152 f: Array of frequency points in Hz
153 length: Physical length in µm
154 cross_section: The cross-section of the waveguide.
156 Returns:
157 sax.SDict: S-parameters dictionary
158 """
159 instances = {
160 "straight": straight(f=f, length=length, cross_section=cross_section),
161 "short": short_2_port(f=f),
162 }
163 connections = {
164 "straight,o2": "short,o1",
165 }
166 ports = {
167 "o1": "straight,o1",
168 "o2": "short_2_port,o2", # don't use: shorted!
169 }
170 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
173def straight_open(
174 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
175 length: sax.Float = 1000,
176 cross_section: CrossSectionSpec = "cpw",
177) -> sax.SType:
178 """S-parameter model for a straight waveguide with one open end.
180 Note:
181 The port ``o2`` is internally open-circuited and should not be used.
182 It is provided to match the number of ports in the layout component.
184 Args:
185 f: Array of frequency points in Hz
186 length: Physical length in µm
187 cross_section: The cross-section of the waveguide.
189 Returns:
190 sax.SType: S-parameters dictionary
191 """
192 instances = {
193 "straight": straight(f=f, length=length, cross_section=cross_section),
194 "open": electrical_open(f=f, n_ports=2),
195 }
196 connections = {
197 "straight,o2": "open,o1",
198 }
199 ports = {
200 "o1": "straight,o1",
201 "o2": "open,o2", # don't use: opened!
202 }
203 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
206def straight_double_open(
207 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
208 length: sax.Float = 1000,
209 cross_section: CrossSectionSpec = "cpw",
210) -> sax.SType:
211 """S-parameter model for a straight waveguide with open ends.
213 Note:
214 Ports ``o1`` and ``o2`` are internally open-circuited and should not be used.
215 They are provided to match the number of ports in the layout component.
217 Args:
218 f: Array of frequency points in Hz
219 length: Physical length in µm
220 cross_section: The cross-section of the waveguide.
222 Returns:
223 sax.SType: S-parameters dictionary
224 """
225 instances = {
226 "straight": straight(f=f, length=length, cross_section=cross_section),
227 "open1": electrical_open(f=f, n_ports=2),
228 "open2": electrical_open(f=f, n_ports=2),
229 }
230 connections = {
231 "straight,o1": "open1,o1",
232 "straight,o2": "open2,o1",
233 }
234 ports = {
235 "o1": "open1,o2", # don't use: opened!
236 "o2": "open2,o2", # don't use: opened!
237 }
238 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
241def tee(
242 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
243) -> sax.SType:
244 """S-parameter model for a 3-port tee junction.
246 This wraps the generic tee model.
248 Args:
249 f: Array of frequency points in Hz.
251 Returns:
252 sax.SType: S-parameters dictionary.
253 """
254 from qpdk.models.generic import tee as _generic_tee
256 return _generic_tee(f=f)
259@partial(jax.jit, static_argnames=["west", "east", "north", "south"])
260def nxn(
261 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
262 west: int = 1,
263 east: int = 1,
264 north: int = 1,
265 south: int = 1,
266) -> sax.SType:
267 """NxN junction model using tee components.
269 This model creates an N-port divider/combiner by chaining 3-port tee
270 junctions. All ports are connected to a single node.
272 Args:
273 f: Array of frequency points in Hz.
274 west: Number of ports on the west side.
275 east: Number of ports on the east side.
276 north: Number of ports on the north side.
277 south: Number of ports on the south side.
279 Returns:
280 sax.SType: S-parameters dictionary with ports o1, o2, ..., oN.
281 """
282 from qpdk.models.generic import tee as _generic_tee
284 f = jnp.asarray(f)
285 n_ports = west + east + north + south
287 if n_ports <= 0:
288 raise ValueError("Total number of ports must be positive.")
289 if n_ports == 1:
290 return electrical_open(f=f)
291 if n_ports == 2:
292 return electrical_short(f=f, n_ports=2)
294 instances = {f"tee_{i}": _generic_tee(f=f) for i in range(n_ports - 2)}
295 connections = {f"tee_{i},o3": f"tee_{i + 1},o1" for i in range(n_ports - 3)}
297 ports = {
298 "o1": "tee_0,o1",
299 "o2": "tee_0,o2",
300 }
301 for i in range(1, n_ports - 2):
302 ports[f"o{i + 2}"] = f"tee_{i},o2"
304 # Last tee's o3 is the last external port
305 ports[f"o{n_ports}"] = f"tee_{n_ports - 3},o3"
307 return sax.evaluate_circuit_fg((connections, ports), instances)
310@partial(jax.jit, inline=True)
311def airbridge(
312 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
313 cpw_width: sax.Float = 10.0,
314 bridge_width: sax.Float = 10.0,
315 airgap_height: sax.Float = 3.0,
316 loss_tangent: sax.Float = 1.2e-8,
317) -> sax.SType:
318 r"""S-parameter model for a superconducting CPW airbridge.
320 The airbridge is modeled as a lumped lossy shunt admittance (accounting for
321 dielectric loss and shunt capacitance) embedded between two sections of
322 transmission line that represent the physical footprint of the bridge.
324 Parallel plate capacitor model is as done in :cite:`chenFabricationCharacterizationAluminum2014`
325 The default value for the loss tangent :math:`\tan\,\delta` is also taken from there.
327 Args:
328 f: Array of frequency points in Hz
329 cpw_width: Width of the CPW center conductor in µm.
330 bridge_width: Width of the airbridge in µm.
331 airgap_height: Height of the airgap in µm.
332 loss_tangent: Dielectric loss tangent of the supporting layer/residues.
334 Returns:
335 sax.SDict: S-parameters dictionary
336 """
337 f = jnp.asarray(f)
338 ω = 2 * π * f
340 # Parallel plate capacitance
341 c_pp = (ε_0 * cpw_width * 1e-6 * bridge_width * 1e-6) / (airgap_height * 1e-6)
343 # Heuristics: fringing capacitance assumed to be 20% of the parallel plate for small bridges.
344 c_bridge = c_pp * 1.2
346 # Admittance of the bridge (Conductance from dielectric loss + Susceptance)
347 Y_bridge = ω * c_bridge * (loss_tangent + 1j)
349 return admittance(f=f, y=Y_bridge)
352def tsv(
353 f: ArrayLike = DEFAULT_FREQUENCY,
354 via_height: float = 1000.0,
355) -> sax.SDict:
356 """S-parameter model for a through-silicon via (TSV), wrapped to :func:`~straight`.
358 TODO: add a constant loss channel for TSVs.
360 Args:
361 f: Array of frequency points in Hz
362 via_height: Physical height (length) of the TSV in µm.
364 Returns:
365 sax.SDict: S-parameters dictionary
366 """
367 return straight(f=f, length=via_height)
370def indium_bump(
371 f: ArrayLike = DEFAULT_FREQUENCY,
372 bump_height: float = 10.0,
373) -> sax.SType:
374 """S-parameter model for an indium bump, wrapped to :func:`~straight`.
376 TODO: add a constant loss channel for indium bumps.
378 Args:
379 f: Array of frequency points in Hz
380 bump_height: Physical height (length) of the indium bump in µm.
382 Returns:
383 sax.SType: S-parameters dictionary
384 """
385 return straight(f=f, length=bump_height)
388def bend_circular(
389 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
390 length: sax.Float = 1000,
391 cross_section: CrossSectionSpec = "cpw",
392) -> sax.SDict:
393 """S-parameter model for a circular bend, wrapped to :func:`~straight`.
395 Args:
396 f: Array of frequency points in Hz
397 length: Physical length in µm
398 cross_section: The cross-section of the waveguide.
400 Returns:
401 sax.SDict: S-parameters dictionary
402 """
403 return straight(f=f, length=length, cross_section=cross_section)
406def bend_euler(
407 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
408 length: sax.Float = 1000,
409 cross_section: CrossSectionSpec = "cpw",
410) -> sax.SDict:
411 """S-parameter model for an Euler bend, wrapped to :func:`~straight`.
413 Args:
414 f: Array of frequency points in Hz
415 length: Physical length in µm
416 cross_section: The cross-section of the waveguide.
418 Returns:
419 sax.SDict: S-parameters dictionary
420 """
421 return straight(f=f, length=length, cross_section=cross_section)
424def bend_s(
425 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
426 length: sax.Float = 1000,
427 cross_section: CrossSectionSpec = "cpw",
428) -> sax.SDict:
429 """S-parameter model for an S-bend, wrapped to :func:`~straight`.
431 Args:
432 f: Array of frequency points in Hz
433 length: Physical length in µm
434 cross_section: The cross-section of the waveguide.
436 Returns:
437 sax.SDict: S-parameters dictionary
438 """
439 return straight(f=f, length=length, cross_section=cross_section)
442def rectangle(
443 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
444 length: sax.Float = 1000,
445 cross_section: CrossSectionSpec = "cpw",
446) -> sax.SDict:
447 """S-parameter model for a rectangular section, wrapped to :func:`~straight`.
449 Args:
450 f: Array of frequency points in Hz
451 length: Physical length in µm
452 cross_section: The cross-section of the waveguide.
454 Returns:
455 sax.SDict: S-parameters dictionary
456 """
457 return straight(f=f, length=length, cross_section=cross_section)
460def taper_cross_section(
461 f: ArrayLike = DEFAULT_FREQUENCY,
462 length: sax.Float = 1000,
463 cross_section_1: CrossSectionSpec = "cpw",
464 cross_section_2: CrossSectionSpec = "cpw",
465 n_points: int = 50,
466) -> sax.SDict:
467 """S-parameter model for a cross-section taper using linear interpolation.
469 Args:
470 f: Array of frequency points in Hz
471 length: Physical length in µm
472 cross_section_1: Cross-section for the start of the taper.
473 cross_section_2: Cross-section for the end of the taper.
474 n_points: Number of segments to divide the taper into for simulation.
476 Returns:
477 sax.SDict: S-parameters dictionary
478 """
479 n_points = int(n_points)
480 w1, g1 = get_cpw_dimensions(cross_section_1)
481 w2, g2 = get_cpw_dimensions(cross_section_2)
483 f = jnp.asarray(f)
484 segment_length = length / n_points
486 ws = jnp.linspace(w1, w2, n_points)
487 gs = jnp.linspace(g1, g2, n_points)
489 instances = {
490 f"straight_{i}": straight(
491 f=f,
492 length=segment_length,
493 cross_section=coplanar_waveguide(width=float(ws[i]), gap=float(gs[i])),
494 )
495 for i in range(n_points)
496 }
497 connections = {
498 f"straight_{i},o2": f"straight_{i + 1},o1" for i in range(n_points - 1)
499 }
500 ports = {
501 "o1": "straight_0,o1",
502 "o2": f"straight_{n_points - 1},o2",
503 }
504 return sax.backends.evaluate_circuit_fg((connections, ports), instances)
507def launcher(
508 f: ArrayLike = DEFAULT_FREQUENCY,
509 straight_length: sax.Float = 200.0,
510 taper_length: sax.Float = 100.0,
511 cross_section_big: CrossSectionSpec | None = None,
512 cross_section_small: CrossSectionSpec = "cpw",
513) -> sax.SDict:
514 """S-parameter model for a launcher, effectively a straight section followed by a taper.
516 Args:
517 f: Array of frequency points in Hz
518 straight_length: Length of the straight section in µm.
519 taper_length: Length of the taper section in µm.
520 cross_section_big: Cross-section for the wide section.
521 cross_section_small: Cross-section for the narrow section.
523 Returns:
524 sax.SDict: S-parameters dictionary
525 """
526 f = jnp.asarray(f)
527 if cross_section_big is None:
528 cross_section_big = coplanar_waveguide(width=200, gap=100)
530 instances = {
531 "straight": straight(
532 f=f,
533 length=straight_length,
534 cross_section=cross_section_big,
535 ),
536 "taper": taper_cross_section(
537 f=f,
538 length=taper_length,
539 cross_section_1=cross_section_big,
540 cross_section_2=cross_section_small,
541 ),
542 }
543 connections = {
544 "straight,o2": "taper,o1",
545 }
546 ports = {
547 "waveport": "straight,o1",
548 "o1": "taper,o2",
549 }
550 return sax.backends.evaluate_circuit_fg((connections, ports), instances)