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.
Files changed (41) hide show
  1. sqil_core/__init__.py +6 -2
  2. sqil_core/config.py +13 -0
  3. sqil_core/config_log.py +42 -0
  4. sqil_core/experiment/__init__.py +11 -0
  5. sqil_core/experiment/_analysis.py +95 -0
  6. sqil_core/experiment/_events.py +25 -0
  7. sqil_core/experiment/_experiment.py +553 -0
  8. sqil_core/experiment/data/plottr.py +778 -0
  9. sqil_core/experiment/helpers/_function_override_handler.py +111 -0
  10. sqil_core/experiment/helpers/_labone_wrappers.py +12 -0
  11. sqil_core/experiment/instruments/__init__.py +2 -0
  12. sqil_core/experiment/instruments/_instrument.py +190 -0
  13. sqil_core/experiment/instruments/drivers/SignalCore_SC5511A.py +515 -0
  14. sqil_core/experiment/instruments/local_oscillator.py +205 -0
  15. sqil_core/experiment/instruments/server.py +175 -0
  16. sqil_core/experiment/instruments/setup.yaml +21 -0
  17. sqil_core/experiment/instruments/zurich_instruments.py +55 -0
  18. sqil_core/fit/__init__.py +38 -0
  19. sqil_core/fit/_core.py +1084 -0
  20. sqil_core/fit/_fit.py +1191 -0
  21. sqil_core/fit/_guess.py +232 -0
  22. sqil_core/fit/_models.py +127 -0
  23. sqil_core/fit/_quality.py +266 -0
  24. sqil_core/resonator/__init__.py +13 -0
  25. sqil_core/resonator/_resonator.py +989 -0
  26. sqil_core/utils/__init__.py +85 -5
  27. sqil_core/utils/_analysis.py +415 -0
  28. sqil_core/utils/_const.py +105 -0
  29. sqil_core/utils/_formatter.py +259 -0
  30. sqil_core/utils/_plot.py +373 -0
  31. sqil_core/utils/_read.py +262 -0
  32. sqil_core/utils/_utils.py +164 -0
  33. {sqil_core-0.0.2.dist-info → sqil_core-1.0.0.dist-info}/METADATA +40 -7
  34. sqil_core-1.0.0.dist-info/RECORD +36 -0
  35. {sqil_core-0.0.2.dist-info → sqil_core-1.0.0.dist-info}/WHEEL +1 -1
  36. {sqil_core-0.0.2.dist-info → sqil_core-1.0.0.dist-info}/entry_points.txt +1 -1
  37. sqil_core/utils/analysis.py +0 -68
  38. sqil_core/utils/const.py +0 -38
  39. sqil_core/utils/formatter.py +0 -134
  40. sqil_core/utils/read.py +0 -156
  41. 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
@@ -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