Coverage for qpdk / models / couplers.py: 87%
53 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"""Coupler models."""
3from functools import partial
4from typing import cast
6import gdsfactory as gf
7import jax
8import jax.numpy as jnp
9import sax
10from gdsfactory.cross_section import CrossSection
11from gdsfactory.typings import CrossSectionSpec
12from jax.typing import ArrayLike
13from sax.models.rf import capacitor, tee
15from qpdk.logger import logger
16from qpdk.models.constants import DEFAULT_FREQUENCY, ε_0
17from qpdk.models.cpw import (
18 cpw_ep_r_from_cross_section,
19 cpw_z0_from_cross_section,
20 get_cpw_dimensions,
21)
22from qpdk.models.math import (
23 capacitance_per_length_conformal,
24 ellipk_ratio,
25 epsilon_eff,
26)
27from qpdk.models.waveguides import straight
30@partial(jax.jit, inline=True)
31def cpw_cpw_coupling_capacitance_per_length_analytical(
32 gap: float | ArrayLike,
33 width: float | ArrayLike,
34 cpw_gap: float | ArrayLike,
35 ep_r: float | ArrayLike,
36) -> float | jax.Array:
37 r"""Analytical formula for ECCPW mutual capacitance per unit length.
39 The model follows the edge-coupled coplanar waveguide (ECCPW) formula
40 using conformal mapping for even and odd modes:
42 .. math::
44 \begin{aligned}
45 x_1 &= s_c / 2 \\
46 x_2 &= x_1 + W \\
47 x_3 &= x_2 + G \\
48 k_e &= \sqrt{\frac{x_2^2 - x_1^2}{x_3^2 - x_1^2}} \\
49 k_o &= \frac{x_1}{x_2} \sqrt{\frac{x_3^2 - x_2^2}{x_3^2 - x_1^2}} \\
50 C_{\text{even}} &= 2 \epsilon_0 \epsilon_{\text{eff}} \frac{K(k_e)}{K(k_e')} \\
51 C_{\text{odd}} &= 2 \epsilon_0 \epsilon_{\text{eff}} \frac{K(k_o')}{K(k_o)} \\
52 C_m &= \frac{C_{\text{odd}} - C_{\text{even}}}{2}
53 \end{aligned}
55 where :math:`s_c` is the separation (gap) between inner edges, :math:`W` is the
56 center conductor width, and :math:`G` is the gap to the ground plane.
58 See :cite:`simonsCoplanarWaveguideCircuits2001`.
60 Args:
61 gap: The gap (separation) between the two center conductors in µm.
62 width: Center conductor width in µm.
63 cpw_gap: Gap between center conductor and ground plane in µm.
64 ep_r: Relative permittivity of the substrate.
66 Returns:
67 The mutual coupling capacitance per unit length in Farads/meter.
68 """
69 # Geometric parameters in m (convert from μm)
70 s_c = gap * 1e-6
71 w_m = width * 1e-6
72 g_m = cpw_gap * 1e-6
74 x1 = s_c / 2
75 x2 = x1 + w_m
76 x3 = x2 + g_m
78 # Even-mode modulus squared
79 ke_sq = (x2**2 - x1**2) / (x3**2 - x1**2)
81 # Odd-mode modulus squared
82 ko_sq = (x1**2 / x2**2) * ((x3**2 - x2**2) / (x3**2 - x1**2))
84 # Capacitances per unit length
85 # Factor is 2.0 since ECCPW formula uses 2 * ε_0 * ε_eff
86 c_even_pul = 2.0 * capacitance_per_length_conformal(m=ke_sq, ep_r=ep_r)
87 # c_odd uses K(1-m)/K(m) which is the inverse of ellipk_ratio(m)
88 c_odd_pul = 2.0 * ε_0 * epsilon_eff(ep_r) / ellipk_ratio(ko_sq)
90 # Mutual capacitance per unit length
91 return (c_odd_pul - c_even_pul) / 2
94def cpw_cpw_coupling_capacitance(
95 f: sax.FloatArrayLike, # noqa: ARG001
96 length: float | ArrayLike,
97 gap: float | ArrayLike,
98 cross_section: CrossSectionSpec,
99) -> float | jax.Array:
100 r"""Calculate the coupling capacitance between two parallel CPWs.
102 Args:
103 f: Frequency array in Hz.
104 length: The coupling length in µm.
105 gap: The gap between the two center conductors in µm.
106 cross_section: The cross-section of the CPW.
108 Returns:
109 The total coupling capacitance in Farads.
110 """
111 ep_r = cpw_ep_r_from_cross_section(cross_section)
113 try:
114 width, cpw_gap = get_cpw_dimensions(cross_section)
115 except ValueError:
116 # Fallback to default CPW width and gap if not found in sections
117 # Not sure if width needs fallback, but gap previously fell back to 6.0
118 logger.warning(
119 "CPW gap not found in cross-section sections. Using default gap of 6.0 µm."
120 )
121 xs = (
122 gf.get_cross_section(cross_section)
123 if isinstance(cross_section, str)
124 else cross_section
125 )
126 if callable(xs):
127 xs = cast(CrossSection, xs())
128 width = xs.width
129 cpw_gap = 6.0
131 c_pul = cpw_cpw_coupling_capacitance_per_length_analytical(
132 gap=gap,
133 width=width,
134 cpw_gap=cpw_gap,
135 ep_r=ep_r,
136 )
137 return c_pul * length * 1e-6
140def coupler_straight(
141 f: ArrayLike = DEFAULT_FREQUENCY,
142 length: int | float = 20.0,
143 gap: int | float = 0.27,
144 cross_section: CrossSectionSpec = "cpw",
145) -> sax.SDict:
146 """S-parameter model for two coupled coplanar waveguides, :func:`~qpdk.cells.waveguides.coupler_straight`.
148 Args:
149 f: Array of frequency points in Hz
150 length: Physical length of coupling section in µm
151 gap: Gap between the coupled waveguides in µm
152 cross_section: The cross-section of the CPW.
154 Returns:
155 sax.SDict: S-parameters dictionary
157 .. code::
159 o2──────▲───────o3
160 │gap
161 o1──────▼───────o4
162 """
163 f = jnp.asarray(f)
164 straight_settings = {"length": length / 2, "cross_section": cross_section}
165 capacitor_settings = {
166 "capacitance": cpw_cpw_coupling_capacitance(f, length, gap, cross_section),
167 "z0": cpw_z0_from_cross_section(cross_section, f),
168 }
170 # Create straight instances with shared settings
171 straight_instances = {
172 f"straight_{i}_{j}": straight(f=f, **straight_settings)
173 for i in [1, 2]
174 for j in [1, 2]
175 }
176 tee_instances = {f"tee_{i}": tee(f=f) for i in [1, 2]}
178 instances = {
179 **straight_instances,
180 **tee_instances,
181 "capacitor": capacitor(f=f, **capacitor_settings),
182 }
183 connections = {
184 "straight_1_1,o1": "tee_1,o1",
185 "straight_1_2,o1": "tee_1,o2",
186 "straight_2_1,o1": "tee_2,o1",
187 "straight_2_2,o1": "tee_2,o2",
188 "tee_1,o3": "capacitor,o1",
189 "tee_2,o3": "capacitor,o2",
190 }
191 ports = {
192 "o2": "straight_1_1,o2",
193 "o3": "straight_1_2,o2",
194 "o1": "straight_2_1,o2",
195 "o4": "straight_2_2,o2",
196 }
198 return sax.evaluate_circuit_fg((connections, ports), instances)
201def coupler_ring(
202 f: ArrayLike = DEFAULT_FREQUENCY,
203 length: int | float = 20.0,
204 gap: int | float = 0.27,
205 cross_section: CrossSectionSpec = "cpw",
206) -> sax.SDict:
207 """S-parameter model for two coupled coplanar waveguides in a ring configuration.
209 The implementation is the same as straight coupler for now.
211 TODO: Fetch coupling capacitance from a curved simulation library.
213 Args:
214 f: Array of frequency points in Hz
215 length: Physical length of coupling section in µm
216 gap: Gap between the coupled waveguides in µm
217 cross_section: The cross-section of the CPW.
219 Returns:
220 sax.SDict: S-parameters dictionary
221 """
222 return coupler_straight(f=f, length=length, gap=gap, cross_section=cross_section)
225if __name__ == "__main__":
226 import matplotlib.pyplot as plt
228 lengths = jnp.linspace(10, 1000, 10)
229 gaps = jnp.geomspace(0.1, 5.0, 6)
230 width = 10.0
231 cpw_gap = 6.0
232 ep_r = 11.7
234 plt.figure(figsize=(10, 6))
236 # Calculate capacitance per unit length for all gaps simultaneously (shape: (6,))
237 c_pul = cpw_cpw_coupling_capacitance_per_length_analytical(
238 gap=gaps, width=width, cpw_gap=cpw_gap, ep_r=ep_r
239 )
241 # Broadcast to compute total capacitance for all lengths and gaps (shape: (6, 1000))
242 capacitances = c_pul[:, None] * lengths[None, :] * 1e-6 * 1e15 # Convert to fF
244 for i, gap in enumerate(gaps):
245 plt.plot(lengths, capacitances[i], label=f"gap = {gap:.1f} µm")
247 plt.xlabel("Coupling Length (µm)")
248 plt.ylabel("Mutual Capacitance (fF)")
249 plt.title(
250 rf"CPW-CPW Coupling Capacitance ($\mathtt{{width}}=${width} µm, $\mathtt{{cpw\_gap}}=${cpw_gap} µm, $\epsilon_r={ep_r}$)"
251 )
252 plt.grid(True)
253 plt.legend()
254 plt.tight_layout()
255 plt.show()