sccircuits 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.
sccircuits/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ SCCircuits - Superconducting Circuit Analysis Package
3
+
4
+ A comprehensive Python package for analyzing superconducting quantum circuits,
5
+ including chain-based circuit models, Black Box Quantization (BBQ), and
6
+ spectroscopy-fitting workflows.
7
+
8
+ Main Classes:
9
+ Circuit: Main circuit class for superconducting quantum circuits
10
+ BBQ: Black Box Quantization analysis
11
+ TransitionFitter: General transition frequency fitting
12
+ FitAnalysis: Post-fit diagnostics for least-squares results
13
+ PointPicker: Interactive point selection tool for data analysis
14
+
15
+ Utilities:
16
+ lanczos_krylov: Lanczos algorithm for Hermitian matrices
17
+ IterativeHamiltonianDiagonalizer: Multi-mode Hamiltonian diagonalization
18
+ """
19
+
20
+ __version__ = "0.1.0"
21
+ __author__ = "Joan Caceres"
22
+ __email__ = "contact@joancaceres.com"
23
+
24
+ # Core circuit analysis classes
25
+ from .circuit import Circuit
26
+ from .bbq import BBQ
27
+
28
+ # Fitting and analysis tools
29
+ from .transition_fitter import TransitionFitter
30
+ from .pointpicker import PointPicker
31
+ from .fit_analysis import FitAnalysis
32
+
33
+ # Numerical utilities
34
+ from .iterative_diagonalizer import IterativeHamiltonianDiagonalizer
35
+ from .utilities import lanczos_krylov
36
+
37
+ # Public API - what gets imported with "from sccircuits import *"
38
+ __all__ = [
39
+ # Core classes
40
+ "Circuit",
41
+ "BBQ",
42
+ # Analysis tools
43
+ "TransitionFitter",
44
+ "PointPicker",
45
+ "FitAnalysis",
46
+ # Utilities
47
+ "IterativeHamiltonianDiagonalizer",
48
+ "lanczos_krylov",
49
+ # Package metadata
50
+ "__version__",
51
+ "__author__",
52
+ "__email__",
53
+ ]
54
+
55
+
56
+ # Package information
57
+ def get_version():
58
+ """Return the version of the sccircuits package."""
59
+ return __version__
60
+
61
+
62
+ def get_info():
63
+ """Return basic information about the sccircuits package."""
64
+ return {
65
+ "name": "sccircuits",
66
+ "version": __version__,
67
+ "author": __author__,
68
+ "description": "Superconducting Circuit Analysis Package",
69
+ "main_classes": [
70
+ "Circuit",
71
+ "BBQ",
72
+ "TransitionFitter",
73
+ "FitAnalysis",
74
+ ],
75
+ }
@@ -0,0 +1,422 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, TYPE_CHECKING
5
+
6
+ from matplotlib.widgets import Button, TextBox
7
+
8
+ if TYPE_CHECKING: # pragma: no cover - typing helper
9
+ from .pointpicker import PointPicker
10
+
11
+
12
+ class BaseTagUI:
13
+ """Interface for tag input backends."""
14
+
15
+ def __init__(self, picker: "PointPicker") -> None:
16
+ self.picker = picker
17
+
18
+ def is_available(self) -> bool:
19
+ return True
20
+
21
+ def show(self, idx: int) -> None: # pragma: no cover - abstract
22
+ raise NotImplementedError
23
+
24
+ def close(self) -> None:
25
+ """Release UI resources if any."""
26
+ # Default implementation just forgets about pending state.
27
+ return
28
+
29
+
30
+ def _running_inside_jupyter() -> bool:
31
+ try:
32
+ from IPython import get_ipython # type: ignore
33
+ except Exception:
34
+ return False
35
+ shell = get_ipython()
36
+ return bool(shell and hasattr(shell, "kernel"))
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Jupyter ipywidgets implementation
41
+ # ---------------------------------------------------------------------------
42
+ @dataclass
43
+ class _JupyterWidgetState:
44
+ box: object
45
+ i_field: object
46
+ j_field: object
47
+ sigma_field: object
48
+ ok_button: object
49
+ remove_button: object
50
+ output: object
51
+
52
+
53
+ class JupyterTagUI(BaseTagUI):
54
+ """ipywidgets-based tagging UI for notebook environments."""
55
+
56
+ def __init__(self, picker: "PointPicker") -> None:
57
+ super().__init__(picker)
58
+ self._widget: Optional[_JupyterWidgetState] = None
59
+ self._pending_idx: Optional[int] = None
60
+ self._available = False
61
+ self._ipyw = None
62
+ self._display = None
63
+ self._clear_output = None
64
+
65
+ if not _running_inside_jupyter():
66
+ return
67
+ try:
68
+ import ipywidgets as ipyw # type: ignore
69
+ from IPython.display import clear_output, display # type: ignore
70
+ except Exception:
71
+ return
72
+
73
+ self._ipyw = ipyw
74
+ self._display = display
75
+ self._clear_output = clear_output
76
+ self._available = True
77
+
78
+ # Public API ---------------------------------------------------------
79
+ def is_available(self) -> bool:
80
+ return bool(self._available)
81
+
82
+ def show(self, idx: int) -> None:
83
+ if not self.is_available():
84
+ raise RuntimeError("ipywidgets backend is unavailable")
85
+ self._pending_idx = idx
86
+ if self._widget is None:
87
+ self._build_widget()
88
+ assert self._widget is not None
89
+ self._display(self._widget.box) # type: ignore[arg-type]
90
+ self._populate_defaults(idx)
91
+ print(
92
+ "Please enter i, j and optional sigma in the widget and press OK to tag point #{}, "
93
+ "or Remove Tag to remove label".format(idx)
94
+ )
95
+
96
+ def close(self) -> None:
97
+ if self._widget is None:
98
+ self._pending_idx = None
99
+ return
100
+ if hasattr(self._widget.box, "close"):
101
+ self._widget.box.close() # type: ignore[attr-defined]
102
+ self._widget = None
103
+ self._pending_idx = None
104
+
105
+ # Internal helpers ---------------------------------------------------
106
+ def _build_widget(self) -> None:
107
+ assert self._ipyw is not None and self._display is not None
108
+ ipyw = self._ipyw
109
+ i_field = ipyw.IntText(description="i:", layout=ipyw.Layout(width="140px"))
110
+ j_field = ipyw.IntText(description="j:", layout=ipyw.Layout(width="140px"))
111
+ sigma_field = ipyw.Text(description="σ:", layout=ipyw.Layout(width="160px"))
112
+ ok_button = ipyw.Button(description="OK", layout=ipyw.Layout(width="80px"))
113
+ remove_button = ipyw.Button(
114
+ description="Remove Tag", layout=ipyw.Layout(width="120px")
115
+ )
116
+ output = ipyw.Output(layout=ipyw.Layout(font_size="12px"))
117
+ box = ipyw.VBox(
118
+ [ipyw.HBox([i_field, j_field, sigma_field, ok_button, remove_button]), output]
119
+ )
120
+ box.layout = ipyw.Layout(width="640px")
121
+
122
+ self._widget = _JupyterWidgetState(
123
+ box=box,
124
+ i_field=i_field,
125
+ j_field=j_field,
126
+ sigma_field=sigma_field,
127
+ ok_button=ok_button,
128
+ remove_button=remove_button,
129
+ output=output,
130
+ )
131
+ ok_button.on_click(self._handle_submit)
132
+ remove_button.on_click(self._handle_remove)
133
+
134
+ def _populate_defaults(self, idx: int) -> None:
135
+ assert self._widget is not None
136
+ lab, sigma = self.picker._tag_defaults_for(idx)
137
+ if lab is not None:
138
+ self._widget.i_field.value = int(lab[0])
139
+ self._widget.j_field.value = int(lab[1])
140
+ elif self.picker._current_label is not None:
141
+ i_def, j_def = self.picker._current_label
142
+ self._widget.i_field.value = int(i_def)
143
+ self._widget.j_field.value = int(j_def)
144
+ else:
145
+ self._widget.i_field.value = 0
146
+ self._widget.j_field.value = 0
147
+ if sigma is not None:
148
+ self._widget.sigma_field.value = f"{float(sigma):g}"
149
+ elif self.picker._current_sigma is not None:
150
+ self._widget.sigma_field.value = f"{float(self.picker._current_sigma):g}"
151
+ else:
152
+ self._widget.sigma_field.value = ""
153
+ self._write_feedback(
154
+ f"Point #{idx} selected. Modify i, j, sigma or press OK. Or remove tag."
155
+ )
156
+
157
+ def _handle_submit(self, _button=None) -> None:
158
+ if self._pending_idx is None:
159
+ self._write_feedback("No point selected.")
160
+ return
161
+ assert self._widget is not None
162
+ idx = self._pending_idx
163
+ try:
164
+ i = int(self._widget.i_field.value)
165
+ j = int(self._widget.j_field.value)
166
+ except Exception:
167
+ self._write_feedback("Invalid values.")
168
+ return
169
+ sigma_raw = str(self._widget.sigma_field.value).strip()
170
+ sigma_val: Optional[float]
171
+ if sigma_raw:
172
+ try:
173
+ sigma_val = float(sigma_raw)
174
+ except Exception:
175
+ self._write_feedback("Valores inválidos.")
176
+ return
177
+ if sigma_val <= 0:
178
+ self._write_feedback("sigma must be > 0 or leave empty.")
179
+ return
180
+ else:
181
+ sigma_val = None
182
+ try:
183
+ self.picker._apply_tag(idx, i, j, sigma_val)
184
+ except ValueError as exc:
185
+ self._write_feedback(str(exc))
186
+ return
187
+ msg = (
188
+ f"Point #{idx} tagged with ({i},{j}) without sigma"
189
+ if sigma_val is None
190
+ else f"Point #{idx} tagged with ({i},{j}) and sigma={sigma_val}"
191
+ )
192
+ self._write_feedback(msg)
193
+ self._pending_idx = None
194
+ self.close()
195
+
196
+ def _handle_remove(self, _button=None) -> None:
197
+ if self._pending_idx is None:
198
+ self._write_feedback("No point selected.")
199
+ return
200
+ idx = self._pending_idx
201
+ self.picker._remove_tag(idx)
202
+ self._write_feedback(f"Point #{idx} tag removed ✓")
203
+ assert self._widget is not None
204
+ self._widget.i_field.value = 0
205
+ self._widget.j_field.value = 0
206
+ self._widget.sigma_field.value = ""
207
+ self._pending_idx = None
208
+ self.close()
209
+
210
+ def _write_feedback(self, message: str) -> None:
211
+ if self._widget is None or self._clear_output is None:
212
+ print(message)
213
+ return
214
+ with self._widget.output:
215
+ self._clear_output()
216
+ print(message)
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Matplotlib widgets implementation
221
+ # ---------------------------------------------------------------------------
222
+ @dataclass
223
+ class _MatplotlibWidgetBundle:
224
+ i_ax: object
225
+ j_ax: object
226
+ sigma_ax: object
227
+ ok_ax: object
228
+ remove_ax: object
229
+ i_box: TextBox
230
+ j_box: TextBox
231
+ sigma_box: TextBox
232
+ ok_button: Button
233
+ remove_button: Button
234
+ ok_click_cid: int
235
+ remove_click_cid: int
236
+
237
+
238
+ class MatplotlibTagUI(BaseTagUI):
239
+ """Matplotlib TextBox/Button tagging UI for script environments."""
240
+
241
+ def __init__(self, picker: "PointPicker") -> None:
242
+ super().__init__(picker)
243
+ self._widgets: Optional[_MatplotlibWidgetBundle] = None
244
+ self._pending_idx: Optional[int] = None
245
+ self._updating_fields = False
246
+
247
+ def show(self, idx: int) -> None:
248
+ self._pending_idx = idx
249
+ if self._widgets is None:
250
+ self._widgets = self._build_widgets()
251
+ self._populate_defaults(idx)
252
+ current_label = self.picker.labels[idx]
253
+ x, y = self.picker.points[idx]
254
+ if current_label is not None:
255
+ print(
256
+ f"Point #{idx} at ({x:.2f}, {y:.2f}) currently labeled as ({current_label[0]}, {current_label[1]})"
257
+ )
258
+ else:
259
+ print(f"Point #{idx} at ({x:.2f}, {y:.2f}) currently unlabeled")
260
+ print(
261
+ "Enter i, j and optional sigma in the text boxes below the figure and press Enter or click OK."
262
+ )
263
+ self.picker.ax.figure.canvas.draw_idle()
264
+
265
+ def close(self) -> None:
266
+ if self._widgets is None:
267
+ self._pending_idx = None
268
+ return
269
+ fig = self.picker.ax.figure
270
+ widgets = self._widgets
271
+ for box in (widgets.i_box, widgets.j_box, widgets.sigma_box):
272
+ try:
273
+ box.disconnect_events()
274
+ except Exception:
275
+ pass
276
+ for button, cid in (
277
+ (widgets.ok_button, widgets.ok_click_cid),
278
+ (widgets.remove_button, widgets.remove_click_cid),
279
+ ):
280
+ try:
281
+ button.disconnect(cid)
282
+ except Exception:
283
+ pass
284
+ for ax in (widgets.i_ax, widgets.j_ax, widgets.sigma_ax, widgets.ok_ax, widgets.remove_ax):
285
+ try:
286
+ fig.delaxes(ax)
287
+ except Exception:
288
+ try:
289
+ ax.remove() # type: ignore[attr-defined]
290
+ except Exception:
291
+ pass
292
+ self._widgets = None
293
+ self._pending_idx = None
294
+ self.picker.ax.figure.canvas.draw_idle()
295
+
296
+ # Internal helpers ---------------------------------------------------
297
+ def _build_widgets(self) -> _MatplotlibWidgetBundle:
298
+ fig = self.picker.ax.figure
299
+ # Ensure there is enough room for the extra controls below the axes.
300
+ # The bottom margin is set to 0.15 to provide space for widget controls.
301
+ MATPLOTLIB_WIDGETS_BOTTOM_MARGIN = 0.15
302
+ if fig.subplotpars.bottom < MATPLOTLIB_WIDGETS_BOTTOM_MARGIN:
303
+ fig.subplots_adjust(bottom=MATPLOTLIB_WIDGETS_BOTTOM_MARGIN)
304
+
305
+ bottom = 0.02
306
+ height = 0.05
307
+ i_ax = fig.add_axes([0.06, bottom, 0.11, height])
308
+ j_ax = fig.add_axes([0.19, bottom, 0.11, height])
309
+ sigma_ax = fig.add_axes([0.34, bottom, 0.18, height])
310
+ ok_ax = fig.add_axes([0.55, bottom, 0.10, height])
311
+ remove_ax = fig.add_axes([0.68, bottom, 0.20, height])
312
+
313
+ i_box = TextBox(i_ax, "i:", initial="")
314
+ j_box = TextBox(j_ax, "j:", initial="")
315
+ sigma_box = TextBox(sigma_ax, "σ:", initial="")
316
+ ok_button = Button(ok_ax, "OK")
317
+ remove_button = Button(remove_ax, "Remove Tag")
318
+
319
+ ok_click_cid = ok_button.on_clicked(self._handle_submit)
320
+ remove_click_cid = remove_button.on_clicked(self._handle_remove)
321
+
322
+ return _MatplotlibWidgetBundle(
323
+ i_ax=i_ax,
324
+ j_ax=j_ax,
325
+ sigma_ax=sigma_ax,
326
+ ok_ax=ok_ax,
327
+ remove_ax=remove_ax,
328
+ i_box=i_box,
329
+ j_box=j_box,
330
+ sigma_box=sigma_box,
331
+ ok_button=ok_button,
332
+ remove_button=remove_button,
333
+ ok_click_cid=ok_click_cid,
334
+ remove_click_cid=remove_click_cid,
335
+ )
336
+
337
+ def _populate_defaults(self, idx: int) -> None:
338
+ assert self._widgets is not None
339
+ lab, sigma = self.picker._tag_defaults_for(idx)
340
+ self._updating_fields = True
341
+ try:
342
+ if lab is not None:
343
+ self._widgets.i_box.set_val(str(int(lab[0])))
344
+ self._widgets.j_box.set_val(str(int(lab[1])))
345
+ elif self.picker._current_label is not None:
346
+ i_def, j_def = self.picker._current_label
347
+ self._widgets.i_box.set_val(str(int(i_def)))
348
+ self._widgets.j_box.set_val(str(int(j_def)))
349
+ else:
350
+ self._widgets.i_box.set_val("")
351
+ self._widgets.j_box.set_val("")
352
+
353
+ if sigma is not None:
354
+ self._widgets.sigma_box.set_val(f"{float(sigma):g}")
355
+ elif self.picker._current_sigma is not None:
356
+ self._widgets.sigma_box.set_val(f"{float(self.picker._current_sigma):g}")
357
+ else:
358
+ self._widgets.sigma_box.set_val("")
359
+ finally:
360
+ self._updating_fields = False
361
+
362
+ def _handle_submit(self, _event=None) -> None:
363
+ if self._updating_fields:
364
+ return
365
+ if self._pending_idx is None or self._widgets is None:
366
+ print("No point selected.")
367
+ return
368
+ try:
369
+ i = int(self._widgets.i_box.text.strip())
370
+ j = int(self._widgets.j_box.text.strip())
371
+ except Exception:
372
+ print("Invalid format. Please enter integers in both i and j fields.")
373
+ return
374
+ sigma_raw = self._widgets.sigma_box.text.strip()
375
+ sigma_val: Optional[float]
376
+ if sigma_raw:
377
+ try:
378
+ sigma_val = float(sigma_raw)
379
+ except Exception:
380
+ print("Invalid sigma. Please enter a numeric value or leave blank.")
381
+ return
382
+ if sigma_val <= 0:
383
+ print("Invalid sigma. Enter a positive value or leave blank.")
384
+ return
385
+ else:
386
+ sigma_val = None
387
+ idx = self._pending_idx
388
+ try:
389
+ self.picker._apply_tag(idx, i, j, sigma_val)
390
+ except ValueError as exc:
391
+ print(str(exc))
392
+ return
393
+ if sigma_val is None:
394
+ print(f"Point #{idx} labeled as ({i}, {j}) without sigma ✓")
395
+ else:
396
+ print(f"Point #{idx} labeled as ({i}, {j}) with sigma={sigma_val:.4g} ✓")
397
+ self.close()
398
+
399
+ def _handle_remove(self, _event=None) -> None:
400
+ if self._pending_idx is None or self._widgets is None:
401
+ print("No point selected.")
402
+ return
403
+ idx = self._pending_idx
404
+ self.picker._remove_tag(idx)
405
+ print(f"Point #{idx} tag removed ✓")
406
+ self._updating_fields = True
407
+ try:
408
+ self._widgets.i_box.set_val("")
409
+ self._widgets.j_box.set_val("")
410
+ self._widgets.sigma_box.set_val("")
411
+ finally:
412
+ self._updating_fields = False
413
+ self.close()
414
+
415
+
416
+ def create_tag_ui(picker: "PointPicker", prefer_widgets: bool) -> BaseTagUI:
417
+ """Return the best available tag UI for the current environment."""
418
+ if prefer_widgets:
419
+ jupyter_ui = JupyterTagUI(picker)
420
+ if jupyter_ui.is_available():
421
+ return jupyter_ui
422
+ return MatplotlibTagUI(picker)