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/utils/_analysis.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import numpy as np
|
2
|
+
from scipy.signal import argrelextrema, find_peaks
|
2
3
|
|
3
4
|
|
4
5
|
def remove_offset(data: np.ndarray, avg: int = 3) -> np.ndarray:
|
@@ -211,6 +212,90 @@ def line_between_2_points(
|
|
211
212
|
return slope, intercept
|
212
213
|
|
213
214
|
|
215
|
+
def soft_normalize(data: np.ndarray) -> np.ndarray:
|
216
|
+
"""
|
217
|
+
Apply soft normalization to a 1D or 2D array with optional NaNs.
|
218
|
+
|
219
|
+
This function performs z-score normalization followed by a smooth
|
220
|
+
non-linear compression using a hyperbolic tangent (tanh) function.
|
221
|
+
It is designed to reduce the effect of outliers while preserving
|
222
|
+
the dynamic range of typical data values. The result is rescaled to [0, 1].
|
223
|
+
|
224
|
+
For 2D arrays, normalization is done row-wise, but compression is
|
225
|
+
based on a global threshold across all non-NaN entries.
|
226
|
+
|
227
|
+
Parameters
|
228
|
+
----------
|
229
|
+
data : np.ndarray
|
230
|
+
Input data, must be a 1D or 2D NumPy array. Can contain NaNs.
|
231
|
+
|
232
|
+
Returns
|
233
|
+
-------
|
234
|
+
np.ndarray
|
235
|
+
Normalized data, same shape as input, with values scaled to [0, 1].
|
236
|
+
NaNs are preserved.
|
237
|
+
|
238
|
+
Raises
|
239
|
+
------
|
240
|
+
ValueError
|
241
|
+
If `data` is not 1D or 2D.
|
242
|
+
|
243
|
+
Notes
|
244
|
+
-----
|
245
|
+
- Z-score normalization is done using nanmean and nanstd.
|
246
|
+
- Outliers are compressed using a tanh centered at a scaled threshold.
|
247
|
+
- Output values are guaranteed to be in [0, 1] range, except NaNs.
|
248
|
+
- Rows with zero standard deviation are flattened to 0.5.
|
249
|
+
"""
|
250
|
+
|
251
|
+
if data.ndim not in [1, 2]:
|
252
|
+
raise ValueError("Input must be 1D or 2D")
|
253
|
+
|
254
|
+
data = np.array(data, dtype=np.float64)
|
255
|
+
nan_mask = np.isnan(data)
|
256
|
+
|
257
|
+
if data.ndim == 1:
|
258
|
+
mean = np.nanmean(data)
|
259
|
+
std = np.nanstd(data)
|
260
|
+
std = 1.0 if std == 0 else std
|
261
|
+
abs_z = np.abs((data - mean) / std)
|
262
|
+
else:
|
263
|
+
mean = np.nanmean(data, axis=1, keepdims=True)
|
264
|
+
std = np.nanstd(data, axis=1, keepdims=True)
|
265
|
+
std = np.where(std == 0, 1.0, std)
|
266
|
+
abs_z = np.abs((data - mean) / std)
|
267
|
+
|
268
|
+
# Flatten over all values for global thresholding
|
269
|
+
flat_abs_z = abs_z[~nan_mask]
|
270
|
+
if flat_abs_z.size == 0:
|
271
|
+
return np.full_like(data, 0.5)
|
272
|
+
|
273
|
+
threshold = 4.0 * np.mean(flat_abs_z)
|
274
|
+
alpha = 1.0 / (4.0 * np.std(flat_abs_z)) if np.std(flat_abs_z) != 0 else 1.0
|
275
|
+
|
276
|
+
compressed = np.tanh(alpha * (abs_z - threshold))
|
277
|
+
|
278
|
+
# Rescale to [0, 1]
|
279
|
+
compressed[nan_mask] = np.nan
|
280
|
+
min_val = np.nanmin(compressed)
|
281
|
+
max_val = np.nanmax(compressed)
|
282
|
+
if max_val == min_val:
|
283
|
+
rescaled = np.full_like(compressed, 0.5)
|
284
|
+
else:
|
285
|
+
rescaled = (compressed - min_val) / (max_val - min_val)
|
286
|
+
|
287
|
+
rescaled[nan_mask] = np.nan
|
288
|
+
return rescaled
|
289
|
+
|
290
|
+
|
291
|
+
def find_closest_index(arr, target):
|
292
|
+
"""
|
293
|
+
Find the index of the element in `arr` closest to the `target` value.
|
294
|
+
"""
|
295
|
+
|
296
|
+
return np.abs(arr - target).argmin()
|
297
|
+
|
298
|
+
|
214
299
|
def compute_snr_peaked(
|
215
300
|
x_data: np.ndarray,
|
216
301
|
y_data: np.ndarray,
|
@@ -290,3 +375,144 @@ def compute_snr_peaked(
|
|
290
375
|
snr = signal / noise_std if noise_std > 0 else np.inf # Avoid division by zero
|
291
376
|
|
292
377
|
return snr
|
378
|
+
|
379
|
+
|
380
|
+
def find_first_minima_idx(data):
|
381
|
+
"""
|
382
|
+
Find the index of the first local minimum in a 1D array.
|
383
|
+
|
384
|
+
Parameters
|
385
|
+
----------
|
386
|
+
data : array-like
|
387
|
+
1D sequence of numerical values.
|
388
|
+
|
389
|
+
Returns
|
390
|
+
-------
|
391
|
+
int or None
|
392
|
+
Index of the first local minimum, or None if no local minimum is found.
|
393
|
+
|
394
|
+
Notes
|
395
|
+
-----
|
396
|
+
A local minimum is defined as a point that is smaller than its immediate neighbors.
|
397
|
+
Uses `scipy.signal.argrelextrema` to detect local minima.
|
398
|
+
|
399
|
+
Examples
|
400
|
+
--------
|
401
|
+
>>> data = [3, 2, 4, 1, 5]
|
402
|
+
>>> find_first_minima_idx(data)
|
403
|
+
1
|
404
|
+
"""
|
405
|
+
data = np.array(data)
|
406
|
+
minima_indices = argrelextrema(data, np.less)[0]
|
407
|
+
|
408
|
+
# Check boundaries for minima (optional)
|
409
|
+
if data.size < 2:
|
410
|
+
return None
|
411
|
+
|
412
|
+
if len(minima_indices) > 0:
|
413
|
+
return minima_indices[0]
|
414
|
+
|
415
|
+
return None
|
416
|
+
|
417
|
+
|
418
|
+
def compute_fft(x_data, y_data):
|
419
|
+
"""
|
420
|
+
Computes the Fast Fourier Transform (FFT) of a signal and returns the positive frequency spectrum.
|
421
|
+
|
422
|
+
Parameters
|
423
|
+
----------
|
424
|
+
x_data : np.ndarray
|
425
|
+
Time or independent variable array, assumed to be uniformly spaced.
|
426
|
+
y_data : np.ndarray
|
427
|
+
Signal data corresponding to `x_data`. Can be real or complex.
|
428
|
+
|
429
|
+
Returns
|
430
|
+
-------
|
431
|
+
positive_freqs : np.ndarray
|
432
|
+
Array of positive frequency components corresponding to the FFT.
|
433
|
+
fft_magnitude : np.ndarray
|
434
|
+
Magnitude of the FFT at the positive frequencies.
|
435
|
+
|
436
|
+
Notes
|
437
|
+
-----
|
438
|
+
- The signal is centered by subtracting its mean before computing the FFT, which removes the DC component.
|
439
|
+
- Only the positive frequency half of the FFT spectrum is returned, assuming a real-valued input signal.
|
440
|
+
- If `y_data` is complex, returned FFT values still reflect magnitude only.
|
441
|
+
- The input `x_data` must be uniformly spaced for the frequency axis to be accurate.
|
442
|
+
|
443
|
+
Examples
|
444
|
+
--------
|
445
|
+
>>> t = np.linspace(0, 1, 1000)
|
446
|
+
>>> y = np.sin(2 * np.pi * 50 * t)
|
447
|
+
>>> freqs, spectrum = compute_fft(t, y)
|
448
|
+
"""
|
449
|
+
|
450
|
+
# Subtract DC offset to focus on oscillations
|
451
|
+
y_data_centered = y_data - np.mean(y_data)
|
452
|
+
|
453
|
+
# Calculate time step (assumes uniform spacing)
|
454
|
+
dt = x_data[1] - x_data[0]
|
455
|
+
N = len(x_data)
|
456
|
+
|
457
|
+
# Compute FFT and frequency axis
|
458
|
+
fft_vals = np.fft.fft(y_data_centered)
|
459
|
+
freqs = np.fft.fftfreq(N, dt)
|
460
|
+
|
461
|
+
# Take only positive frequencies
|
462
|
+
positive_freqs = freqs[: N // 2]
|
463
|
+
fft_magnitude = np.abs(fft_vals[: N // 2])
|
464
|
+
|
465
|
+
return positive_freqs, fft_magnitude
|
466
|
+
|
467
|
+
|
468
|
+
def get_peaks(x_data, y_data, prominence: float | None = None, sort=True):
|
469
|
+
"""
|
470
|
+
Detects and returns peaks in a 1D signal based on prominence.
|
471
|
+
|
472
|
+
Parameters
|
473
|
+
----------
|
474
|
+
x_data : np.ndarray
|
475
|
+
1D array of x-values corresponding to `y_data` (e.g., frequency or time axis).
|
476
|
+
y_data : np.ndarray
|
477
|
+
1D array of y-values representing the signal in which to find peaks.
|
478
|
+
prominence : float or None, optional
|
479
|
+
Minimum prominence of peaks to detect. If None, defaults to 5% of the maximum
|
480
|
+
value in `y_data`.
|
481
|
+
sort : bool, optional
|
482
|
+
If True, peaks are sorted in descending order of magnitude. Default is True.
|
483
|
+
|
484
|
+
Returns
|
485
|
+
-------
|
486
|
+
peak_freqs : np.ndarray
|
487
|
+
x-values at which peaks occur.
|
488
|
+
peak_magnitudes : np.ndarray
|
489
|
+
y-values (magnitudes) at the detected peaks.
|
490
|
+
|
491
|
+
Notes
|
492
|
+
-----
|
493
|
+
- Uses `scipy.signal.find_peaks` for detection.
|
494
|
+
|
495
|
+
Examples
|
496
|
+
--------
|
497
|
+
>>> x = np.linspace(0, 10, 1000)
|
498
|
+
>>> y = np.sin(2 * np.pi * x) + 0.1 * np.random.randn(1000)
|
499
|
+
>>> freqs, mags = get_peaks(x, np.abs(y))
|
500
|
+
>>> print(freqs[:3], mags[:3]) # Show top 3 peak locations and magnitudes
|
501
|
+
"""
|
502
|
+
|
503
|
+
if prominence is None:
|
504
|
+
prominence = 0.05 * np.max(y_data)
|
505
|
+
|
506
|
+
# Find peaks
|
507
|
+
peaks, properties = find_peaks(y_data, prominence=prominence)
|
508
|
+
|
509
|
+
# Get the corresponding frequencies and magnitudes
|
510
|
+
peak_freqs = x_data[peaks]
|
511
|
+
peak_magnitudes = y_data[peaks]
|
512
|
+
|
513
|
+
if sort:
|
514
|
+
sorted_indices = np.argsort(peak_magnitudes)[::-1]
|
515
|
+
peak_freqs = peak_freqs[sorted_indices]
|
516
|
+
peak_magnitudes = peak_magnitudes[sorted_indices]
|
517
|
+
|
518
|
+
return peak_freqs, peak_magnitudes
|
sqil_core/utils/_const.py
CHANGED
@@ -4,7 +4,7 @@ _EXP_UNIT_MAP = {
|
|
4
4
|
-15: "p",
|
5
5
|
-12: "f",
|
6
6
|
-9: "n",
|
7
|
-
-6: r"\mu",
|
7
|
+
-6: r"\mu ",
|
8
8
|
-3: "m",
|
9
9
|
0: "",
|
10
10
|
3: "k",
|
@@ -14,36 +14,101 @@ _EXP_UNIT_MAP = {
|
|
14
14
|
15: "P",
|
15
15
|
}
|
16
16
|
|
17
|
-
|
18
|
-
"
|
19
|
-
"ro_freq": {
|
17
|
+
PARAM_METADATA = {
|
18
|
+
"readout_resonator_frequency": {
|
20
19
|
"name": "Readout frequency",
|
21
20
|
"symbol": "f_{RO}",
|
22
21
|
"unit": "Hz",
|
23
22
|
"scale": 1e-9,
|
23
|
+
"precision": 5,
|
24
24
|
},
|
25
|
-
"
|
26
|
-
"name": "Readout power",
|
27
|
-
"symbol": "
|
25
|
+
"readout_range_out": {
|
26
|
+
"name": "Readout power offset",
|
27
|
+
"symbol": "P_0^{RO}",
|
28
28
|
"unit": "dBm",
|
29
29
|
"scale": 1,
|
30
30
|
},
|
31
|
-
"
|
32
|
-
"name": "
|
33
|
-
"symbol": "
|
31
|
+
"readout_amplitude": {
|
32
|
+
"name": "Readout amplitude",
|
33
|
+
"symbol": "A_{RO}",
|
34
|
+
"unit": "",
|
35
|
+
"scale": 1,
|
36
|
+
},
|
37
|
+
"readout_length": {
|
38
|
+
"name": "Readout length",
|
39
|
+
"symbol": "T_{RO}",
|
40
|
+
"unit": "s",
|
41
|
+
"scale": 1e6,
|
42
|
+
},
|
43
|
+
"readout_lo_frequency": {
|
44
|
+
"name": "Internal readout LO frequency",
|
45
|
+
"symbol": "f_{LO-int}^{RO}",
|
46
|
+
"unit": "Hz",
|
47
|
+
"scale": 1e-9,
|
48
|
+
},
|
49
|
+
"readout_external_lo_frequency": {
|
50
|
+
"name": "External LO frequency",
|
51
|
+
"symbol": "f_{LO}^{Ext}",
|
52
|
+
"unit": "Hz",
|
53
|
+
"scale": 1e-9,
|
54
|
+
},
|
55
|
+
"readout_external_lo_power": {
|
56
|
+
"name": "External LO power",
|
57
|
+
"symbol": "P_{LO}^{Ext}",
|
58
|
+
"unit": "dBm",
|
59
|
+
"scale": 1,
|
60
|
+
},
|
61
|
+
"readout_kappa_tot": {"symbol": r"\kappa_{tot}", "unit": "Hz", "scale": "MHz"},
|
62
|
+
"resonance_frequency_ge": {
|
63
|
+
"name": "Readout frequency",
|
64
|
+
"symbol": "f_{ge}",
|
65
|
+
"unit": "Hz",
|
66
|
+
"scale": 1e-9,
|
67
|
+
"precision": 5,
|
68
|
+
},
|
69
|
+
"resonance_frequency_ef": {
|
70
|
+
"name": "Readout frequency",
|
71
|
+
"symbol": "f_{ef}",
|
34
72
|
"unit": "Hz",
|
35
73
|
"scale": 1e-9,
|
74
|
+
"precision": 5,
|
75
|
+
},
|
76
|
+
"spectroscopy_amplitude": {
|
77
|
+
"name": "Spectroscopy amplitude",
|
78
|
+
"symbol": "A_{sp}",
|
79
|
+
"unit": "",
|
80
|
+
"scale": 1,
|
81
|
+
},
|
82
|
+
"ge_drive_amplitude_pi": {
|
83
|
+
"name": "Drive amplitude pi ge",
|
84
|
+
"symbol": r"A_{\pi}^{ge}",
|
85
|
+
"unit": "",
|
86
|
+
"scale": 1,
|
87
|
+
},
|
88
|
+
"ge_drive_length": {
|
89
|
+
"name": "Drive length ge",
|
90
|
+
"symbol": r"T_{\pi}^{ge}",
|
91
|
+
"unit": "s",
|
92
|
+
"scale": 1e9,
|
93
|
+
},
|
94
|
+
"ge_T1": {"name": "T1", "symbol": "T_1", "unit": "s", "scale": 1e6},
|
95
|
+
"ge_T2": {"name": "T2", "symbol": "T_2", "unit": "s", "scale": 1e6},
|
96
|
+
"ge_T2_star": {"name": "T2*", "symbol": "T_2^*", "unit": "s", "scale": 1e6},
|
97
|
+
"reset_delay_length": {
|
98
|
+
"name": "Reset delay",
|
99
|
+
"symbol": "T_{reset}",
|
100
|
+
"unit": "s",
|
101
|
+
"scale": 1e6,
|
36
102
|
},
|
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
103
|
}
|
42
104
|
|
43
105
|
ONE_TONE_PARAMS = np.array(
|
44
|
-
[
|
106
|
+
[
|
107
|
+
"readout_amplitude",
|
108
|
+
"readout_length",
|
109
|
+
"readout_external_lo_frequency",
|
110
|
+
"readout_external_lo_power",
|
111
|
+
]
|
45
112
|
)
|
46
113
|
|
47
|
-
TWO_TONE_PARAMS = np.array(
|
48
|
-
["ro_freq", "ro_power", "current", "vna_bw", "vna_avg", "qu_power"]
|
49
|
-
)
|
114
|
+
TWO_TONE_PARAMS = np.array(["spectroscopy_amplitude"])
|
sqil_core/utils/_formatter.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
import json
|
1
2
|
from decimal import ROUND_DOWN, Decimal
|
2
3
|
|
4
|
+
import attrs
|
3
5
|
import numpy as np
|
4
6
|
from scipy.stats import norm
|
5
7
|
from tabulate import tabulate
|
6
8
|
|
7
|
-
from ._const import _EXP_UNIT_MAP,
|
9
|
+
from ._const import _EXP_UNIT_MAP, PARAM_METADATA
|
8
10
|
|
9
11
|
|
10
12
|
def _cut_to_significant_digits(number, n):
|
@@ -88,13 +90,13 @@ def get_name_and_unit(param_id: str) -> str:
|
|
88
90
|
str
|
89
91
|
Name and [unit]
|
90
92
|
"""
|
91
|
-
meta =
|
93
|
+
meta = PARAM_METADATA[param_id]
|
92
94
|
scale = meta["scale"] if "scale" in meta else 1
|
93
95
|
exponent = -(int(f"{scale:.0e}".split("e")[1]) // 3) * 3
|
94
96
|
return f"{meta['name']} [{_EXP_UNIT_MAP[exponent]}{meta['unit']}]"
|
95
97
|
|
96
98
|
|
97
|
-
def
|
99
|
+
def format_fit_params(param_names, params, std_errs=None, perc_errs=None):
|
98
100
|
matrix = [param_names, params]
|
99
101
|
|
100
102
|
headers = ["Param", "Fitted value"]
|
@@ -111,58 +113,7 @@ def print_fit_params(param_names, params, std_errs=None, perc_errs=None):
|
|
111
113
|
data = [matrix[:, i] for i in range(len(params))]
|
112
114
|
|
113
115
|
table = tabulate(data, headers=headers, tablefmt="github")
|
114
|
-
|
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}")
|
116
|
+
return table + "\n"
|
166
117
|
|
167
118
|
|
168
119
|
def _sigma_for_confidence(confidence_level: float) -> float:
|
@@ -186,3 +137,124 @@ def _sigma_for_confidence(confidence_level: float) -> float:
|
|
186
137
|
sigma_multiplier = norm.ppf(1 - alpha / 2)
|
187
138
|
|
188
139
|
return sigma_multiplier
|
140
|
+
|
141
|
+
|
142
|
+
class ParamInfo:
|
143
|
+
"""Parameter information for items of param_dict
|
144
|
+
|
145
|
+
Attributes:
|
146
|
+
id (str): QPU key
|
147
|
+
value (any): the value of the parameter
|
148
|
+
name (str): full name of the parameter (e.g. Readout frequency)
|
149
|
+
symbol (str): symbol of the parameter in Latex notation (e.g. f_{RO})
|
150
|
+
unit (str): base unit of measurement (e.g. Hz)
|
151
|
+
scale (int): the scale that should be generally applied to raw data (e.g. 1e-9 to take raw Hz to GHz)
|
152
|
+
"""
|
153
|
+
|
154
|
+
def __init__(self, id, value=None, metadata=None):
|
155
|
+
self.id = id
|
156
|
+
self.value = value
|
157
|
+
|
158
|
+
if metadata is not None:
|
159
|
+
meta = metadata
|
160
|
+
elif id in PARAM_METADATA:
|
161
|
+
meta = PARAM_METADATA[id]
|
162
|
+
else:
|
163
|
+
meta = {}
|
164
|
+
|
165
|
+
self.name = meta.get("name", None)
|
166
|
+
self.symbol = meta.get("symbol", id)
|
167
|
+
self.unit = meta.get("unit", "")
|
168
|
+
self.scale = meta.get("scale", 1)
|
169
|
+
self.precision = meta.get("precision", 3)
|
170
|
+
|
171
|
+
if self.name is None:
|
172
|
+
self.name = self.id[0].upper() + self.id[1:].replace("_", " ")
|
173
|
+
|
174
|
+
def to_dict(self):
|
175
|
+
"""Convert ParamInfo to a dictionary."""
|
176
|
+
return {
|
177
|
+
"id": self.id,
|
178
|
+
"value": self.value,
|
179
|
+
"name": self.name,
|
180
|
+
"symbol": self.symbol,
|
181
|
+
"unit": self.unit,
|
182
|
+
"scale": self.scale,
|
183
|
+
"precision": self.precision,
|
184
|
+
}
|
185
|
+
|
186
|
+
@property
|
187
|
+
def name_and_unit(self, latex=True):
|
188
|
+
unit = f"[{self.rescaled_unit}]" if self.unit or self.scale != 1 else ""
|
189
|
+
if unit == "":
|
190
|
+
return unit
|
191
|
+
return self.name + rf" ${unit}$" if latex else rf" {unit}"
|
192
|
+
|
193
|
+
@property
|
194
|
+
def rescaled_unit(self):
|
195
|
+
# if self.unit == "":
|
196
|
+
# return self.unit
|
197
|
+
exponent = -(int(f"{self.scale:.0e}".split("e")[1]) // 3) * 3
|
198
|
+
unit = f"{_EXP_UNIT_MAP[exponent]}{self.unit}"
|
199
|
+
return unit
|
200
|
+
|
201
|
+
@property
|
202
|
+
def symbol_and_value(self, latex=True):
|
203
|
+
sym = f"${self.symbol}$" if latex else self.symbol
|
204
|
+
equal = f"$=$" if latex else " = "
|
205
|
+
val = format_number(self.value, self.precision, self.unit, latex=latex)
|
206
|
+
return f"{sym}{equal}{val}"
|
207
|
+
|
208
|
+
def __str__(self):
|
209
|
+
"""Return a JSON-formatted string of the object."""
|
210
|
+
return json.dumps(self.to_dict())
|
211
|
+
|
212
|
+
def __eq__(self, other):
|
213
|
+
if isinstance(other, ParamInfo):
|
214
|
+
return (self.id == other.id) & (self.value == other.value)
|
215
|
+
if isinstance(other, (int, float, complex, str)):
|
216
|
+
return self.value == other
|
217
|
+
return False
|
218
|
+
|
219
|
+
def __bool__(self):
|
220
|
+
return bool(self.id)
|
221
|
+
|
222
|
+
|
223
|
+
ParamDict = dict[str, ParamInfo]
|
224
|
+
|
225
|
+
|
226
|
+
def param_info_from_schema(key, metadata) -> ParamInfo:
|
227
|
+
metadata_id = metadata.get("param_id")
|
228
|
+
if metadata_id is not None:
|
229
|
+
return ParamInfo(metadata_id)
|
230
|
+
return ParamInfo(key, metadata=metadata)
|
231
|
+
|
232
|
+
|
233
|
+
def enrich_qubit_params(qubit) -> ParamDict:
|
234
|
+
qubit_params = attrs.asdict(qubit.parameters)
|
235
|
+
res = {}
|
236
|
+
for key, value in qubit_params.items():
|
237
|
+
res[key] = ParamInfo(key, value)
|
238
|
+
return res
|
239
|
+
|
240
|
+
|
241
|
+
def get_relevant_exp_parameters(
|
242
|
+
qubit_params: ParamDict, exp_param_ids: list, sweep_ids: list, only_keys=True
|
243
|
+
):
|
244
|
+
# Filter out sweeps
|
245
|
+
filtered = [id for id in exp_param_ids if id not in sweep_ids]
|
246
|
+
|
247
|
+
# Filter special cases
|
248
|
+
# No external LO frequency => external Lo info is irrelevant
|
249
|
+
if (["readout_external_lo_frequency"] in exp_param_ids) and (
|
250
|
+
not qubit_params.get("readout_external_lo_frequency").value
|
251
|
+
):
|
252
|
+
parms_to_exclude = [
|
253
|
+
"readout_external_lo_frequency",
|
254
|
+
"readout_external_lo_power",
|
255
|
+
]
|
256
|
+
filtered = [id for id in filtered if id not in parms_to_exclude]
|
257
|
+
|
258
|
+
result = {key: value for key, value in qubit_params.items() if key in filtered}
|
259
|
+
|
260
|
+
return list(result.keys()) if only_keys else result
|