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

1"""Waveguides.""" 

2 

3from functools import partial 

4 

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 

11 

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 

24 

25 

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. 

32 

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`. 

38 

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``. 

42 

43 .. _Qucs technical documentation: 

44 https://qucs.sourceforge.net/docs/technical/technical.pdf 

45 

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. 

50 

51 Returns: 

52 sax.SDict: S-parameters dictionary 

53 """ 

54 f = jnp.asarray(f) 

55 f_flat = f.ravel() 

56 

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

61 

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) 

66 

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) 

73 

74 

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. 

85 

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

92 

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``. 

96 

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

105 

106 Returns: 

107 sax.SDict: S-parameters dictionary. 

108 """ 

109 f = jnp.asarray(f) 

110 f_flat = f.ravel() 

111 

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 

117 

118 # Effective permittivity (Hammerstad-Jensen) 

119 ep_eff = microstrip_epsilon_eff(w_m, h_m, ep_r) 

120 

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 ) 

125 

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) 

129 

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) 

136 

137 

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. 

144 

145 This may be used to model a quarter-wave coplanar waveguide resonator. 

146 

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. 

150 

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. 

155 

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) 

171 

172 

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. 

179 

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. 

183 

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. 

188 

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) 

204 

205 

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. 

212 

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. 

216 

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. 

221 

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) 

239 

240 

241def tee( 

242 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

243) -> sax.SType: 

244 """S-parameter model for a 3-port tee junction. 

245 

246 This wraps the generic tee model. 

247 

248 Args: 

249 f: Array of frequency points in Hz. 

250 

251 Returns: 

252 sax.SType: S-parameters dictionary. 

253 """ 

254 from qpdk.models.generic import tee as _generic_tee 

255 

256 return _generic_tee(f=f) 

257 

258 

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. 

268 

269 This model creates an N-port divider/combiner by chaining 3-port tee 

270 junctions. All ports are connected to a single node. 

271 

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. 

278 

279 Returns: 

280 sax.SType: S-parameters dictionary with ports o1, o2, ..., oN. 

281 """ 

282 from qpdk.models.generic import tee as _generic_tee 

283 

284 f = jnp.asarray(f) 

285 n_ports = west + east + north + south 

286 

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) 

293 

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

296 

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" 

303 

304 # Last tee's o3 is the last external port 

305 ports[f"o{n_ports}"] = f"tee_{n_ports - 3},o3" 

306 

307 return sax.evaluate_circuit_fg((connections, ports), instances) 

308 

309 

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. 

319 

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. 

323 

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. 

326 

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. 

333 

334 Returns: 

335 sax.SDict: S-parameters dictionary 

336 """ 

337 f = jnp.asarray(f) 

338 ω = 2 * π * f 

339 

340 # Parallel plate capacitance 

341 c_pp = (ε_0 * cpw_width * 1e-6 * bridge_width * 1e-6) / (airgap_height * 1e-6) 

342 

343 # Heuristics: fringing capacitance assumed to be 20% of the parallel plate for small bridges. 

344 c_bridge = c_pp * 1.2 

345 

346 # Admittance of the bridge (Conductance from dielectric loss + Susceptance) 

347 Y_bridge = ω * c_bridge * (loss_tangent + 1j) 

348 

349 return admittance(f=f, y=Y_bridge) 

350 

351 

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`. 

357 

358 TODO: add a constant loss channel for TSVs. 

359 

360 Args: 

361 f: Array of frequency points in Hz 

362 via_height: Physical height (length) of the TSV in µm. 

363 

364 Returns: 

365 sax.SDict: S-parameters dictionary 

366 """ 

367 return straight(f=f, length=via_height) 

368 

369 

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`. 

375 

376 TODO: add a constant loss channel for indium bumps. 

377 

378 Args: 

379 f: Array of frequency points in Hz 

380 bump_height: Physical height (length) of the indium bump in µm. 

381 

382 Returns: 

383 sax.SType: S-parameters dictionary 

384 """ 

385 return straight(f=f, length=bump_height) 

386 

387 

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`. 

394 

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. 

399 

400 Returns: 

401 sax.SDict: S-parameters dictionary 

402 """ 

403 return straight(f=f, length=length, cross_section=cross_section) 

404 

405 

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`. 

412 

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. 

417 

418 Returns: 

419 sax.SDict: S-parameters dictionary 

420 """ 

421 return straight(f=f, length=length, cross_section=cross_section) 

422 

423 

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`. 

430 

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. 

435 

436 Returns: 

437 sax.SDict: S-parameters dictionary 

438 """ 

439 return straight(f=f, length=length, cross_section=cross_section) 

440 

441 

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`. 

448 

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. 

453 

454 Returns: 

455 sax.SDict: S-parameters dictionary 

456 """ 

457 return straight(f=f, length=length, cross_section=cross_section) 

458 

459 

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. 

468 

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. 

475 

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) 

482 

483 f = jnp.asarray(f) 

484 segment_length = length / n_points 

485 

486 ws = jnp.linspace(w1, w2, n_points) 

487 gs = jnp.linspace(g1, g2, n_points) 

488 

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) 

505 

506 

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. 

515 

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. 

522 

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) 

529 

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)