sqil-core 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqil_core/__init__.py +5 -2
- sqil_core/config.py +13 -0
- sqil_core/fit/__init__.py +16 -0
- sqil_core/fit/_core.py +936 -0
- sqil_core/fit/_fit.py +782 -0
- sqil_core/fit/_models.py +96 -0
- sqil_core/resonator/__init__.py +11 -0
- sqil_core/resonator/_resonator.py +807 -0
- sqil_core/utils/__init__.py +62 -5
- sqil_core/utils/_analysis.py +292 -0
- sqil_core/utils/{const.py → _const.py} +49 -38
- sqil_core/utils/_formatter.py +188 -0
- sqil_core/utils/_plot.py +107 -0
- sqil_core/utils/{read.py → _read.py} +179 -156
- sqil_core/utils/_utils.py +17 -0
- {sqil_core-0.0.1.dist-info → sqil_core-0.1.0.dist-info}/METADATA +32 -7
- sqil_core-0.1.0.dist-info/RECORD +19 -0
- {sqil_core-0.0.1.dist-info → sqil_core-0.1.0.dist-info}/WHEEL +1 -1
- {sqil_core-0.0.1.dist-info → sqil_core-0.1.0.dist-info}/entry_points.txt +1 -1
- sqil_core/utils/analysis.py +0 -68
- sqil_core/utils/formatter.py +0 -134
- sqil_core-0.0.1.dist-info/RECORD +0 -10
@@ -0,0 +1,807 @@
|
|
1
|
+
import matplotlib.pyplot as plt
|
2
|
+
import numpy as np
|
3
|
+
from lmfit import Model
|
4
|
+
from scipy.optimize import leastsq, minimize
|
5
|
+
|
6
|
+
from sqil_core.fit import fit_circle_algebraic, fit_output, fit_skewed_lorentzian
|
7
|
+
from sqil_core.utils import estimate_linear_background
|
8
|
+
|
9
|
+
|
10
|
+
@fit_output
|
11
|
+
def fit_phase_vs_freq_global(
|
12
|
+
freq: np.ndarray,
|
13
|
+
phase: np.ndarray,
|
14
|
+
theta0: float | None = None,
|
15
|
+
Q_tot: float | None = None,
|
16
|
+
fr: float | None = None,
|
17
|
+
disp: bool = False,
|
18
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
19
|
+
"""
|
20
|
+
Fits phase response data as a function of frequency using an arctangent model.
|
21
|
+
|
22
|
+
This function models the phase response of a superconducting resonator or circuit
|
23
|
+
as a function of frequency. It fits the data using the model:
|
24
|
+
θ(f) = θ₀ + 2 * arctan(2 * Q_tot * (1 - f / fr))
|
25
|
+
where θ₀ is the phase offset, Q_tot is the total quality factor, and fr is the
|
26
|
+
resonant frequency. The fitting is performed using the Nelder-Mead optimization
|
27
|
+
method to minimize the sum of squared residuals between the measured and modeled phase.
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
freq : np.ndarray
|
32
|
+
Array of frequency data points (in Hz).
|
33
|
+
phase : np.ndarray
|
34
|
+
Array of measured phase data (in radians).
|
35
|
+
theta0 : float, optional
|
36
|
+
Initial guess for the phase offset θ₀. If not provided, defaults to the mean of `phase`.
|
37
|
+
Q_tot : float, optional
|
38
|
+
Initial guess for the total quality factor. If not provided, defaults to 0.01.
|
39
|
+
fr : float, optional
|
40
|
+
Initial guess for the resonant frequency. If not provided, defaults to the mean of `freq`.
|
41
|
+
disp : bool, optional
|
42
|
+
If True, displays optimization progress. Default is True.
|
43
|
+
|
44
|
+
Returns
|
45
|
+
-------
|
46
|
+
FitResult
|
47
|
+
A `FitResult` object containing:
|
48
|
+
- Fitted parameters (`params`).
|
49
|
+
- Standard errors (`std_err`).
|
50
|
+
- Goodness-of-fit metrics (`red_chi2`).
|
51
|
+
- A callable `predict` function for generating fitted responses.
|
52
|
+
|
53
|
+
Notes
|
54
|
+
-----
|
55
|
+
- The model assumes the phase response follows the arctangent behavior typical in
|
56
|
+
superconducting resonators near resonance.
|
57
|
+
|
58
|
+
Examples
|
59
|
+
--------
|
60
|
+
>>> freq = np.linspace(5e9, 6e9, 1000) # Frequency in Hz
|
61
|
+
>>> phase = np.random.normal(0, 0.1, size=freq.size) # Simulated noisy phase data
|
62
|
+
>>> popt, perr = fit_phase_vs_freq(freq, phase)
|
63
|
+
>>> print("Fitted Parameters (θ₀, Q_tot, fr):", popt)
|
64
|
+
>>> print("Percentage Errors:", perr)
|
65
|
+
"""
|
66
|
+
if theta0 is None:
|
67
|
+
theta0 = np.mean(phase)
|
68
|
+
if Q_tot is None:
|
69
|
+
Q_tot = 0.01
|
70
|
+
if fr is None:
|
71
|
+
fr = np.mean(freq) # freq[np.argmin(np.abs(phase - np.mean(phase)))]
|
72
|
+
|
73
|
+
def objective(x):
|
74
|
+
theta0, Q_tot, fr = x
|
75
|
+
model = theta0 + 2 * np.arctan(2 * Q_tot * (1 - freq / fr))
|
76
|
+
residuals = phase - model
|
77
|
+
return np.square(residuals).sum()
|
78
|
+
|
79
|
+
res = minimize(
|
80
|
+
fun=objective,
|
81
|
+
x0=[theta0, Q_tot, fr],
|
82
|
+
method="Nelder-Mead",
|
83
|
+
options={"maxiter": 3000000, "disp": disp},
|
84
|
+
)
|
85
|
+
|
86
|
+
return res, {
|
87
|
+
"predict": lambda f: theta0 + 2 * np.arctan(2 * Q_tot * (1 - f / fr)),
|
88
|
+
"param_names": ["θ₀", "Q_tot", "fr"],
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
@fit_output
|
93
|
+
def fit_phase_vs_freq(freq, phase, theta0, Q_tot, fr):
|
94
|
+
"""
|
95
|
+
Fits the phase response of a superconducting resonator using an arctangent model.
|
96
|
+
|
97
|
+
Reference: https://arxiv.org/abs/1410.3365
|
98
|
+
This function models the phase response as:
|
99
|
+
φ(f) = θ₀ + 2 * arctan(2 * Q_tot * (1 - f / f_r))
|
100
|
+
|
101
|
+
where:
|
102
|
+
- φ(f) is the measured phase response (in radians),
|
103
|
+
- θ₀ is the phase offset,
|
104
|
+
- Q_tot is the total (loaded) quality factor,
|
105
|
+
- f_r is the resonant frequency.
|
106
|
+
|
107
|
+
The fitting is performed using a stepwise least-squares optimization to accurately
|
108
|
+
estimate the parameters θ₀, Q_tot, and f_r from experimental data.
|
109
|
+
|
110
|
+
Parameters
|
111
|
+
----------
|
112
|
+
freq : array-like
|
113
|
+
Frequency data (in Hz) at which the phase response was measured.
|
114
|
+
phase : array-like
|
115
|
+
Unwrapped phase response data (in radians) corresponding to `freq`.
|
116
|
+
theta0 : float
|
117
|
+
Initial guess for the phase offset θ₀ (in radians).
|
118
|
+
Q_tot : float
|
119
|
+
Initial guess for the total (loaded) quality factor Q_tot.
|
120
|
+
fr : float
|
121
|
+
Initial guess for the resonant frequency f_r (in Hz).
|
122
|
+
|
123
|
+
Returns
|
124
|
+
-------
|
125
|
+
FitResult
|
126
|
+
A `FitResult` object containing:
|
127
|
+
- Fitted parameters (`params`).
|
128
|
+
- Standard errors (`std_err`).
|
129
|
+
- Goodness-of-fit metrics (`red_chi2`).
|
130
|
+
- A callable `predict` function for generating fitted responses.
|
131
|
+
|
132
|
+
Notes
|
133
|
+
-----
|
134
|
+
- The fitting is performed in multiple stages for improved stability:
|
135
|
+
1. Optimize θ₀ and f_r (fixing Q_tot).
|
136
|
+
2. Optimize Q_tot and f_r (fixing θ₀).
|
137
|
+
3. Optimize f_r alone.
|
138
|
+
4. Optimize Q_tot alone.
|
139
|
+
5. Joint optimization of θ₀, Q_tot, and f_r.
|
140
|
+
- This stepwise optimization handles parameter coupling and improves convergence.
|
141
|
+
|
142
|
+
Example
|
143
|
+
-------
|
144
|
+
>>> fitted_params, percent_errors = fit_phase_vs_freq(freq, phase, 0.0, 1000, 5e9)
|
145
|
+
>>> print(f"Fitted Parameters: θ₀ = {fitted_params[0]}, Q_tot = {fitted_params[1]}, f_r = {fitted_params[2]}")
|
146
|
+
>>> print(f"Percentage Errors: θ₀ = {percent_errors[0]}%, Q_tot = {percent_errors[1]}%, f_r = {percent_errors[2]}%")
|
147
|
+
"""
|
148
|
+
# Unwrap the phase of the complex data to avoid discontinuities
|
149
|
+
phase = np.unwrap(phase)
|
150
|
+
|
151
|
+
# Define the distance function to handle phase wrapping
|
152
|
+
def dist(x):
|
153
|
+
np.absolute(x, x)
|
154
|
+
c = (x > np.pi).astype(int)
|
155
|
+
return x + c * (-2.0 * x + 2.0 * np.pi)
|
156
|
+
|
157
|
+
# Step 1: Optimize θ₀ and fr with Q_tot fixed
|
158
|
+
def residuals_1(p, x, y, Q_tot):
|
159
|
+
theta0, fr = p
|
160
|
+
err = dist(y - (theta0 + 2.0 * np.arctan(2.0 * Q_tot * (1.0 - x / fr))))
|
161
|
+
return err
|
162
|
+
|
163
|
+
p0 = [theta0, fr]
|
164
|
+
p_final = leastsq(
|
165
|
+
lambda a, b, c: residuals_1(a, b, c, Q_tot), p0, args=(freq, phase)
|
166
|
+
)
|
167
|
+
theta0, fr = p_final[0]
|
168
|
+
|
169
|
+
# Step 2: Optimize Q_tot and fr with θ₀ fixed
|
170
|
+
def residuals_2(p, x, y, theta0):
|
171
|
+
Q_tot, fr = p
|
172
|
+
err = dist(y - (theta0 + 2.0 * np.arctan(2.0 * Q_tot * (1.0 - x / fr))))
|
173
|
+
return err
|
174
|
+
|
175
|
+
p0 = [Q_tot, fr]
|
176
|
+
p_final = leastsq(
|
177
|
+
lambda a, b, c: residuals_2(a, b, c, theta0), p0, args=(freq, phase)
|
178
|
+
)
|
179
|
+
Q_tot, fr = p_final[0]
|
180
|
+
|
181
|
+
# Step 3: Optimize fr alone
|
182
|
+
def residuals_3(p, x, y, theta0, Q_tot):
|
183
|
+
fr = p
|
184
|
+
err = dist(y - (theta0 + 2.0 * np.arctan(2.0 * Q_tot * (1.0 - x / fr))))
|
185
|
+
return err
|
186
|
+
|
187
|
+
p0 = fr
|
188
|
+
p_final = leastsq(
|
189
|
+
lambda a, b, c: residuals_3(a, b, c, theta0, Q_tot), p0, args=(freq, phase)
|
190
|
+
)
|
191
|
+
fr = float(p_final[0])
|
192
|
+
|
193
|
+
# Step 4: Optimize Q_tot alone
|
194
|
+
def residuals_4(p, x, y, theta0, fr):
|
195
|
+
Q_tot = p
|
196
|
+
err = dist(y - (theta0 + 2.0 * np.arctan(2.0 * Q_tot * (1.0 - x / fr))))
|
197
|
+
return err
|
198
|
+
|
199
|
+
p0 = Q_tot
|
200
|
+
p_final = leastsq(
|
201
|
+
lambda a, b, c: residuals_4(a, b, c, theta0, fr), p0, args=(freq, phase)
|
202
|
+
)
|
203
|
+
Q_tot = float(p_final[0])
|
204
|
+
|
205
|
+
# Step 5: Joint optimization of θ₀, Q_tot, and fr
|
206
|
+
def residuals_5(p, x, y):
|
207
|
+
theta0, Q_tot, fr = p
|
208
|
+
err = dist(y - (theta0 + 2.0 * np.arctan(2.0 * Q_tot * (1.0 - x / fr))))
|
209
|
+
return err
|
210
|
+
|
211
|
+
p0 = [theta0, Q_tot, fr]
|
212
|
+
final_result = leastsq(residuals_5, p0, args=(freq, phase), full_output=True)
|
213
|
+
|
214
|
+
return (
|
215
|
+
final_result,
|
216
|
+
{
|
217
|
+
"predict": lambda f: theta0 + 2 * np.arctan(2 * Q_tot * (1 - f / fr)),
|
218
|
+
"param_names": ["θ₀", "Q_tot", "fr"],
|
219
|
+
},
|
220
|
+
)
|
221
|
+
|
222
|
+
|
223
|
+
def S11_reflection(
|
224
|
+
freq: np.ndarray,
|
225
|
+
a: float,
|
226
|
+
alpha: float,
|
227
|
+
tau: float,
|
228
|
+
Q_tot: float,
|
229
|
+
Q_ext: float,
|
230
|
+
fr: float,
|
231
|
+
phi: float,
|
232
|
+
mag_bg: np.ndarray | None = None,
|
233
|
+
) -> np.ndarray:
|
234
|
+
"""
|
235
|
+
Calculates the S11 reflection coefficient for a superconducting resonator with an optional magnitude background.
|
236
|
+
|
237
|
+
This function models the S11 reflection parameter, representing how much of an
|
238
|
+
incident signal is reflected by a resonator. It includes both the resonator's
|
239
|
+
frequency-dependent response and an optional magnitude background correction,
|
240
|
+
providing a more accurate fit for experimental data.
|
241
|
+
|
242
|
+
The S11 reflection is computed as:
|
243
|
+
S11(f) = env(f) * resonator(f)
|
244
|
+
where:
|
245
|
+
- env(f) = a * mag_bg(f) * exp(i * α) * exp(2πi * (f - f₀) * τ)
|
246
|
+
models the environmental response, including amplitude scaling, phase shifts,
|
247
|
+
time delays, and optional frequency-dependent magnitude background.
|
248
|
+
- resonator(f) = 1 - [2 * Q_tot / |Q_ext|] * exp(i * φ) / [1 + 2i * Q_tot * (f / fr - 1)]
|
249
|
+
models the resonator's frequency response.
|
250
|
+
|
251
|
+
Parameters
|
252
|
+
----------
|
253
|
+
freq : np.ndarray
|
254
|
+
Array of frequency points (in Hz) at which to evaluate the S11 parameter.
|
255
|
+
a : float
|
256
|
+
Amplitude scaling factor for the environmental response.
|
257
|
+
alpha : float
|
258
|
+
Phase offset (in radians) for the environmental response.
|
259
|
+
tau : float
|
260
|
+
Time delay (in seconds) representing the signal path delay.
|
261
|
+
Q_tot : float
|
262
|
+
Total quality factor of the resonator (includes internal and external losses).
|
263
|
+
Q_ext : float
|
264
|
+
External quality factor, representing coupling losses to external circuitry.
|
265
|
+
fr : float
|
266
|
+
Resonant frequency of the resonator (in Hz).
|
267
|
+
phi : float
|
268
|
+
Additional phase shift (in radians) in the resonator response.
|
269
|
+
mag_bg : np.ndarray or None, optional
|
270
|
+
Frequency-dependent magnitude background correction. If provided, it should be
|
271
|
+
an array of the same shape as `freq`. Defaults to 1 (no correction).
|
272
|
+
|
273
|
+
Returns
|
274
|
+
-------
|
275
|
+
S11 : np.ndarray
|
276
|
+
Complex array representing the S11 reflection coefficient across the input frequencies.
|
277
|
+
|
278
|
+
Notes
|
279
|
+
-----
|
280
|
+
- Passing mag_bg = np.nan has the same effect of passing mag_bg = None
|
281
|
+
|
282
|
+
Examples
|
283
|
+
--------
|
284
|
+
>>> freq = np.linspace(4.9e9, 5.1e9, 500) # Frequency sweep around 5 GHz
|
285
|
+
>>> mag_bg = freq**2 + 3 * freq # Example magnitude background
|
286
|
+
>>> S11 = S11_reflection(freq, a=1.0, alpha=0.0, tau=1e-9,
|
287
|
+
... Q_tot=5000, Q_ext=10000, fr=5e9, phi=0.0, mag_bg=mag_bg)
|
288
|
+
>>> import matplotlib.pyplot as plt
|
289
|
+
>>> plt.plot(freq, 20 * np.log10(np.abs(S11))) # Plot magnitude in dB
|
290
|
+
>>> plt.xlabel("Frequency (Hz)")
|
291
|
+
>>> plt.ylabel("S11 Magnitude (dB)")
|
292
|
+
>>> plt.title("S11 Reflection Coefficient with Magnitude Background")
|
293
|
+
>>> plt.show()
|
294
|
+
"""
|
295
|
+
if mag_bg is None:
|
296
|
+
mag_bg = 1
|
297
|
+
elif np.isscalar(mag_bg) and np.isnan(mag_bg):
|
298
|
+
mag_bg = 1
|
299
|
+
|
300
|
+
env = a * mag_bg * np.exp(1j * alpha) * np.exp(2j * np.pi * (freq - freq[0]) * tau)
|
301
|
+
resonator = 1 - (2 * Q_tot / np.abs(Q_ext)) * np.exp(1j * phi) / (
|
302
|
+
1 + 2j * Q_tot * (freq / fr - 1)
|
303
|
+
)
|
304
|
+
return env * resonator
|
305
|
+
|
306
|
+
|
307
|
+
def S21_hanger(
|
308
|
+
freq: np.ndarray,
|
309
|
+
a: float,
|
310
|
+
alpha: float,
|
311
|
+
tau: float,
|
312
|
+
Q_tot: float,
|
313
|
+
Q_ext: float,
|
314
|
+
fr: float,
|
315
|
+
phi: float,
|
316
|
+
mag_bg: np.ndarray | None = None,
|
317
|
+
) -> np.ndarray:
|
318
|
+
"""
|
319
|
+
Calculates the S21 transmission coefficient using the hanger resonator model with an optional magnitude background.
|
320
|
+
|
321
|
+
This function models the S21 transmission parameter, which describes how much of an
|
322
|
+
incident signal is transmitted through a superconducting resonator. The model combines
|
323
|
+
the resonator's frequency-dependent response with an environmental background response
|
324
|
+
and an optional magnitude background correction to more accurately reflect experimental data.
|
325
|
+
|
326
|
+
The S21 transmission is computed as:
|
327
|
+
S21(f) = env(f) * resonator(f)
|
328
|
+
where:
|
329
|
+
- env(f) = a * mag_bg(f) * exp(i * α) * exp(2πi * (f - f₀) * τ)
|
330
|
+
models the environmental response, accounting for amplitude scaling, phase shifts,
|
331
|
+
and signal path delays.
|
332
|
+
- resonator(f) = 1 - [Q_tot / |Q_ext|] * exp(i * φ) / [1 + 2i * Q_tot * (f / fr - 1)]
|
333
|
+
models the frequency response of the hanger-type resonator.
|
334
|
+
|
335
|
+
Parameters
|
336
|
+
----------
|
337
|
+
freq : np.ndarray
|
338
|
+
Array of frequency points (in Hz) at which to evaluate the S21 parameter.
|
339
|
+
a : float
|
340
|
+
Amplitude scaling factor for the environmental response.
|
341
|
+
alpha : float
|
342
|
+
Phase offset (in radians) for the environmental response.
|
343
|
+
tau : float
|
344
|
+
Time delay (in seconds) representing the signal path delay.
|
345
|
+
Q_tot : float
|
346
|
+
Total quality factor of the resonator (includes internal and external losses).
|
347
|
+
Q_ext : float
|
348
|
+
External quality factor, representing coupling losses to external circuitry.
|
349
|
+
fr : float
|
350
|
+
Resonant frequency of the resonator (in Hz).
|
351
|
+
phi : float
|
352
|
+
Additional phase shift (in radians) in the resonator response.
|
353
|
+
mag_bg : np.ndarray or None, optional
|
354
|
+
Frequency-dependent magnitude background correction. If provided, it should be
|
355
|
+
an array of the same shape as `freq`. Defaults to 1 (no correction).
|
356
|
+
|
357
|
+
Returns
|
358
|
+
-------
|
359
|
+
S21 : np.ndarray
|
360
|
+
Complex array representing the S21 transmission coefficient across the input frequencies.
|
361
|
+
|
362
|
+
Notes
|
363
|
+
-----
|
364
|
+
- Passing mag_bg = np.nan has the same effect of passing mag_bg = None
|
365
|
+
|
366
|
+
Examples
|
367
|
+
--------
|
368
|
+
>>> freq = np.linspace(4.9e9, 5.1e9, 500) # Frequency sweep around 5 GHz
|
369
|
+
>>> mag_bg = freq**2 + 3 * freq # Example magnitude background
|
370
|
+
>>> S21 = S21_hanger(freq, a=1.0, alpha=0.0, tau=1e-9,
|
371
|
+
... Q_tot=5000, Q_ext=10000, fr=5e9, phi=0.0, mag_bg=mag_bg)
|
372
|
+
>>> import matplotlib.pyplot as plt
|
373
|
+
>>> plt.plot(freq, 20 * np.log10(np.abs(S21))) # Plot magnitude in dB
|
374
|
+
>>> plt.xlabel("Frequency (Hz)")
|
375
|
+
>>> plt.ylabel("S21 Magnitude (dB)")
|
376
|
+
>>> plt.title("S21 Transmission Coefficient with Magnitude Background")
|
377
|
+
>>> plt.show()
|
378
|
+
"""
|
379
|
+
if mag_bg is None:
|
380
|
+
mag_bg = 1
|
381
|
+
elif np.isscalar(mag_bg) and np.isnan(mag_bg):
|
382
|
+
mag_bg = 1
|
383
|
+
|
384
|
+
env = a * mag_bg * np.exp(1j * alpha) * np.exp(2j * np.pi * (freq - freq[0]) * tau)
|
385
|
+
resonator = 1 - (Q_tot / np.abs(Q_ext)) * np.exp(1j * phi) / (
|
386
|
+
1 + 2j * Q_tot * (freq / fr - 1)
|
387
|
+
)
|
388
|
+
return env * resonator
|
389
|
+
|
390
|
+
|
391
|
+
def S11_reflection_mesh(freq, a, alpha, tau, Q_tot, Q_ext, fr, phi):
|
392
|
+
"""
|
393
|
+
Vectorized S11 reflection function.
|
394
|
+
|
395
|
+
Parameters
|
396
|
+
----------
|
397
|
+
freq : array, shape (N,)
|
398
|
+
Frequency points.
|
399
|
+
a, alpha, tau, Q_tot, Q_ext, fr, phi : scalar or array
|
400
|
+
Parameters of the S11 model.
|
401
|
+
|
402
|
+
Returns
|
403
|
+
-------
|
404
|
+
S11 : array
|
405
|
+
Complex reflection coefficient. Shape is (M1, M2, ..., N) where M1, M2, ... are the broadcasted shapes of the parameters.
|
406
|
+
"""
|
407
|
+
# Ensure freq is at least 2D for broadcasting (1, N)
|
408
|
+
freq = np.atleast_1d(freq) # (N,)
|
409
|
+
|
410
|
+
# Ensure all parameters are at least 1D arrays for broadcasting
|
411
|
+
a = np.atleast_1d(a) # (M1,)
|
412
|
+
alpha = np.atleast_1d(alpha) # (M2,)
|
413
|
+
tau = np.atleast_1d(tau) # (M3,)
|
414
|
+
Q_tot = np.atleast_1d(Q_tot) # (M4,)
|
415
|
+
Q_ext = np.atleast_1d(Q_ext) # (M5,)
|
416
|
+
fr = np.atleast_1d(fr) # (M6,)
|
417
|
+
phi = np.atleast_1d(phi) # (M7,)
|
418
|
+
|
419
|
+
# Reshape frequency to (1, 1, ..., 1, N) for proper broadcasting
|
420
|
+
# This makes sure freq has shape (1, 1, ..., N)
|
421
|
+
freq = freq[np.newaxis, ...]
|
422
|
+
|
423
|
+
# Calculate the envelope part
|
424
|
+
env = (
|
425
|
+
a[..., np.newaxis]
|
426
|
+
* np.exp(1j * alpha[..., np.newaxis])
|
427
|
+
* np.exp(2j * np.pi * (freq - freq[..., 0:1]) * tau[..., np.newaxis])
|
428
|
+
)
|
429
|
+
|
430
|
+
# Calculate the resonator part
|
431
|
+
resonator = 1 - (
|
432
|
+
2 * Q_tot[..., np.newaxis] / np.abs(Q_ext[..., np.newaxis])
|
433
|
+
) * np.exp(1j * phi[..., np.newaxis]) / (
|
434
|
+
1 + 2j * Q_tot[..., np.newaxis] * (freq / fr[..., np.newaxis] - 1)
|
435
|
+
)
|
436
|
+
|
437
|
+
return env * resonator
|
438
|
+
|
439
|
+
|
440
|
+
def quick_fit(
|
441
|
+
freq: np.ndarray,
|
442
|
+
data: np.ndarray,
|
443
|
+
measurement: str,
|
444
|
+
tau: float | None = None,
|
445
|
+
Q_tot: float | None = None,
|
446
|
+
fr: float | None = None,
|
447
|
+
mag_bg: np.ndarray | None = None,
|
448
|
+
fit_range: float | None = None,
|
449
|
+
bias_toward_fr: bool = False,
|
450
|
+
verbose: bool = False,
|
451
|
+
do_plot: bool = False,
|
452
|
+
) -> tuple[float, float, float, complex, float, float, float]:
|
453
|
+
"""
|
454
|
+
Extracts resonator parameters from complex S-parameter data using circle fitting for reflection or hanger measurements.
|
455
|
+
|
456
|
+
This function analyzes complex-valued resonator data by fitting a circle in the complex plane and
|
457
|
+
refining key resonator parameters. It estimates or refines the total quality factor (Q_tot),
|
458
|
+
resonance frequency (fr), and external quality factor (Q_ext), while correcting for impedance mismatch.
|
459
|
+
|
460
|
+
Parameters
|
461
|
+
----------
|
462
|
+
freq : np.ndarray
|
463
|
+
Frequency data.
|
464
|
+
data : np.ndarray
|
465
|
+
Complex-valued S-parameter data (e.g., S11 for reflection or S21 for hanger configuration).
|
466
|
+
measurement : {'reflection', 'hanger'}
|
467
|
+
Type of measurement setup. Use 'reflection' for S11 or 'hanger' for S21.
|
468
|
+
tau : float, optional
|
469
|
+
Initial guess for the cable delay IN RADIANS. If you are passing a value obtained from a linear fit
|
470
|
+
divide it by 2pi. If None, it is estimated from a linear fit.
|
471
|
+
Q_tot : float, optional
|
472
|
+
Initial guess for the total quality factor. If None, it is estimated from a skewed Lorentzian fit.
|
473
|
+
fr : float, optional
|
474
|
+
Initial guess for the resonance frequency. If None, it is estimated from a skewed Lorentzian fit.
|
475
|
+
mag_bg : np.ndarray, optional
|
476
|
+
Magnitude background correction data. Defaults to NaN if not provided.
|
477
|
+
fit_range: float, optional
|
478
|
+
The number x that defines the range [fr-x, fr+x] in which the fitting should be performed. The estimation of
|
479
|
+
cable delay and amplitude background will be perfomed on the full data. Defaults to `3 * fr / Q_tot`.
|
480
|
+
bias_toward_fr : bool, optional
|
481
|
+
If true performs circle fits using the 50% of points closest to the resonance. Defaults is False.
|
482
|
+
verbose : bool, optional
|
483
|
+
If True, detailed fitting results and progress are printed. Default is False.
|
484
|
+
do_plot : bool, optional
|
485
|
+
If True, plots the fitted circle and off-resonant point for visualization. Default is False.
|
486
|
+
|
487
|
+
Returns
|
488
|
+
-------
|
489
|
+
tuple
|
490
|
+
A tuple containing:
|
491
|
+
- a (float): Amplitude scaling factor from the off-resonant point.
|
492
|
+
- alpha (float): Phase offset from the off-resonant point (in radians).
|
493
|
+
- Q_tot (float): Total quality factor.
|
494
|
+
- Q_ext (complex): External quality factor, accounting for impedance mismatch.
|
495
|
+
- fr (float): Resonance frequency.
|
496
|
+
- phi0 (float): Phase shift due to impedance mismatch (in radians).
|
497
|
+
- theta0 (float): Refined phase offset at resonance.
|
498
|
+
|
499
|
+
Notes
|
500
|
+
-----
|
501
|
+
- If `tau` is not provided it is estimated by fitting a line through the last 5% of phase points.
|
502
|
+
- If `Q_tot` or `fr` is not provided, they are estimated by fitting a skewed Lorentzian model.
|
503
|
+
- Visualization helps assess the quality of intermediate steps.
|
504
|
+
|
505
|
+
Examples
|
506
|
+
--------
|
507
|
+
>>> import numpy as np
|
508
|
+
>>> data = np.random.rand(100) + 1j * np.random.rand(100)
|
509
|
+
>>> a, alpha, Q_tot, Q_ext, fr, phi0, theta0 = quick_fit(data, measurement='reflection', verbose=True, do_plot=True)
|
510
|
+
>>> print(f"Resonance Frequency: {fr} Hz, Q_tot: {Q_tot}, Q_ext: {Q_ext}")
|
511
|
+
"""
|
512
|
+
# Sanitize inputs
|
513
|
+
if measurement != "reflection" and measurement != "hanger":
|
514
|
+
raise Exception(
|
515
|
+
f"Invalid measurement type {measurement}. Must be either 'reflection' or 'hanger'"
|
516
|
+
)
|
517
|
+
if mag_bg is None:
|
518
|
+
mag_bg = np.nan
|
519
|
+
|
520
|
+
# Define amplitude and phase
|
521
|
+
linmag = np.abs(data)
|
522
|
+
phase = np.unwrap(np.angle(data))
|
523
|
+
|
524
|
+
# Inital estimate for Q_tot and fr by fitting a skewed lorentzian on
|
525
|
+
# the squared manitude data
|
526
|
+
if (Q_tot is None) or (fr is None):
|
527
|
+
if verbose:
|
528
|
+
print("* Skewed lorentzian estimation of fr and Q_tot")
|
529
|
+
norm_linmag = linmag / np.max(linmag)
|
530
|
+
fit_res = fit_skewed_lorentzian(freq, norm_linmag**2)
|
531
|
+
(A1, A2, A3, A4, efr, eQ_tot) = fit_res.params
|
532
|
+
# Assign only parameters for which no initial guess was provided
|
533
|
+
Q_tot = Q_tot or eQ_tot
|
534
|
+
fr = fr or efr
|
535
|
+
if verbose:
|
536
|
+
fit_res.summary()
|
537
|
+
if fr != efr:
|
538
|
+
print(f" -> Still considering fr = {fr}\n")
|
539
|
+
elif Q_tot != eQ_tot:
|
540
|
+
print(f" -> Still considering Q_tot = {Q_tot}\n")
|
541
|
+
|
542
|
+
# Initial estimate for tau by fitting a line through the last 5% of phase points
|
543
|
+
if tau is None:
|
544
|
+
if verbose:
|
545
|
+
print("* Linear estimation of cable delay from the last 5% of phase points")
|
546
|
+
[_, tau] = estimate_linear_background(
|
547
|
+
freq, phase, points_cut=0.05, cut_from_back=True
|
548
|
+
)
|
549
|
+
tau /= 2 * np.pi
|
550
|
+
if verbose:
|
551
|
+
print(f" -> tau [rad]: {tau}\n")
|
552
|
+
|
553
|
+
# Remove cable delay
|
554
|
+
phase1 = phase - 2 * np.pi * tau * (freq - freq[0])
|
555
|
+
data1 = linmag * np.exp(1j * phase1)
|
556
|
+
|
557
|
+
# Cut data around the estimated resonance frequency
|
558
|
+
if fit_range is None:
|
559
|
+
fit_range = 3 * fr / Q_tot
|
560
|
+
mask = (freq > fr - fit_range) & (freq < fr + fit_range)
|
561
|
+
freq, linmag, phase, data = freq[mask], linmag[mask], phase[mask], data[mask]
|
562
|
+
phase1, data1 = phase1[mask], data1[mask]
|
563
|
+
if not np.isscalar(mag_bg):
|
564
|
+
mag_bg = mag_bg[mask]
|
565
|
+
|
566
|
+
# Move cirle to center
|
567
|
+
if bias_toward_fr:
|
568
|
+
fr_idx = np.abs(freq - fr).argmin()
|
569
|
+
idx_range = int(len(freq) / 4)
|
570
|
+
re, im = np.real(data1), np.imag(data1)
|
571
|
+
fit_res = fit_circle_algebraic(
|
572
|
+
re[fr_idx - idx_range : fr_idx + idx_range],
|
573
|
+
im[fr_idx - idx_range : fr_idx + idx_range],
|
574
|
+
)
|
575
|
+
else:
|
576
|
+
fit_res = fit_circle_algebraic(np.real(data1), np.imag(data1))
|
577
|
+
(xc, yc, r0) = fit_res.params
|
578
|
+
data3 = data1 - xc - 1j * yc
|
579
|
+
phase3 = np.unwrap(np.angle(data3))
|
580
|
+
|
581
|
+
# Fit phase vs frequency
|
582
|
+
if verbose:
|
583
|
+
print("* Phase vs frequency fit")
|
584
|
+
fit_res = fit_phase_vs_freq(freq, phase3, theta0=0, Q_tot=Q_tot, fr=fr)
|
585
|
+
(theta0, Q_tot, fr) = fit_res.params
|
586
|
+
if verbose:
|
587
|
+
fit_res.summary()
|
588
|
+
|
589
|
+
# Find the off-resonant point
|
590
|
+
p_offres = (xc + 1j * yc) + r0 * np.exp(1j * (theta0 + np.pi))
|
591
|
+
a = np.abs(p_offres)
|
592
|
+
alpha = np.angle(p_offres)
|
593
|
+
# Rescale data
|
594
|
+
linmag5 = linmag / a
|
595
|
+
phase5 = phase1 - alpha
|
596
|
+
data5 = linmag5 * np.exp(1j * phase5)
|
597
|
+
|
598
|
+
# Find impedence mismatch
|
599
|
+
if bias_toward_fr:
|
600
|
+
fr_idx = np.abs(freq - fr).argmin()
|
601
|
+
re, im = np.real(data5), np.imag(data5)
|
602
|
+
fit_res = fit_circle_algebraic(
|
603
|
+
re[fr_idx - idx_range : fr_idx + idx_range],
|
604
|
+
im[fr_idx - idx_range : fr_idx + idx_range],
|
605
|
+
)
|
606
|
+
else:
|
607
|
+
fit_res = fit_circle_algebraic(np.real(data5), np.imag(data5))
|
608
|
+
(xc6, yc6, r06) = fit_res.params
|
609
|
+
phi0 = -np.arcsin(yc6 / r06)
|
610
|
+
|
611
|
+
# Q_ext and Q_int
|
612
|
+
if measurement == "reflection":
|
613
|
+
Q_ext = Q_tot / (r06 * np.exp(-1j * phi0))
|
614
|
+
elif measurement == "hanger":
|
615
|
+
Q_ext = Q_tot / (2 * r06 * np.exp(-1j * phi0))
|
616
|
+
|
617
|
+
# Refine theta0
|
618
|
+
if measurement == "reflection":
|
619
|
+
res6 = S11_reflection(freq, a, alpha, tau, Q_tot, Q_ext, fr, phi0, mag_bg / a)
|
620
|
+
elif measurement == "hanger":
|
621
|
+
res6 = S21_hanger(freq, a, alpha, tau, Q_tot, Q_ext, fr, phi0, mag_bg / a)
|
622
|
+
theta0 = phase[0] - np.unwrap(np.angle(res6))[0]
|
623
|
+
|
624
|
+
# Plot small summary
|
625
|
+
if do_plot:
|
626
|
+
v = np.linspace(0, 2 * np.pi, 100)
|
627
|
+
fig, ax = plt.subplots(1, 1, figsize=(6, 6))
|
628
|
+
ax.plot(
|
629
|
+
np.real(data1),
|
630
|
+
np.imag(data1),
|
631
|
+
"o",
|
632
|
+
label="Data cut (without cable delay)",
|
633
|
+
zorder=1,
|
634
|
+
)
|
635
|
+
ax.plot(xc + r0 * np.cos(v), yc + r0 * np.sin(v), label="Circle fit", zorder=2)
|
636
|
+
ax.scatter(
|
637
|
+
np.real(p_offres),
|
638
|
+
np.imag(p_offres),
|
639
|
+
color="tab:cyan",
|
640
|
+
label="Off-resonant point",
|
641
|
+
zorder=3,
|
642
|
+
s=120,
|
643
|
+
marker="*",
|
644
|
+
)
|
645
|
+
ax.scatter(xc, yc, color="tab:orange")
|
646
|
+
ax.set_xlabel("Re")
|
647
|
+
ax.set_ylabel("Im")
|
648
|
+
ax.axis("equal")
|
649
|
+
ax.margins(x=0.25, y=0.25)
|
650
|
+
ax.grid(True)
|
651
|
+
ax.set_title("Data fit and off-resonant point")
|
652
|
+
ax.legend()
|
653
|
+
# Show
|
654
|
+
fig.tight_layout()
|
655
|
+
plt.show()
|
656
|
+
|
657
|
+
return a, alpha, Q_tot, Q_ext, fr, phi0, theta0
|
658
|
+
|
659
|
+
|
660
|
+
@fit_output
|
661
|
+
def full_fit(
|
662
|
+
freq, data, measurement, a, alpha, tau, Q_tot, Q_ext, fr, phi0, mag_bg=None
|
663
|
+
):
|
664
|
+
"""
|
665
|
+
Performs a full fit of the measured resonator data using a selected model
|
666
|
+
(either reflection or hanger-type measurement). The fitting is handled
|
667
|
+
using the lmfit Model framework.
|
668
|
+
|
669
|
+
IMPORTANT: This fitting function should only be used to refine already
|
670
|
+
good initial guesses!
|
671
|
+
|
672
|
+
Parameters
|
673
|
+
----------
|
674
|
+
freq : np.ndarray
|
675
|
+
A 1D array of frequency values in Hz.
|
676
|
+
|
677
|
+
data : np.ndarray
|
678
|
+
A 1D array of complex-valued measured resonator data.
|
679
|
+
|
680
|
+
measurement : str
|
681
|
+
Type of measurement. Should be either:
|
682
|
+
- `"reflection"`: Uses the `S11_reflection` model.
|
683
|
+
- `"hanger"`: Uses the `S21_hanger` model.
|
684
|
+
|
685
|
+
a : float
|
686
|
+
Amplitude scaling factor.
|
687
|
+
|
688
|
+
alpha : float
|
689
|
+
Phase offset parameter.
|
690
|
+
|
691
|
+
tau : float
|
692
|
+
Cable delay or propagation time.
|
693
|
+
|
694
|
+
Q_tot : float
|
695
|
+
Total quality factor of the resonator.
|
696
|
+
|
697
|
+
Q_ext : float
|
698
|
+
External quality factor (coupling quality factor).
|
699
|
+
|
700
|
+
fr : float
|
701
|
+
Resonant frequency.
|
702
|
+
|
703
|
+
phi0 : float
|
704
|
+
Phase offset at resonance.
|
705
|
+
|
706
|
+
mag_bg : np.ndarray, optional
|
707
|
+
A 1D array representing the magnitude background response, if available.
|
708
|
+
Default is None.
|
709
|
+
|
710
|
+
Returns
|
711
|
+
-------
|
712
|
+
FitResult
|
713
|
+
A `FitResult` object containing:
|
714
|
+
- Fitted parameters (`params`).
|
715
|
+
- Standard errors (`std_err`).
|
716
|
+
- Goodness-of-fit metrics (`red_chi2`).
|
717
|
+
- A callable `predict` function for generating fitted responses.
|
718
|
+
|
719
|
+
Example
|
720
|
+
-------
|
721
|
+
>>> freq = np.linspace(1e9, 2e9, 1000) # Example frequency range
|
722
|
+
>>> data = np.exp(1j * freq * 2 * np.pi / 1e9) # Example complex response
|
723
|
+
>>> fit_result = full_fit(freq, data, "reflection", 1, 0, 0, 1e4, 2e4, 1.5e9, 0)
|
724
|
+
>>> fit_result.summary()
|
725
|
+
"""
|
726
|
+
|
727
|
+
def S11_reflection_fixed(freq, a, alpha, tau, Q_tot, Q_ext_mag, f_r, phi):
|
728
|
+
return S11_reflection(freq, a, alpha, tau, Q_tot, Q_ext_mag, f_r, phi, mag_bg)
|
729
|
+
|
730
|
+
def S21_hanger_fixed(freq, a, alpha, tau, Q_tot, Q_ext_mag, f_r, phi):
|
731
|
+
return S21_hanger(freq, a, alpha, tau, Q_tot, Q_ext_mag, f_r, phi, mag_bg)
|
732
|
+
|
733
|
+
if measurement == "reflection":
|
734
|
+
model = Model(S11_reflection_fixed)
|
735
|
+
elif measurement == "hanger":
|
736
|
+
model = Model(S21_hanger_fixed)
|
737
|
+
|
738
|
+
params = model.make_params(
|
739
|
+
a=a, alpha=alpha, tau=tau, Q_tot=Q_tot, Q_ext_mag=Q_ext, f_r=fr, phi=phi0
|
740
|
+
)
|
741
|
+
res = model.fit(data, params, freq=freq)
|
742
|
+
|
743
|
+
return res
|
744
|
+
|
745
|
+
|
746
|
+
def plot_resonator(freq, data, fit=None, mag_bg: np.ndarray | None = None, title=""):
|
747
|
+
"""
|
748
|
+
Plots the resonator response in three different representations:
|
749
|
+
- Complex plane (Re vs. Im)
|
750
|
+
- Magnitude response (Amplitude vs. Frequency)
|
751
|
+
- Phase response (Phase vs. Frequency)
|
752
|
+
|
753
|
+
Parameters
|
754
|
+
----------
|
755
|
+
freq : np.ndarray
|
756
|
+
A 1D array representing the frequency values.
|
757
|
+
|
758
|
+
data : np.ndarray
|
759
|
+
A 1D array of complex-valued data points corresponding to the resonator response.
|
760
|
+
|
761
|
+
fit : np.ndarray, optional
|
762
|
+
A 1D array of complex values representing the fitted model response. Default is None.
|
763
|
+
|
764
|
+
mag_bg : np.ndarray, optional
|
765
|
+
A 1D array representing the background magnitude response, if available. Default is None.
|
766
|
+
|
767
|
+
title : str, optional
|
768
|
+
The title of the plot. Default is an empty string.
|
769
|
+
"""
|
770
|
+
|
771
|
+
fig = plt.figure(figsize=(10, 5))
|
772
|
+
gs = fig.add_gridspec(2, 2)
|
773
|
+
|
774
|
+
# Subplot on the left (full height, first column)
|
775
|
+
ax1 = fig.add_subplot(gs[:, 0]) # Left side spans both rows
|
776
|
+
ax1.scatter(np.real(data), np.imag(data), color="tab:blue", s=20)
|
777
|
+
ax1.plot(np.real(fit), np.imag(fit), color="tab:orange")
|
778
|
+
ax1.set_aspect("equal")
|
779
|
+
ax1.set_xlabel("Re")
|
780
|
+
ax1.set_ylabel("Im")
|
781
|
+
ax1.grid()
|
782
|
+
|
783
|
+
# Subplot on the top-right (first row, second column)
|
784
|
+
ax2 = fig.add_subplot(gs[0, 1])
|
785
|
+
ax2.scatter(freq, np.abs(data), color="tab:blue", s=5)
|
786
|
+
ax2.plot(freq, np.abs(fit), color="tab:orange")
|
787
|
+
if (mag_bg is not None) and (not np.isnan(mag_bg).any()):
|
788
|
+
ax2.plot(freq, mag_bg, "-.", color="tab:green")
|
789
|
+
ax2.set_ylabel("Amplitude")
|
790
|
+
ax2.grid(True)
|
791
|
+
|
792
|
+
# Subplot on the bottom-right (second row, second column)
|
793
|
+
ax3 = fig.add_subplot(gs[1, 1])
|
794
|
+
ax3.scatter(freq, np.unwrap(np.angle(data)), color="tab:blue", s=5)
|
795
|
+
ax3.plot(freq, np.unwrap(np.angle(fit)), color="tab:orange")
|
796
|
+
ax3.set_ylabel("Phase")
|
797
|
+
ax3.set_xlabel("Frequency [Hz]")
|
798
|
+
ax3.grid(True)
|
799
|
+
|
800
|
+
fig.suptitle(title)
|
801
|
+
fig.tight_layout()
|
802
|
+
plt.show()
|
803
|
+
|
804
|
+
|
805
|
+
def compute_Q_int(Q_tot, Q_ext_mag, Q_ext_phase):
|
806
|
+
"""Compute Q_internal given Q_total and the manitude and phase of Q_external."""
|
807
|
+
return 1 / (1 / Q_tot - np.real(1 / (Q_ext_mag * np.exp(-1j * Q_ext_phase))))
|