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/utils/__init__.py
CHANGED
@@ -1,6 +1,63 @@
|
|
1
|
-
from .
|
2
|
-
|
3
|
-
|
1
|
+
from ._analysis import (
|
2
|
+
compute_snr_peaked,
|
3
|
+
estimate_linear_background,
|
4
|
+
line_between_2_points,
|
5
|
+
linear_interpolation,
|
6
|
+
remove_linear_background,
|
7
|
+
remove_offset,
|
8
|
+
)
|
9
|
+
from ._const import ONE_TONE_PARAMS, TWO_TONE_PARAMS
|
10
|
+
from ._formatter import (
|
11
|
+
format_number,
|
12
|
+
get_name_and_unit,
|
13
|
+
print_fit_metrics,
|
14
|
+
print_fit_params,
|
15
|
+
)
|
16
|
+
from ._plot import (
|
17
|
+
build_title,
|
18
|
+
get_x_id_by_plot_dim,
|
19
|
+
guess_plot_dimension,
|
20
|
+
reset_plot_style,
|
21
|
+
set_plot_style,
|
22
|
+
)
|
23
|
+
from ._read import (
|
24
|
+
ParamDict,
|
25
|
+
ParamInfo,
|
26
|
+
extract_h5_data,
|
27
|
+
get_measurement_id,
|
28
|
+
get_sweep_param,
|
29
|
+
read_json,
|
30
|
+
read_param_dict,
|
31
|
+
)
|
4
32
|
|
5
|
-
__all__ = [
|
6
|
-
|
33
|
+
__all__ = [
|
34
|
+
# Analysis
|
35
|
+
"remove_offset",
|
36
|
+
"estimate_linear_background",
|
37
|
+
"remove_linear_background",
|
38
|
+
"linear_interpolation",
|
39
|
+
"line_between_2_points",
|
40
|
+
"compute_snr_peaked",
|
41
|
+
# Const
|
42
|
+
"ONE_TONE_PARAMS",
|
43
|
+
"TWO_TONE_PARAMS",
|
44
|
+
# Formatter
|
45
|
+
"format_number",
|
46
|
+
"get_name_and_unit",
|
47
|
+
"print_fit_params",
|
48
|
+
"print_fit_metrics",
|
49
|
+
# Plot
|
50
|
+
"set_plot_style",
|
51
|
+
"reset_plot_style",
|
52
|
+
"get_x_id_by_plot_dim",
|
53
|
+
"build_title",
|
54
|
+
"guess_plot_dimension",
|
55
|
+
# Read
|
56
|
+
"extract_h5_data",
|
57
|
+
"read_json",
|
58
|
+
"ParamInfo",
|
59
|
+
"ParamDict",
|
60
|
+
"read_param_dict",
|
61
|
+
"get_sweep_param",
|
62
|
+
"get_measurement_id",
|
63
|
+
]
|
@@ -0,0 +1,292 @@
|
|
1
|
+
import numpy as np
|
2
|
+
|
3
|
+
|
4
|
+
def remove_offset(data: np.ndarray, avg: int = 3) -> np.ndarray:
|
5
|
+
"""Removes the initial offset from a data matrix or vector by subtracting
|
6
|
+
the average of the first `avg` points. After applying this function,
|
7
|
+
the first point of each column of the data will be shifted to (about) 0.
|
8
|
+
|
9
|
+
Parameters
|
10
|
+
----------
|
11
|
+
data : np.ndarray
|
12
|
+
Input data, either a 1D vector or a 2D matrix
|
13
|
+
avg : int, optional
|
14
|
+
The number of initial points to average when calculating
|
15
|
+
the offset, by default 3
|
16
|
+
|
17
|
+
Returns
|
18
|
+
-------
|
19
|
+
np.ndarray
|
20
|
+
The input data with the offset removed
|
21
|
+
"""
|
22
|
+
is1D = len(data.shape) == 1
|
23
|
+
if is1D:
|
24
|
+
return data - np.mean(data[0:avg])
|
25
|
+
return data - np.mean(data[:, 0:avg], axis=1).reshape(data.shape[0], 1)
|
26
|
+
|
27
|
+
|
28
|
+
def estimate_linear_background(
|
29
|
+
x: np.ndarray,
|
30
|
+
data: np.ndarray,
|
31
|
+
points_cut: float = 0.1,
|
32
|
+
cut_from_back: bool = False,
|
33
|
+
) -> list:
|
34
|
+
"""
|
35
|
+
Estimates the linear background for a given data set by fitting a linear model to a subset of the data.
|
36
|
+
|
37
|
+
This function performs a linear regression to estimate the background (offset and slope) from the
|
38
|
+
given data by selecting a portion of the data as specified by the `points_cut` parameter. The linear
|
39
|
+
fit is applied to either the first or last `points_cut` fraction of the data, depending on the `cut_from_back`
|
40
|
+
flag. The estimated background is returned as the coefficients of the linear fit.
|
41
|
+
|
42
|
+
Parameters
|
43
|
+
----------
|
44
|
+
x : np.ndarray
|
45
|
+
The independent variable data.
|
46
|
+
data : np.ndarray
|
47
|
+
The dependent variable data, which can be 1D or 2D (e.g., multiple measurements or data points).
|
48
|
+
points_cut : float, optional
|
49
|
+
The fraction of the data to be considered for the linear fit. Default is 0.1 (10% of the data).
|
50
|
+
cut_from_back : bool, optional
|
51
|
+
Whether to use the last `points_cut` fraction of the data (True) or the first fraction (False).
|
52
|
+
Default is False.
|
53
|
+
|
54
|
+
Returns
|
55
|
+
-------
|
56
|
+
list
|
57
|
+
The coefficients of the linear fit: a list with two elements, where the first is the offset (intercept)
|
58
|
+
and the second is the slope.
|
59
|
+
|
60
|
+
Notes
|
61
|
+
-----
|
62
|
+
- If `data` is 2D, the fit is performed on each column of the data separately.
|
63
|
+
- The function assumes that `x` and `data` have compatible shapes.
|
64
|
+
|
65
|
+
Examples
|
66
|
+
--------
|
67
|
+
>>> import numpy as np
|
68
|
+
>>> x = np.linspace(0, 10, 100)
|
69
|
+
>>> data = 3 * x + 2 + np.random.normal(0, 1, size=(100,))
|
70
|
+
>>> coefficients = estimate_linear_background(x, data, points_cut=0.2)
|
71
|
+
>>> print("Estimated coefficients:", coefficients)
|
72
|
+
"""
|
73
|
+
is1D = len(data.shape) == 1
|
74
|
+
points = data.shape[0] if is1D else data.shape[1]
|
75
|
+
cut = int(points * points_cut)
|
76
|
+
|
77
|
+
# Consider just the cut points
|
78
|
+
if not cut_from_back:
|
79
|
+
x_data = x[0:cut] if is1D else x[:, 0:cut]
|
80
|
+
y_data = data[0:cut] if is1D else data[:, 0:cut]
|
81
|
+
else:
|
82
|
+
x_data = x[-cut:] if is1D else x[:, -cut:]
|
83
|
+
y_data = data[-cut:] if is1D else data[:, -cut:]
|
84
|
+
|
85
|
+
ones_column = np.ones_like(x_data[0, :]) if not is1D else np.ones_like(x_data)
|
86
|
+
X = np.vstack([ones_column, x_data[0, :] if not is1D else x_data]).T
|
87
|
+
# Linear fit
|
88
|
+
coefficients, residuals, _, _ = np.linalg.lstsq(
|
89
|
+
X, y_data if is1D else y_data.T, rcond=None
|
90
|
+
)
|
91
|
+
|
92
|
+
return coefficients.T
|
93
|
+
|
94
|
+
|
95
|
+
def remove_linear_background(
|
96
|
+
x: np.ndarray, data: np.ndarray, points_cut=0.1
|
97
|
+
) -> np.ndarray:
|
98
|
+
"""Removes a linear background from the input data (e.g. the phase background
|
99
|
+
of a spectroscopy).
|
100
|
+
|
101
|
+
|
102
|
+
Parameters
|
103
|
+
----------
|
104
|
+
data : np.ndarray
|
105
|
+
Input data. Can be a 1D vector or a 2D matrix.
|
106
|
+
|
107
|
+
Returns
|
108
|
+
-------
|
109
|
+
np.ndarray
|
110
|
+
The input data with the linear background removed. The shape of the
|
111
|
+
returned array matches the input `data`.
|
112
|
+
"""
|
113
|
+
coefficients = estimate_linear_background(x, data, points_cut)
|
114
|
+
|
115
|
+
# Remove background over the whole array
|
116
|
+
is1D = len(data.shape) == 1
|
117
|
+
ones_column = np.ones_like(x[0, :]) if not is1D else np.ones_like(x)
|
118
|
+
X = np.vstack([ones_column, x[0, :] if not is1D else x]).T
|
119
|
+
return data - (X @ coefficients.T).T
|
120
|
+
|
121
|
+
|
122
|
+
def linear_interpolation(
|
123
|
+
x: float | np.ndarray, x1: float, y1: float, x2: float, y2: float
|
124
|
+
) -> float | np.ndarray:
|
125
|
+
"""
|
126
|
+
Performs linear interpolation to estimate the value of y at a given x.
|
127
|
+
|
128
|
+
This function computes the interpolated y-value for a given x using two known points (x1, y1) and (x2, y2)
|
129
|
+
on a straight line. It supports both scalar and array inputs for x, enabling vectorized operations.
|
130
|
+
|
131
|
+
Parameters
|
132
|
+
----------
|
133
|
+
x : float or np.ndarray
|
134
|
+
The x-coordinate(s) at which to interpolate.
|
135
|
+
x1 : float
|
136
|
+
The x-coordinate of the first known point.
|
137
|
+
y1 : float
|
138
|
+
The y-coordinate of the first known point.
|
139
|
+
x2 : float
|
140
|
+
The x-coordinate of the second known point.
|
141
|
+
y2 : float
|
142
|
+
The y-coordinate of the second known point.
|
143
|
+
|
144
|
+
Returns
|
145
|
+
-------
|
146
|
+
float or np.ndarray
|
147
|
+
The interpolated y-value(s) at x.
|
148
|
+
|
149
|
+
Notes
|
150
|
+
-----
|
151
|
+
- If x1 and x2 are the same, the function returns y1 to prevent division by zero.
|
152
|
+
- Assumes that x lies between x1 and x2 for meaningful interpolation.
|
153
|
+
|
154
|
+
Examples
|
155
|
+
--------
|
156
|
+
>>> linear_interpolation(3, 2, 4, 6, 8)
|
157
|
+
5.0
|
158
|
+
>>> x_vals = np.array([3, 4, 5])
|
159
|
+
>>> linear_interpolation(x_vals, 2, 4, 6, 8)
|
160
|
+
array([5., 6., 7.])
|
161
|
+
"""
|
162
|
+
if x1 == x2:
|
163
|
+
return y1
|
164
|
+
return y1 + (x - x1) * (y2 - y1) / (x2 - x1)
|
165
|
+
|
166
|
+
|
167
|
+
def line_between_2_points(
|
168
|
+
x1: float, y1: float, x2: float, y2: float
|
169
|
+
) -> tuple[float, float]:
|
170
|
+
"""
|
171
|
+
Computes the equation of a line passing through two points.
|
172
|
+
|
173
|
+
Given two points (x1, y1) and (x2, y2), this function returns the y-intercept and slope of the line
|
174
|
+
connecting them. If x1 and x2 are the same, the function returns y1 as the intercept and a slope of 0
|
175
|
+
to avoid division by zero.
|
176
|
+
|
177
|
+
Parameters
|
178
|
+
----------
|
179
|
+
x1 : float
|
180
|
+
The x-coordinate of the first point.
|
181
|
+
y1 : float
|
182
|
+
The y-coordinate of the first point.
|
183
|
+
x2 : float
|
184
|
+
The x-coordinate of the second point.
|
185
|
+
y2 : float
|
186
|
+
The y-coordinate of the second point.
|
187
|
+
|
188
|
+
Returns
|
189
|
+
-------
|
190
|
+
tuple[float, float]
|
191
|
+
A tuple containing:
|
192
|
+
- The y-intercept (float), which is y1.
|
193
|
+
- The slope (float) of the line passing through the points.
|
194
|
+
|
195
|
+
Notes
|
196
|
+
-----
|
197
|
+
- If x1 and x2 are the same, the function assumes a vertical line and returns a slope of 0.
|
198
|
+
- The returned y-intercept is based on y1 for consistency in edge cases.
|
199
|
+
|
200
|
+
Examples
|
201
|
+
--------
|
202
|
+
>>> line_between_2_points(1, 2, 3, 4)
|
203
|
+
(2, 1.0)
|
204
|
+
>>> line_between_2_points(2, 5, 2, 10)
|
205
|
+
(5, 0)
|
206
|
+
"""
|
207
|
+
if x1 == x2:
|
208
|
+
return np.inf, y1
|
209
|
+
slope = (y2 - y1) / (x2 - x1)
|
210
|
+
intercept = y1 - slope * x1
|
211
|
+
return slope, intercept
|
212
|
+
|
213
|
+
|
214
|
+
def compute_snr_peaked(
|
215
|
+
x_data: np.ndarray,
|
216
|
+
y_data: np.ndarray,
|
217
|
+
x0: float,
|
218
|
+
fwhm: float,
|
219
|
+
noise_region_factor: float = 2.5,
|
220
|
+
min_points: int = 20,
|
221
|
+
) -> float:
|
222
|
+
"""
|
223
|
+
Computes the Signal-to-Noise Ratio (SNR) for a peaked function (e.g., Lorentzian, Gaussian)
|
224
|
+
based on the provided fit parameters. The SNR is calculated by comparing the signal strength
|
225
|
+
at the peak (x0) with the noise level estimated from a region outside the peak.
|
226
|
+
|
227
|
+
Parameters
|
228
|
+
----------
|
229
|
+
x_data : np.ndarray
|
230
|
+
Array of x values (independent variable), typically representing frequency or position.
|
231
|
+
|
232
|
+
y_data : np.ndarray
|
233
|
+
Array of y values (dependent variable), representing the measured values (e.g., intensity, amplitude).
|
234
|
+
|
235
|
+
x0 : float
|
236
|
+
The location of the peak (center of the distribution), often the resonance frequency or peak position.
|
237
|
+
|
238
|
+
fwhm : float
|
239
|
+
The Full Width at Half Maximum (FWHM) of the peak. This defines the width of the peak and helps determine
|
240
|
+
the region for noise estimation.
|
241
|
+
|
242
|
+
noise_region_factor : float, optional, default=2.5
|
243
|
+
The factor used to define the width of the noise region as a multiple of the FWHM. The noise region is
|
244
|
+
considered outside the interval `(x0 - noise_region_factor * fwhm, x0 + noise_region_factor * fwhm)`.
|
245
|
+
|
246
|
+
min_points : int, optional, default=20
|
247
|
+
The minimum number of data points required in the noise region to estimate the noise level. If the number
|
248
|
+
of points in the noise region is smaller than this threshold, a warning is issued.
|
249
|
+
|
250
|
+
Returns
|
251
|
+
-------
|
252
|
+
float
|
253
|
+
The computed Signal-to-Noise Ratio (SNR), which is the ratio of the signal strength at `x0` to the
|
254
|
+
standard deviation of the noise. If the noise standard deviation is zero, the SNR is set to infinity.
|
255
|
+
|
256
|
+
Notes
|
257
|
+
-----
|
258
|
+
- The function assumes that the signal has a clear peak at `x0` and that the surrounding data represents noise.
|
259
|
+
- If the noise region contains fewer than `min_points` data points, a warning is raised suggesting the adjustment of `noise_region_factor`.
|
260
|
+
|
261
|
+
Example
|
262
|
+
-------
|
263
|
+
>>> x_data = np.linspace(-10, 10, 1000)
|
264
|
+
>>> y_data = np.exp(-(x_data**2)) # Example Gaussian
|
265
|
+
>>> x0 = 0
|
266
|
+
>>> fwhm = 2.0
|
267
|
+
>>> snr = compute_snr_peaked(x_data, y_data, x0, fwhm)
|
268
|
+
>>> print(snr)
|
269
|
+
"""
|
270
|
+
|
271
|
+
# Signal strength at x0
|
272
|
+
signal = y_data[np.argmin(np.abs(x_data - x0))]
|
273
|
+
|
274
|
+
# Define noise region (outside noise_region_factor * FWHM)
|
275
|
+
noise_mask = (x_data < (x0 - noise_region_factor * fwhm)) | (
|
276
|
+
x_data > (x0 + noise_region_factor * fwhm)
|
277
|
+
)
|
278
|
+
noise_data = y_data[noise_mask]
|
279
|
+
|
280
|
+
# Check if there are enough data points for noise estimation
|
281
|
+
if len(noise_data) < min_points:
|
282
|
+
Warning(
|
283
|
+
f"Only {len(noise_data)} points found in the noise region. Consider reducing noise_region_factor."
|
284
|
+
)
|
285
|
+
|
286
|
+
# Compute noise standard deviation
|
287
|
+
noise_std = np.std(noise_data)
|
288
|
+
|
289
|
+
# Compute SNR
|
290
|
+
snr = signal / noise_std if noise_std > 0 else np.inf # Avoid division by zero
|
291
|
+
|
292
|
+
return snr
|
@@ -1,38 +1,49 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
-
|
5
|
-
-
|
6
|
-
-
|
7
|
-
|
8
|
-
3: "
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
"
|
21
|
-
"
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
"
|
27
|
-
"
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
"
|
33
|
-
"
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
"
|
38
|
-
}
|
1
|
+
import numpy as np
|
2
|
+
|
3
|
+
_EXP_UNIT_MAP = {
|
4
|
+
-15: "p",
|
5
|
+
-12: "f",
|
6
|
+
-9: "n",
|
7
|
+
-6: r"\mu",
|
8
|
+
-3: "m",
|
9
|
+
0: "",
|
10
|
+
3: "k",
|
11
|
+
6: "M",
|
12
|
+
9: "G",
|
13
|
+
12: "T",
|
14
|
+
15: "P",
|
15
|
+
}
|
16
|
+
|
17
|
+
_PARAM_METADATA = {
|
18
|
+
"current": {"name": "Current", "symbol": "I", "unit": "A", "scale": 1e3},
|
19
|
+
"ro_freq": {
|
20
|
+
"name": "Readout frequency",
|
21
|
+
"symbol": "f_{RO}",
|
22
|
+
"unit": "Hz",
|
23
|
+
"scale": 1e-9,
|
24
|
+
},
|
25
|
+
"ro_power": {
|
26
|
+
"name": "Readout power",
|
27
|
+
"symbol": "P_{RO}",
|
28
|
+
"unit": "dBm",
|
29
|
+
"scale": 1,
|
30
|
+
},
|
31
|
+
"qu_freq": {
|
32
|
+
"name": "Qubit frequency",
|
33
|
+
"symbol": "f_q",
|
34
|
+
"unit": "Hz",
|
35
|
+
"scale": 1e-9,
|
36
|
+
},
|
37
|
+
"qu_power": {"name": "Qubit power", "symbol": "P_q", "unit": "dBm", "scale": 1},
|
38
|
+
"vna_bw": {"name": "VNA bandwidth", "symbol": "BW_{VNA}", "unit": "Hz", "scale": 1},
|
39
|
+
"vna_avg": {"name": "VNA averages", "symbol": "avg_{VNA}", "unit": "", "scale": 1},
|
40
|
+
"index": {"name": "Index", "symbol": "idx", "unit": "", "scale": 1},
|
41
|
+
}
|
42
|
+
|
43
|
+
ONE_TONE_PARAMS = np.array(
|
44
|
+
["current", "ro_power", "vna_bw", "vna_avg", "qu_power", "qu_freq"]
|
45
|
+
)
|
46
|
+
|
47
|
+
TWO_TONE_PARAMS = np.array(
|
48
|
+
["ro_freq", "ro_power", "current", "vna_bw", "vna_avg", "qu_power"]
|
49
|
+
)
|
@@ -0,0 +1,188 @@
|
|
1
|
+
from decimal import ROUND_DOWN, Decimal
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
from scipy.stats import norm
|
5
|
+
from tabulate import tabulate
|
6
|
+
|
7
|
+
from ._const import _EXP_UNIT_MAP, _PARAM_METADATA
|
8
|
+
|
9
|
+
|
10
|
+
def _cut_to_significant_digits(number, n):
|
11
|
+
"""Cut a number to n significant digits."""
|
12
|
+
if number == 0:
|
13
|
+
return 0 # Zero has no significant digits
|
14
|
+
d = Decimal(str(number))
|
15
|
+
shift = d.adjusted() # Get the exponent of the number
|
16
|
+
rounded = d.scaleb(-shift).quantize(
|
17
|
+
Decimal("1e-{0}".format(n - 1)), rounding=ROUND_DOWN
|
18
|
+
)
|
19
|
+
return float(rounded.scaleb(shift))
|
20
|
+
|
21
|
+
|
22
|
+
def format_number(
|
23
|
+
num: float | np.ndarray, precision: int = 3, unit: str = "", latex: bool = True
|
24
|
+
) -> str:
|
25
|
+
"""Format a number (or an array of numbers) in a nice way for printing.
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
num : float | np.ndarray
|
30
|
+
Input number (or array). Should not be rescaled,
|
31
|
+
e.g. input values in Hz, NOT GHz
|
32
|
+
precision : int
|
33
|
+
The number of digits of the output number. Must be >= 3.
|
34
|
+
unit : str, optional
|
35
|
+
Unit of measurement, by default ''
|
36
|
+
latex : bool, optional
|
37
|
+
Include Latex syntax, by default True
|
38
|
+
|
39
|
+
Returns
|
40
|
+
-------
|
41
|
+
str
|
42
|
+
Formatted number
|
43
|
+
"""
|
44
|
+
# Handle arrays
|
45
|
+
if isinstance(num, (list, np.ndarray)):
|
46
|
+
return [format_number(n, precision, unit, latex) for n in num]
|
47
|
+
|
48
|
+
# Return if not a number
|
49
|
+
if not isinstance(num, (int, float, complex)):
|
50
|
+
return num
|
51
|
+
|
52
|
+
# Format number
|
53
|
+
exp_form = f"{num:.12e}"
|
54
|
+
base, exponent = exp_form.split("e")
|
55
|
+
# Make exponent a multiple of 3
|
56
|
+
base = float(base) * 10 ** (int(exponent) % 3)
|
57
|
+
exponent = (int(exponent) // 3) * 3
|
58
|
+
# Apply precision to the base
|
59
|
+
if precision < 3:
|
60
|
+
precision = 3
|
61
|
+
base_precise = _cut_to_significant_digits(
|
62
|
+
base, precision + 1
|
63
|
+
) # np.round(base, precision - (int(exponent) % 3))
|
64
|
+
base_precise = np.round(
|
65
|
+
base_precise, precision - len(str(base_precise).split(".")[0])
|
66
|
+
)
|
67
|
+
if int(base_precise) == float(base_precise):
|
68
|
+
base_precise = int(base_precise)
|
69
|
+
|
70
|
+
# Build string
|
71
|
+
if unit:
|
72
|
+
res = f"{base_precise}{'~' if latex else ' '}{_EXP_UNIT_MAP[exponent]}{unit}"
|
73
|
+
else:
|
74
|
+
res = f"{base_precise}" + (f" x 10^{{{exponent}}}" if exponent != 0 else "")
|
75
|
+
return f"${res}$" if latex else res
|
76
|
+
|
77
|
+
|
78
|
+
def get_name_and_unit(param_id: str) -> str:
|
79
|
+
"""Get the name and unit of measurement of a prameter, e.g. Frequency [GHz].
|
80
|
+
|
81
|
+
Parameters
|
82
|
+
----------
|
83
|
+
param : str
|
84
|
+
Parameter ID, as defined in the param_dict.json file.
|
85
|
+
|
86
|
+
Returns
|
87
|
+
-------
|
88
|
+
str
|
89
|
+
Name and [unit]
|
90
|
+
"""
|
91
|
+
meta = _PARAM_METADATA[param_id]
|
92
|
+
scale = meta["scale"] if "scale" in meta else 1
|
93
|
+
exponent = -(int(f"{scale:.0e}".split("e")[1]) // 3) * 3
|
94
|
+
return f"{meta['name']} [{_EXP_UNIT_MAP[exponent]}{meta['unit']}]"
|
95
|
+
|
96
|
+
|
97
|
+
def print_fit_params(param_names, params, std_errs=None, perc_errs=None):
|
98
|
+
matrix = [param_names, params]
|
99
|
+
|
100
|
+
headers = ["Param", "Fitted value"]
|
101
|
+
if std_errs is not None:
|
102
|
+
headers.append("STD error")
|
103
|
+
std_errs = [f"{n:.3e}" for n in std_errs]
|
104
|
+
matrix.append(std_errs)
|
105
|
+
if perc_errs is not None:
|
106
|
+
headers.append("% Error")
|
107
|
+
perc_errs = [f"{n:.2f}" for n in perc_errs]
|
108
|
+
matrix.append(perc_errs)
|
109
|
+
|
110
|
+
matrix = np.array(matrix)
|
111
|
+
data = [matrix[:, i] for i in range(len(params))]
|
112
|
+
|
113
|
+
table = tabulate(data, headers=headers, tablefmt="github")
|
114
|
+
print(table + "\n")
|
115
|
+
|
116
|
+
|
117
|
+
def print_fit_metrics(fit_quality, keys: list[str] | None = None):
|
118
|
+
if keys is None:
|
119
|
+
keys = fit_quality.keys() if fit_quality else []
|
120
|
+
|
121
|
+
# Print fit quality parameters
|
122
|
+
for key in keys:
|
123
|
+
value = fit_quality[key]
|
124
|
+
quality = ""
|
125
|
+
# Evaluate reduced Chi-squared
|
126
|
+
if key == "red_chi2":
|
127
|
+
key = "reduced χ²"
|
128
|
+
if value <= 0.5:
|
129
|
+
quality = "GREAT (or overfitting)"
|
130
|
+
elif (value > 0.9) and (value <= 1.1):
|
131
|
+
quality = "GREAT"
|
132
|
+
elif (value > 0.5) and (value <= 2):
|
133
|
+
quality = "GOOD"
|
134
|
+
elif (value > 2) and (value <= 5):
|
135
|
+
quality = "MEDIUM"
|
136
|
+
elif value > 5:
|
137
|
+
quality = "BAD"
|
138
|
+
# Evaluate R-squared
|
139
|
+
elif key == "r2":
|
140
|
+
# Skip if complex
|
141
|
+
if isinstance(value, complex):
|
142
|
+
continue
|
143
|
+
key = "R²"
|
144
|
+
if value < 0:
|
145
|
+
quality = "BAD - a horizontal line would be better"
|
146
|
+
elif value > 0.97:
|
147
|
+
quality = "GREAT"
|
148
|
+
elif value > 0.95:
|
149
|
+
quality = "GOOD"
|
150
|
+
elif value > 0.80:
|
151
|
+
quality = "MEDIUM"
|
152
|
+
else:
|
153
|
+
quality = "BAD"
|
154
|
+
# Normalized mean absolute error NMAE and
|
155
|
+
# normalized root mean square error NRMSE
|
156
|
+
elif (key == "nmae") or (key == "nrmse"):
|
157
|
+
if value < 0.1:
|
158
|
+
quality = "GREAT"
|
159
|
+
elif value < 0.2:
|
160
|
+
quality = "GOOD"
|
161
|
+
else:
|
162
|
+
quality = "BAD"
|
163
|
+
|
164
|
+
# Print result
|
165
|
+
print(f"{key}\t{value:.3e}\t{quality}")
|
166
|
+
|
167
|
+
|
168
|
+
def _sigma_for_confidence(confidence_level: float) -> float:
|
169
|
+
"""
|
170
|
+
Calculates the sigma multiplier (z-score) for a given confidence level.
|
171
|
+
|
172
|
+
Parameters
|
173
|
+
----------
|
174
|
+
confidence_level : float
|
175
|
+
The desired confidence level (e.g., 0.95 for 95%, 0.99 for 99%).
|
176
|
+
|
177
|
+
Returns
|
178
|
+
-------
|
179
|
+
float
|
180
|
+
The sigma multiplier to use for the confidence interval.
|
181
|
+
"""
|
182
|
+
if not (0 < confidence_level < 1):
|
183
|
+
raise ValueError("Confidence level must be between 0 and 1 (exclusive).")
|
184
|
+
|
185
|
+
alpha = 1 - confidence_level
|
186
|
+
sigma_multiplier = norm.ppf(1 - alpha / 2)
|
187
|
+
|
188
|
+
return sigma_multiplier
|