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.
@@ -1,6 +1,63 @@
1
- from .analysis import *
2
- from .formatter import *
3
- from .read import *
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
- __all__.extend(name for name in dir() if not name.startswith("_"))
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
- EXP_UNIT_MAP = {
2
- -15: "p",
3
- -12: "f",
4
- -9: "n",
5
- -6: "\mu",
6
- -3: "m",
7
- 0: "",
8
- 3: "k",
9
- 6: "M",
10
- 9: "G",
11
- 12: "T",
12
- 15: "P",
13
- }
14
-
15
- PARAM_METADATA = {
16
- "current": {"name": "Current", "symbol": "I", "unit": "A", "scale": 1e3},
17
- "ro_freq": {
18
- "name": "Readout frequency",
19
- "symbol": "f_{RO}",
20
- "unit": "Hz",
21
- "scale": 1e-9,
22
- },
23
- "ro_power": {
24
- "name": "Readout power",
25
- "symbol": "P_{RO}",
26
- "unit": "dBm",
27
- "scale": 1,
28
- },
29
- "qu_freq": {
30
- "name": "Qubit frequency",
31
- "symbol": "f_q",
32
- "unit": "Hz",
33
- "scale": 1e-9,
34
- },
35
- "qu_power": {"name": "Qubit power", "symbol": "P_q", "unit": "dBm", "scale": 1},
36
- "vna_bw": {"name": "VNA bandwidth", "symbol": "BW_{VNA}", "unit": "Hz", "scale": 1},
37
- "vna_avg": {"name": "VNA averages", "symbol": "avg_{VNA}", "unit": "", "scale": 1},
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