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.
Files changed (36) hide show
  1. sqil_core/__init__.py +1 -0
  2. sqil_core/config_log.py +42 -0
  3. sqil_core/experiment/__init__.py +11 -0
  4. sqil_core/experiment/_analysis.py +125 -0
  5. sqil_core/experiment/_events.py +25 -0
  6. sqil_core/experiment/_experiment.py +553 -0
  7. sqil_core/experiment/data/plottr.py +778 -0
  8. sqil_core/experiment/helpers/_function_override_handler.py +111 -0
  9. sqil_core/experiment/helpers/_labone_wrappers.py +12 -0
  10. sqil_core/experiment/instruments/__init__.py +2 -0
  11. sqil_core/experiment/instruments/_instrument.py +190 -0
  12. sqil_core/experiment/instruments/drivers/SignalCore_SC5511A.py +515 -0
  13. sqil_core/experiment/instruments/local_oscillator.py +205 -0
  14. sqil_core/experiment/instruments/server.py +175 -0
  15. sqil_core/experiment/instruments/setup.yaml +21 -0
  16. sqil_core/experiment/instruments/zurich_instruments.py +55 -0
  17. sqil_core/fit/__init__.py +23 -0
  18. sqil_core/fit/_core.py +179 -31
  19. sqil_core/fit/_fit.py +544 -94
  20. sqil_core/fit/_guess.py +304 -0
  21. sqil_core/fit/_models.py +50 -1
  22. sqil_core/fit/_quality.py +266 -0
  23. sqil_core/resonator/__init__.py +2 -0
  24. sqil_core/resonator/_resonator.py +256 -74
  25. sqil_core/utils/__init__.py +40 -13
  26. sqil_core/utils/_analysis.py +226 -0
  27. sqil_core/utils/_const.py +83 -18
  28. sqil_core/utils/_formatter.py +127 -55
  29. sqil_core/utils/_plot.py +272 -6
  30. sqil_core/utils/_read.py +178 -95
  31. sqil_core/utils/_utils.py +147 -0
  32. {sqil_core-0.1.0.dist-info → sqil_core-1.1.0.dist-info}/METADATA +9 -1
  33. sqil_core-1.1.0.dist-info/RECORD +36 -0
  34. {sqil_core-0.1.0.dist-info → sqil_core-1.1.0.dist-info}/WHEEL +1 -1
  35. sqil_core-0.1.0.dist-info/RECORD +0 -19
  36. {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.utils import print_fit_metrics as _print_fit_metrics
10
- from sqil_core.utils import print_fit_params as _print_fit_params
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=None,
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
- _print_fit_metrics(self.metrics)
78
- _print_fit_params(
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
- if _is_scipy_tuple(raw_fit_output):
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(raw_fit_output, has_sigma=has_sigma)
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", None),
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
- x_data,
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 (not guess):
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 = {"x_data": x_data, "y_data": y_data, **kwargs}
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
- return fit_func(**fit_args)
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 = np.sum(residuals**2) / dof
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
- return {"params": popt, "std_err": std_err, "metrics": {"red_chi2": red_chi2}}
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": {"red_chi2": red_chi2}}
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
- metrics = {"red_chi2": red_chi2}
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": {"red_chi2": result.redchi},
955
+ "metrics": metrics,
808
956
  "predict": fit_function,
809
957
  "param_names": param_names,
810
958
  }