Coverage for qpdk / models / capacitor.py: 100%

38 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 17:50 +0000

1"""Capacitor Models.""" 

2 

3from functools import partial 

4 

5import jax 

6import jax.numpy as jnp 

7import sax 

8from gdsfactory.typings import CrossSectionSpec 

9 

10from qpdk.models.constants import DEFAULT_FREQUENCY, ε_0 

11from qpdk.models.cpw import cpw_ep_r_from_cross_section, cpw_z0_from_cross_section 

12from qpdk.models.generic import capacitor 

13from qpdk.models.math import ( 

14 capacitance_per_length_conformal, 

15 ellipk_ratio, 

16 epsilon_eff, 

17) 

18 

19 

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

21def plate_capacitor_capacitance_analytical( 

22 length: float, 

23 width: float, 

24 gap: float, 

25 ep_r: float, 

26) -> float: 

27 r"""Analytical formula for plate capacitor capacitance. 

28 

29 The model assumes two coplanar rectangular pads on a substrate. 

30 The capacitance is calculated using conformal mapping: 

31 

32 .. math:: 

33 

34 k &= \frac{s}{s + 2W} \\ 

35 k' &= \sqrt{1 - k^2} \\ 

36 \epsilon_{\text{eff}} &= \frac{\epsilon_r + 1}{2} \\ 

37 C &= \epsilon_0 \epsilon_{\text{eff}} L \frac{K(k')}{K(k)} 

38 

39 where :math:`s` is the gap, :math:`W` is the pad width, and :math:`L` is the pad length. 

40 

41 See :cite:`chenCompactInductorcapacitorResonators2023`. 

42 

43 Returns: 

44 The calculated capacitance in Farads. 

45 """ 

46 # Conformal mapping for coplanar pads 

47 k_sq = (gap / (gap + 2 * width)) ** 2 

48 

49 # C = ε_0 * ε_eff * L * K(k') / K(k) 

50 # Uses K(1-m)/K(m) which is the inverse of ellipk_ratio(m) 

51 c_pul = ε_0 * epsilon_eff(ep_r) / ellipk_ratio(k_sq) 

52 

53 return (length * 1e-6) * c_pul 

54 

55 

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

57def interdigital_capacitor_capacitance_analytical( 

58 fingers: int, 

59 finger_length: float, 

60 finger_gap: float, 

61 thickness: float, 

62 ep_r: float, 

63) -> jax.Array: 

64 r"""Analytical formula for interdigital capacitor capacitance. 

65 

66 The formula uses conformal mapping for the interior and exterior regions of 

67 the interdigital structure: 

68 

69 .. math:: 

70 

71 \eta &= \frac{w}{w + g} \\ 

72 k_i &= \sin\left(\frac{\pi \eta}{2}\right) \\ 

73 k_e &= \frac{2\sqrt{\eta}}{1 + \eta} \\ 

74 C_i &= \epsilon_0 L (\epsilon_r + 1) \frac{K(k_i)}{K(k_i')} \\ 

75 C_e &= \epsilon_0 L (\epsilon_r + 1) \frac{K(k_e)}{K(k_e')} 

76 

77 The total mutual capacitance for :math:`n` fingers is: 

78 

79 .. math:: 

80 

81 C = \begin{cases} 

82 C_e / 2 & \text{if } n=2 \\ 

83 (n - 3) \frac{C_i}{2} + 2 \frac{C_i C_e}{C_i + C_e} & \text{if } n > 2 

84 \end{cases} 

85 

86 where :math:`w` is the finger thickness (width), :math:`g` is the finger gap, and 

87 :math:`L` is the overlap length. 

88 

89 See :cite:`igrejaAnalyticalEvaluationInterdigital2004,gonzalezDesignFabricationInterdigital2015`. 

90 

91 Returns: 

92 The calculated mutual capacitance in Farads. 

93 """ 

94 # Geometric parameters 

95 n = fingers 

96 l_overlap = finger_length * 1e-6 # Overlap length in m 

97 w = thickness # Finger width 

98 g = finger_gap # Finger gap 

99 η = w / (w + g) # Metallization ratio 

100 

101 # Elliptic integral moduli squared 

102 ki_sq = jnp.sin(jnp.pi * η / 2) ** 2 

103 ke_sq = (2 * jnp.sqrt(η) / (1 + η)) ** 2 

104 

105 # Capacitances per unit length (interior and exterior) 

106 # Factor is 2.0 since interdigital formula uses (ep_r + 1) = 2 * ep_eff 

107 c_i = l_overlap * 2.0 * capacitance_per_length_conformal(m=ki_sq, ep_r=ep_r) 

108 c_e = l_overlap * 2.0 * capacitance_per_length_conformal(m=ke_sq, ep_r=ep_r) 

109 

110 # Total mutual capacitance 

111 # Simplifies to c_e/2 for n=2 

112 return jnp.where( # pyrefly: ignore[bad-return] 

113 n == 2, 

114 c_e / 2, 

115 (n - 3) * c_i / 2 + 2 * (c_i * c_e) / (c_i + c_e), 

116 ) 

117 

118 

119def plate_capacitor( 

120 *, 

121 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

122 length: float = 26.0, 

123 width: float = 5.0, 

124 gap: float = 7.0, 

125 cross_section: CrossSectionSpec = "cpw", 

126) -> sax.SDict: 

127 r"""Plate capacitor Sax model. 

128 

129 Args: 

130 f: Array of frequency points in Hz 

131 length: Length of the capacitor pad in μm 

132 width: Width of the capacitor pad in μm 

133 gap: Gap between plates in μm 

134 cross_section: Cross-section specification 

135 

136 Returns: 

137 sax.SDict: S-parameters dictionary 

138 """ 

139 f_arr = jnp.asarray(f) 

140 z0 = cpw_z0_from_cross_section(cross_section, f_arr) 

141 ep_r = cpw_ep_r_from_cross_section(cross_section) 

142 capacitance = plate_capacitor_capacitance_analytical( 

143 length=length, width=width, gap=gap, ep_r=ep_r 

144 ) 

145 return capacitor(f=f_arr, capacitance=capacitance, z0=z0) 

146 

147 

148def interdigital_capacitor( 

149 *, 

150 f: sax.FloatArrayLike = DEFAULT_FREQUENCY, 

151 fingers: int = 4, 

152 finger_length: float = 20.0, 

153 finger_gap: float = 2.0, 

154 thickness: float = 5.0, 

155 cross_section: CrossSectionSpec = "cpw", 

156) -> sax.SDict: 

157 r"""Interdigital capacitor Sax model. 

158 

159 Args: 

160 f: Array of frequency points in Hz 

161 fingers: Total number of fingers (must be >= 2) 

162 finger_length: Length of each finger in μm 

163 finger_gap: Gap between adjacent fingers in μm 

164 thickness: Thickness of fingers in μm 

165 cross_section: Cross-section specification 

166 

167 Returns: 

168 sax.SDict: S-parameters dictionary 

169 """ 

170 f_arr = jnp.asarray(f) 

171 z0 = cpw_z0_from_cross_section(cross_section, f_arr) 

172 ep_r = cpw_ep_r_from_cross_section(cross_section) 

173 capacitance = interdigital_capacitor_capacitance_analytical( 

174 fingers=fingers, 

175 finger_length=finger_length, 

176 finger_gap=finger_gap, 

177 thickness=thickness, 

178 ep_r=ep_r, 

179 ) 

180 return capacitor(f=f_arr, capacitance=capacitance, z0=z0) 

181 

182 

183if __name__ == "__main__": 

184 import matplotlib.pyplot as plt 

185 

186 # 1. Plot Plate Capacitor Capacitance vs. Length for different Gaps 

187 lengths = jnp.linspace(10, 500, 100) 

188 gaps_plate = jnp.geomspace(1.0, 20.0, 5) 

189 width_plate = 10.0 

190 ep_r = 11.7 

191 

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

193 

194 # Broadcast to compute total capacitance for all lengths and gaps (shape: (5, 100)) 

195 # length * 1e-6 happens in the analytical formula, here we replicate it 

196 capacitances_plate = ( 

197 plate_capacitor_capacitance_analytical( 

198 length=lengths[None, :], 

199 width=width_plate, 

200 gap=gaps_plate[:, None], 

201 ep_r=ep_r, 

202 ) 

203 * 1e15 

204 ) # Convert to fF 

205 

206 for i, gap in enumerate(gaps_plate): 

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

208 

209 plt.xlabel("Pad Length (µm)") 

210 plt.ylabel("Capacitance (fF)") 

211 plt.title( 

212 rf"Plate Capacitor Capacitance ($\mathtt{{width}}=${width_plate} µm, $\epsilon_r={ep_r}$)" 

213 ) 

214 plt.grid(True) 

215 plt.legend() 

216 plt.tight_layout() 

217 plt.show() 

218 

219 # 2. Plot Interdigital Capacitor Capacitance vs. Finger Length for different Finger Counts 

220 finger_lengths = jnp.linspace(10, 100, 100) 

221 finger_counts = jnp.arange(2, 11, 2) # [2, 4, 6, 8, 10] 

222 finger_gap = 2.0 

223 thickness = 5.0 

224 

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

226 

227 # Broadcast to compute total capacitance for all lengths and counts (shape: (5, 100)) 

228 capacitances_idc = ( 

229 interdigital_capacitor_capacitance_analytical( 

230 fingers=finger_counts[:, None], 

231 finger_length=finger_lengths[None, :], 

232 finger_gap=finger_gap, 

233 thickness=thickness, 

234 ep_r=ep_r, 

235 ) 

236 * 1e15 

237 ) # Convert to fF 

238 

239 for i, n in enumerate(finger_counts): 

240 plt.plot(finger_lengths, capacitances_idc[i], label=f"n = {n} fingers") 

241 

242 plt.xlabel("Overlap Length (µm)") 

243 plt.ylabel("Mutual Capacitance (fF)") 

244 plt.title( 

245 rf"Interdigital Capacitor Capacitance ($\mathtt{{finger\_gap}}=${finger_gap} µm, $\mathtt{{thickness}}=${thickness} µm, $\epsilon_r={ep_r}$)" 

246 ) 

247 plt.grid(True) 

248 plt.legend() 

249 plt.tight_layout() 

250 plt.show()