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

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 ( 

11 coplanar_waveguide as _sax_coplanar_waveguide, 

12 electrical_open, 

13 electrical_short, 

14 microstrip as _sax_microstrip, 

15) 

16 

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 

21 

22 

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. 

29 

30 Wraps :func:`sax.models.rf.coplanar_waveguide`, extracting the physical 

31 dimensions from the given *cross_section* and the PDK layer stack. 

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 .. _Qucs technical documentation: 

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

41 

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. 

46 

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

52 

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 ) 

62 

63 

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. 

74 

75 Wraps :func:`sax.models.rf.microstrip`, mapping the qpdk parameter 

76 names to the upstream model. 

77 

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

84 

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

93 

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 ) 

106 

107 

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. 

114 

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

116 

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. 

120 

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. 

125 

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) 

141 

142 

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. 

149 

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. 

153 

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. 

158 

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) 

174 

175 

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. 

182 

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. 

186 

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. 

191 

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) 

209 

210 

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. 

220 

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

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

223 

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. 

230 

231 Returns: 

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

233 

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 

239 

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) 

246 

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

249 

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" 

256 

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

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

259 

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

261 

262 

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. 

272 

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. 

276 

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. 

279 

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. 

286 

287 Returns: 

288 sax.SDict: S-parameters dictionary 

289 """ 

290 f = jnp.asarray(f) 

291 ω = 2 * π * f 

292 

293 # Parallel plate capacitance 

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

295 

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

297 c_bridge = c_pp * 1.2 

298 

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

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

301 

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

303 

304 

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

310 

311 TODO: add a constant loss channel for TSVs. 

312 

313 Args: 

314 f: Array of frequency points in Hz 

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

316 

317 Returns: 

318 sax.SDict: S-parameters dictionary 

319 """ 

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

321 

322 

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

328 

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

330 

331 Args: 

332 f: Array of frequency points in Hz 

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

334 

335 Returns: 

336 sax.SType: S-parameters dictionary 

337 """ 

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

339 

340 

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

347 

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. 

352 

353 Returns: 

354 sax.SDict: S-parameters dictionary 

355 """ 

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

357 

358 

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

365 

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. 

370 

371 Returns: 

372 sax.SDict: S-parameters dictionary 

373 """ 

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

375 

376 

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

383 

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. 

388 

389 Returns: 

390 sax.SDict: S-parameters dictionary 

391 """ 

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

393 

394 

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

401 

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. 

406 

407 Returns: 

408 sax.SDict: S-parameters dictionary 

409 """ 

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

411 

412 

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. 

421 

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. 

428 

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) 

435 

436 f = jnp.asarray(f) 

437 segment_length = length / n_points 

438 

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

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

441 

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) 

458 

459 

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. 

468 

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. 

475 

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) 

482 

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)