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
sqil_core/fit/_fit.py
ADDED
@@ -0,0 +1,782 @@
|
|
1
|
+
import warnings
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
from scipy.optimize import curve_fit, fsolve, least_squares, leastsq
|
5
|
+
|
6
|
+
import sqil_core.fit._models as _models
|
7
|
+
|
8
|
+
from ._core import FitResult, fit_input, fit_output
|
9
|
+
|
10
|
+
|
11
|
+
@fit_input
|
12
|
+
@fit_output
|
13
|
+
def fit_lorentzian(
|
14
|
+
x_data: np.ndarray,
|
15
|
+
y_data: np.ndarray,
|
16
|
+
guess: list = None,
|
17
|
+
bounds: list[tuple[float]] | tuple = (-np.inf, np.inf),
|
18
|
+
) -> FitResult:
|
19
|
+
r"""
|
20
|
+
Fits a Lorentzian function to the provided data. The function estimates the
|
21
|
+
amplitude (A), center (x0), full width at half maximum (FWHM), and baseline (y0)
|
22
|
+
of the Lorentzian function.
|
23
|
+
|
24
|
+
L(x) = A * (|FWHM| / 2) / ((x - x0)^2 + (FWHM^2 / 4)) + y0
|
25
|
+
|
26
|
+
$$L(x) = A \frac{\left| \text{FWHM} \right|}{2} \frac{1}{(x - x_0)^2 + \frac{\text{FWHM}^2}{4}} + y_0$$
|
27
|
+
|
28
|
+
Parameters
|
29
|
+
----------
|
30
|
+
x_data : np.ndarray
|
31
|
+
The independent variable (e.g., x values of the data).
|
32
|
+
|
33
|
+
y_data : np.ndarray
|
34
|
+
The dependent variable (e.g., y values of the data).
|
35
|
+
|
36
|
+
guess : list, optional
|
37
|
+
Initial guesses for the fit parameters [A, x0, fwhm, y0]. If not provided,
|
38
|
+
defaults are calculated based on the data.
|
39
|
+
|
40
|
+
bounds : list[tuple[float]], optional
|
41
|
+
The bounds for the fit parameters in the format [(min, max), ...].
|
42
|
+
If not provided, defaults are calculated.
|
43
|
+
|
44
|
+
fixed_params : list[int], optional, default: None
|
45
|
+
A list of indices representing parameters in the initial guess that should
|
46
|
+
remain unchanged during the fitting process.
|
47
|
+
|
48
|
+
Returns
|
49
|
+
-------
|
50
|
+
FitResult
|
51
|
+
A `FitResult` object containing:
|
52
|
+
- Fitted parameters (`params`).
|
53
|
+
- Standard errors (`std_err`).
|
54
|
+
- Goodness-of-fit metrics (`rmse`, root mean squared error).
|
55
|
+
- A callable `predict` function for generating fitted responses.
|
56
|
+
"""
|
57
|
+
|
58
|
+
x, y = x_data, y_data
|
59
|
+
|
60
|
+
# Default intial guess if not provided
|
61
|
+
if guess is None:
|
62
|
+
median_y = np.median(y)
|
63
|
+
max_y, min_y = np.max(y), np.min(y)
|
64
|
+
|
65
|
+
# Determine A, x0, y0 based on peak prominence
|
66
|
+
if max_y - median_y >= median_y - min_y:
|
67
|
+
y0 = min_y
|
68
|
+
idx = np.argmax(y)
|
69
|
+
A = 1 / (max_y - median_y)
|
70
|
+
else:
|
71
|
+
y0 = max_y
|
72
|
+
idx = np.argmin(y)
|
73
|
+
A = 1 / (min_y - median_y)
|
74
|
+
|
75
|
+
x0 = x[idx]
|
76
|
+
half = y0 + A / 2.0
|
77
|
+
dx = np.abs(np.diff(x[np.argsort(np.abs(y - half))]))
|
78
|
+
dx_min = np.abs(np.diff(x))
|
79
|
+
dx = dx[dx >= 2.0 * dx_min]
|
80
|
+
|
81
|
+
fwhm = dx[0] / 2.0 if dx.size else dx_min
|
82
|
+
guess = [A, x0, fwhm, y0]
|
83
|
+
|
84
|
+
# Default bounds if not provided
|
85
|
+
if bounds is None:
|
86
|
+
bounds = (
|
87
|
+
[-5.0 * np.abs(guess[0]), np.min(x), guess[2] / 100.0, np.min(y)],
|
88
|
+
[5.0 * np.abs(guess[0]), np.max(x), 10.0 * guess[2], np.max(y)],
|
89
|
+
)
|
90
|
+
|
91
|
+
res = curve_fit(_models.lorentzian, x, y, p0=guess, bounds=bounds, full_output=True)
|
92
|
+
|
93
|
+
return res, {
|
94
|
+
"param_names": ["A", "x0", "fwhm", "y0"],
|
95
|
+
"predict": _models.lorentzian,
|
96
|
+
}
|
97
|
+
|
98
|
+
|
99
|
+
@fit_input
|
100
|
+
@fit_output
|
101
|
+
def fit_gaussian(
|
102
|
+
x_data: np.ndarray,
|
103
|
+
y_data: np.ndarray,
|
104
|
+
guess: list = None,
|
105
|
+
bounds: list[tuple[float]] | tuple = (-np.inf, np.inf),
|
106
|
+
) -> FitResult:
|
107
|
+
r"""
|
108
|
+
Fits a Gaussian function to the provided data. The function estimates the
|
109
|
+
amplitude, mean, standard deviation (sigma), and baseline of the Gaussian
|
110
|
+
function, and computes the full width at half maximum (FWHM).
|
111
|
+
|
112
|
+
G(x) = A / (|σ| * sqrt(2π)) * exp(- (x - x0)^2 / (2σ^2)) + y0
|
113
|
+
|
114
|
+
$$G(x) = A \frac{1}{\left| \sigma \right| \sqrt{2\pi}} \exp\left( -\frac{(x - x_0)^2}{2\sigma^2} \right) + y_0$$
|
115
|
+
|
116
|
+
Parameters
|
117
|
+
----------
|
118
|
+
x_data : np.ndarray
|
119
|
+
The independent variable (e.g., x values of the data).
|
120
|
+
|
121
|
+
y_data : np.ndarray
|
122
|
+
The dependent variable (e.g., y values of the data).
|
123
|
+
|
124
|
+
guess : list, optional
|
125
|
+
Initial guesses for the fit parameters [A, x0, sigma, y0]. If not provided,
|
126
|
+
defaults are calculated based on the data.
|
127
|
+
|
128
|
+
bounds : list[tuple[float]], optional
|
129
|
+
The bounds for the fit parameters in the format [(min, max), ...].
|
130
|
+
If not provided, defaults are calculated.
|
131
|
+
|
132
|
+
fixed_params : list[int], optional, default: None
|
133
|
+
A list of indices representing parameters in the initial guess that should
|
134
|
+
remain unchanged during the fitting process.
|
135
|
+
|
136
|
+
Returns
|
137
|
+
-------
|
138
|
+
FitResult
|
139
|
+
A `FitResult` object containing:
|
140
|
+
- Fitted parameters (`params`).
|
141
|
+
- Standard errors (`std_err`).
|
142
|
+
- Goodness-of-fit metrics (`rmse`, root mean squared error).
|
143
|
+
- A callable `predict` function for generating fitted responses.
|
144
|
+
- A metadata dictionary containing the FWHM.
|
145
|
+
"""
|
146
|
+
|
147
|
+
x, y = x_data, y_data
|
148
|
+
|
149
|
+
# Default initial guess if not provided
|
150
|
+
if guess is None:
|
151
|
+
median_y = np.median(y)
|
152
|
+
max_x, min_x = np.max(x), np.min(x)
|
153
|
+
max_y, min_y = np.max(y), np.min(y)
|
154
|
+
|
155
|
+
# Determine A, x0, y0 based on peak prominence
|
156
|
+
if max_y - median_y >= median_y - min_y:
|
157
|
+
y0 = min_y
|
158
|
+
idx = np.argmax(y)
|
159
|
+
A = max_y - median_y
|
160
|
+
else:
|
161
|
+
y0 = max_y
|
162
|
+
idx = np.argmin(y)
|
163
|
+
A = min_y - median_y
|
164
|
+
|
165
|
+
x0 = x[idx]
|
166
|
+
half = y0 + A / 2.0
|
167
|
+
dx = np.abs(np.diff(x[np.argsort(np.abs(y - half))]))
|
168
|
+
dx_min = np.abs(np.diff(x))
|
169
|
+
dx = dx[dx >= 2.0 * dx_min]
|
170
|
+
|
171
|
+
sigma = dx[0] / 2.0 if dx.size else dx_min
|
172
|
+
guess = [A, x0, sigma, y0]
|
173
|
+
|
174
|
+
# Default bounds if not provided
|
175
|
+
if bounds is None:
|
176
|
+
bounds = (
|
177
|
+
[-5.0 * np.abs(guess[0]), np.min(x), guess[2] / 100.0, np.min(y)],
|
178
|
+
[5.0 * np.abs(guess[0]), np.max(x), 10.0 * guess[2], np.max(y)],
|
179
|
+
)
|
180
|
+
|
181
|
+
res = curve_fit(_models.gaussian, x, y, p0=guess, bounds=bounds, full_output=True)
|
182
|
+
|
183
|
+
# Compute FWHM from sigma
|
184
|
+
_, _, sigma, _ = res[0]
|
185
|
+
fwhm = 2 * np.sqrt(2 * np.log(2)) * sigma
|
186
|
+
|
187
|
+
return res, {
|
188
|
+
"param_names": ["A", "x0", "sigma", "y0"],
|
189
|
+
"predict": _models.gaussian,
|
190
|
+
"fwhm": fwhm,
|
191
|
+
}
|
192
|
+
|
193
|
+
|
194
|
+
@fit_input
|
195
|
+
@fit_output
|
196
|
+
def fit_decaying_exp(
|
197
|
+
x_data: np.ndarray,
|
198
|
+
y_data: np.ndarray,
|
199
|
+
guess: list = None,
|
200
|
+
bounds: list[tuple[float]] | tuple = (-np.inf, np.inf),
|
201
|
+
) -> FitResult:
|
202
|
+
r"""
|
203
|
+
Fits a decaying exponential function to the provided data. The function estimates
|
204
|
+
the amplitude (A), decay time constant (tau), and baseline (y0) of the decaying
|
205
|
+
exponential function.
|
206
|
+
|
207
|
+
f(x) = A * exp(-x / τ) + y0
|
208
|
+
|
209
|
+
$$f(x) = A \exp\left( -\frac{x}{\tau} \right) + y_0$$
|
210
|
+
|
211
|
+
Parameters
|
212
|
+
----------
|
213
|
+
x_data : np.ndarray
|
214
|
+
The independent variable (e.g., x values of the data).
|
215
|
+
|
216
|
+
y_data : np.ndarray
|
217
|
+
The dependent variable (e.g., y values of the data).
|
218
|
+
|
219
|
+
guess : list, optional
|
220
|
+
Initial guesses for the fit parameters [A, tau, y0]. If not provided,
|
221
|
+
defaults are calculated based on the data.
|
222
|
+
|
223
|
+
bounds : list[tuple[float]], optional
|
224
|
+
The bounds for the fit parameters in the format [(min, max), ...].
|
225
|
+
If not provided, defaults are calculated.
|
226
|
+
|
227
|
+
fixed_params : list[int], optional, default: None
|
228
|
+
A list of indices representing parameters in the initial guess that should
|
229
|
+
remain unchanged during the fitting process.
|
230
|
+
|
231
|
+
Returns
|
232
|
+
-------
|
233
|
+
FitResult
|
234
|
+
A `FitResult` object containing:
|
235
|
+
- Fitted parameters (`params`).
|
236
|
+
- Standard errors (`std_err`).
|
237
|
+
- Goodness-of-fit metrics (`rmse`, root mean squared error).
|
238
|
+
- A callable `predict` function for generating fitted responses.
|
239
|
+
"""
|
240
|
+
x, y = x_data, y_data
|
241
|
+
|
242
|
+
# Default initial guess if not provided
|
243
|
+
if guess is None:
|
244
|
+
max_y = np.max(y)
|
245
|
+
min_y = np.min(y)
|
246
|
+
half = 0.5 * (max_y + min_y)
|
247
|
+
|
248
|
+
if y[0] > y[-1]:
|
249
|
+
tau0_idx = np.argmax(y < half)
|
250
|
+
else:
|
251
|
+
tau0_idx = np.argmax(y > half)
|
252
|
+
|
253
|
+
b0 = x[tau0_idx] if tau0_idx != 0 else 0.5 * (x[0] + x[-1])
|
254
|
+
guess = [y[0] - y[-1], b0, y[-1]]
|
255
|
+
|
256
|
+
# Default bounds if not provided
|
257
|
+
if bounds is None:
|
258
|
+
span_y = np.max(y) - np.min(y)
|
259
|
+
c0_min = np.min(y) - 100.0 * span_y
|
260
|
+
c0_max = np.max(y) + 100.0 * span_y
|
261
|
+
bounds = (
|
262
|
+
[-100.0 * span_y, 0.0, c0_min],
|
263
|
+
[100.0 * span_y, 100.0 * (np.max(x) - np.min(x)), c0_max],
|
264
|
+
)
|
265
|
+
|
266
|
+
res = curve_fit(
|
267
|
+
_models.decaying_exp, x, y, p0=guess, bounds=bounds, full_output=True
|
268
|
+
)
|
269
|
+
|
270
|
+
return res, {
|
271
|
+
"param_names": ["A", "tau", "y0"],
|
272
|
+
"predict": _models.decaying_exp,
|
273
|
+
}
|
274
|
+
|
275
|
+
|
276
|
+
@fit_input
|
277
|
+
@fit_output
|
278
|
+
def fit_qubit_relaxation_qp(
|
279
|
+
x_data: np.ndarray,
|
280
|
+
y_data: np.ndarray,
|
281
|
+
guess: list[float] | None = None,
|
282
|
+
bounds: list[tuple[float]] | tuple = (-np.inf, np.inf),
|
283
|
+
maxfev: int = 10000,
|
284
|
+
ftol: float = 1e-11,
|
285
|
+
) -> FitResult:
|
286
|
+
r"""
|
287
|
+
Fits a qubit relaxation model with quasiparticle (QP) effects using a
|
288
|
+
biexponential decay function. The fitting procedure starts with an initial
|
289
|
+
guess derived from a single exponential fit.
|
290
|
+
|
291
|
+
f(x) = A * exp(|nQP| * (exp(-x / T1QP) - 1)) * exp(-x / T1R) + y0
|
292
|
+
|
293
|
+
$$f(x) = A \exp\left( |\text{n}_{\text{QP}}| \left( \exp\left(-\frac{x}{T_{1QP}}\right)
|
294
|
+
- 1 \right) \right) \exp\left(-\frac{x}{T_{1R}}\right) + y_0$$
|
295
|
+
|
296
|
+
Parameters
|
297
|
+
----------
|
298
|
+
x_data : np.ndarray
|
299
|
+
Time data points for the relaxation curve.
|
300
|
+
|
301
|
+
y_data : np.ndarray
|
302
|
+
Measured relaxation data.
|
303
|
+
|
304
|
+
guess : list[float], optional
|
305
|
+
Initial parameter guesses. If None, a default guess is computed
|
306
|
+
using a single exponential fit.
|
307
|
+
|
308
|
+
bounds : tuple[list[float], list[float]], optional
|
309
|
+
The bounds for the fit parameters in the format [(min, max), ...].
|
310
|
+
If None, reasonable bounds based on the initial guess are applied.
|
311
|
+
|
312
|
+
maxfev : int, optional, default=10000
|
313
|
+
Maximum number of function evaluations allowed for the curve fitting.
|
314
|
+
|
315
|
+
ftol : float, optional, default=1e-11
|
316
|
+
Relative tolerance for convergence in the least-squares optimization.
|
317
|
+
|
318
|
+
fixed_params : list[int], optional, default: None
|
319
|
+
A list of indices representing parameters in the initial guess that should
|
320
|
+
remain unchanged during the fitting process.
|
321
|
+
|
322
|
+
Returns
|
323
|
+
-------
|
324
|
+
FitResult
|
325
|
+
A `FitResult` object containing:
|
326
|
+
- Fitted parameters (`params`).
|
327
|
+
- Standard errors (`std_err`).
|
328
|
+
- Goodness-of-fit metrics (`rmse`, root mean squared error).
|
329
|
+
- A callable `predict` function for generating fitted responses.
|
330
|
+
"""
|
331
|
+
|
332
|
+
# Use a single exponential fit for initial parameter guesses
|
333
|
+
from scipy.optimize import curve_fit
|
334
|
+
|
335
|
+
def single_exp(x, a, tau, c):
|
336
|
+
return a * np.exp(-x / tau) + c
|
337
|
+
|
338
|
+
single_guess = [y_data[0] - y_data[-1], np.mean(x_data), y_data[-1]]
|
339
|
+
single_popt, _ = curve_fit(single_exp, x_data, y_data, p0=single_guess)
|
340
|
+
|
341
|
+
a_guess, T1R_guess, c_guess = single_popt
|
342
|
+
T1QP_guess = 0.1 * T1R_guess
|
343
|
+
nQP_guess = 1.0
|
344
|
+
|
345
|
+
# Default initial guess
|
346
|
+
if guess is None:
|
347
|
+
guess = [a_guess * np.exp(1.0), 2.0 * T1R_guess, c_guess, T1QP_guess, nQP_guess]
|
348
|
+
|
349
|
+
# Default parameter bounds
|
350
|
+
if bounds is None:
|
351
|
+
bounds = (
|
352
|
+
[
|
353
|
+
-20.0 * np.abs(a_guess),
|
354
|
+
1.0e-1 * T1R_guess,
|
355
|
+
-10.0 * np.abs(c_guess),
|
356
|
+
1.0e-4 * T1R_guess,
|
357
|
+
0.0,
|
358
|
+
],
|
359
|
+
[
|
360
|
+
20.0 * np.abs(a_guess),
|
361
|
+
1.0e3 * T1R_guess,
|
362
|
+
10.0 * np.abs(c_guess),
|
363
|
+
10.0 * T1R_guess,
|
364
|
+
1.0e3,
|
365
|
+
],
|
366
|
+
)
|
367
|
+
|
368
|
+
res = curve_fit(
|
369
|
+
_models.qubit_relaxation_qp,
|
370
|
+
x_data,
|
371
|
+
y_data,
|
372
|
+
p0=guess,
|
373
|
+
bounds=bounds,
|
374
|
+
maxfev=maxfev,
|
375
|
+
ftol=ftol,
|
376
|
+
full_output=True,
|
377
|
+
)
|
378
|
+
|
379
|
+
return res, {
|
380
|
+
"param_names": ["A", "T1R", "y0", "T1QP", "nQP"],
|
381
|
+
"predict": _models.qubit_relaxation_qp,
|
382
|
+
}
|
383
|
+
|
384
|
+
|
385
|
+
@fit_output
|
386
|
+
def fit_decaying_oscillations(
|
387
|
+
x_data: np.ndarray, y_data: np.ndarray, num_init: int = 10
|
388
|
+
) -> FitResult:
|
389
|
+
r"""
|
390
|
+
Fits a decaying oscillation model to data. The function estimates key features
|
391
|
+
like the oscillation period and phase, and tries multiple initial guesses for
|
392
|
+
the optimization process.
|
393
|
+
|
394
|
+
f(x) = A * exp(-x / τ) * cos(2π * (x - φ) / T) + y0
|
395
|
+
|
396
|
+
$$f(x) = A \exp\left( -\frac{x}{\tau} \right) \cos\left( 2\pi \frac{x - \phi}{T} \right) + y_0$$
|
397
|
+
|
398
|
+
Parameters
|
399
|
+
----------
|
400
|
+
x_data : np.ndarray
|
401
|
+
The independent variable (e.g., time) of the data.
|
402
|
+
|
403
|
+
y_data : np.ndarray
|
404
|
+
The dependent variable (e.g., signal) of the data.
|
405
|
+
|
406
|
+
num_init : int, optional, default=10
|
407
|
+
The number of initial guesses for the phase to use in the fitting process.
|
408
|
+
|
409
|
+
Returns
|
410
|
+
-------
|
411
|
+
FitResult
|
412
|
+
A `FitResult` object containing:
|
413
|
+
- Fitted parameters (`params`).
|
414
|
+
- Standard errors (`std_err`).
|
415
|
+
- Goodness-of-fit metrics (`rmse`, root mean squared error).
|
416
|
+
- A callable `predict` function for generating fitted responses.
|
417
|
+
- A metadata dictionary containing the pi_time and its standard error.
|
418
|
+
"""
|
419
|
+
# Extract key features from the data
|
420
|
+
min_y, max_y = np.min(y_data), np.max(y_data)
|
421
|
+
period_guess = 2.0 * np.abs(x_data[np.argmax(y_data)] - x_data[np.argmin(y_data)])
|
422
|
+
time_span = np.max(x_data) - np.min(x_data)
|
423
|
+
|
424
|
+
best_fit = None
|
425
|
+
best_popt = None
|
426
|
+
|
427
|
+
# Try multiple initializations
|
428
|
+
for phi_guess in np.linspace(0.0, np.pi * period_guess, num_init):
|
429
|
+
for factor in [y_data[-1], np.mean(y_data)]:
|
430
|
+
p0 = [y_data[0] - y_data[-1], time_span, factor, phi_guess, period_guess]
|
431
|
+
|
432
|
+
try:
|
433
|
+
with warnings.catch_warnings():
|
434
|
+
warnings.simplefilter("ignore")
|
435
|
+
fit_output = curve_fit(
|
436
|
+
_models.decaying_oscillations,
|
437
|
+
x_data,
|
438
|
+
y_data,
|
439
|
+
p0,
|
440
|
+
full_output=True,
|
441
|
+
)
|
442
|
+
popt = fit_output[0]
|
443
|
+
best_fit, best_popt = fit_output, popt
|
444
|
+
except:
|
445
|
+
if best_fit is None:
|
446
|
+
|
447
|
+
def _decaying_osc_res(p, x, y):
|
448
|
+
return _models.decaying_oscillations(x, *p) - y
|
449
|
+
|
450
|
+
result = least_squares(
|
451
|
+
_decaying_osc_res,
|
452
|
+
p0,
|
453
|
+
loss="soft_l1",
|
454
|
+
f_scale=0.1,
|
455
|
+
args=(x_data, y_data),
|
456
|
+
)
|
457
|
+
best_fit, best_popt = result, result.x
|
458
|
+
|
459
|
+
# Compute π-time (half-period + phase offset)
|
460
|
+
pi_time_raw = 0.5 * best_popt[4] + best_popt[3]
|
461
|
+
while pi_time_raw > 0.75 * np.abs(best_popt[4]):
|
462
|
+
pi_time_raw -= 0.5 * np.abs(best_popt[4])
|
463
|
+
while pi_time_raw < 0.25 * np.abs(best_popt[4]):
|
464
|
+
pi_time_raw += 0.5 * np.abs(best_popt[4])
|
465
|
+
|
466
|
+
def _get_pi_time_std_err(sqil_dict):
|
467
|
+
if sqil_dict["std_err"] is not None:
|
468
|
+
phi_err = sqil_dict["std_err"][3]
|
469
|
+
T_err = sqil_dict["std_err"][4]
|
470
|
+
if np.isfinite(T_err) and np.isfinite(phi_err):
|
471
|
+
return np.sqrt((T_err / 2) ** 2 + phi_err**2)
|
472
|
+
return np.nan
|
473
|
+
|
474
|
+
# Metadata dictionary
|
475
|
+
metadata = {
|
476
|
+
"param_names": ["A", "tau", "y0", "phi", "T"],
|
477
|
+
"predict": _models.decaying_oscillations,
|
478
|
+
"pi_time": pi_time_raw,
|
479
|
+
"@pi_time_std_err": _get_pi_time_std_err,
|
480
|
+
}
|
481
|
+
|
482
|
+
return best_fit, metadata
|
483
|
+
|
484
|
+
|
485
|
+
@fit_output
|
486
|
+
def fit_circle_algebraic(x_data: np.ndarray, y_data: np.ndarray) -> FitResult:
|
487
|
+
"""Fits a circle in the xy plane and returns the radius and the position of the center.
|
488
|
+
|
489
|
+
Reference: https://arxiv.org/abs/1410.3365
|
490
|
+
This function uses an algebraic method to fit a circle to the provided data points.
|
491
|
+
The algebraic approach is generally faster and more precise than iterative methods,
|
492
|
+
but it can be more sensitive to noise in the data.
|
493
|
+
|
494
|
+
Parameters
|
495
|
+
----------
|
496
|
+
x : np.ndarray
|
497
|
+
Array of x-coordinates of the data points.
|
498
|
+
y : np.ndarray
|
499
|
+
Array of y-coordinates of the data points.
|
500
|
+
|
501
|
+
Returns
|
502
|
+
-------
|
503
|
+
FitResult
|
504
|
+
A `FitResult` object containing:
|
505
|
+
- Fitted parameters (`params`).
|
506
|
+
- Standard errors (`std_err`).
|
507
|
+
- Goodness-of-fit metrics (`rmse`, root mean squared error).
|
508
|
+
- A callable `predict` function for generating fitted responses.
|
509
|
+
|
510
|
+
Examples
|
511
|
+
--------
|
512
|
+
>>> fit_result = fit_circle_algebraic(x_data, y_data)
|
513
|
+
>>> fit_result.summary()
|
514
|
+
"""
|
515
|
+
z_data = x_data + 1j * y_data
|
516
|
+
|
517
|
+
def calc_moments(z_data):
|
518
|
+
xi = z_data.real
|
519
|
+
xi_sqr = xi * xi
|
520
|
+
yi = z_data.imag
|
521
|
+
yi_sqr = yi * yi
|
522
|
+
zi = xi_sqr + yi_sqr
|
523
|
+
Nd = float(len(xi))
|
524
|
+
xi_sum = xi.sum()
|
525
|
+
yi_sum = yi.sum()
|
526
|
+
zi_sum = zi.sum()
|
527
|
+
xiyi_sum = (xi * yi).sum()
|
528
|
+
xizi_sum = (xi * zi).sum()
|
529
|
+
yizi_sum = (yi * zi).sum()
|
530
|
+
return np.array(
|
531
|
+
[
|
532
|
+
[(zi * zi).sum(), xizi_sum, yizi_sum, zi_sum],
|
533
|
+
[xizi_sum, xi_sqr.sum(), xiyi_sum, xi_sum],
|
534
|
+
[yizi_sum, xiyi_sum, yi_sqr.sum(), yi_sum],
|
535
|
+
[zi_sum, xi_sum, yi_sum, Nd],
|
536
|
+
]
|
537
|
+
)
|
538
|
+
|
539
|
+
M = calc_moments(z_data)
|
540
|
+
|
541
|
+
a0 = (
|
542
|
+
(
|
543
|
+
(M[2][0] * M[3][2] - M[2][2] * M[3][0]) * M[1][1]
|
544
|
+
- M[1][2] * M[2][0] * M[3][1]
|
545
|
+
- M[1][0] * M[2][1] * M[3][2]
|
546
|
+
+ M[1][0] * M[2][2] * M[3][1]
|
547
|
+
+ M[1][2] * M[2][1] * M[3][0]
|
548
|
+
)
|
549
|
+
* M[0][3]
|
550
|
+
+ (
|
551
|
+
M[0][2] * M[2][3] * M[3][0]
|
552
|
+
- M[0][2] * M[2][0] * M[3][3]
|
553
|
+
+ M[0][0] * M[2][2] * M[3][3]
|
554
|
+
- M[0][0] * M[2][3] * M[3][2]
|
555
|
+
)
|
556
|
+
* M[1][1]
|
557
|
+
+ (
|
558
|
+
M[0][1] * M[1][3] * M[3][0]
|
559
|
+
- M[0][1] * M[1][0] * M[3][3]
|
560
|
+
- M[0][0] * M[1][3] * M[3][1]
|
561
|
+
)
|
562
|
+
* M[2][2]
|
563
|
+
+ (-M[0][1] * M[1][2] * M[2][3] - M[0][2] * M[1][3] * M[2][1]) * M[3][0]
|
564
|
+
+ (
|
565
|
+
(M[2][3] * M[3][1] - M[2][1] * M[3][3]) * M[1][2]
|
566
|
+
+ M[2][1] * M[3][2] * M[1][3]
|
567
|
+
)
|
568
|
+
* M[0][0]
|
569
|
+
+ (
|
570
|
+
M[1][0] * M[2][3] * M[3][2]
|
571
|
+
+ M[2][0] * (M[1][2] * M[3][3] - M[1][3] * M[3][2])
|
572
|
+
)
|
573
|
+
* M[0][1]
|
574
|
+
+ (
|
575
|
+
(M[2][1] * M[3][3] - M[2][3] * M[3][1]) * M[1][0]
|
576
|
+
+ M[1][3] * M[2][0] * M[3][1]
|
577
|
+
)
|
578
|
+
* M[0][2]
|
579
|
+
)
|
580
|
+
a1 = (
|
581
|
+
(
|
582
|
+
(M[3][0] - 2.0 * M[2][2]) * M[1][1]
|
583
|
+
- M[1][0] * M[3][1]
|
584
|
+
+ M[2][2] * M[3][0]
|
585
|
+
+ 2.0 * M[1][2] * M[2][1]
|
586
|
+
- M[2][0] * M[3][2]
|
587
|
+
)
|
588
|
+
* M[0][3]
|
589
|
+
+ (
|
590
|
+
2.0 * M[2][0] * M[3][2]
|
591
|
+
- M[0][0] * M[3][3]
|
592
|
+
- 2.0 * M[2][2] * M[3][0]
|
593
|
+
+ 2.0 * M[0][2] * M[2][3]
|
594
|
+
)
|
595
|
+
* M[1][1]
|
596
|
+
+ (-M[0][0] * M[3][3] + 2.0 * M[0][1] * M[1][3] + 2.0 * M[1][0] * M[3][1])
|
597
|
+
* M[2][2]
|
598
|
+
+ (-M[0][1] * M[1][3] + 2.0 * M[1][2] * M[2][1] - M[0][2] * M[2][3]) * M[3][0]
|
599
|
+
+ (M[1][3] * M[3][1] + M[2][3] * M[3][2]) * M[0][0]
|
600
|
+
+ (M[1][0] * M[3][3] - 2.0 * M[1][2] * M[2][3]) * M[0][1]
|
601
|
+
+ (M[2][0] * M[3][3] - 2.0 * M[1][3] * M[2][1]) * M[0][2]
|
602
|
+
- 2.0 * M[1][2] * M[2][0] * M[3][1]
|
603
|
+
- 2.0 * M[1][0] * M[2][1] * M[3][2]
|
604
|
+
)
|
605
|
+
a2 = (
|
606
|
+
(2.0 * M[1][1] - M[3][0] + 2.0 * M[2][2]) * M[0][3]
|
607
|
+
+ (2.0 * M[3][0] - 4.0 * M[2][2]) * M[1][1]
|
608
|
+
- 2.0 * M[2][0] * M[3][2]
|
609
|
+
+ 2.0 * M[2][2] * M[3][0]
|
610
|
+
+ M[0][0] * M[3][3]
|
611
|
+
+ 4.0 * M[1][2] * M[2][1]
|
612
|
+
- 2.0 * M[0][1] * M[1][3]
|
613
|
+
- 2.0 * M[1][0] * M[3][1]
|
614
|
+
- 2.0 * M[0][2] * M[2][3]
|
615
|
+
)
|
616
|
+
a3 = -2.0 * M[3][0] + 4.0 * M[1][1] + 4.0 * M[2][2] - 2.0 * M[0][3]
|
617
|
+
a4 = -4.0
|
618
|
+
|
619
|
+
def func(x):
|
620
|
+
return a0 + a1 * x + a2 * x * x + a3 * x * x * x + a4 * x * x * x * x
|
621
|
+
|
622
|
+
def d_func(x):
|
623
|
+
return a1 + 2 * a2 * x + 3 * a3 * x * x + 4 * a4 * x * x * x
|
624
|
+
|
625
|
+
x0 = fsolve(func, 0.0, fprime=d_func)
|
626
|
+
|
627
|
+
def solve_eq_sys(val, M):
|
628
|
+
# prepare
|
629
|
+
M[3][0] = M[3][0] + 2 * val
|
630
|
+
M[0][3] = M[0][3] + 2 * val
|
631
|
+
M[1][1] = M[1][1] - val
|
632
|
+
M[2][2] = M[2][2] - val
|
633
|
+
return np.linalg.svd(M)
|
634
|
+
|
635
|
+
U, s, Vt = solve_eq_sys(x0[0], M)
|
636
|
+
|
637
|
+
A_vec = Vt[np.argmin(s), :]
|
638
|
+
|
639
|
+
xc = -A_vec[1] / (2.0 * A_vec[0])
|
640
|
+
yc = -A_vec[2] / (2.0 * A_vec[0])
|
641
|
+
# the term *sqrt term corrects for the constraint, because it may be altered due to numerical inaccuracies during calculation
|
642
|
+
r0 = (
|
643
|
+
1.0
|
644
|
+
/ (2.0 * np.absolute(A_vec[0]))
|
645
|
+
* np.sqrt(A_vec[1] * A_vec[1] + A_vec[2] * A_vec[2] - 4.0 * A_vec[0] * A_vec[3])
|
646
|
+
)
|
647
|
+
|
648
|
+
std_err = _compute_circle_fit_errors(x_data, y_data, xc, yc, r0)
|
649
|
+
return {
|
650
|
+
"params": [xc, yc, r0],
|
651
|
+
"std_err": std_err,
|
652
|
+
"metrics": _compute_circle_fit_metrics(x_data, y_data, xc, yc, r0),
|
653
|
+
"predict": lambda theta: (xc + r0 * np.cos(theta), yc + r0 * np.sin(theta)),
|
654
|
+
"output": {},
|
655
|
+
"param_names": ["xc", "yc", "r0"],
|
656
|
+
}
|
657
|
+
|
658
|
+
|
659
|
+
def _compute_circle_fit_errors(x, y, xc, yc, r0):
|
660
|
+
"""Compute the standard errors for the algebraic circle fit"""
|
661
|
+
# Residuals: distance from each point to the fitted circle
|
662
|
+
distances = np.sqrt((x - xc) ** 2 + (y - yc) ** 2)
|
663
|
+
residuals = distances - r0
|
664
|
+
|
665
|
+
# Estimate variance of the residuals
|
666
|
+
dof = len(x) - 3 # Degrees of freedom: N - number of parameters
|
667
|
+
variance = np.sum(residuals**2) / dof
|
668
|
+
|
669
|
+
# Jacobian matrix of residuals with respect to (xc, yc, r0)
|
670
|
+
J = np.zeros((len(x), 3))
|
671
|
+
J[:, 0] = (xc - x) / distances # ∂residual/∂xc
|
672
|
+
J[:, 1] = (yc - y) / distances # ∂residual/∂yc
|
673
|
+
J[:, 2] = -1 # ∂residual/∂r0
|
674
|
+
|
675
|
+
# Covariance matrix approximation: variance * (JᵗJ)⁻¹
|
676
|
+
JTJ_inv = np.linalg.inv(J.T @ J)
|
677
|
+
pcov = variance * JTJ_inv
|
678
|
+
|
679
|
+
# Standard errors are the square roots of the diagonal of the covariance matrix
|
680
|
+
standard_errors = np.sqrt(np.diag(pcov))
|
681
|
+
|
682
|
+
return standard_errors
|
683
|
+
|
684
|
+
|
685
|
+
def _compute_circle_fit_metrics(x_data, y_data, xc, yc, r0):
|
686
|
+
"""Computed metrics for the algebraic circle fit"""
|
687
|
+
# Compute the distance of each data point to the fitted circle center
|
688
|
+
r_data = np.sqrt((x_data - xc) ** 2 + (y_data - yc) ** 2)
|
689
|
+
|
690
|
+
# Compute residuals
|
691
|
+
residuals = r_data - r0
|
692
|
+
|
693
|
+
# Calculate R-squared (R²)
|
694
|
+
ssr = np.sum(residuals**2)
|
695
|
+
sst = np.sum((r_data - np.mean(r_data)) ** 2)
|
696
|
+
r2 = 1 - (ssr / sst) if sst > 0 else 0
|
697
|
+
|
698
|
+
# Compute RMSE
|
699
|
+
rmse = np.sqrt(np.mean(residuals**2))
|
700
|
+
|
701
|
+
# Return results
|
702
|
+
return {"rmse": rmse}
|
703
|
+
|
704
|
+
|
705
|
+
@fit_output
|
706
|
+
def fit_skewed_lorentzian(x_data: np.ndarray, y_data: np.ndarray):
|
707
|
+
r"""
|
708
|
+
Fits a skewed Lorentzian model to the given data using least squares optimization.
|
709
|
+
|
710
|
+
This function performs a two-step fitting process to find the best-fitting parameters for a skewed Lorentzian model.
|
711
|
+
The first fitting step provides initial estimates for the parameters, and the second step refines those estimates
|
712
|
+
using a full model fit.
|
713
|
+
|
714
|
+
L(f) = A1 + A2 * (f - fr) + (A3 + A4 * (f - fr)) / [1 + (2 * Q_tot * ((f / fr) - 1))²]
|
715
|
+
|
716
|
+
$$L(f) = A_1 + A_2 \cdot (f - f_r)+ \frac{A_3 + A_4 \cdot (f - f_r)}{1
|
717
|
+
+ 4 Q_{\text{tot}}^2 \left( \frac{f - f_r}{f_r} \right)^2}$$
|
718
|
+
|
719
|
+
Parameters
|
720
|
+
----------
|
721
|
+
x_data : np.ndarray
|
722
|
+
A 1D numpy array containing the x data points for the fit.
|
723
|
+
|
724
|
+
y_data : np.ndarray
|
725
|
+
A 1D numpy array containing the y data points for the fit.
|
726
|
+
|
727
|
+
Returns
|
728
|
+
-------
|
729
|
+
FitResult
|
730
|
+
A `FitResult` object containing:
|
731
|
+
- Fitted parameters (`params`).
|
732
|
+
- Standard errors (`std_err`).
|
733
|
+
- Goodness-of-fit metrics (`red_chi2`).
|
734
|
+
- A callable `predict` function for generating fitted responses.
|
735
|
+
|
736
|
+
Examples
|
737
|
+
--------
|
738
|
+
>>> fit_result = fit_skewed_lorentzian(x_data, y_data)
|
739
|
+
>>> fit_result.summary()
|
740
|
+
"""
|
741
|
+
A1a = np.minimum(y_data[0], y_data[-1])
|
742
|
+
A3a = -np.max(y_data)
|
743
|
+
fra = x_data[np.argmin(y_data)]
|
744
|
+
|
745
|
+
# First fit to get initial estimates for the more complex fit
|
746
|
+
def residuals(p, x, y):
|
747
|
+
A2, A4, Q_tot = p
|
748
|
+
err = y - (
|
749
|
+
A1a
|
750
|
+
+ A2 * (x - fra)
|
751
|
+
+ (A3a + A4 * (x - fra)) / (1.0 + 4.0 * Q_tot**2 * ((x - fra) / fra) ** 2)
|
752
|
+
)
|
753
|
+
return err
|
754
|
+
|
755
|
+
p0 = [0.0, 0.0, 1e3]
|
756
|
+
p_final, _ = leastsq(residuals, p0, args=(np.array(x_data), np.array(y_data)))
|
757
|
+
A2a, A4a, Q_tota = p_final
|
758
|
+
|
759
|
+
# Full parameter fit
|
760
|
+
def residuals2(p, x, y):
|
761
|
+
A1, A2, A3, A4, fr, Q_tot = p
|
762
|
+
err = y - (
|
763
|
+
A1
|
764
|
+
+ A2 * (x - fr)
|
765
|
+
+ (A3 + A4 * (x - fr)) / (1.0 + 4.0 * Q_tot**2 * ((x - fr) / fr) ** 2)
|
766
|
+
)
|
767
|
+
return err
|
768
|
+
|
769
|
+
p0 = [A1a, A2a, A3a, A4a, fra, Q_tota]
|
770
|
+
popt, pcov, infodict, errmsg, ier = leastsq(
|
771
|
+
residuals2, p0, args=(np.array(x_data), np.array(y_data)), full_output=True
|
772
|
+
)
|
773
|
+
# Since Q_tot is always present as a square it may turn out negative
|
774
|
+
popt[-1] = np.abs(popt[-1])
|
775
|
+
|
776
|
+
return (
|
777
|
+
(popt, pcov, infodict, errmsg, ier),
|
778
|
+
{
|
779
|
+
"predict": lambda x: _models.skewed_lorentzian(x, *popt),
|
780
|
+
"param_names": ["A1", "A2", "A3", "A4", "fr", "Q_tot"],
|
781
|
+
},
|
782
|
+
)
|