sqil-core 0.1.0__py3-none-any.whl → 1.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 +1 -0
- sqil_core/config_log.py +42 -0
- sqil_core/experiment/__init__.py +11 -0
- sqil_core/experiment/_analysis.py +125 -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 +23 -0
- sqil_core/fit/_core.py +179 -31
- sqil_core/fit/_fit.py +544 -94
- sqil_core/fit/_guess.py +304 -0
- sqil_core/fit/_models.py +50 -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 +40 -13
- sqil_core/utils/_analysis.py +226 -0
- sqil_core/utils/_const.py +83 -18
- sqil_core/utils/_formatter.py +127 -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.1.0.dist-info}/METADATA +9 -1
- sqil_core-1.1.0.dist-info/RECORD +36 -0
- {sqil_core-0.1.0.dist-info → sqil_core-1.1.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.1.0.dist-info}/entry_points.txt +0 -0
sqil_core/fit/_core.py
CHANGED
@@ -6,8 +6,8 @@ import numpy as np
|
|
6
6
|
import scipy.optimize as spopt
|
7
7
|
from lmfit.model import ModelResult
|
8
8
|
|
9
|
-
from sqil_core.
|
10
|
-
from sqil_core.utils import
|
9
|
+
from sqil_core.fit._quality import FitQuality, evaluate_fit_quality, format_fit_metrics
|
10
|
+
from sqil_core.utils._formatter import format_fit_params
|
11
11
|
from sqil_core.utils._utils import _count_function_parameters
|
12
12
|
|
13
13
|
|
@@ -34,6 +34,8 @@ class FitResult:
|
|
34
34
|
If not provided, an exception will be raised when calling it.
|
35
35
|
param_names : list, optional
|
36
36
|
List of parameter names, defaulting to a range based on the number of parameters.
|
37
|
+
model_name : str, optional
|
38
|
+
Name of the model used to fit the data.
|
37
39
|
metadata : dict, optional
|
38
40
|
Additional information that can be passed in the fit result.
|
39
41
|
|
@@ -51,9 +53,10 @@ class FitResult:
|
|
51
53
|
params,
|
52
54
|
std_err,
|
53
55
|
fit_output,
|
54
|
-
metrics=
|
56
|
+
metrics={},
|
55
57
|
predict=None,
|
56
58
|
param_names=None,
|
59
|
+
model_name=None,
|
57
60
|
metadata={},
|
58
61
|
):
|
59
62
|
self.params = params
|
@@ -62,8 +65,11 @@ class FitResult:
|
|
62
65
|
self.metrics = metrics
|
63
66
|
self.predict = predict or self._no_prediction
|
64
67
|
self.param_names = param_names or list(range(len(params)))
|
68
|
+
self.model_name = model_name
|
65
69
|
self.metadata = metadata
|
66
70
|
|
71
|
+
self.params_by_name = dict(zip(self.param_names, self.params))
|
72
|
+
|
67
73
|
def __repr__(self):
|
68
74
|
return (
|
69
75
|
f"FitResult(\n"
|
@@ -72,15 +78,24 @@ class FitResult:
|
|
72
78
|
f" metrics={self.metrics}\n)"
|
73
79
|
)
|
74
80
|
|
75
|
-
def summary(self):
|
81
|
+
def summary(self, no_print=False):
|
76
82
|
"""Prints a detailed summary of the fit results."""
|
77
|
-
|
78
|
-
|
83
|
+
s = format_fit_metrics(self.metrics) + "\n"
|
84
|
+
s += format_fit_params(
|
79
85
|
self.param_names,
|
80
86
|
self.params,
|
81
87
|
self.std_err,
|
82
|
-
self.std_err / self.params * 100,
|
88
|
+
np.array(self.std_err) / self.params * 100,
|
83
89
|
)
|
90
|
+
if not no_print:
|
91
|
+
print(s)
|
92
|
+
return s
|
93
|
+
|
94
|
+
def quality(self, recipe="nrmse"):
|
95
|
+
return evaluate_fit_quality(self.metrics, recipe)
|
96
|
+
|
97
|
+
def is_acceptable(self, recipe="nrmse", threshold=FitQuality.ACCEPTABLE):
|
98
|
+
return self.quality(recipe) >= threshold
|
84
99
|
|
85
100
|
def _no_prediction(self):
|
86
101
|
raise Exception("No predition function available")
|
@@ -173,23 +188,34 @@ def fit_output(fit_func):
|
|
173
188
|
raw_fit_output = fit_result
|
174
189
|
sqil_dict["output"] = raw_fit_output
|
175
190
|
|
191
|
+
# Check if there are variables to override in metadata before continuing
|
192
|
+
if "fit_output_vars" in metadata:
|
193
|
+
overrides = metadata["fit_output_vars"]
|
194
|
+
x_data = overrides.get("x_data", x_data)
|
195
|
+
y_data = overrides.get("y_data", y_data)
|
196
|
+
del metadata["fit_output_vars"]
|
197
|
+
|
176
198
|
# Format the raw_fit_output into a standardized dict
|
199
|
+
if raw_fit_output is None:
|
200
|
+
raise TypeError("Fit didn't coverge, result is None")
|
177
201
|
# Scipy tuple (curve_fit, leastsq)
|
178
|
-
|
179
|
-
formatted = _format_scipy_tuple(raw_fit_output, has_sigma=has_sigma)
|
202
|
+
elif _is_scipy_tuple(raw_fit_output):
|
203
|
+
formatted = _format_scipy_tuple(raw_fit_output, y_data, has_sigma=has_sigma)
|
180
204
|
|
181
205
|
# Scipy least squares
|
182
206
|
elif _is_scipy_least_squares(raw_fit_output):
|
183
|
-
formatted = _format_scipy_least_squares(
|
207
|
+
formatted = _format_scipy_least_squares(
|
208
|
+
raw_fit_output, y_data, has_sigma=has_sigma
|
209
|
+
)
|
184
210
|
|
185
211
|
# Scipy minimize
|
186
212
|
elif _is_scipy_minimize(raw_fit_output):
|
187
213
|
residuals = None
|
188
214
|
predict = metadata.get("predict", None)
|
189
|
-
if predict and callable(predict):
|
215
|
+
if (x_data is not None) and (predict is not None) and callable(predict):
|
190
216
|
residuals = y_data - metadata["predict"](x_data, *raw_fit_output.x)
|
191
217
|
formatted = _format_scipy_minimize(
|
192
|
-
raw_fit_output, residuals=residuals, has_sigma=has_sigma
|
218
|
+
raw_fit_output, residuals=residuals, y_data=y_data, has_sigma=has_sigma
|
193
219
|
)
|
194
220
|
|
195
221
|
# lmfit
|
@@ -219,7 +245,10 @@ def fit_output(fit_func):
|
|
219
245
|
filtered_metadata = {k: v for k, v in metadata.items() if k not in sqil_keys}
|
220
246
|
|
221
247
|
# Assign the optimized parameters to the prediction function
|
248
|
+
model_name = metadata.get("model_name", None)
|
222
249
|
if sqil_dict["predict"] is not None:
|
250
|
+
if model_name is None:
|
251
|
+
model_name = sqil_dict["predict"].__name__
|
223
252
|
params = sqil_dict["params"]
|
224
253
|
predict = sqil_dict["predict"]
|
225
254
|
n_inputs = _count_function_parameters(predict)
|
@@ -230,9 +259,10 @@ def fit_output(fit_func):
|
|
230
259
|
params=sqil_dict.get("params", []),
|
231
260
|
std_err=sqil_dict.get("std_err", None),
|
232
261
|
fit_output=raw_fit_output,
|
233
|
-
metrics=sqil_dict.get("metrics",
|
262
|
+
metrics=sqil_dict.get("metrics", {}),
|
234
263
|
predict=sqil_dict.get("predict", None),
|
235
264
|
param_names=sqil_dict.get("param_names", None),
|
265
|
+
model_name=model_name,
|
236
266
|
metadata=filtered_metadata,
|
237
267
|
)
|
238
268
|
|
@@ -296,27 +326,27 @@ def fit_input(fit_func):
|
|
296
326
|
|
297
327
|
@wraps(fit_func)
|
298
328
|
def wrapper(
|
299
|
-
|
300
|
-
y_data,
|
329
|
+
*params,
|
301
330
|
guess=None,
|
302
331
|
bounds=None,
|
303
332
|
fixed_params=None,
|
304
333
|
fixed_bound_factor=1e-6,
|
334
|
+
sigma=None,
|
305
335
|
**kwargs,
|
306
336
|
):
|
307
337
|
# Inspect function to check if it requires guess and bounds
|
308
338
|
func_params = inspect.signature(fit_func).parameters
|
309
339
|
|
310
340
|
# Check if the user passed parameters that are not supported by the fit fun
|
311
|
-
if guess and ("guess" not in func_params):
|
341
|
+
if (guess is not None) and ("guess" not in func_params):
|
312
342
|
warnings.warn("The fit function doesn't allow any initial guess.")
|
313
|
-
if bounds and ("bounds" not in func_params):
|
343
|
+
if (bounds is not None) and ("bounds" not in func_params):
|
314
344
|
warnings.warn("The fit function doesn't allow any bounds.")
|
315
|
-
if fixed_params and (
|
345
|
+
if (fixed_params is not None) and (guess is None):
|
316
346
|
raise ValueError("Using fixed_params requires an initial guess.")
|
317
347
|
|
318
348
|
# Process bounds if the function accepts it
|
319
|
-
if bounds and ("bounds" in func_params):
|
349
|
+
if (bounds is not None) and ("bounds" in func_params):
|
320
350
|
processed_bounds = np.array(
|
321
351
|
[(-np.inf, np.inf) if b is None else b for b in bounds],
|
322
352
|
dtype=np.float64,
|
@@ -343,7 +373,7 @@ def fit_input(fit_func):
|
|
343
373
|
upper_bounds[idx] = guess[idx] + tolerance
|
344
374
|
|
345
375
|
# Prepare arguments dynamically
|
346
|
-
fit_args = {
|
376
|
+
fit_args = {**kwargs}
|
347
377
|
|
348
378
|
if guess is not None and "guess" in func_params:
|
349
379
|
fit_args["guess"] = guess
|
@@ -353,7 +383,8 @@ def fit_input(fit_func):
|
|
353
383
|
fit_args["bounds"] = (lower_bounds, upper_bounds)
|
354
384
|
|
355
385
|
# Call the wrapped function with preprocessed inputs
|
356
|
-
|
386
|
+
fit_args = {**kwargs, **fit_args}
|
387
|
+
return fit_func(*params, **fit_args)
|
357
388
|
|
358
389
|
return wrapper
|
359
390
|
|
@@ -549,11 +580,87 @@ def compute_chi2(residuals, n_params=None, cov_rescaled=True, sigma: np.ndarray
|
|
549
580
|
)
|
550
581
|
red_chi2 = np.nan
|
551
582
|
else:
|
552
|
-
red_chi2 =
|
583
|
+
red_chi2 = chi2 / dof
|
553
584
|
|
554
585
|
return chi2, red_chi2
|
555
586
|
|
556
587
|
|
588
|
+
def compute_aic(residuals: np.ndarray, n_params: int) -> float:
|
589
|
+
"""
|
590
|
+
Computes the Akaike Information Criterion (AIC) for a given model fit.
|
591
|
+
|
592
|
+
The AIC is a metric used to compare the relative quality of statistical models
|
593
|
+
for a given dataset. It balances model fit with complexity, penalizing models
|
594
|
+
with more parameters to prevent overfitting.
|
595
|
+
|
596
|
+
Interpretation: The AIC has no maeaning on its own, only the difference between
|
597
|
+
the AIC of model1 and the one of model2.
|
598
|
+
ΔAIC = AIC_1 - AIC_2
|
599
|
+
If ΔAIC > 10 -> model 2 fits much better.
|
600
|
+
|
601
|
+
Parameters
|
602
|
+
----------
|
603
|
+
residuals : np.ndarray
|
604
|
+
Array of residuals between the observed data and model predictions.
|
605
|
+
n_params : int
|
606
|
+
Number of free parameters in the fitted model.
|
607
|
+
|
608
|
+
Returns
|
609
|
+
-------
|
610
|
+
float
|
611
|
+
The Akaike Information Criterion value.
|
612
|
+
"""
|
613
|
+
|
614
|
+
n = len(residuals)
|
615
|
+
rss = np.sum(residuals**2)
|
616
|
+
return 2 * n_params + n * np.log(rss / n)
|
617
|
+
|
618
|
+
|
619
|
+
def compute_nrmse(residuals: np.ndarray, y_data: np.ndarray) -> float:
|
620
|
+
"""
|
621
|
+
Computes the Normalized Root Mean Squared Error (NRMSE) of a model fit.
|
622
|
+
|
623
|
+
Lower is better.
|
624
|
+
|
625
|
+
The NRMSE is a scale-independent metric that quantifies the average magnitude
|
626
|
+
of residual errors normalized by the range of the observed data. It is useful
|
627
|
+
for comparing the fit quality across different datasets or models.
|
628
|
+
|
629
|
+
For complex data it's computed using the L2 norm and the span of the magnitude.
|
630
|
+
|
631
|
+
Parameters
|
632
|
+
----------
|
633
|
+
residuals : np.ndarray
|
634
|
+
Array of residuals between the observed data and model predictions.
|
635
|
+
y_data : np.ndarray
|
636
|
+
The original observed data used in the model fitting.
|
637
|
+
|
638
|
+
Returns
|
639
|
+
-------
|
640
|
+
float
|
641
|
+
The normalized root mean squared error (NRMSE).
|
642
|
+
"""
|
643
|
+
n = len(residuals)
|
644
|
+
if np.iscomplexobj(y_data):
|
645
|
+
y_abs_span = np.max(np.abs(y_data)) - np.min(np.abs(y_data))
|
646
|
+
if y_abs_span == 0:
|
647
|
+
warnings.warn(
|
648
|
+
"y_data has zero span in magnitude. NRMSE is undefined.", RuntimeWarning
|
649
|
+
)
|
650
|
+
return np.nan
|
651
|
+
rmse = np.linalg.norm(residuals) / np.sqrt(n)
|
652
|
+
nrmse = rmse / y_abs_span
|
653
|
+
else:
|
654
|
+
y_span = np.max(y_data) - np.min(y_data)
|
655
|
+
if y_span == 0:
|
656
|
+
warnings.warn("y_data has zero span. NRMSE is undefined.", RuntimeWarning)
|
657
|
+
return np.nan
|
658
|
+
rss = np.sum(residuals**2)
|
659
|
+
nrmse = np.sqrt(rss / n) / y_span
|
660
|
+
|
661
|
+
return nrmse
|
662
|
+
|
663
|
+
|
557
664
|
def _is_scipy_tuple(result):
|
558
665
|
"""
|
559
666
|
Check whether the given result follows the expected structure of a SciPy optimization tuple.
|
@@ -612,7 +719,7 @@ def _is_lmfit(result):
|
|
612
719
|
return isinstance(result, ModelResult)
|
613
720
|
|
614
721
|
|
615
|
-
def _format_scipy_tuple(result, has_sigma=False):
|
722
|
+
def _format_scipy_tuple(result, y_data=None, has_sigma=False):
|
616
723
|
"""
|
617
724
|
Formats the output of a SciPy fitting function into a standardized dictionary.
|
618
725
|
|
@@ -628,6 +735,9 @@ def _format_scipy_tuple(result, has_sigma=False):
|
|
628
735
|
- `result[1]`: `pcov` (covariance matrix, NumPy array or None)
|
629
736
|
- `result[2]`: `infodict` (dictionary containing residuals, required for error computation)
|
630
737
|
|
738
|
+
y_data: bool, optional
|
739
|
+
The y data that has been fit. Used to compute some fit metrics.
|
740
|
+
|
631
741
|
has_sigma : bool, optional
|
632
742
|
Indicates whether the fitting procedure considered experimental errors (`sigma`).
|
633
743
|
If `True`, the covariance matrix (`pcov`) does not need rescaling.
|
@@ -643,8 +753,9 @@ def _format_scipy_tuple(result, has_sigma=False):
|
|
643
753
|
if not isinstance(result, tuple):
|
644
754
|
raise TypeError("Fit result must be a tuple")
|
645
755
|
|
646
|
-
std_err = None
|
647
756
|
popt, pcov, infodict = None, None, None
|
757
|
+
std_err = None
|
758
|
+
metrics = {}
|
648
759
|
|
649
760
|
# Extract output parameters
|
650
761
|
length = len(result)
|
@@ -654,18 +765,30 @@ def _format_scipy_tuple(result, has_sigma=False):
|
|
654
765
|
|
655
766
|
if infodict is not None:
|
656
767
|
residuals = infodict["fvec"]
|
768
|
+
# Reduced chi squared
|
657
769
|
_, red_chi2 = compute_chi2(
|
658
770
|
residuals, n_params=len(popt), cov_rescaled=has_sigma
|
659
771
|
)
|
772
|
+
# AIC
|
773
|
+
aic = compute_aic(residuals, len(popt))
|
774
|
+
# NRMSE
|
775
|
+
if y_data is not None:
|
776
|
+
nrmse = compute_nrmse(residuals, y_data)
|
777
|
+
metrics.update({"nrmse": nrmse})
|
778
|
+
metrics.update({"red_chi2": red_chi2, "aic": aic})
|
779
|
+
# Standard error
|
660
780
|
if pcov is not None:
|
661
781
|
std_err = compute_adjusted_standard_errors(
|
662
782
|
pcov, residuals, cov_rescaled=has_sigma, red_chi2=red_chi2
|
663
783
|
)
|
664
|
-
|
665
|
-
|
784
|
+
return {
|
785
|
+
"params": popt,
|
786
|
+
"std_err": std_err,
|
787
|
+
"metrics": metrics,
|
788
|
+
}
|
666
789
|
|
667
790
|
|
668
|
-
def _format_scipy_least_squares(result, has_sigma=False):
|
791
|
+
def _format_scipy_least_squares(result, y_data=None, has_sigma=False):
|
669
792
|
"""
|
670
793
|
Formats the output of a SciPy least-squares optimization into a standardized dictionary.
|
671
794
|
|
@@ -681,6 +804,9 @@ def _format_scipy_least_squares(result, has_sigma=False):
|
|
681
804
|
- `result.fun`: Residuals (array of differences between the observed and fitted data)
|
682
805
|
- `result.jac`: Jacobian matrix (used to estimate covariance)
|
683
806
|
|
807
|
+
y_data: bool, optional
|
808
|
+
The y data that has been fit. Used to compute some fit metrics.
|
809
|
+
|
684
810
|
has_sigma : bool, optional
|
685
811
|
Indicates whether the fitting procedure considered experimental errors (`sigma`).
|
686
812
|
If `True`, the covariance matrix does not need rescaling.
|
@@ -693,18 +819,27 @@ def _format_scipy_least_squares(result, has_sigma=False):
|
|
693
819
|
- `"std_err"`: Standard errors computed from the covariance matrix and residuals.
|
694
820
|
- `"metrics"`: A dictionary containing the reduced chi-squared (`red_chi2`).
|
695
821
|
"""
|
822
|
+
metrics = {}
|
823
|
+
|
696
824
|
params = result.x
|
697
825
|
residuals = result.fun
|
698
826
|
cov = np.linalg.inv(result.jac.T @ result.jac)
|
827
|
+
|
699
828
|
_, red_chi2 = compute_chi2(residuals, n_params=len(params), cov_rescaled=has_sigma)
|
829
|
+
aic = compute_aic(residuals, len(params))
|
830
|
+
if y_data is not None:
|
831
|
+
nrmse = compute_nrmse(residuals, y_data)
|
832
|
+
metrics.update({"nrmse": nrmse})
|
833
|
+
metrics.update({"red_chi2": red_chi2, "aic": aic})
|
834
|
+
|
700
835
|
std_err = compute_adjusted_standard_errors(
|
701
836
|
cov, residuals, cov_rescaled=has_sigma, red_chi2=red_chi2
|
702
837
|
)
|
703
838
|
|
704
|
-
return {"params": params, "std_err": std_err, "metrics":
|
839
|
+
return {"params": params, "std_err": std_err, "metrics": metrics}
|
705
840
|
|
706
841
|
|
707
|
-
def _format_scipy_minimize(result, residuals=None, has_sigma=False):
|
842
|
+
def _format_scipy_minimize(result, residuals=None, y_data=None, has_sigma=False):
|
708
843
|
"""
|
709
844
|
Formats the output of a SciPy minimize optimization into a standardized dictionary.
|
710
845
|
|
@@ -723,6 +858,9 @@ def _format_scipy_minimize(result, residuals=None, has_sigma=False):
|
|
723
858
|
The residuals (differences between observed data and fitted model).
|
724
859
|
If not provided, standard errors will be computed based on the inverse Hessian matrix.
|
725
860
|
|
861
|
+
y_data: bool, optional
|
862
|
+
The y data that has been fit. Used to compute some fit metrics.
|
863
|
+
|
726
864
|
has_sigma : bool, optional
|
727
865
|
Indicates whether the fitting procedure considered experimental errors (`sigma`).
|
728
866
|
If `True`, the covariance matrix does not need rescaling.
|
@@ -745,10 +883,15 @@ def _format_scipy_minimize(result, residuals=None, has_sigma=False):
|
|
745
883
|
std_err = compute_adjusted_standard_errors(
|
746
884
|
cov, residuals, cov_rescaled=has_sigma
|
747
885
|
)
|
886
|
+
|
748
887
|
_, red_chi2 = compute_chi2(
|
749
888
|
residuals, n_params=len(params), cov_rescaled=has_sigma
|
750
889
|
)
|
751
|
-
|
890
|
+
aic = compute_aic(residuals, len(params))
|
891
|
+
if y_data is not None:
|
892
|
+
nrmse = compute_nrmse(residuals, y_data)
|
893
|
+
metrics.update({"nrmse": nrmse})
|
894
|
+
metrics.update({"red_chi2": red_chi2, "aic": aic})
|
752
895
|
|
753
896
|
return {"params": params, "std_err": std_err, "metrics": metrics}
|
754
897
|
|
@@ -794,6 +937,11 @@ def _format_lmfit(result: ModelResult):
|
|
794
937
|
for param in result.params.values()
|
795
938
|
]
|
796
939
|
)
|
940
|
+
|
941
|
+
aic = compute_aic(result.residual, len(params))
|
942
|
+
nrmse = compute_nrmse(result.residual, result.data)
|
943
|
+
metrics = {"red_chi2": result.redchi, "aic": aic, "nrmse": nrmse}
|
944
|
+
|
797
945
|
# Determine the independent variable name used in the fit
|
798
946
|
independent_var = result.userkws.keys() & result.model.independent_vars
|
799
947
|
independent_var = (
|
@@ -804,7 +952,7 @@ def _format_lmfit(result: ModelResult):
|
|
804
952
|
return {
|
805
953
|
"params": params,
|
806
954
|
"std_err": std_err,
|
807
|
-
"metrics":
|
955
|
+
"metrics": metrics,
|
808
956
|
"predict": fit_function,
|
809
957
|
"param_names": param_names,
|
810
958
|
}
|