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

1"""Coupler models.""" 

2 

3from functools import partial 

4from typing import cast 

5 

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 

14 

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 

28 

29 

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. 

38 

39 The model follows the edge-coupled coplanar waveguide (ECCPW) formula 

40 using conformal mapping for even and odd modes: 

41 

42 .. math:: 

43 

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} 

54 

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. 

57 

58 See :cite:`simonsCoplanarWaveguideCircuits2001`. 

59 

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. 

65 

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 

73 

74 x1 = s_c / 2 

75 x2 = x1 + w_m 

76 x3 = x2 + g_m 

77 

78 # Even-mode modulus squared 

79 ke_sq = (x2**2 - x1**2) / (x3**2 - x1**2) 

80 

81 # Odd-mode modulus squared 

82 ko_sq = (x1**2 / x2**2) * ((x3**2 - x2**2) / (x3**2 - x1**2)) 

83 

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) 

89 

90 # Mutual capacitance per unit length 

91 return (c_odd_pul - c_even_pul) / 2 

92 

93 

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. 

101 

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. 

107 

108 Returns: 

109 The total coupling capacitance in Farads. 

110 """ 

111 ep_r = cpw_ep_r_from_cross_section(cross_section) 

112 

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 

130 

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 

138 

139 

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

147 

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. 

153 

154 Returns: 

155 sax.SDict: S-parameters dictionary 

156 

157 .. code:: 

158 

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 } 

169 

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

177 

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 } 

197 

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

199 

200 

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. 

208 

209 The implementation is the same as straight coupler for now. 

210 

211 TODO: Fetch coupling capacitance from a curved simulation library. 

212 

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. 

218 

219 Returns: 

220 sax.SDict: S-parameters dictionary 

221 """ 

222 return coupler_straight(f=f, length=length, gap=gap, cross_section=cross_section) 

223 

224 

225if __name__ == "__main__": 

226 import matplotlib.pyplot as plt 

227 

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 

233 

234 plt.figure(figsize=(10, 6)) 

235 

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 ) 

240 

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 

243 

244 for i, gap in enumerate(gaps): 

245 plt.plot(lengths, capacitances[i], label=f"gap = {gap:.1f} µm") 

246 

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