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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
1"""Capacitor Models."""
3from functools import partial
5import jax
6import jax.numpy as jnp
7import sax
8from gdsfactory.typings import CrossSectionSpec
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)
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.
29 The model assumes two coplanar rectangular pads on a substrate.
30 The capacitance is calculated using conformal mapping:
32 .. math::
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)}
39 where :math:`s` is the gap, :math:`W` is the pad width, and :math:`L` is the pad length.
41 See :cite:`chenCompactInductorcapacitorResonators2023`.
43 Returns:
44 The calculated capacitance in Farads.
45 """
46 # Conformal mapping for coplanar pads
47 k_sq = (gap / (gap + 2 * width)) ** 2
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)
53 return (length * 1e-6) * c_pul
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.
66 The formula uses conformal mapping for the interior and exterior regions of
67 the interdigital structure:
69 .. math::
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')}
77 The total mutual capacitance for :math:`n` fingers is:
79 .. math::
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}
86 where :math:`w` is the finger thickness (width), :math:`g` is the finger gap, and
87 :math:`L` is the overlap length.
89 See :cite:`igrejaAnalyticalEvaluationInterdigital2004,gonzalezDesignFabricationInterdigital2015`.
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
101 # Elliptic integral moduli squared
102 ki_sq = jnp.sin(jnp.pi * η / 2) ** 2
103 ke_sq = (2 * jnp.sqrt(η) / (1 + η)) ** 2
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)
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 )
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.
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
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)
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.
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
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)
183if __name__ == "__main__":
184 import matplotlib.pyplot as plt
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
192 plt.figure(figsize=(10, 6))
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
206 for i, gap in enumerate(gaps_plate):
207 plt.plot(lengths, capacitances_plate[i], label=f"gap = {gap:.1f} µm")
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()
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
225 plt.figure(figsize=(10, 6))
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
239 for i, n in enumerate(finger_counts):
240 plt.plot(finger_lengths, capacitances_idc[i], label=f"n = {n} fingers")
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()