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 +75 -0
- sccircuits/_pointpicker_tagui.py +422 -0
- sccircuits/bbq.py +1509 -0
- sccircuits/circuit.py +1265 -0
- sccircuits/fit_analysis.py +309 -0
- sccircuits/iterative_diagonalizer.py +399 -0
- sccircuits/pointpicker.py +618 -0
- sccircuits/transition_fitter.py +617 -0
- sccircuits/utilities.py +142 -0
- sccircuits-0.1.0.dist-info/METADATA +233 -0
- sccircuits-0.1.0.dist-info/RECORD +14 -0
- sccircuits-0.1.0.dist-info/WHEEL +5 -0
- sccircuits-0.1.0.dist-info/licenses/LICENSE +21 -0
- sccircuits-0.1.0.dist-info/top_level.txt +1 -0
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)
|