Coverage for qpdk / models / inductor.py: 100%
51 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"""Inductor and lumped-element resonator models."""
3from functools import partial
5import jax
6import jax.numpy as jnp
7import sax
8from gdsfactory.typings import CrossSectionSpec
10from qpdk.models.capacitor import interdigital_capacitor_capacitance_analytical
11from qpdk.models.constants import DEFAULT_FREQUENCY, μ_0, π
12from qpdk.models.cpw import (
13 cpw_ep_r_from_cross_section,
14 get_cpw_dimensions,
15 get_cpw_substrate_params,
16)
17from qpdk.models.generic import inductor, lc_resonator
20@partial(jax.jit, inline=True)
21def self_inductance_strip(length: float, width: float, thickness: float) -> jax.Array:
22 r"""Analytical formula for the self-inductance of a rectangular metal strip.
24 Uses the formula from :cite:`chenCompactInductorcapacitorResonators2023`:
26 .. math::
28 L_s = \frac{\mu_0 l}{2\pi} \left[ \ln\left(\frac{2l}{w+t}\right) + 0.5 + \frac{w+t}{3l} \right]
30 Args:
31 length: Length of the strip in m.
32 width: Width of the strip in m.
33 thickness: Thickness of the strip in m.
35 Returns:
36 Self-inductance in Henries.
37 """
38 return (μ_0 * length / (2 * π)) * (
39 jnp.log(2 * length / (width + thickness))
40 + 0.5
41 + (width + thickness) / (3 * length)
42 )
45@partial(jax.jit, inline=True)
46def mutual_inductance_parallel_strips(length: float, d: float) -> jax.Array:
47 r"""Analytical formula for the mutual inductance between two parallel metal strips.
49 Uses the formula from :cite:`chenCompactInductorcapacitorResonators2023`:
51 .. math::
53 L_m(d) = \frac{\mu_0 l}{2\pi} \left[ \ln \left( \frac{l}{d} + \sqrt{1 + \frac{l^2}{d^2}} \right) - \sqrt{1 + \frac{d^2}{l^2}} + \frac{d}{l} \right]
55 Args:
56 length: Length of the strips in m.
57 d: Center-to-center distance between the strips in m.
59 Returns:
60 Mutual inductance in Henries.
61 """
62 return (μ_0 * length / (2 * π)) * (
63 jnp.log(length / d + jnp.sqrt(1 + (length / d) ** 2))
64 - jnp.sqrt(1 + (d / length) ** 2)
65 + d / length
66 )
69@partial(jax.jit, inline=True)
70def meander_inductor_inductance_analytical(
71 n_turns: int,
72 turn_length: float,
73 wire_width: float,
74 wire_gap: float,
75 sheet_inductance: float,
76 thickness: float | None = None,
77) -> jax.Array:
78 r"""Analytical formula for meander inductor inductance.
80 The total inductance is the sum of geometric and kinetic contributions:
82 .. math::
84 L_{\text{total}} = L_g + L_k
86 The geometric inductance :math:`L_g` is calculated by summing the
87 self-inductances of all horizontal segments and the mutual inductances
88 between all pairs of parallel segments, following
89 :cite:`chenCompactInductorcapacitorResonators2023`:
91 .. math::
93 L_g = N L_s + 2 \sum_{k=1}^{N-1} (N-k) (-1)^k L_m(k p)
95 where :math:`N` is the number of turns and :math:`p` is the pitch.
97 The kinetic inductance :math:`L_k` is calculated from the sheet
98 inductance :math:`L_\square`:
100 .. math::
102 L_k = L_\square \cdot \frac{\ell_{\text{total}}}{w}
104 Args:
105 n_turns: Number of horizontal meander runs.
106 turn_length: Length of each horizontal run in µm.
107 wire_width: Width of the meander wire in µm.
108 wire_gap: Gap between adjacent meander runs in µm.
109 sheet_inductance: Sheet inductance per square in H/□.
110 thickness: Thickness of the metal film in µm. If None, it is
111 fetched from the PDK technology parameters.
113 Returns:
114 Total inductance in Henries.
115 """
116 if thickness is None:
117 _h, thickness, _ep_r = get_cpw_substrate_params()
119 # Convert to SI (meters)
120 l_m = turn_length * 1e-6
121 w_m = wire_width * 1e-6
122 g_m = wire_gap * 1e-6
123 t_m = thickness * 1e-6
124 p_m = w_m + g_m # Pitch (center-to-center)
126 # 1. Geometric Inductance
127 # Self-inductance of horizontal segments (turns)
128 L_s_horiz = self_inductance_strip(l_m, w_m, t_m)
130 # Self-inductance of vertical connection segments
131 L_s_vert = self_inductance_strip(p_m, w_m, t_m)
133 # Mutual inductance sum between horizontal segments
134 # Formula: L_g = sum(L_s_i) + sum_{i!=j} M_ij
135 # Current directions alternate: sign(i, j) = (-1)**(i-j)
136 # L_g_horiz = N*L_s_horiz + 2 * sum_{i=0 to N-2} sum_{j=i+1 to N-1} (-1)**(j-i) * L_m(abs(j-i)*p)
137 # This simplifies to the sum used in Chen et al. (2023):
138 # L_g_horiz = N*L_s_horiz + 2 * sum_{k=1 to N-1} (N-k) * (-1)**k * L_m(k*p)
140 offsets = jnp.arange(1, 501)
141 mask = offsets < n_turns
142 L_m_sum = jnp.sum(
143 jnp.where(
144 mask,
145 (n_turns - offsets)
146 * ((-1.0) ** offsets)
147 * mutual_inductance_parallel_strips(l_m, offsets * p_m),
148 0.0,
149 )
150 )
152 # Ensure L_g_horiz calculation is accurate. The negative mutual inductance
153 # should be outweighed by the self-inductance for physically valid meanders.
154 L_g_horiz = n_turns * L_s_horiz + 2 * L_m_sum
155 L_g = L_g_horiz + (n_turns - 1) * L_s_vert
157 # 2. Kinetic Inductance
158 # Total wire length in µm (horizontal runs + vertical connections)
159 total_length_um = n_turns * turn_length + jnp.maximum(0, n_turns - 1) * wire_gap
160 n_squares = total_length_um / wire_width
161 L_k = sheet_inductance * n_squares
163 return L_g + L_k
166def meander_inductor(
167 *,
168 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
169 n_turns: int = 5,
170 turn_length: float = 200.0,
171 cross_section: CrossSectionSpec = "meander_inductor_cross_section",
172 sheet_inductance: float = 0.4e-12,
173) -> sax.SDict:
174 r"""Meander inductor SAX model.
176 Computes the inductance from the meander geometry and returns
177 S-parameters of an equivalent lumped inductor.
179 The model extracts the center conductor width and gap from the provided
180 cross-section. To ensure the etched regions of adjacent meander runs
181 do not overlap and interfere with the characteristic impedance of each other,
182 the vertical pitch is calculated as:
184 .. math::
186 p = w + 2 \cdot g
188 where :math:`w` is the center conductor width and :math:`g` is the gap
189 width. This corresponds to a metal-to-metal spacing of :math:`2g`.
191 Args:
192 f: Array of frequency points in Hz.
193 n_turns: Number of horizontal meander runs.
194 turn_length: Length of each horizontal run in µm.
195 cross_section: Cross-section specification for the meander wire.
196 Used to determine the wire width and the gap between runs.
197 sheet_inductance: Sheet inductance per square in H/□.
199 Returns:
200 sax.SDict: S-parameters dictionary.
201 """
202 f_arr = jnp.asarray(f)
203 wire_width, wire_gap_half = get_cpw_dimensions(cross_section)
204 wire_gap = 2 * wire_gap_half
206 inductance = meander_inductor_inductance_analytical(
207 n_turns=n_turns,
208 turn_length=turn_length,
209 wire_width=wire_width,
210 wire_gap=wire_gap,
211 sheet_inductance=sheet_inductance,
212 )
213 return inductor(f=f_arr, inductance=inductance)
216def lumped_element_resonator(
217 *,
218 f: sax.FloatArrayLike = DEFAULT_FREQUENCY,
219 fingers: int = 20,
220 finger_length: float = 20.0,
221 finger_gap: float = 2.0,
222 finger_thickness: float = 5.0,
223 n_turns: int = 5,
224 sheet_inductance: float = 0.4e-12,
225 cross_section: CrossSectionSpec = "meander_inductor_cross_section",
226 grounded: bool = False,
227) -> sax.SDict:
228 r"""Lumped-element LC resonator SAX model.
230 Combines an interdigital capacitor and a meander inductor in parallel
231 to form an LC resonator. The resonance frequency is:
233 .. math::
235 f_r = \frac{1}{2\pi\sqrt{LC}}
237 where :math:`C` is computed from the interdigital capacitor geometry
238 using :func:`~qpdk.models.capacitor.interdigital_capacitor_capacitance_analytical`
239 and :math:`L` is computed from the meander inductor geometry using
240 :func:`meander_inductor_inductance_analytical`.
242 The inductor section uses the width and gap derived from the
243 `cross_section` to ensure consistent RF behavior across the meander.
244 The vertical spacing between meander runs is set to twice the etch gap
245 to prevent overlap of the etched regions.
247 See :cite:`kimThinfilmSuperconductingResonator2011,chenCompactInductorcapacitorResonators2023`.
249 Args:
250 f: Array of frequency points in Hz.
251 fingers: Number of interdigital capacitor fingers.
252 finger_length: Length of each capacitor finger in µm.
253 finger_gap: Gap between adjacent capacitor fingers in µm.
254 finger_thickness: Width of each capacitor finger in µm.
255 n_turns: Number of horizontal meander inductor runs (must be odd to
256 match the cell geometry where the path spans left-to-right bus bars).
257 sheet_inductance: Sheet inductance per square in H/□.
258 cross_section: Cross-section specification. Used for substrate
259 permittivity and to determine inductor wire width and gap.
260 grounded: If True, one port of the resonator is grounded.
262 Returns:
263 sax.SDict: S-parameters dictionary with ports o1 and o2.
264 """
265 f_arr = jnp.asarray(f)
267 ep_r = cpw_ep_r_from_cross_section(cross_section)
269 capacitance = interdigital_capacitor_capacitance_analytical(
270 fingers=fingers,
271 finger_length=finger_length,
272 finger_gap=finger_gap,
273 thickness=finger_thickness,
274 ep_r=ep_r,
275 )
277 wire_width, wire_gap_half = get_cpw_dimensions(cross_section)
278 wire_gap = 2 * wire_gap_half
280 cap_width = 2 * finger_thickness + finger_length + finger_gap
281 meander_turn_length = cap_width - 4 * wire_width
283 inductance = meander_inductor_inductance_analytical(
284 n_turns=n_turns,
285 turn_length=meander_turn_length,
286 wire_width=wire_width,
287 wire_gap=wire_gap,
288 sheet_inductance=sheet_inductance,
289 )
291 return lc_resonator(
292 f=f_arr,
293 capacitance=capacitance,
294 inductance=inductance,
295 grounded=grounded,
296 )