Coverage for qpdk / models / cpw.py: 99%

151 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-14 10:27 +0000

1r"""Coplanar waveguide (CPW) and microstrip electromagnetic analysis. 

2 

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``, …). 

8 

9CPW Theory 

10---------- 

11The quasi-static CPW analysis follows the conformal-mapping approach 

12described by Simons :cite:`simonsCoplanarWaveguideCircuits2001` (ch. 2) and 

13Ghione & Naldi :cite:`ghioneAnalyticalFormulasCoplanar1984`. 

14Conductor thickness corrections use the first-order formulae of 

15Gupta, Garg, Bahl & Bhartia :cite:`guptaMicrostripLinesSlotlines1996` 

16(§7.3, Eqs. 7.98-7.100). 

17 

18Microstrip Theory 

19----------------- 

20The microstrip analysis uses the Hammerstad-Jensen 

21:cite:`hammerstadAccurateModelsMicrostrip1980` closed-form expressions for 

22effective permittivity and characteristic impedance, as presented in 

23Pozar :cite:`m.pozarMicrowaveEngineering2012` (ch. 3, §3.8). 

24 

25General 

26------- 

27The ABCD-to-S-parameter conversion is the standard microwave-network 

28relation from Pozar :cite:`m.pozarMicrowaveEngineering2012` (ch. 4). 

29 

30The implementation was cross-checked against the Qucs-S model 

31(see `Qucs technical documentation`_, §12 for CPW, §11 for microstrip). 

32 

33.. _Qucs technical documentation: 

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

35 

36Functions 

37--------- 

38All geometry parameters are in **SI base units** (metres, etc.) unless 

39noted otherwise. Frequency is in **Hz**. 

40""" 

41 

42from functools import cache, partial 

43from typing import cast 

44 

45import gdsfactory as gf 

46import jax 

47import jax.numpy as jnp 

48from gdsfactory.typings import CrossSectionSpec 

49from jax.typing import ArrayLike 

50 

51from qpdk.models.constants import c_0, π 

52from qpdk.models.math import ellipk_ratio 

53from qpdk.tech import LAYER_STACK, material_properties 

54 

55# =================================================================== 

56# Coplanar Waveguide (CPW) 

57# =================================================================== 

58 

59 

60@partial(jax.jit, inline=True) 

61def cpw_epsilon_eff( 

62 w: ArrayLike, 

63 s: ArrayLike, 

64 h: ArrayLike, 

65 ep_r: ArrayLike, 

66) -> jax.Array: 

67 r"""Effective permittivity of a CPW on a finite-height substrate. 

68 

69 Uses conformal mapping 

70 (Simons :cite:`simonsCoplanarWaveguideCircuits2001`, Eq. 2.37; 

71 Ghione & Naldi :cite:`ghioneAnalyticalFormulasCoplanar1984`): 

72 

73 .. math:: 

74 

75 \begin{aligned} 

76 k_0 &= \frac{w}{w + 2s} \\ 

77 k_1 &= \frac{\sinh(\pi w / 4h)} 

78 {\sinh\bigl(\pi(w + 2s) / 4h\bigr)} \\ 

79 q_1 &= \frac{K(k_1^2)\,/\,K(1 - k_1^2)} 

80 {K(k_0^2)\,/\,K(1 - k_0^2)} \\ 

81 \varepsilon_{\mathrm{eff}} 

82 &= 1 + \frac{q_1\,(\varepsilon_r - 1)}{2} 

83 \end{aligned} 

84 

85 where :math:`K` is the complete elliptic integral of the first kind in 

86 the *parameter* convention (:math:`m = k^2`). 

87 

88 Args: 

89 w: Centre-conductor width (m). 

90 s: Gap to ground plane (m). 

91 h: Substrate height (m). 

92 ep_r: Relative permittivity of the substrate. 

93 

94 Returns: 

95 Effective permittivity (dimensionless). 

96 """ 

97 w = jnp.asarray(w, dtype=float) 

98 s = jnp.asarray(s, dtype=float) 

99 h = jnp.asarray(h, dtype=float) 

100 ε_r = jnp.asarray(ep_r, dtype=float) 

101 

102 # Free-space modulus 

103 k0 = w / (w + 2.0 * s) 

104 

105 # Substrate-corrected modulus 

106 k1 = jnp.sinh(π * w / (4.0 * h)) / jnp.sinh(π * (w + 2.0 * s) / (4.0 * h)) 

107 

108 # Filling factor q₁ = [K(k₁)/K(k₁')] / [K(k₀)/K(k₀')] 

109 q1 = ellipk_ratio(k1**2) / ellipk_ratio(k0**2) 

110 

111 return 1.0 + q1 * (ε_r - 1.0) / 2.0 

112 

113 

114@partial(jax.jit, inline=True) 

115def cpw_z0( 

116 w: ArrayLike, 

117 s: ArrayLike, 

118 ep_eff: ArrayLike, 

119) -> jax.Array: 

120 r"""Characteristic impedance of a CPW. 

121 

122 .. math:: 

123 

124 Z_0 = \frac{30\,\pi} 

125 {\sqrt{\varepsilon_{\mathrm{eff}}}\; 

126 K(k_0^2)\,/\,K(1 - k_0^2)} 

127 

128 (Simons :cite:`simonsCoplanarWaveguideCircuits2001`, Eq. 2.38.) 

129 

130 Note that our :math:`w` and :math:`s` correspond to Simons' :math:`s` and :math:`w`, respectively. 

131 

132 Args: 

133 w: Centre-conductor width (m). 

134 s: Gap to ground plane (m). 

135 ep_eff: Effective permittivity (see :func:`cpw_epsilon_eff`). 

136 

137 Returns: 

138 Characteristic impedance (Ω). 

139 """ 

140 w = jnp.asarray(w, dtype=float) 

141 s = jnp.asarray(s, dtype=float) 

142 ε_eff = jnp.asarray(ep_eff, dtype=float) 

143 

144 k0 = w / (w + 2.0 * s) 

145 return 30.0 * π / (jnp.sqrt(ε_eff) * ellipk_ratio(k0**2)) 

146 

147 

148@partial(jax.jit, inline=True) 

149def cpw_thickness_correction( 

150 w: ArrayLike, 

151 s: ArrayLike, 

152 t: ArrayLike, 

153 ep_eff: ArrayLike, 

154) -> tuple[jax.Array, jax.Array]: 

155 r"""Apply conductor thickness correction to CPW ε_eff and Z₀. 

156 

157 First-order correction from 

158 Gupta, Garg, Bahl & Bhartia :cite:`guptaMicrostripLinesSlotlines1996` 

159 (§7.3, Eqs. 7.98-7.100): 

160 

161 .. math:: 

162 

163 \Delta &= \frac{1.25\,t}{\pi} 

164 \left(1 + \ln\\frac{4\pi w}{t}\right) \\ 

165 k_e &= k_0 + (1 - k_0^2)\,\frac{\Delta}{2s} \\ 

166 \varepsilon_{\mathrm{eff},t} 

167 &= \varepsilon_{\mathrm{eff}} 

168 - \frac{0.7\,(\varepsilon_{\mathrm{eff}} - 1)\,t/s} 

169 {K(k_0^2)/K(1-k_0^2) + 0.7\,t/s} \\ 

170 Z_{0,t} &= \frac{30\pi} 

171 {\sqrt{\varepsilon_{\mathrm{eff},t}}\; 

172 K(k_e^2)/K(1-k_e^2)} 

173 

174 Args: 

175 w: Centre-conductor width (m). 

176 s: Gap to ground plane (m). 

177 t: Conductor thickness (m). 

178 ep_eff: Uncorrected effective permittivity. 

179 

180 Returns: 

181 ``(ep_eff_t, z0_t)`` — thickness-corrected effective permittivity 

182 and characteristic impedance (Ω). 

183 """ 

184 w = jnp.asarray(w, dtype=float) 

185 s = jnp.asarray(s, dtype=float) 

186 t = jnp.asarray(t, dtype=float) 

187 ε_eff = jnp.asarray(ep_eff, dtype=float) 

188 

189 k0 = w / (w + 2.0 * s) 

190 q0 = ellipk_ratio(k0**2) 

191 

192 # Avoid 0 * log(inf) -> NaN when t = 0 

193 t_safe = jnp.where(t < 1e-15, 1e-15, t) 

194 

195 # Effective width increase (GGBB96 Eq. 7.98) 

196 Δ = (1.25 * t / π) * (1.0 + jnp.log(4.0 * π * w / t_safe)) 

197 

198 # Modified modulus for Z₀ (GGBB96, between 7.99 and 7.100) 

199 ke = k0 + (1.0 - k0**2) * Δ / (2.0 * s) 

200 ke = jnp.clip(ke, 1e-12, 1.0 - 1e-12) 

201 

202 # Modified ε_eff (GGBB96 Eq. 7.100) 

203 ε_eff_t = ε_eff - (0.7 * (ε_eff - 1.0) * t / s) / (q0 + 0.7 * t / s) 

204 

205 # Modified Z₀ 

206 z0_t = 30.0 * π / (jnp.sqrt(ε_eff_t) * ellipk_ratio(ke**2)) 

207 

208 # Disable thickness correction if t <= 0 

209 ε_eff_t = jnp.where(t <= 0, ε_eff, ε_eff_t) 

210 z0_t = jnp.where(t <= 0, cpw_z0(w, s, ε_eff), z0_t) 

211 

212 return ε_eff_t, z0_t 

213 

214 

215# =================================================================== 

216# Microstrip 

217# =================================================================== 

218 

219 

220@partial(jax.jit, inline=True) 

221def microstrip_epsilon_eff( 

222 w: ArrayLike, 

223 h: ArrayLike, 

224 ep_r: ArrayLike, 

225) -> jax.Array: 

226 r"""Effective permittivity of a microstrip line. 

227 

228 Uses the Hammerstad-Jensen 

229 :cite:`hammerstadAccurateModelsMicrostrip1980` formula as given in 

230 Pozar :cite:`m.pozarMicrowaveEngineering2012` (Eq. 3.195-3.196): 

231 

232 .. math:: 

233 

234 \varepsilon_{\mathrm{eff}} = \frac{\varepsilon_r + 1}{2} 

235 + \frac{\varepsilon_r - 1}{2} 

236 \left(\frac{1}{\sqrt{1 + 12\,h/w}} 

237 + 0.04\,(1 - w/h)^2\;\Theta(1 - w/h)\right) 

238 

239 where the last term contributes only for narrow strips (:math:`w/h < 1`). 

240 

241 Args: 

242 w: Strip width (m). 

243 h: Substrate height (m). 

244 ep_r: Relative permittivity of the substrate. 

245 

246 Returns: 

247 Effective permittivity (dimensionless). 

248 """ 

249 w = jnp.asarray(w, dtype=float) 

250 h = jnp.asarray(h, dtype=float) 

251 ε_r = jnp.asarray(ep_r, dtype=float) 

252 

253 u = w / h 

254 f_u = 1.0 / jnp.sqrt(1.0 + 12.0 / u) 

255 

256 # Extra correction for narrow strips (w/h < 1) 

257 narrow_correction = 0.04 * (1.0 - u) ** 2 

258 f_u = jnp.where(u < 1.0, f_u + narrow_correction, f_u) 

259 

260 return (ε_r + 1.0) / 2.0 + (ε_r - 1.0) / 2.0 * f_u 

261 

262 

263@partial(jax.jit, inline=True) 

264def microstrip_z0( 

265 w: ArrayLike, 

266 h: ArrayLike, 

267 ep_eff: ArrayLike, 

268) -> jax.Array: 

269 r"""Characteristic impedance of a microstrip line. 

270 

271 Uses the Hammerstad-Jensen 

272 :cite:`hammerstadAccurateModelsMicrostrip1980` approximation as given in 

273 Pozar :cite:`m.pozarMicrowaveEngineering2012` (Eq. 3.197-3.198): 

274 

275 .. math:: 

276 

277 Z_0 = \begin{cases} 

278 \displaystyle\frac{60}{\sqrt{\varepsilon_{\mathrm{eff}}}} 

279 \ln\!\left(\frac{8h}{w} + \frac{w}{4h}\right) 

280 & w/h \le 1 \\[6pt] 

281 \displaystyle\frac{120\pi} 

282 {\sqrt{\varepsilon_{\mathrm{eff}}}\, 

283 \bigl[w/h + 1.393 + 0.667\ln(w/h + 1.444)\bigr]} 

284 & w/h \ge 1 

285 \end{cases} 

286 

287 Args: 

288 w: Strip width (m). 

289 h: Substrate height (m). 

290 ep_eff: Effective permittivity (see :func:`microstrip_epsilon_eff`). 

291 

292 Returns: 

293 Characteristic impedance (Ω). 

294 """ 

295 w = jnp.asarray(w, dtype=float) 

296 h = jnp.asarray(h, dtype=float) 

297 ε_eff = jnp.asarray(ep_eff, dtype=float) 

298 

299 u = w / h 

300 

301 # Narrow strip (w/h <= 1) 

302 z_narrow = (60.0 / jnp.sqrt(ε_eff)) * jnp.log(8.0 / u + u / 4.0) 

303 

304 # Wide strip (w/h >= 1) 

305 z_wide = 120.0 * π / (jnp.sqrt(ε_eff) * (u + 1.393 + 0.667 * jnp.log(u + 1.444))) 

306 

307 return jnp.where(u <= 1.0, z_narrow, z_wide) 

308 

309 

310@partial(jax.jit, inline=True) 

311def microstrip_thickness_correction( 

312 w: ArrayLike, 

313 h: ArrayLike, 

314 t: ArrayLike, 

315 ep_r: ArrayLike, 

316 ep_eff: ArrayLike, 

317) -> tuple[jax.Array, jax.Array, jax.Array]: 

318 r"""Conductor thickness correction for a microstrip line. 

319 

320 Uses the widely-adopted Schneider correction as presented in 

321 Pozar :cite:`m.pozarMicrowaveEngineering2012` (§3.8) and 

322 Gupta et al. :cite:`guptaMicrostripLinesSlotlines1996`: 

323 

324 .. math:: 

325 

326 w_e &= w + \frac{t}{\pi} 

327 \ln\frac{4e}{\sqrt{(t/h)^2 + (t/(w\pi + 1.1t\pi))^2}} \\ 

328 \varepsilon_{\mathrm{eff},t} 

329 &= \varepsilon_{\mathrm{eff}} 

330 - \frac{(\varepsilon_r - 1)\,t/h} 

331 {4.6\,\sqrt{w/h}} 

332 

333 Then the corrected :math:`Z_0` is computed with the effective width 

334 :math:`w_e` and corrected :math:`\varepsilon_{\mathrm{eff},t}`. 

335 

336 Args: 

337 w: Strip width (m). 

338 h: Substrate height (m). 

339 t: Conductor thickness (m). 

340 ep_r: Relative permittivity of the substrate. 

341 ep_eff: Uncorrected effective permittivity. 

342 

343 Returns: 

344 ``(w_eff, ep_eff_t, z0_t)`` — effective width (m), 

345 thickness-corrected effective permittivity, 

346 and characteristic impedance (Ω). 

347 """ 

348 w = jnp.asarray(w, dtype=float) 

349 h = jnp.asarray(h, dtype=float) 

350 t = jnp.asarray(t, dtype=float) 

351 ε_r = jnp.asarray(ep_r, dtype=float) 

352 ε_eff = jnp.asarray(ep_eff, dtype=float) 

353 

354 # Effective width (Schneider) 

355 term = jnp.sqrt((t / h) ** 2 + (t / (w * π + 1.1 * t * π)) ** 2) 

356 # Avoid 0 * log(inf) -> NaN when t = 0 

357 term_safe = jnp.where(term < 1e-15, 1.0, term) 

358 w_eff = w + (t / π) * jnp.log(4.0 * jnp.e / term_safe) 

359 

360 # Corrected epsilon_eff 

361 ε_eff_t = ε_eff - (ε_r - 1.0) * t / h / (4.6 * jnp.sqrt(w / h)) 

362 

363 # Corrected Z0 

364 z0_t = microstrip_z0(w_eff, h, ε_eff_t) 

365 

366 # Disable thickness correction if t <= 0 

367 w_eff = jnp.where(t <= 0, w, w_eff) 

368 ε_eff_t = jnp.where(t <= 0, ε_eff, ε_eff_t) 

369 z0_t = jnp.where(t <= 0, microstrip_z0(w, h, ε_eff), z0_t) 

370 

371 return w_eff, ε_eff_t, z0_t 

372 

373 

374# =================================================================== 

375# Common: propagation & S-parameters 

376# =================================================================== 

377 

378 

379@partial(jax.jit, inline=True) 

380def propagation_constant( 

381 f: ArrayLike, 

382 ep_eff: ArrayLike, 

383 tand: ArrayLike = 0.0, 

384 ep_r: ArrayLike = 1.0, 

385) -> jax.Array: 

386 r"""Complex propagation constant of a quasi-TEM transmission line. 

387 

388 For the general lossy case 

389 (Pozar :cite:`m.pozarMicrowaveEngineering2012`, §3.8): 

390 

391 .. math:: 

392 

393 \gamma = \alpha_d + j\,\beta 

394 

395 where the **dielectric attenuation** is 

396 

397 .. math:: 

398 

399 \alpha_d = \frac{\pi f}{c_0} 

400 \frac{\varepsilon_r}{\sqrt{\varepsilon_{\mathrm{eff}}}} 

401 \frac{\varepsilon_{\mathrm{eff}} - 1} 

402 {\varepsilon_r - 1} 

403 \tan\delta 

404 

405 and the **phase constant** is 

406 

407 .. math:: 

408 

409 \beta = \frac{2\pi f}{c_0}\,\sqrt{\varepsilon_{\mathrm{eff}}} 

410 

411 For a superconducting line (:math:`\tan\delta = 0`) the propagation 

412 is purely imaginary: :math:`\gamma = j\beta`. 

413 

414 Args: 

415 f: Frequency (Hz). 

416 ep_eff: Effective permittivity. 

417 tand: Dielectric loss tangent (default 0 — lossless). 

418 ep_r: Substrate relative permittivity (only needed when ``tand > 0``). 

419 

420 Returns: 

421 Complex propagation constant :math:`\gamma` (1/m). 

422 """ 

423 f = jnp.asarray(f, dtype=float) 

424 ε_eff = jnp.asarray(ep_eff, dtype=float) 

425 tanδ = jnp.asarray(tand, dtype=float) 

426 ε_r = jnp.asarray(ep_r, dtype=float) 

427 

428 β = 2.0 * π * f * jnp.sqrt(ε_eff) / c_0 

429 

430 # Use safe denominator to prevent NaN gradients in JAX backward pass 

431 denom = jnp.where(jnp.abs(ε_r - 1.0) < 1e-15, 1.0, ε_r - 1.0) 

432 

433 # Dielectric attenuation constant (Simons Eq. 2.2.41) 

434 α_d = π * f / c_0 * (ε_r / jnp.sqrt(ε_eff)) * ((ε_eff - 1.0) / denom) * tanδ 

435 # Guard against ep_r == 1 (vacuum) where division would be 0/0. 

436 # When ep_r == 1 there is no substrate, so α_d = 0 by definition. 

437 α_d = jnp.where(jnp.abs(ε_r - 1.0) < 1e-15, 0.0, α_d) 

438 

439 return α_d + 1j * β 

440 

441 

442@partial(jax.jit, inline=True) 

443def transmission_line_s_params( 

444 gamma: ArrayLike, 

445 z0: ArrayLike, 

446 length: ArrayLike, 

447 z_ref: ArrayLike | None = None, 

448) -> tuple[jax.Array, jax.Array]: 

449 r"""S-parameters of a uniform transmission line (ABCD→S conversion). 

450 

451 The ABCD matrix of a line with characteristic impedance :math:`Z_0`, 

452 propagation constant :math:`\gamma`, and length :math:`\ell` is 

453 

454 .. math:: 

455 

456 \begin{pmatrix} A & B \\ C & D \end{pmatrix} 

457 = \begin{pmatrix} 

458 \cosh\theta & Z_0\sinh\theta \\ 

459 \sinh\theta / Z_0 & \cosh\theta 

460 \end{pmatrix}, \quad \theta = \gamma\,\ell. 

461 

462 Converting to S-parameters referenced to :math:`Z_{\mathrm{ref}}` 

463 (Pozar :cite:`m.pozarMicrowaveEngineering2012`, Table 4.2): 

464 

465 .. math:: 

466 

467 \begin{aligned} 

468 S_{11} &= \frac{A + B/Z_{\mathrm{ref}} - C\,Z_{\mathrm{ref}} - D} 

469 {A + B/Z_{\mathrm{ref}} + C\,Z_{\mathrm{ref}} + D} \\ 

470 S_{21} &= \frac{2} 

471 {A + B/Z_{\mathrm{ref}} + C\,Z_{\mathrm{ref}} + D} 

472 \end{aligned} 

473 

474 When ``z_ref`` is ``None`` the reference impedance defaults to ``z0`` 

475 (matched case), giving :math:`S_{11} = 0` and 

476 :math:`S_{21} = e^{-\gamma\ell}`. 

477 

478 Args: 

479 gamma: Complex propagation constant (1/m). 

480 z0: Characteristic impedance (Ω). 

481 length: Physical length (m). 

482 z_ref: Reference (port) impedance (Ω). Defaults to ``z0``. 

483 

484 Returns: 

485 ``(S11, S21)`` — complex S-parameter arrays. 

486 """ 

487 γ = jnp.asarray(gamma, dtype=complex) 

488 z0 = jnp.asarray(z0, dtype=complex) 

489 length = jnp.asarray(length, dtype=float) 

490 

491 if z_ref is None: 

492 z_ref = z0 

493 z_ref = jnp.asarray(z_ref, dtype=complex) 

494 

495 θ = γ * length 

496 

497 cosh_t = jnp.cosh(θ) 

498 sinh_t = jnp.sinh(θ) 

499 

500 # ABCD elements (symmetric line: A = D) 

501 a = cosh_t 

502 b = z0 * sinh_t 

503 c = sinh_t / z0 

504 

505 denom = a + b / z_ref + c * z_ref + a # A + B/Zr + C·Zr + D with D = A 

506 s11 = (b / z_ref - c * z_ref) / denom # (A-D) = 0 for symmetric line 

507 s21 = 2.0 / denom 

508 

509 return s11, s21 

510 

511 

512# =================================================================== 

513# Layout-to-Model Helpers 

514# =================================================================== 

515 

516 

517@cache 

518def get_cpw_substrate_params() -> tuple[float, float, float]: 

519 """Extract substrate parameters from the PDK layer stack. 

520 

521 Returns: 

522 ``(h, t, ep_r)`` — substrate height (µm), conductor thickness (µm), 

523 and relative permittivity. 

524 """ 

525 h = LAYER_STACK.layers["Substrate"].thickness # µm 

526 t = LAYER_STACK.layers["M1"].thickness # µm 

527 ep_r = material_properties[cast(str, LAYER_STACK.layers["Substrate"].material)][ 

528 "relative_permittivity" 

529 ] 

530 return float(h), float(t), float(ep_r) 

531 

532 

533def get_cpw_dimensions( 

534 cross_section: CrossSectionSpec, **kwargs 

535) -> tuple[float, float]: 

536 """Extracts CPW width and gap from a cross-section specification. 

537 

538 Args: 

539 cross_section: A gdsfactory cross-section specification. 

540 **kwargs: Additional keyword arguments passed to `gf.get_cross_section`. 

541 

542 Returns: 

543 tuple[float, float]: Width and gap of the CPW. 

544 """ 

545 # Make sure a PDK is activated 

546 from qpdk import PDK 

547 

548 PDK.activate() 

549 xs = gf.get_cross_section(cross_section, **kwargs) 

550 

551 width = xs.width 

552 try: 

553 gap = next( 

554 section.width 

555 for section in xs.sections 

556 if section.name and "etch_offset" in section.name 

557 ) 

558 except StopIteration as e: 

559 msg = ( 

560 f"Cross-section does not have a section with 'etch_offset' in the name. " 

561 f"Found sections: {[s.name for s in xs.sections]}" 

562 ) 

563 raise ValueError(msg) from e 

564 return width, gap 

565 

566 

567@cache 

568def cpw_parameters( 

569 width: float, 

570 gap: float, 

571) -> tuple[float, float]: 

572 r"""Compute effective permittivity and characteristic impedance for a CPW. 

573 

574 Uses the JAX-jittable functions from :mod:`qpdk.models.cpw` with the 

575 PDK layer stack (substrate height, conductor thickness, material 

576 permittivity). 

577 

578 Conductor thickness corrections follow 

579 Gupta, Garg, Bahl & Bhartia :cite:`guptaMicrostripLinesSlotlines1996` 

580 (§7.3, Eqs. 7.98-7.100). 

581 

582 Args: 

583 width: Centre-conductor width in µm. 

584 gap: Gap between centre conductor and ground plane in µm. 

585 

586 Returns: 

587 ``(ep_eff, z0)`` — effective permittivity (dimensionless) and 

588 characteristic impedance (Ω). 

589 """ 

590 width = float(width) 

591 gap = float(gap) 

592 

593 h_um, t_um, ep_r = get_cpw_substrate_params() 

594 

595 # Convert to SI (metres) 

596 w_m = width * 1e-6 

597 s_m = gap * 1e-6 

598 h_m = h_um * 1e-6 

599 t_m = t_um * 1e-6 

600 

601 # Base (zero-thickness) quantities 

602 ep_eff = cpw_epsilon_eff(w_m, s_m, h_m, ep_r) 

603 

604 if t_um > 0: 

605 ep_eff_t, z0_val = cpw_thickness_correction(w_m, s_m, t_m, ep_eff) 

606 return float(ep_eff_t), float(z0_val) 

607 

608 z0_val = cpw_z0(w_m, s_m, ep_eff) 

609 return float(ep_eff), float(z0_val) 

610 

611 

612def cpw_z0_from_cross_section( 

613 cross_section: CrossSectionSpec, 

614 f: ArrayLike | None = None, 

615) -> jnp.ndarray: 

616 """Characteristic impedance of a CPW defined by a layout cross-section. 

617 

618 Args: 

619 cross_section: A gdsfactory cross-section specification. 

620 f: Frequency array (Hz). Used only to determine the output shape; 

621 the impedance is frequency-independent in the quasi-static model. 

622 

623 Returns: 

624 Characteristic impedance broadcast to the shape of *f* (Ω). 

625 """ 

626 width, gap = get_cpw_dimensions(cross_section) 

627 _ep_eff, z0_val = cpw_parameters(width, gap) 

628 z0 = jnp.asarray(z0_val) 

629 if f is not None: 

630 f = jnp.asarray(f) 

631 z0 = jnp.broadcast_to(z0, f.shape) 

632 return z0 

633 

634 

635def cpw_ep_r_from_cross_section( 

636 cross_section: CrossSectionSpec, # noqa: ARG001 

637) -> float: 

638 r"""Substrate relative permittivity for a given cross-section. 

639 

640 .. note:: 

641 The substrate permittivity is determined by the PDK layer stack 

642 (``LAYER_STACK["Substrate"]``), not by the cross-section geometry. 

643 All CPW cross-sections on the same substrate share the same 

644 :math:`\varepsilon_r`. The *cross_section* parameter is accepted 

645 for API symmetry with :func:`cpw_z0_from_cross_section`. 

646 

647 Args: 

648 cross_section: A gdsfactory cross-section specification. 

649 

650 Returns: 

651 Relative permittivity of the substrate. 

652 """ 

653 _h, _t, ep_r = get_cpw_substrate_params() 

654 return ep_r