sqil-core 0.0.2__py3-none-any.whl → 1.0.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 +6 -2
- sqil_core/config.py +13 -0
- sqil_core/config_log.py +42 -0
- sqil_core/experiment/__init__.py +11 -0
- sqil_core/experiment/_analysis.py +95 -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 +38 -0
- sqil_core/fit/_core.py +1084 -0
- sqil_core/fit/_fit.py +1191 -0
- sqil_core/fit/_guess.py +232 -0
- sqil_core/fit/_models.py +127 -0
- sqil_core/fit/_quality.py +266 -0
- sqil_core/resonator/__init__.py +13 -0
- sqil_core/resonator/_resonator.py +989 -0
- sqil_core/utils/__init__.py +85 -5
- sqil_core/utils/_analysis.py +415 -0
- sqil_core/utils/_const.py +105 -0
- sqil_core/utils/_formatter.py +259 -0
- sqil_core/utils/_plot.py +373 -0
- sqil_core/utils/_read.py +262 -0
- sqil_core/utils/_utils.py +164 -0
- {sqil_core-0.0.2.dist-info → sqil_core-1.0.0.dist-info}/METADATA +40 -7
- sqil_core-1.0.0.dist-info/RECORD +36 -0
- {sqil_core-0.0.2.dist-info → sqil_core-1.0.0.dist-info}/WHEEL +1 -1
- {sqil_core-0.0.2.dist-info → sqil_core-1.0.0.dist-info}/entry_points.txt +1 -1
- sqil_core/utils/analysis.py +0 -68
- sqil_core/utils/const.py +0 -38
- sqil_core/utils/formatter.py +0 -134
- sqil_core/utils/read.py +0 -156
- sqil_core-0.0.2.dist-info/RECORD +0 -10
@@ -0,0 +1,259 @@
|
|
1
|
+
import json
|
2
|
+
from decimal import ROUND_DOWN, Decimal
|
3
|
+
|
4
|
+
import attrs
|
5
|
+
import numpy as np
|
6
|
+
from scipy.stats import norm
|
7
|
+
from tabulate import tabulate
|
8
|
+
|
9
|
+
from ._const import _EXP_UNIT_MAP, PARAM_METADATA
|
10
|
+
|
11
|
+
|
12
|
+
def _cut_to_significant_digits(number, n):
|
13
|
+
"""Cut a number to n significant digits."""
|
14
|
+
if number == 0:
|
15
|
+
return 0 # Zero has no significant digits
|
16
|
+
d = Decimal(str(number))
|
17
|
+
shift = d.adjusted() # Get the exponent of the number
|
18
|
+
rounded = d.scaleb(-shift).quantize(
|
19
|
+
Decimal("1e-{0}".format(n - 1)), rounding=ROUND_DOWN
|
20
|
+
)
|
21
|
+
return float(rounded.scaleb(shift))
|
22
|
+
|
23
|
+
|
24
|
+
def format_number(
|
25
|
+
num: float | np.ndarray, precision: int = 3, unit: str = "", latex: bool = True
|
26
|
+
) -> str:
|
27
|
+
"""Format a number (or an array of numbers) in a nice way for printing.
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
num : float | np.ndarray
|
32
|
+
Input number (or array). Should not be rescaled,
|
33
|
+
e.g. input values in Hz, NOT GHz
|
34
|
+
precision : int
|
35
|
+
The number of digits of the output number. Must be >= 3.
|
36
|
+
unit : str, optional
|
37
|
+
Unit of measurement, by default ''
|
38
|
+
latex : bool, optional
|
39
|
+
Include Latex syntax, by default True
|
40
|
+
|
41
|
+
Returns
|
42
|
+
-------
|
43
|
+
str
|
44
|
+
Formatted number
|
45
|
+
"""
|
46
|
+
# Handle arrays
|
47
|
+
if isinstance(num, (list, np.ndarray)):
|
48
|
+
return [format_number(n, precision, unit, latex) for n in num]
|
49
|
+
|
50
|
+
# Return if not a number
|
51
|
+
if not isinstance(num, (int, float, complex)):
|
52
|
+
return num
|
53
|
+
|
54
|
+
# Format number
|
55
|
+
exp_form = f"{num:.12e}"
|
56
|
+
base, exponent = exp_form.split("e")
|
57
|
+
# Make exponent a multiple of 3
|
58
|
+
base = float(base) * 10 ** (int(exponent) % 3)
|
59
|
+
exponent = (int(exponent) // 3) * 3
|
60
|
+
# Apply precision to the base
|
61
|
+
if precision < 3:
|
62
|
+
precision = 3
|
63
|
+
base_precise = _cut_to_significant_digits(
|
64
|
+
base, precision + 1
|
65
|
+
) # np.round(base, precision - (int(exponent) % 3))
|
66
|
+
base_precise = np.round(
|
67
|
+
base_precise, precision - len(str(base_precise).split(".")[0])
|
68
|
+
)
|
69
|
+
if int(base_precise) == float(base_precise):
|
70
|
+
base_precise = int(base_precise)
|
71
|
+
|
72
|
+
# Build string
|
73
|
+
if unit:
|
74
|
+
res = f"{base_precise}{'~' if latex else ' '}{_EXP_UNIT_MAP[exponent]}{unit}"
|
75
|
+
else:
|
76
|
+
res = f"{base_precise}" + (f" x 10^{{{exponent}}}" if exponent != 0 else "")
|
77
|
+
return f"${res}$" if latex else res
|
78
|
+
|
79
|
+
|
80
|
+
def get_name_and_unit(param_id: str) -> str:
|
81
|
+
"""Get the name and unit of measurement of a prameter, e.g. Frequency [GHz].
|
82
|
+
|
83
|
+
Parameters
|
84
|
+
----------
|
85
|
+
param : str
|
86
|
+
Parameter ID, as defined in the param_dict.json file.
|
87
|
+
|
88
|
+
Returns
|
89
|
+
-------
|
90
|
+
str
|
91
|
+
Name and [unit]
|
92
|
+
"""
|
93
|
+
meta = PARAM_METADATA[param_id]
|
94
|
+
scale = meta["scale"] if "scale" in meta else 1
|
95
|
+
exponent = -(int(f"{scale:.0e}".split("e")[1]) // 3) * 3
|
96
|
+
return f"{meta['name']} [{_EXP_UNIT_MAP[exponent]}{meta['unit']}]"
|
97
|
+
|
98
|
+
|
99
|
+
def format_fit_params(param_names, params, std_errs=None, perc_errs=None):
|
100
|
+
matrix = [param_names, params]
|
101
|
+
|
102
|
+
headers = ["Param", "Fitted value"]
|
103
|
+
if std_errs is not None:
|
104
|
+
headers.append("STD error")
|
105
|
+
std_errs = [f"{n:.3e}" for n in std_errs]
|
106
|
+
matrix.append(std_errs)
|
107
|
+
if perc_errs is not None:
|
108
|
+
headers.append("% Error")
|
109
|
+
perc_errs = [f"{n:.2f}" for n in perc_errs]
|
110
|
+
matrix.append(perc_errs)
|
111
|
+
|
112
|
+
matrix = np.array(matrix)
|
113
|
+
data = [matrix[:, i] for i in range(len(params))]
|
114
|
+
|
115
|
+
table = tabulate(data, headers=headers, tablefmt="github")
|
116
|
+
return table + "\n"
|
117
|
+
|
118
|
+
|
119
|
+
def _sigma_for_confidence(confidence_level: float) -> float:
|
120
|
+
"""
|
121
|
+
Calculates the sigma multiplier (z-score) for a given confidence level.
|
122
|
+
|
123
|
+
Parameters
|
124
|
+
----------
|
125
|
+
confidence_level : float
|
126
|
+
The desired confidence level (e.g., 0.95 for 95%, 0.99 for 99%).
|
127
|
+
|
128
|
+
Returns
|
129
|
+
-------
|
130
|
+
float
|
131
|
+
The sigma multiplier to use for the confidence interval.
|
132
|
+
"""
|
133
|
+
if not (0 < confidence_level < 1):
|
134
|
+
raise ValueError("Confidence level must be between 0 and 1 (exclusive).")
|
135
|
+
|
136
|
+
alpha = 1 - confidence_level
|
137
|
+
sigma_multiplier = norm.ppf(1 - alpha / 2)
|
138
|
+
|
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):
|
188
|
+
return self.name + (
|
189
|
+
f" [{self.rescaled_unit}]" if self.unit or self.scale != 1 else ""
|
190
|
+
)
|
191
|
+
|
192
|
+
@property
|
193
|
+
def rescaled_unit(self):
|
194
|
+
# if self.unit == "":
|
195
|
+
# return self.unit
|
196
|
+
exponent = -(int(f"{self.scale:.0e}".split("e")[1]) // 3) * 3
|
197
|
+
unit = f"{_EXP_UNIT_MAP[exponent]}{self.unit}"
|
198
|
+
return unit
|
199
|
+
|
200
|
+
@property
|
201
|
+
def symbol_and_value(self, latex=True):
|
202
|
+
sym = f"${self.symbol}$" if latex else self.symbol
|
203
|
+
equal = f"$=$" if latex else " = "
|
204
|
+
val = format_number(self.value, self.precision, self.unit, latex=latex)
|
205
|
+
return f"{sym}{equal}{val}"
|
206
|
+
|
207
|
+
def __str__(self):
|
208
|
+
"""Return a JSON-formatted string of the object."""
|
209
|
+
return json.dumps(self.to_dict())
|
210
|
+
|
211
|
+
def __eq__(self, other):
|
212
|
+
if isinstance(other, ParamInfo):
|
213
|
+
return (self.id == other.id) & (self.value == other.value)
|
214
|
+
if isinstance(other, (int, float, complex, str)):
|
215
|
+
return self.value == other
|
216
|
+
return False
|
217
|
+
|
218
|
+
def __bool__(self):
|
219
|
+
return bool(self.id)
|
220
|
+
|
221
|
+
|
222
|
+
ParamDict = dict[str, ParamInfo]
|
223
|
+
|
224
|
+
|
225
|
+
def param_info_from_schema(key, metadata) -> ParamInfo:
|
226
|
+
metadata_id = metadata.get("param_id")
|
227
|
+
if metadata_id is not None:
|
228
|
+
return ParamInfo(metadata_id)
|
229
|
+
return ParamInfo(key, metadata=metadata)
|
230
|
+
|
231
|
+
|
232
|
+
def enrich_qubit_params(qubit) -> ParamDict:
|
233
|
+
qubit_params = attrs.asdict(qubit.parameters)
|
234
|
+
res = {}
|
235
|
+
for key, value in qubit_params.items():
|
236
|
+
res[key] = ParamInfo(key, value)
|
237
|
+
return res
|
238
|
+
|
239
|
+
|
240
|
+
def get_relevant_exp_parameters(
|
241
|
+
qubit_params: ParamDict, exp_param_ids: list, sweep_ids: list, only_keys=True
|
242
|
+
):
|
243
|
+
# Filter out sweeps
|
244
|
+
filtered = [id for id in exp_param_ids if id not in sweep_ids]
|
245
|
+
|
246
|
+
# Filter special cases
|
247
|
+
# No external LO frequency => external Lo info is irrelevant
|
248
|
+
if (["readout_external_lo_frequency"] in exp_param_ids) and (
|
249
|
+
not qubit_params.get("readout_external_lo_frequency").value
|
250
|
+
):
|
251
|
+
parms_to_exclude = [
|
252
|
+
"readout_external_lo_frequency",
|
253
|
+
"readout_external_lo_power",
|
254
|
+
]
|
255
|
+
filtered = [id for id in filtered if id not in parms_to_exclude]
|
256
|
+
|
257
|
+
result = {key: value for key, value in qubit_params.items() if key in filtered}
|
258
|
+
|
259
|
+
return list(result.keys()) if only_keys else result
|
sqil_core/utils/_plot.py
ADDED
@@ -0,0 +1,373 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
import matplotlib.pyplot as plt
|
6
|
+
import numpy as np
|
7
|
+
from matplotlib.gridspec import GridSpec
|
8
|
+
|
9
|
+
from sqil_core.fit import transform_data
|
10
|
+
|
11
|
+
from ._analysis import remove_linear_background, remove_offset, soft_normalize
|
12
|
+
from ._const import PARAM_METADATA
|
13
|
+
from ._formatter import (
|
14
|
+
ParamInfo,
|
15
|
+
format_number,
|
16
|
+
get_relevant_exp_parameters,
|
17
|
+
param_info_from_schema,
|
18
|
+
)
|
19
|
+
from ._read import extract_h5_data, get_data_and_info, map_data_dict, read_json
|
20
|
+
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from sqil_core.fit._core import FitResult
|
23
|
+
from sqil_core.utils import ParamDict
|
24
|
+
|
25
|
+
|
26
|
+
def set_plot_style(plt):
|
27
|
+
"""Sets the matplotlib plotting style to a SQIL curated one."""
|
28
|
+
style = {
|
29
|
+
"font.size": 20,
|
30
|
+
"xtick.labelsize": 18, # X-axis tick labels
|
31
|
+
"ytick.labelsize": 18, # Y-axis tick labels
|
32
|
+
"lines.linewidth": 2.5, # Line width
|
33
|
+
# "lines.marker": "o",
|
34
|
+
"lines.markersize": 7, # Marker size
|
35
|
+
"lines.markeredgewidth": 1.5, # Marker line width
|
36
|
+
"lines.markerfacecolor": "none",
|
37
|
+
"axes.grid": True,
|
38
|
+
"grid.linestyle": "--",
|
39
|
+
"xtick.major.size": 8,
|
40
|
+
"xtick.major.width": 1.5,
|
41
|
+
"ytick.major.size": 8,
|
42
|
+
"ytick.major.width": 1.5,
|
43
|
+
"figure.figsize": (20, 7),
|
44
|
+
}
|
45
|
+
reset_plot_style(plt)
|
46
|
+
return plt.rcParams.update(style)
|
47
|
+
|
48
|
+
|
49
|
+
def reset_plot_style(plt):
|
50
|
+
"""Resets the matplotlib plotting style to its default value."""
|
51
|
+
return plt.rcParams.update(plt.rcParamsDefault)
|
52
|
+
|
53
|
+
|
54
|
+
def get_x_id_by_plot_dim(exp_id: str, plot_dim: str, sweep_param_id: str | None) -> str:
|
55
|
+
"""Returns the param_id of the parameter that should be used as the x-axis."""
|
56
|
+
if exp_id == "CW_onetone" or exp_id == "pulsed_onetone":
|
57
|
+
if plot_dim == "1":
|
58
|
+
return sweep_param_id or "ro_freq"
|
59
|
+
return "ro_freq"
|
60
|
+
elif exp_id == "CW_twotone" or exp_id == "pulsed_twotone":
|
61
|
+
if plot_dim == "1":
|
62
|
+
return sweep_param_id or "qu_freq"
|
63
|
+
return "qu_freq"
|
64
|
+
|
65
|
+
|
66
|
+
def build_title(title: str, path: str, params: list[str]) -> str:
|
67
|
+
"""Build a plot title that includes the values of given parameters found in
|
68
|
+
the params_dict.json file, e.g. One tone with I = 0.5 mA.
|
69
|
+
|
70
|
+
Parameters
|
71
|
+
----------
|
72
|
+
title : str
|
73
|
+
Title of the plot to which the parameters will be appended.
|
74
|
+
|
75
|
+
path: str
|
76
|
+
Path to the param_dict.json file.
|
77
|
+
|
78
|
+
params : List[str]
|
79
|
+
List of keys of parameters in the param_dict.json file.
|
80
|
+
|
81
|
+
Returns
|
82
|
+
-------
|
83
|
+
str
|
84
|
+
The original title followed by parameter values.
|
85
|
+
"""
|
86
|
+
dic = read_json(f"{path}/param_dict.json")
|
87
|
+
title += " with "
|
88
|
+
for idx, param in enumerate(params):
|
89
|
+
if not (param in PARAM_METADATA.keys()) or not (param in dic):
|
90
|
+
title += f"{param} = ? & "
|
91
|
+
continue
|
92
|
+
meta = PARAM_METADATA[param]
|
93
|
+
value = format_number(dic[param], 3, meta["unit"])
|
94
|
+
title += f"${meta['symbol']} =${value} & "
|
95
|
+
if idx % 2 == 0 and idx != 0:
|
96
|
+
title += "\n"
|
97
|
+
return title[0:-3]
|
98
|
+
|
99
|
+
|
100
|
+
def guess_plot_dimension(
|
101
|
+
f: np.ndarray, sweep: np.ndarray | list = [], threshold_2D=10
|
102
|
+
) -> tuple[list["1", "1.5", "2"] | np.ndarray]:
|
103
|
+
"""Guess if the plot should be a 1D line, a collection of 1D lines (1.5D),
|
104
|
+
or a 2D color plot.
|
105
|
+
|
106
|
+
Parameters
|
107
|
+
----------
|
108
|
+
f : np.ndarray
|
109
|
+
Main variable, usually frequency
|
110
|
+
sweep : Union[np.ndarray, List], optional
|
111
|
+
Sweep variable, by default []
|
112
|
+
threshold_2D : int, optional
|
113
|
+
Threshold of sweeping parameters after which the data is considered, by default 10
|
114
|
+
|
115
|
+
Returns
|
116
|
+
-------
|
117
|
+
Tuple[Union['1', '1.5', '2'], np.ndarray]
|
118
|
+
The plot dimension ('1', '1.5' or '2') and the vector that should be used as the x
|
119
|
+
axis in the plot.
|
120
|
+
"""
|
121
|
+
if len(sweep) > threshold_2D:
|
122
|
+
return "2"
|
123
|
+
elif len(f.shape) == 2 and len(sweep.shape) == 1:
|
124
|
+
return "1.5"
|
125
|
+
else:
|
126
|
+
return "1"
|
127
|
+
|
128
|
+
|
129
|
+
def finalize_plot(
|
130
|
+
fig,
|
131
|
+
title,
|
132
|
+
fit_res: FitResult = None,
|
133
|
+
qubit_params: ParamDict = {},
|
134
|
+
updated_params: dict = {},
|
135
|
+
sweep_info={},
|
136
|
+
relevant_params=[],
|
137
|
+
):
|
138
|
+
"""
|
139
|
+
Annotates a matplotlib figure with experiment parameters, fit quality, and title.
|
140
|
+
|
141
|
+
Parameters
|
142
|
+
----------
|
143
|
+
fig : matplotlib.figure.Figure
|
144
|
+
The figure object to annotate.
|
145
|
+
title : str
|
146
|
+
Title text to use for the plot.
|
147
|
+
fit_res : FitResult, optional
|
148
|
+
Fit result object containing model name and quality summary.
|
149
|
+
qubit_params : ParamDict, optional
|
150
|
+
Dictionary of experimental qubit parameters, indexed by parameter ID.
|
151
|
+
updated_params : dict, optional
|
152
|
+
Dictionary of updated parameters (e.g., from fitting), where keys are param IDs
|
153
|
+
and values are numeric or symbolic parameter values.
|
154
|
+
sweep_info : dict, optional
|
155
|
+
Information about sweep parameters (e.g., their IDs and labels).
|
156
|
+
relevant_params : list, optional
|
157
|
+
List of parameter IDs considered relevant for display under "Experiment".
|
158
|
+
"""
|
159
|
+
# Make a summary of relevant experimental parameters
|
160
|
+
exp_params_keys = get_relevant_exp_parameters(
|
161
|
+
qubit_params, relevant_params, [info.id for info in sweep_info]
|
162
|
+
)
|
163
|
+
params_str = ", ".join(
|
164
|
+
[qubit_params[id].symbol_and_value for id in exp_params_keys]
|
165
|
+
)
|
166
|
+
# Make a summary of the updated qubit parameters
|
167
|
+
updated_params_info = {k: ParamInfo(k, v) for k, v in updated_params.items()}
|
168
|
+
update_params_str = ", ".join(
|
169
|
+
[updated_params_info[id].symbol_and_value for id in updated_params_info.keys()]
|
170
|
+
)
|
171
|
+
|
172
|
+
# Find appropriate y_position to print text
|
173
|
+
bbox = fig.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
174
|
+
fig_height_inches = bbox.height
|
175
|
+
if fig_height_inches < 8:
|
176
|
+
y_pos = -0.05
|
177
|
+
elif fig_height_inches < 10:
|
178
|
+
y_pos = -0.03
|
179
|
+
elif fig_height_inches < 13:
|
180
|
+
y_pos = -0.02
|
181
|
+
else:
|
182
|
+
y_pos = -0.01
|
183
|
+
|
184
|
+
# Add text to the plot
|
185
|
+
fig.suptitle(f"{title}\n" + update_params_str)
|
186
|
+
if fit_res:
|
187
|
+
fig.text(0.02, y_pos, f"Model: {fit_res.model_name} - {fit_res.quality()}")
|
188
|
+
if params_str:
|
189
|
+
fig.text(0.4, y_pos, "Experiment: " + params_str, ha="left")
|
190
|
+
|
191
|
+
|
192
|
+
def plot_mag_phase(path=None, datadict=None, raw=False):
|
193
|
+
"""
|
194
|
+
Plot the magnitude and phase of complex measurement data from an db path or in-memory dictionary.
|
195
|
+
|
196
|
+
This function generates either a 1D or 2D plot of the magnitude and phase of complex data,
|
197
|
+
depending on the presence of sweep parameters. It supports normalization and background
|
198
|
+
subtraction.
|
199
|
+
|
200
|
+
Parameters
|
201
|
+
----------
|
202
|
+
path : str or None, optional
|
203
|
+
Path to the folder containing measurement data. Required if `datadict` is not provided.
|
204
|
+
datadict : dict or None, optional
|
205
|
+
Pre-loaded data dictionary with schema, typically extracted using `extract_h5_data`.
|
206
|
+
Required if `path` is not provided.
|
207
|
+
raw : bool, default False
|
208
|
+
If True, skip normalization and background subtraction for 2D plots. Useful for viewing raw data.
|
209
|
+
|
210
|
+
Returns
|
211
|
+
-------
|
212
|
+
fig : matplotlib.figure.Figure
|
213
|
+
The matplotlib Figure object containing the plot.
|
214
|
+
axs : matplotlib.axes.Axes or ndarray of Axes
|
215
|
+
The Axes object(s) used for the subplot(s).
|
216
|
+
|
217
|
+
Raises
|
218
|
+
------
|
219
|
+
Exception
|
220
|
+
If neither `path` nor `datadict` is provided.
|
221
|
+
|
222
|
+
Notes
|
223
|
+
-----
|
224
|
+
- Axes and units are automatically inferred from the schema in the dataset.
|
225
|
+
"""
|
226
|
+
|
227
|
+
all_data, all_info, _ = get_data_and_info(path=path, datadict=datadict)
|
228
|
+
x_data, y_data, sweeps = all_data
|
229
|
+
x_info, y_info, sweep_info = all_info
|
230
|
+
|
231
|
+
# Rescale data
|
232
|
+
x_data_scaled = x_data * x_info.scale
|
233
|
+
y_data_scaled = y_data * y_info.scale
|
234
|
+
y_unit = f" [{y_info.rescaled_unit}]" if y_info.unit else ""
|
235
|
+
|
236
|
+
set_plot_style(plt)
|
237
|
+
|
238
|
+
if len(sweeps) == 0: # 1D plot
|
239
|
+
fig, axs = plt.subplots(2, 1, figsize=(20, 12), sharex=True)
|
240
|
+
|
241
|
+
axs[0].plot(x_data_scaled, np.abs(y_data_scaled), "o")
|
242
|
+
axs[0].set_ylabel("Magnitude" + y_unit)
|
243
|
+
axs[0].tick_params(labelbottom=True)
|
244
|
+
axs[0].xaxis.set_tick_params(
|
245
|
+
which="both", labelbottom=True
|
246
|
+
) # Redundant for safety
|
247
|
+
|
248
|
+
axs[1].plot(x_data_scaled, np.unwrap(np.angle(y_data_scaled)), "o")
|
249
|
+
axs[1].set_xlabel(x_info.name_and_unit)
|
250
|
+
axs[1].set_ylabel("Phase [rad]")
|
251
|
+
else: # 2D plot
|
252
|
+
fig, axs = plt.subplots(1, 2, figsize=(24, 12), sharex=True, sharey=True)
|
253
|
+
|
254
|
+
# Process mag and phase
|
255
|
+
mag, phase = np.abs(y_data), np.unwrap(np.angle(y_data))
|
256
|
+
if not raw:
|
257
|
+
mag = soft_normalize(remove_offset(mag))
|
258
|
+
flat_phase = remove_linear_background(x_data, phase, points_cut=1)
|
259
|
+
phase = soft_normalize(flat_phase)
|
260
|
+
# Load sweep parameter
|
261
|
+
sweep0_info = sweep_info[0]
|
262
|
+
sweep0_scaled = sweeps[0] * sweep0_info.scale
|
263
|
+
|
264
|
+
c0 = axs[0].pcolormesh(
|
265
|
+
x_data_scaled,
|
266
|
+
sweep0_scaled,
|
267
|
+
mag,
|
268
|
+
shading="auto",
|
269
|
+
cmap="PuBu",
|
270
|
+
)
|
271
|
+
if raw:
|
272
|
+
fig.colorbar(c0, ax=axs[0])
|
273
|
+
axs[0].set_title("Magnitude" + y_unit)
|
274
|
+
else:
|
275
|
+
axs[0].set_title("Magnitude (normalized)")
|
276
|
+
axs[0].set_xlabel(x_info.name_and_unit)
|
277
|
+
axs[0].set_ylabel(sweep0_info.name_and_unit)
|
278
|
+
|
279
|
+
c1 = axs[1].pcolormesh(
|
280
|
+
x_data_scaled,
|
281
|
+
sweep0_scaled,
|
282
|
+
phase,
|
283
|
+
shading="auto",
|
284
|
+
cmap="PuBu",
|
285
|
+
)
|
286
|
+
if raw:
|
287
|
+
fig.colorbar(c1, ax=axs[1])
|
288
|
+
axs[1].set_title("Phase [rad]")
|
289
|
+
else:
|
290
|
+
axs[1].set_title("Phase (normalized)")
|
291
|
+
axs[1].set_xlabel(x_info.name_and_unit)
|
292
|
+
axs[1].tick_params(labelleft=True)
|
293
|
+
axs[1].xaxis.set_tick_params(
|
294
|
+
which="both", labelleft=True
|
295
|
+
) # Redundant for safety
|
296
|
+
|
297
|
+
fig.tight_layout()
|
298
|
+
return fig, axs
|
299
|
+
|
300
|
+
|
301
|
+
def plot_projection_IQ(path=None, datadict=None, proj_data=None, full_output=False):
|
302
|
+
"""
|
303
|
+
Plots the real projection of complex I/Q data versus the x-axis and the full IQ plane.
|
304
|
+
|
305
|
+
Parameters
|
306
|
+
----------
|
307
|
+
path : str, optional
|
308
|
+
Path to the HDF5 file containing the data. Required if `datadict` is not provided.
|
309
|
+
datadict : dict, optional
|
310
|
+
Pre-loaded data dictionary with schema, typically extracted using `extract_h5_data`.
|
311
|
+
Required if `path` is not provided.
|
312
|
+
proj_data : np.ndarray, optional
|
313
|
+
Precomputed projected data (real part of transformed complex values).
|
314
|
+
If not provided, it will be computed using `transform_data`.
|
315
|
+
full_output : bool, default False
|
316
|
+
Whether to return projected data and the inverse transformation function.
|
317
|
+
|
318
|
+
Returns
|
319
|
+
-------
|
320
|
+
res : tuple
|
321
|
+
If `full_output` is False:
|
322
|
+
(fig, [ax_proj, ax_iq])
|
323
|
+
If `full_output` is True:
|
324
|
+
(fig, [ax_proj, ax_iq], proj_data, inv)
|
325
|
+
- `fig`: matplotlib Figure object.
|
326
|
+
- `ax_proj`: Axis for projection vs x-axis.
|
327
|
+
- `ax_iq`: Axis for I/Q scatter plot.
|
328
|
+
- `proj_data`: The real projection of the complex I/Q data.
|
329
|
+
- `inv`: The inverse transformation function used during projection.
|
330
|
+
|
331
|
+
Notes
|
332
|
+
-----
|
333
|
+
This function supports only 1D datasets. If sweep dimensions are detected, no plot is created.
|
334
|
+
The projection is performed using a data transformation routine (e.g., PCA or rotation).
|
335
|
+
"""
|
336
|
+
|
337
|
+
all_data, all_info, _ = get_data_and_info(path=path, datadict=datadict)
|
338
|
+
x_data, y_data, sweeps = all_data
|
339
|
+
x_info, y_info, sweep_info = all_info
|
340
|
+
|
341
|
+
# Get y_unit
|
342
|
+
y_unit = f" [{y_info.rescaled_unit}]" if y_info.unit else ""
|
343
|
+
|
344
|
+
set_plot_style(plt)
|
345
|
+
|
346
|
+
if len(sweeps) == 0:
|
347
|
+
# Project data
|
348
|
+
if proj_data is None:
|
349
|
+
proj_data, inv = transform_data(y_data, inv_transform=True)
|
350
|
+
|
351
|
+
set_plot_style(plt)
|
352
|
+
fig = plt.figure(figsize=(20, 7), constrained_layout=True)
|
353
|
+
gs = GridSpec(nrows=1, ncols=10, figure=fig, wspace=0.2)
|
354
|
+
|
355
|
+
# Plot the projection
|
356
|
+
ax_proj = fig.add_subplot(gs[:, :6]) # 6/10 width
|
357
|
+
ax_proj.plot(x_data * x_info.scale, proj_data.real * y_info.scale, "o")
|
358
|
+
ax_proj.set_xlabel(x_info.name_and_unit)
|
359
|
+
ax_proj.set_ylabel("Projected" + y_unit)
|
360
|
+
|
361
|
+
# Plot IQ data
|
362
|
+
ax_iq = fig.add_subplot(gs[:, 6:]) # 4/10 width
|
363
|
+
ax_iq.scatter(0, 0, marker="+", color="black", s=150)
|
364
|
+
ax_iq.plot(y_data.real * y_info.scale, y_data.imag * y_info.scale, "o")
|
365
|
+
ax_iq.set_xlabel("In-Phase" + y_unit)
|
366
|
+
ax_iq.set_ylabel("Quadrature" + y_unit)
|
367
|
+
ax_iq.set_aspect(aspect="equal", adjustable="datalim")
|
368
|
+
|
369
|
+
if full_output:
|
370
|
+
res = (fig, [ax_proj, ax_iq], proj_data, inv)
|
371
|
+
else:
|
372
|
+
res = (fig, [ax_proj, ax_iq])
|
373
|
+
return res
|