sqil-core 0.1.0__py3-none-any.whl → 1.0.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 +1 -0
- sqil_core/config_log.py +42 -0
- sqil_core/experiment/__init__.py +11 -0
- sqil_core/experiment/_analysis.py +95 -0
- sqil_core/experiment/_events.py +25 -0
- sqil_core/experiment/_experiment.py +553 -0
- sqil_core/experiment/data/plottr.py +778 -0
- sqil_core/experiment/helpers/_function_override_handler.py +111 -0
- sqil_core/experiment/helpers/_labone_wrappers.py +12 -0
- sqil_core/experiment/instruments/__init__.py +2 -0
- sqil_core/experiment/instruments/_instrument.py +190 -0
- sqil_core/experiment/instruments/drivers/SignalCore_SC5511A.py +515 -0
- sqil_core/experiment/instruments/local_oscillator.py +205 -0
- sqil_core/experiment/instruments/server.py +175 -0
- sqil_core/experiment/instruments/setup.yaml +21 -0
- sqil_core/experiment/instruments/zurich_instruments.py +55 -0
- sqil_core/fit/__init__.py +22 -0
- sqil_core/fit/_core.py +179 -31
- sqil_core/fit/_fit.py +490 -81
- sqil_core/fit/_guess.py +232 -0
- sqil_core/fit/_models.py +32 -1
- sqil_core/fit/_quality.py +266 -0
- sqil_core/resonator/__init__.py +2 -0
- sqil_core/resonator/_resonator.py +256 -74
- sqil_core/utils/__init__.py +36 -13
- sqil_core/utils/_analysis.py +123 -0
- sqil_core/utils/_const.py +74 -18
- sqil_core/utils/_formatter.py +126 -55
- sqil_core/utils/_plot.py +272 -6
- sqil_core/utils/_read.py +178 -95
- sqil_core/utils/_utils.py +147 -0
- {sqil_core-0.1.0.dist-info → sqil_core-1.0.0.dist-info}/METADATA +9 -1
- sqil_core-1.0.0.dist-info/RECORD +36 -0
- {sqil_core-0.1.0.dist-info → sqil_core-1.0.0.dist-info}/WHEEL +1 -1
- sqil_core-0.1.0.dist-info/RECORD +0 -19
- {sqil_core-0.1.0.dist-info → sqil_core-1.0.0.dist-info}/entry_points.txt +0 -0
sqil_core/fit/_fit.py
CHANGED
@@ -1,11 +1,25 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import warnings
|
4
|
+
from typing import Callable
|
2
5
|
|
3
6
|
import numpy as np
|
4
|
-
from scipy.optimize import curve_fit, fsolve, least_squares, leastsq
|
7
|
+
from scipy.optimize import curve_fit, fsolve, least_squares, leastsq, minimize
|
5
8
|
|
6
9
|
import sqil_core.fit._models as _models
|
10
|
+
from sqil_core.utils._utils import fill_gaps, has_at_least_one, make_iterable
|
7
11
|
|
8
12
|
from ._core import FitResult, fit_input, fit_output
|
13
|
+
from ._guess import (
|
14
|
+
decaying_oscillations_bounds,
|
15
|
+
decaying_oscillations_guess,
|
16
|
+
gaussian_bounds,
|
17
|
+
gaussian_guess,
|
18
|
+
lorentzian_bounds,
|
19
|
+
lorentzian_guess,
|
20
|
+
oscillations_bounds,
|
21
|
+
oscillations_guess,
|
22
|
+
)
|
9
23
|
|
10
24
|
|
11
25
|
@fit_input
|
@@ -14,7 +28,7 @@ def fit_lorentzian(
|
|
14
28
|
x_data: np.ndarray,
|
15
29
|
y_data: np.ndarray,
|
16
30
|
guess: list = None,
|
17
|
-
bounds: list[tuple[float]] | tuple =
|
31
|
+
bounds: list[tuple[float]] | tuple = None,
|
18
32
|
) -> FitResult:
|
19
33
|
r"""
|
20
34
|
Fits a Lorentzian function to the provided data. The function estimates the
|
@@ -58,35 +72,16 @@ def fit_lorentzian(
|
|
58
72
|
x, y = x_data, y_data
|
59
73
|
|
60
74
|
# Default intial guess if not provided
|
61
|
-
if guess
|
62
|
-
|
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]
|
75
|
+
if has_at_least_one(guess, None):
|
76
|
+
guess = fill_gaps(guess, lorentzian_guess(x_data, y_data))
|
83
77
|
|
84
78
|
# Default bounds if not provided
|
85
79
|
if bounds is None:
|
86
|
-
bounds = (
|
87
|
-
|
88
|
-
|
89
|
-
)
|
80
|
+
bounds = ([None] * len(guess), [None] * len(guess))
|
81
|
+
if has_at_least_one(bounds[0], None) or has_at_least_one(bounds[1], None):
|
82
|
+
lower, upper = bounds
|
83
|
+
lower_guess, upper_guess = lorentzian_bounds(x_data, y_data, guess)
|
84
|
+
bounds = (fill_gaps(lower, lower_guess), fill_gaps(upper, upper_guess))
|
90
85
|
|
91
86
|
res = curve_fit(_models.lorentzian, x, y, p0=guess, bounds=bounds, full_output=True)
|
92
87
|
|
@@ -96,13 +91,80 @@ def fit_lorentzian(
|
|
96
91
|
}
|
97
92
|
|
98
93
|
|
94
|
+
@fit_input
|
95
|
+
@fit_output
|
96
|
+
def fit_two_lorentzians_shared_x0(
|
97
|
+
x_data_1,
|
98
|
+
y_data_1,
|
99
|
+
x_data_2,
|
100
|
+
y_data_2,
|
101
|
+
guess: list = None,
|
102
|
+
bounds: list[tuple[float]] | tuple = None,
|
103
|
+
):
|
104
|
+
y_all = np.concatenate([y_data_1, y_data_2])
|
105
|
+
|
106
|
+
if has_at_least_one(guess, None):
|
107
|
+
guess_1 = lorentzian_guess(x_data_1, y_data_1)
|
108
|
+
guess_2 = lorentzian_guess(x_data_2, y_data_2)
|
109
|
+
x01, x02 = guess_1[1], guess_2[1]
|
110
|
+
x0 = np.mean([x01, x02])
|
111
|
+
guess = fill_gaps(
|
112
|
+
guess, np.concatenate([np.delete(guess_1, 1), np.delete(guess_2, 1), [x0]])
|
113
|
+
)
|
114
|
+
|
115
|
+
if bounds == None:
|
116
|
+
bounds = [[None] * len(guess), [None] * len(guess)]
|
117
|
+
if has_at_least_one(bounds[0], None) or has_at_least_one(bounds[1], None):
|
118
|
+
lower, upper = bounds
|
119
|
+
lower_guess_1, upper_guess_1 = lorentzian_bounds(x_data_1, y_data_1, guess_1)
|
120
|
+
lower_guess_2, upper_guess_2 = lorentzian_bounds(x_data_2, y_data_2, guess_2)
|
121
|
+
# Combine bounds for 1 and 2
|
122
|
+
lower_guess = np.concatenate(
|
123
|
+
[
|
124
|
+
np.delete(lower_guess_1, 1),
|
125
|
+
np.delete(lower_guess_2, 1),
|
126
|
+
[np.min([lower_guess_1, lower_guess_2])],
|
127
|
+
]
|
128
|
+
)
|
129
|
+
upper_guess = np.concatenate(
|
130
|
+
[
|
131
|
+
np.delete(upper_guess_1, 1),
|
132
|
+
np.delete(upper_guess_2, 1),
|
133
|
+
[np.max([upper_guess_1, upper_guess_2])],
|
134
|
+
]
|
135
|
+
)
|
136
|
+
lower = fill_gaps(lower, lower_guess)
|
137
|
+
upper = fill_gaps(upper, upper_guess)
|
138
|
+
bounds = (lower, upper)
|
139
|
+
|
140
|
+
res = curve_fit(
|
141
|
+
lambda _, A1, fwhm1, y01, A2, fwhm2, y02, x0: _models.two_lorentzians_shared_x0(
|
142
|
+
x_data_1, x_data_2, A1, fwhm1, y01, A2, fwhm2, y02, x0
|
143
|
+
),
|
144
|
+
xdata=np.zeros_like(y_all), # dummy x, since x1 and x2 are fixed via closure
|
145
|
+
ydata=y_all,
|
146
|
+
p0=guess,
|
147
|
+
# bounds=bounds,
|
148
|
+
full_output=True,
|
149
|
+
)
|
150
|
+
|
151
|
+
return res, {
|
152
|
+
"param_names": ["A1", "fwhm1", "y01", "A2", "fwhm2", "y02", "x0"],
|
153
|
+
"predict": _models.two_lorentzians_shared_x0,
|
154
|
+
"fit_output_vars": {
|
155
|
+
"x_data": np.concatenate([x_data_1, x_data_2]),
|
156
|
+
"y_data": y_all,
|
157
|
+
},
|
158
|
+
}
|
159
|
+
|
160
|
+
|
99
161
|
@fit_input
|
100
162
|
@fit_output
|
101
163
|
def fit_gaussian(
|
102
164
|
x_data: np.ndarray,
|
103
165
|
y_data: np.ndarray,
|
104
166
|
guess: list = None,
|
105
|
-
bounds: list[tuple[float]] | tuple =
|
167
|
+
bounds: list[tuple[float]] | tuple = None,
|
106
168
|
) -> FitResult:
|
107
169
|
r"""
|
108
170
|
Fits a Gaussian function to the provided data. The function estimates the
|
@@ -147,36 +209,15 @@ def fit_gaussian(
|
|
147
209
|
x, y = x_data, y_data
|
148
210
|
|
149
211
|
# Default initial guess if not provided
|
150
|
-
if guess
|
151
|
-
|
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
|
-
|
212
|
+
if has_at_least_one(guess, None):
|
213
|
+
guess = fill_gaps(guess, gaussian_guess(x_data, y_data))
|
174
214
|
# Default bounds if not provided
|
175
215
|
if bounds is None:
|
176
|
-
bounds = (
|
177
|
-
|
178
|
-
|
179
|
-
)
|
216
|
+
bounds = ([None] * len(guess), [None] * len(guess))
|
217
|
+
if has_at_least_one(bounds[0], None) or has_at_least_one(bounds[1], None):
|
218
|
+
lower, upper = bounds
|
219
|
+
lower_guess, upper_guess = gaussian_bounds(x_data, y_data, guess)
|
220
|
+
bounds = (fill_gaps(lower, lower_guess), fill_gaps(upper, upper_guess))
|
180
221
|
|
181
222
|
res = curve_fit(_models.gaussian, x, y, p0=guess, bounds=bounds, full_output=True)
|
182
223
|
|
@@ -191,6 +232,73 @@ def fit_gaussian(
|
|
191
232
|
}
|
192
233
|
|
193
234
|
|
235
|
+
@fit_input
|
236
|
+
@fit_output
|
237
|
+
def fit_two_gaussians_shared_x0(
|
238
|
+
x_data_1,
|
239
|
+
y_data_1,
|
240
|
+
x_data_2,
|
241
|
+
y_data_2,
|
242
|
+
guess: list = None,
|
243
|
+
bounds: list[tuple[float]] | tuple = None,
|
244
|
+
):
|
245
|
+
y_all = np.concatenate([y_data_1, y_data_2])
|
246
|
+
|
247
|
+
if has_at_least_one(guess, None):
|
248
|
+
guess_1 = gaussian_guess(x_data_1, y_data_1)
|
249
|
+
guess_2 = gaussian_guess(x_data_2, y_data_2)
|
250
|
+
x01, x02 = guess_1[1], guess_2[1]
|
251
|
+
x0 = np.mean([x01, x02])
|
252
|
+
guess = fill_gaps(
|
253
|
+
guess, np.concatenate([np.delete(guess_1, 1), np.delete(guess_2, 1), [x0]])
|
254
|
+
)
|
255
|
+
|
256
|
+
if bounds == None:
|
257
|
+
bounds = [[None] * len(guess), [None] * len(guess)]
|
258
|
+
if has_at_least_one(bounds[0], None) or has_at_least_one(bounds[1], None):
|
259
|
+
lower, upper = bounds
|
260
|
+
lower_guess_1, upper_guess_1 = gaussian_bounds(x_data_1, y_data_1, guess_1)
|
261
|
+
lower_guess_2, upper_guess_2 = gaussian_bounds(x_data_2, y_data_2, guess_2)
|
262
|
+
# Combine bounds for 1 and 2
|
263
|
+
lower_guess = np.concatenate(
|
264
|
+
[
|
265
|
+
np.delete(lower_guess_1, 1),
|
266
|
+
np.delete(lower_guess_2, 1),
|
267
|
+
[np.min([lower_guess_1, lower_guess_2])],
|
268
|
+
]
|
269
|
+
)
|
270
|
+
upper_guess = np.concatenate(
|
271
|
+
[
|
272
|
+
np.delete(upper_guess_1, 1),
|
273
|
+
np.delete(upper_guess_2, 1),
|
274
|
+
[np.max([upper_guess_1, upper_guess_2])],
|
275
|
+
]
|
276
|
+
)
|
277
|
+
lower = fill_gaps(lower, lower_guess)
|
278
|
+
upper = fill_gaps(upper, upper_guess)
|
279
|
+
bounds = (lower, upper)
|
280
|
+
|
281
|
+
res = curve_fit(
|
282
|
+
lambda _, A1, fwhm1, y01, A2, fwhm2, y02, x0: _models.two_gaussians_shared_x0(
|
283
|
+
x_data_1, x_data_2, A1, fwhm1, y01, A2, fwhm2, y02, x0
|
284
|
+
),
|
285
|
+
xdata=np.zeros_like(y_all), # dummy x, since x1 and x2 are fixed via closure
|
286
|
+
ydata=y_all,
|
287
|
+
p0=guess,
|
288
|
+
# bounds=bounds,
|
289
|
+
full_output=True,
|
290
|
+
)
|
291
|
+
|
292
|
+
return res, {
|
293
|
+
"param_names": ["A1", "fwhm1", "y01", "A2", "fwhm2", "y02", "x0"],
|
294
|
+
"predict": _models.two_gaussians_shared_x0,
|
295
|
+
"fit_output_vars": {
|
296
|
+
"x_data": np.concatenate([x_data_1, x_data_2]),
|
297
|
+
"y_data": y_all,
|
298
|
+
},
|
299
|
+
}
|
300
|
+
|
301
|
+
|
194
302
|
@fit_input
|
195
303
|
@fit_output
|
196
304
|
def fit_decaying_exp(
|
@@ -382,9 +490,14 @@ def fit_qubit_relaxation_qp(
|
|
382
490
|
}
|
383
491
|
|
384
492
|
|
493
|
+
@fit_input
|
385
494
|
@fit_output
|
386
495
|
def fit_decaying_oscillations(
|
387
|
-
x_data: np.ndarray,
|
496
|
+
x_data: np.ndarray,
|
497
|
+
y_data: np.ndarray,
|
498
|
+
guess: list[float] | None = None,
|
499
|
+
bounds: list[tuple[float]] | tuple = None,
|
500
|
+
num_init: int = 10,
|
388
501
|
) -> FitResult:
|
389
502
|
r"""
|
390
503
|
Fits a decaying oscillation model to data. The function estimates key features
|
@@ -398,13 +511,15 @@ def fit_decaying_oscillations(
|
|
398
511
|
Parameters
|
399
512
|
----------
|
400
513
|
x_data : np.ndarray
|
401
|
-
|
402
|
-
|
514
|
+
Independent variable array (e.g., time or frequency).
|
403
515
|
y_data : np.ndarray
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
516
|
+
Dependent variable array representing the measured signal.
|
517
|
+
guess : list[float] or None, optional
|
518
|
+
Initial parameter estimates [A, tau, y0, phi, T]. Missing values are automatically filled.
|
519
|
+
bounds : list[tuple[float]] or tuple, optional
|
520
|
+
Lower and upper bounds for parameters during fitting, by default no bounds.
|
521
|
+
num_init : int, optional
|
522
|
+
Number of phase values to try when guessing, by default 10.
|
408
523
|
|
409
524
|
Returns
|
410
525
|
-------
|
@@ -416,31 +531,49 @@ def fit_decaying_oscillations(
|
|
416
531
|
- A callable `predict` function for generating fitted responses.
|
417
532
|
- A metadata dictionary containing the pi_time and its standard error.
|
418
533
|
"""
|
419
|
-
#
|
420
|
-
|
421
|
-
|
422
|
-
|
534
|
+
# Default intial guess if not provided
|
535
|
+
if has_at_least_one(guess, None):
|
536
|
+
guess = fill_gaps(guess, decaying_oscillations_guess(x_data, y_data, num_init))
|
537
|
+
|
538
|
+
# Default bounds if not provided
|
539
|
+
if bounds is None:
|
540
|
+
bounds = ([None] * len(guess), [None] * len(guess))
|
541
|
+
if has_at_least_one(bounds[0], None) or has_at_least_one(bounds[1], None):
|
542
|
+
lower, upper = bounds
|
543
|
+
lower_guess, upper_guess = decaying_oscillations_bounds(x_data, y_data, guess)
|
544
|
+
bounds = (fill_gaps(lower, lower_guess), fill_gaps(upper, upper_guess))
|
545
|
+
|
546
|
+
A, tau, y0, phi, T = guess
|
547
|
+
phi = make_iterable(phi)
|
548
|
+
y0 = make_iterable(y0)
|
423
549
|
|
424
550
|
best_fit = None
|
425
551
|
best_popt = None
|
552
|
+
best_nrmse = np.inf
|
553
|
+
|
554
|
+
@fit_output
|
555
|
+
def _curve_fit_osc(x_data, y_data, p0, bounds):
|
556
|
+
return curve_fit(
|
557
|
+
_models.decaying_oscillations,
|
558
|
+
x_data,
|
559
|
+
y_data,
|
560
|
+
p0,
|
561
|
+
bounds=bounds,
|
562
|
+
full_output=True,
|
563
|
+
)
|
426
564
|
|
427
565
|
# Try multiple initializations
|
428
|
-
for phi_guess in
|
429
|
-
for
|
430
|
-
p0 = [
|
566
|
+
for phi_guess in phi:
|
567
|
+
for offset in y0:
|
568
|
+
p0 = [A, tau, offset, phi_guess, T]
|
431
569
|
|
432
570
|
try:
|
433
571
|
with warnings.catch_warnings():
|
434
572
|
warnings.simplefilter("ignore")
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
p0,
|
440
|
-
full_output=True,
|
441
|
-
)
|
442
|
-
popt = fit_output[0]
|
443
|
-
best_fit, best_popt = fit_output, popt
|
573
|
+
fit_res = _curve_fit_osc(x_data, y_data, p0, bounds)
|
574
|
+
if fit_res.metrics["nrmse"] < best_nrmse:
|
575
|
+
best_fit, best_popt = fit_res.output, fit_res.params
|
576
|
+
best_nrmse = fit_res.metrics["nrmse"]
|
444
577
|
except:
|
445
578
|
if best_fit is None:
|
446
579
|
|
@@ -452,11 +585,15 @@ def fit_decaying_oscillations(
|
|
452
585
|
p0,
|
453
586
|
loss="soft_l1",
|
454
587
|
f_scale=0.1,
|
588
|
+
bounds=bounds,
|
455
589
|
args=(x_data, y_data),
|
456
590
|
)
|
457
591
|
best_fit, best_popt = result, result.x
|
458
592
|
|
459
|
-
|
593
|
+
if best_fit is None:
|
594
|
+
return None
|
595
|
+
|
596
|
+
# Compute pi-time (half-period + phase offset)
|
460
597
|
pi_time_raw = 0.5 * best_popt[4] + best_popt[3]
|
461
598
|
while pi_time_raw > 0.75 * np.abs(best_popt[4]):
|
462
599
|
pi_time_raw -= 0.5 * np.abs(best_popt[4])
|
@@ -482,6 +619,135 @@ def fit_decaying_oscillations(
|
|
482
619
|
return best_fit, metadata
|
483
620
|
|
484
621
|
|
622
|
+
@fit_input
|
623
|
+
@fit_output
|
624
|
+
def fit_oscillations(
|
625
|
+
x_data: np.ndarray,
|
626
|
+
y_data: np.ndarray,
|
627
|
+
guess: list[float] | None = None,
|
628
|
+
bounds: list[tuple[float]] | tuple = None,
|
629
|
+
num_init: int = 10,
|
630
|
+
) -> FitResult:
|
631
|
+
r"""
|
632
|
+
Fits an oscillation model to data. The function estimates key features
|
633
|
+
like the oscillation period and phase, and tries multiple initial guesses for
|
634
|
+
the optimization process.
|
635
|
+
|
636
|
+
f(x) = A * cos(2π * (x - φ) / T) + y0
|
637
|
+
|
638
|
+
$$f(x) = A \cos\left( 2\pi \frac{x - \phi}{T} \right) + y_0$$
|
639
|
+
|
640
|
+
Parameters
|
641
|
+
----------
|
642
|
+
x_data : np.ndarray
|
643
|
+
Independent variable array (e.g., time or frequency).
|
644
|
+
y_data : np.ndarray
|
645
|
+
Dependent variable array representing the measured signal.
|
646
|
+
guess : list[float] or None, optional
|
647
|
+
Initial parameter estimates [A, y0, phi, T]. Missing values are automatically filled.
|
648
|
+
bounds : list[tuple[float]] or tuple, optional
|
649
|
+
Lower and upper bounds for parameters during fitting, by default no bounds.
|
650
|
+
num_init : int, optional
|
651
|
+
Number of phase values to try when guessing, by default 10.
|
652
|
+
|
653
|
+
Returns
|
654
|
+
-------
|
655
|
+
FitResult
|
656
|
+
A `FitResult` object containing:
|
657
|
+
- Fitted parameters (`params`).
|
658
|
+
- Standard errors (`std_err`).
|
659
|
+
- Goodness-of-fit metrics (`rmse`, root mean squared error).
|
660
|
+
- A callable `predict` function for generating fitted responses.
|
661
|
+
- A metadata dictionary containing the pi_time and its standard error.
|
662
|
+
"""
|
663
|
+
# Default intial guess if not provided
|
664
|
+
if has_at_least_one(guess, None):
|
665
|
+
guess = fill_gaps(guess, oscillations_guess(x_data, y_data, num_init))
|
666
|
+
|
667
|
+
# Default bounds if not provided
|
668
|
+
if bounds is None:
|
669
|
+
bounds = ([None] * len(guess), [None] * len(guess))
|
670
|
+
if has_at_least_one(bounds[0], None) or has_at_least_one(bounds[1], None):
|
671
|
+
lower, upper = bounds
|
672
|
+
lower_guess, upper_guess = oscillations_bounds(x_data, y_data, guess)
|
673
|
+
bounds = (fill_gaps(lower, lower_guess), fill_gaps(upper, upper_guess))
|
674
|
+
|
675
|
+
A, y0, phi, T = guess
|
676
|
+
phi = make_iterable(phi)
|
677
|
+
y0 = make_iterable(y0)
|
678
|
+
|
679
|
+
best_fit = None
|
680
|
+
best_popt = None
|
681
|
+
best_nrmse = np.inf
|
682
|
+
|
683
|
+
@fit_output
|
684
|
+
def _curve_fit_osc(x_data, y_data, p0, bounds):
|
685
|
+
return curve_fit(
|
686
|
+
_models.oscillations,
|
687
|
+
x_data,
|
688
|
+
y_data,
|
689
|
+
p0,
|
690
|
+
bounds=bounds,
|
691
|
+
full_output=True,
|
692
|
+
)
|
693
|
+
|
694
|
+
# Try multiple initializations
|
695
|
+
for phi_guess in phi:
|
696
|
+
for offset in y0:
|
697
|
+
p0 = [A, offset, phi_guess, T]
|
698
|
+
|
699
|
+
try:
|
700
|
+
with warnings.catch_warnings():
|
701
|
+
warnings.simplefilter("ignore")
|
702
|
+
fit_res = _curve_fit_osc(x_data, y_data, p0, bounds)
|
703
|
+
if fit_res.metrics["nrmse"] < best_nrmse:
|
704
|
+
best_fit, best_popt = fit_res.output, fit_res.params
|
705
|
+
best_nrmse = fit_res.metrics["nrmse"]
|
706
|
+
except:
|
707
|
+
if best_fit is None:
|
708
|
+
|
709
|
+
def _oscillations_res(p, x, y):
|
710
|
+
return _models.oscillations(x, *p) - y
|
711
|
+
|
712
|
+
result = least_squares(
|
713
|
+
_oscillations_res,
|
714
|
+
p0,
|
715
|
+
loss="soft_l1",
|
716
|
+
f_scale=0.1,
|
717
|
+
bounds=bounds,
|
718
|
+
args=(x_data, y_data),
|
719
|
+
)
|
720
|
+
best_fit, best_popt = result, result.x
|
721
|
+
|
722
|
+
if best_fit is None:
|
723
|
+
return None
|
724
|
+
|
725
|
+
# Compute pi-time (half-period + phase offset)
|
726
|
+
pi_time_raw = 0.5 * best_popt[3] + best_popt[2]
|
727
|
+
while pi_time_raw > 0.75 * np.abs(best_popt[3]):
|
728
|
+
pi_time_raw -= 0.5 * np.abs(best_popt[3])
|
729
|
+
while pi_time_raw < 0.25 * np.abs(best_popt[3]):
|
730
|
+
pi_time_raw += 0.5 * np.abs(best_popt[3])
|
731
|
+
|
732
|
+
def _get_pi_time_std_err(sqil_dict):
|
733
|
+
if sqil_dict["std_err"] is not None:
|
734
|
+
phi_err = sqil_dict["std_err"][2]
|
735
|
+
T_err = sqil_dict["std_err"][3]
|
736
|
+
if np.isfinite(T_err) and np.isfinite(phi_err):
|
737
|
+
return np.sqrt((T_err / 2) ** 2 + phi_err**2)
|
738
|
+
return np.nan
|
739
|
+
|
740
|
+
# Metadata dictionary
|
741
|
+
metadata = {
|
742
|
+
"param_names": ["A", "y0", "phi", "T"],
|
743
|
+
"predict": _models.oscillations,
|
744
|
+
"pi_time": pi_time_raw,
|
745
|
+
"@pi_time_std_err": _get_pi_time_std_err,
|
746
|
+
}
|
747
|
+
|
748
|
+
return best_fit, metadata
|
749
|
+
|
750
|
+
|
485
751
|
@fit_output
|
486
752
|
def fit_circle_algebraic(x_data: np.ndarray, y_data: np.ndarray) -> FitResult:
|
487
753
|
"""Fits a circle in the xy plane and returns the radius and the position of the center.
|
@@ -780,3 +1046,146 @@ def fit_skewed_lorentzian(x_data: np.ndarray, y_data: np.ndarray):
|
|
780
1046
|
"param_names": ["A1", "A2", "A3", "A4", "fr", "Q_tot"],
|
781
1047
|
},
|
782
1048
|
)
|
1049
|
+
|
1050
|
+
|
1051
|
+
def transform_data(
|
1052
|
+
data: np.ndarray,
|
1053
|
+
transform_type: str = "optm",
|
1054
|
+
params: list = None,
|
1055
|
+
deg: bool = True,
|
1056
|
+
inv_transform: bool = False,
|
1057
|
+
full_output: bool = False,
|
1058
|
+
) -> (
|
1059
|
+
np.ndarray
|
1060
|
+
| tuple[np.ndarray, Callable]
|
1061
|
+
| tuple[np.ndarray, Callable, list, np.ndarray]
|
1062
|
+
):
|
1063
|
+
"""
|
1064
|
+
Transforms complex-valued data using various transformation methods, including
|
1065
|
+
optimization-based alignment, real/imaginary extraction, amplitude, and phase.
|
1066
|
+
|
1067
|
+
Parameters
|
1068
|
+
----------
|
1069
|
+
data : np.ndarray
|
1070
|
+
The complex-valued data to be transformed.
|
1071
|
+
|
1072
|
+
transform_type : str, optional
|
1073
|
+
The type of transformation to apply. Options include:
|
1074
|
+
- 'optm' (default): Optimized translation and rotation.
|
1075
|
+
- 'trrt': Translation and rotation using provided params.
|
1076
|
+
- 'real': Extract the real part.
|
1077
|
+
- 'imag': Extract the imaginary part.
|
1078
|
+
- 'ampl': Compute the amplitude.
|
1079
|
+
- 'angl': Compute the phase (in degrees if `deg=True`).
|
1080
|
+
|
1081
|
+
params : list, optional
|
1082
|
+
Transformation parameters [x0, y0, phi]. If None and `transform_type='optm'`,
|
1083
|
+
parameters are estimated automatically.
|
1084
|
+
|
1085
|
+
deg : bool, optional
|
1086
|
+
If True, phase transformations return values in degrees (default: True).
|
1087
|
+
|
1088
|
+
inv_transform : bool, optional
|
1089
|
+
If true returns transformed data and the function to perform the inverse transform.
|
1090
|
+
|
1091
|
+
full_output : bool, optional
|
1092
|
+
If True, returns transformed data, the function to perform the inverse transform,
|
1093
|
+
transformation parameters, and residuals.
|
1094
|
+
|
1095
|
+
Returns
|
1096
|
+
-------
|
1097
|
+
np.ndarray
|
1098
|
+
The transformed data.
|
1099
|
+
|
1100
|
+
tuple[np.ndarray, list, np.ndarray] (if `full_output=True`)
|
1101
|
+
Transformed data, transformation parameters, and residuals.
|
1102
|
+
|
1103
|
+
Notes
|
1104
|
+
-----
|
1105
|
+
- The function applies different transformations based on `transform_type`.
|
1106
|
+
- If `optm` is selected and `params` is not provided, an optimization routine
|
1107
|
+
is used to determine the best transformation parameters.
|
1108
|
+
|
1109
|
+
Example
|
1110
|
+
-------
|
1111
|
+
>>> data = np.array([1 + 1j, 2 + 2j, 3 + 3j])
|
1112
|
+
>>> transformed, params, residuals = transform_data(data, full_output=True)
|
1113
|
+
>>> print(transformed, params, residuals)
|
1114
|
+
"""
|
1115
|
+
|
1116
|
+
def transform(data, x0, y0, phi):
|
1117
|
+
return (data - x0 - 1.0j * y0) * np.exp(1.0j * phi)
|
1118
|
+
|
1119
|
+
def _inv_transform(data, x0, y0, phi):
|
1120
|
+
return data * np.exp(-1.0j * phi) + x0 + 1.0j * y0
|
1121
|
+
|
1122
|
+
def opt_transform(data):
|
1123
|
+
"""Finds optimal transformation parameters."""
|
1124
|
+
|
1125
|
+
def transform_err(x):
|
1126
|
+
return np.sum((transform(data, x[0], x[1], x[2]).imag) ** 2)
|
1127
|
+
|
1128
|
+
res = minimize(
|
1129
|
+
fun=transform_err,
|
1130
|
+
method="Nelder-Mead",
|
1131
|
+
x0=[
|
1132
|
+
np.mean(data.real),
|
1133
|
+
np.mean(data.imag),
|
1134
|
+
-np.arctan2(np.std(data.imag), np.std(data.real)),
|
1135
|
+
],
|
1136
|
+
options={"maxiter": 1000},
|
1137
|
+
)
|
1138
|
+
|
1139
|
+
params = res.x
|
1140
|
+
transformed_data = transform(data, *params)
|
1141
|
+
if transformed_data[0] < transformed_data[-1]:
|
1142
|
+
params[2] += np.pi
|
1143
|
+
return params
|
1144
|
+
|
1145
|
+
# Normalize transform_type
|
1146
|
+
transform_type = str(transform_type).lower()
|
1147
|
+
if transform_type.startswith(("op", "pr")):
|
1148
|
+
transform_type = "optm"
|
1149
|
+
elif transform_type.startswith("translation+rotation"):
|
1150
|
+
transform_type = "trrt"
|
1151
|
+
elif transform_type.startswith(("re", "qu")):
|
1152
|
+
transform_type = "real"
|
1153
|
+
elif transform_type.startswith(("im", "in")):
|
1154
|
+
transform_type = "imag"
|
1155
|
+
elif transform_type.startswith("am"):
|
1156
|
+
transform_type = "ampl"
|
1157
|
+
elif transform_type.startswith(("ph", "an")):
|
1158
|
+
transform_type = "angl"
|
1159
|
+
|
1160
|
+
# Compute parameters if needed
|
1161
|
+
if transform_type == "optm" and params is None:
|
1162
|
+
params = opt_transform(data)
|
1163
|
+
|
1164
|
+
# Apply transformation
|
1165
|
+
if transform_type in ["optm", "trrt"]:
|
1166
|
+
transformed_data = transform(data, *params).real
|
1167
|
+
residual = transform(data, *params).imag
|
1168
|
+
elif transform_type == "real":
|
1169
|
+
transformed_data = data.real
|
1170
|
+
residual = data.imag
|
1171
|
+
elif transform_type == "imag":
|
1172
|
+
transformed_data = data.imag
|
1173
|
+
residual = data.real
|
1174
|
+
elif transform_type == "ampl":
|
1175
|
+
transformed_data = np.abs(data)
|
1176
|
+
residual = np.unwrap(np.angle(data))
|
1177
|
+
if deg:
|
1178
|
+
residual = np.degrees(residual)
|
1179
|
+
elif transform_type == "angl":
|
1180
|
+
transformed_data = np.unwrap(np.angle(data))
|
1181
|
+
residual = np.abs(data)
|
1182
|
+
if deg:
|
1183
|
+
transformed_data = np.degrees(transformed_data)
|
1184
|
+
|
1185
|
+
inv_transform_fun = lambda data: _inv_transform(data, *params)
|
1186
|
+
|
1187
|
+
if full_output:
|
1188
|
+
return np.array(transformed_data), inv_transform_fun, params, residual
|
1189
|
+
if inv_transform:
|
1190
|
+
return np.array(transformed_data), inv_transform_fun
|
1191
|
+
return np.array(transformed_data)
|