Line data Source code
1 : # SPDX-FileCopyrightText: 2025 PairInteraction Developers
2 : # SPDX-License-Identifier: LGPL-3.0-or-later
3 1 : from __future__ import annotations
4 :
5 1 : from typing import TYPE_CHECKING, Any, Literal, TypedDict
6 :
7 1 : from PySide6.QtCore import Signal
8 1 : from PySide6.QtWidgets import QCheckBox, QComboBox, QLabel
9 :
10 1 : import pairinteraction as pi
11 1 : from pairinteraction_gui.app import Application
12 1 : from pairinteraction_gui.config.base_config import BaseConfig
13 1 : from pairinteraction_gui.qobjects import (
14 : NamedStackedWidget,
15 : QnItemDouble,
16 : QnItemHalfInt,
17 : QnItemInt,
18 : WidgetForm,
19 : WidgetV,
20 : )
21 1 : from pairinteraction_gui.utils import (
22 : AVAILABLE_SPECIES,
23 : DatabaseMissingError,
24 : NoStateFoundError,
25 : get_custom_error,
26 : get_species_type,
27 : )
28 :
29 : if TYPE_CHECKING:
30 : from PySide6.QtWidgets import QWidget
31 :
32 : from pairinteraction_gui.page.lifetimes_page import LifetimesPage
33 : from pairinteraction_gui.qobjects.item import _QnItem
34 :
35 1 : CORE_SPIN_DICT = {"Sr87": 9 / 2, "Sr88": 0, "Yb171": 1 / 2, "Yb173": 5 / 2, "Yb174": 0}
36 :
37 :
38 1 : class QuantumNumbers(TypedDict, total=False):
39 1 : n: int
40 1 : nu: float
41 1 : l: float
42 1 : s: float
43 1 : j: float
44 1 : l_ryd: float
45 1 : f: float
46 1 : m: float
47 :
48 :
49 1 : class KetConfig(BaseConfig):
50 : """Section for configuring the ket of interest."""
51 :
52 1 : title = "State of Interest"
53 :
54 1 : signal_species_changed = Signal(int, str)
55 :
56 1 : def setupWidget(self) -> None:
57 1 : self.species_combo_list: list[QComboBox] = []
58 1 : self.stacked_qn_list: list[NamedStackedWidget[QnBase]] = []
59 1 : self.ket_label_list: list[QLabel] = []
60 :
61 1 : def postSetupWidget(self) -> None:
62 1 : super().postSetupWidget()
63 1 : for atom in range(self.n_atoms):
64 1 : self.on_species_changed(atom, self.get_species(atom))
65 1 : self.on_qnitem_changed(atom)
66 :
67 1 : @property
68 1 : def n_atoms(self) -> int:
69 : """Return the number of atoms configured."""
70 1 : return len(self.species_combo_list)
71 :
72 1 : def setupOneKetAtom(self) -> None:
73 : """Set up the UI components for a single ket atom."""
74 1 : atom = len(self.species_combo_list)
75 :
76 : # Species selection
77 1 : species_widget = WidgetForm()
78 1 : species_combo = QComboBox()
79 1 : species_combo.setObjectName(f"species_{atom}")
80 1 : species_combo.addItems(AVAILABLE_SPECIES) # TODO get available species from pairinteraction
81 1 : species_combo.setToolTip("Select the atomic species")
82 1 : species_widget.layout().addRow("Species", species_combo)
83 1 : species_combo.currentTextChanged.connect(
84 : lambda species, atom=atom: self.signal_species_changed.emit(atom, species)
85 : )
86 1 : self.signal_species_changed.connect(self.on_species_changed)
87 1 : self.layout().addWidget(species_widget)
88 :
89 1 : stacked_qn = NamedStackedWidget[QnBase]()
90 1 : self.layout().addWidget(stacked_qn)
91 :
92 : # Add a label to display the current ket
93 1 : ket_label = QLabel()
94 1 : ket_label.setWordWrap(True)
95 1 : self.layout().addWidget(ket_label)
96 :
97 : # Store the widgets for later access
98 1 : self.species_combo_list.append(species_combo)
99 1 : self.stacked_qn_list.append(stacked_qn)
100 1 : self.ket_label_list.append(ket_label)
101 1 : self._set_theme_role(ket_label, "info")
102 :
103 1 : def get_species(self, atom: int = 0) -> str:
104 : """Return the selected species of the ... atom."""
105 1 : return self.species_combo_list[atom].currentText()
106 :
107 1 : def get_quantum_numbers(self, atom: int = 0) -> QuantumNumbers:
108 : """Return the quantum numbers of the ... atom."""
109 1 : qn_widget = self.stacked_qn_list[atom].currentWidget()
110 1 : return {key: item.value() for key, item in qn_widget.items.items() if item.isChecked()} # type: ignore [return-value]
111 :
112 1 : def get_ket_atom(self, atom: int, *, ask_download: bool = False) -> pi.KetAtom:
113 : """Return the ket of interest of the ... atom."""
114 1 : species = self.get_species(atom)
115 1 : qns = self.get_quantum_numbers(atom)
116 :
117 1 : try:
118 1 : return pi.KetAtom(species, **qns)
119 1 : except Exception as err:
120 1 : err = get_custom_error(err)
121 1 : if ask_download and isinstance(err, DatabaseMissingError):
122 0 : Application.signals.ask_download_database.emit(species)
123 1 : raise err
124 :
125 1 : def on_species_changed(self, atom: int, species: str) -> None:
126 : """Handle species selection change."""
127 1 : if species not in self.stacked_qn_list[atom]._widgets:
128 1 : qn_widget = QnBase.from_species(atom, species, parent=self)
129 1 : self.stacked_qn_list[atom].addNamedWidget(qn_widget, species)
130 1 : for item in qn_widget.items.values():
131 1 : item.connectAll(lambda atom=atom: self.on_qnitem_changed(atom)) # type: ignore [misc]
132 :
133 1 : self.stacked_qn_list[atom].setCurrentNamedWidget(species)
134 1 : self.on_qnitem_changed(atom)
135 :
136 1 : def on_qnitem_changed(self, atom: int) -> None:
137 : """Update the ket label with current values."""
138 1 : try:
139 1 : ket = self.get_ket_atom(atom, ask_download=True)
140 1 : self.ket_label_list[atom].setText(str(ket))
141 1 : self._set_theme_role(self.ket_label_list[atom], "info")
142 1 : except Exception as err:
143 1 : if isinstance(err, NoStateFoundError):
144 1 : self.ket_label_list[atom].setText("No state found. Please select different quantum numbers.")
145 0 : elif isinstance(err, DatabaseMissingError):
146 0 : self.ket_label_list[atom].setText(
147 : "Database required but not downloaded. Please select a different species."
148 : )
149 : else:
150 0 : self.ket_label_list[atom].setText(str(err))
151 1 : self._set_theme_role(self.ket_label_list[atom], "error")
152 :
153 1 : @staticmethod
154 1 : def _set_theme_role(label: QLabel, role: str) -> None:
155 1 : label.setProperty("themeRole", role)
156 1 : style = label.style()
157 1 : style.unpolish(label)
158 1 : style.polish(label)
159 1 : label.update()
160 :
161 :
162 1 : class KetConfigOneAtom(KetConfig):
163 1 : def setupWidget(self) -> None:
164 1 : super().setupWidget()
165 1 : self.setupOneKetAtom()
166 :
167 :
168 1 : class KetConfigLifetimes(KetConfig):
169 1 : page: LifetimesPage
170 :
171 1 : def setupWidget(self) -> None:
172 1 : super().setupWidget()
173 1 : self.setupOneKetAtom()
174 1 : self.layout().addSpacing(15)
175 :
176 1 : self.item_temperature = QnItemDouble(
177 : self,
178 : "Temperature",
179 : vdefault=300,
180 : unit="K",
181 : tooltip="Temperature in Kelvin (0K considers only spontaneous decay)",
182 : )
183 1 : self.item_temperature.connectAll(self._reset_results)
184 1 : self.layout().addWidget(self.item_temperature)
185 1 : self.layout().addSpacing(15)
186 :
187 : # Add a label to display the lifetime
188 1 : self.lifetime_label = QLabel()
189 1 : self.layout().addWidget(self.lifetime_label)
190 :
191 1 : mqdt_warning_text = "WARNING: Use lifetimes for MQDT with care, as some low-lying states might be missing!"
192 1 : self.mqdt_warning_label = QLabel(mqdt_warning_text)
193 1 : self.mqdt_warning_label.setWordWrap(True)
194 1 : self._set_theme_role(self.mqdt_warning_label, "error")
195 1 : self.mqdt_warning_label.hide()
196 1 : self.layout().addWidget(self.mqdt_warning_label)
197 :
198 1 : def on_qnitem_changed(self, atom: int) -> None:
199 1 : super().on_qnitem_changed(atom)
200 1 : self._reset_results()
201 :
202 1 : def on_species_changed(self, atom: int, species: str) -> None:
203 1 : super().on_species_changed(atom, species)
204 1 : self.mqdt_warning_label.setVisible("mqdt" in species.lower())
205 :
206 1 : def _reset_results(self) -> None:
207 1 : self.lifetime_label.setText("Lifetime: \u2014") # \u2014 = em dash
208 1 : self._set_theme_role(self.lifetime_label, "info")
209 1 : self.page.plotwidget.clear()
210 :
211 1 : def get_temperature(self) -> float:
212 1 : return self.item_temperature.value(default=0)
213 :
214 1 : def set_lifetime(self, lifetime: float) -> None:
215 0 : self.lifetime_label.setText(f"Lifetime: {lifetime:.3f} \u03bcs") # \u03bc = micro
216 0 : self._set_theme_role(self.lifetime_label, "info")
217 :
218 :
219 1 : class KetConfigTwoAtoms(KetConfig):
220 1 : def setupWidget(self) -> None:
221 1 : super().setupWidget()
222 :
223 1 : self.layout().addWidget(QLabel("<b>Atom 1</b>"))
224 1 : self.setupOneKetAtom()
225 1 : self.layout().addSpacing(15)
226 :
227 1 : self.layout().addWidget(QLabel("<b>Atom 2</b>"))
228 1 : self.setupOneKetAtom()
229 :
230 :
231 1 : class QnBase(WidgetV):
232 : """Base class for quantum number configuration."""
233 :
234 1 : margin = (10, 0, 10, 0)
235 1 : spacing = 5
236 :
237 1 : atom: int
238 1 : items: dict[str, _QnItem[Any]]
239 :
240 1 : def postSetupWidget(self) -> None:
241 1 : for item in self.items.values():
242 1 : self.layout().addWidget(item)
243 :
244 1 : for item in self.items.values():
245 1 : if isinstance(item.checkbox, QCheckBox):
246 1 : item.checkbox.setObjectName(f"atom{self.atom}_{item.checkbox.objectName()}")
247 1 : item.spinbox.setObjectName(f"atom{self.atom}_{item.spinbox.objectName()}")
248 :
249 1 : @classmethod
250 1 : def from_species(cls, atom: int, species: str, parent: QWidget | None = None) -> QnBase:
251 : """Create a quantum number configuration from the species name."""
252 1 : species_type = get_species_type(species)
253 1 : if species_type == "sqdt_duplet":
254 1 : return QnSQDT(atom, parent, s_type="halfint", s=0.5)
255 0 : if species_type == "sqdt_singlet":
256 0 : return QnSQDT(atom, parent, s_type="int", s=0)
257 0 : if species_type == "sqdt_triplet":
258 0 : return QnSQDT(atom, parent, s_type="int", s=1)
259 :
260 0 : element = species.split("_", maxsplit=1)[0]
261 0 : if species_type == "mqdt_halfint":
262 0 : i = CORE_SPIN_DICT.get(element, 0.5)
263 0 : return QnMQDT(atom, parent, f_type="halfint", i=i)
264 0 : if species_type == "mqdt_int":
265 0 : i = CORE_SPIN_DICT.get(element, 0)
266 0 : return QnMQDT(atom, parent, f_type="int", i=i)
267 :
268 0 : raise ValueError(f"Unknown species type: {species_type}")
269 :
270 :
271 1 : class QnSQDT(QnBase):
272 : """Configuration for atoms using SQDT."""
273 :
274 1 : def __init__(
275 : self,
276 : atom: int,
277 : parent: QWidget | None = None,
278 : *,
279 : s_type: Literal["int", "halfint"],
280 : s: float,
281 : ) -> None:
282 1 : assert s_type in ("int", "halfint"), "s_type must be int or halfint"
283 1 : self.atom = atom
284 1 : self.s_type = s_type
285 1 : self.s = s
286 1 : self.items = {}
287 1 : super().__init__(parent)
288 :
289 1 : def setupWidget(self) -> None:
290 1 : self.items["n"] = QnItemInt(self, "n", vmin=1, vdefault=80, tooltip="Principal quantum number n")
291 1 : self.items["l"] = QnItemInt(self, "l", vmin=0, tooltip="Orbital angular momentum l")
292 :
293 1 : s = self.s
294 1 : if self.s_type == "int":
295 0 : if s != 0:
296 0 : self.items["j"] = QnItemInt(self, "j", vmin=int(s), vdefault=int(s), tooltip="Total angular momentum j")
297 0 : self.items["m"] = QnItemInt(self, "m", vmin=-999, vmax=999, vdefault=0, tooltip="Magnetic quantum number m")
298 : else:
299 1 : self.items["j"] = QnItemHalfInt(self, "j", vmin=s, vdefault=s, tooltip="Total angular momentum j")
300 1 : self.items["m"] = QnItemHalfInt(
301 : self, "m", vmin=-999.5, vmax=999.5, vdefault=0.5, tooltip="Magnetic quantum number m"
302 : )
303 :
304 :
305 1 : class QnMQDT(QnBase):
306 : """Configuration for alkaline-earth atoms using MQDT."""
307 :
308 1 : def __init__(
309 : self,
310 : atom: int,
311 : parent: QWidget | None = None,
312 : *,
313 : f_type: Literal["int", "halfint"],
314 : i: float,
315 : ) -> None:
316 0 : assert f_type in ("int", "halfint"), "f_type must be int or halfint"
317 0 : self.atom = atom
318 0 : self.f_type = f_type
319 0 : self.i = i
320 0 : self.items = {}
321 0 : super().__init__(parent)
322 :
323 1 : def setupWidget(self) -> None:
324 0 : self.items["nu"] = QnItemDouble(
325 : self, "nu", vmin=1, vdefault=80, vstep=1, tooltip="Effective principal quantum number nu"
326 : )
327 0 : self.items["s"] = QnItemDouble(self, "s", vmin=0, vmax=1, vstep=0.1, tooltip="Spin s")
328 :
329 0 : if self.i == 0:
330 0 : key = "j"
331 0 : description = "Total angular momentum j (j=f for I=0)"
332 : else:
333 0 : key = "f"
334 0 : description = "Total angular momentum f"
335 0 : if self.f_type == "int":
336 0 : self.items[key] = QnItemInt(self, key, vmin=0, vdefault=int(self.i), tooltip=description)
337 0 : self.items["m"] = QnItemInt(self, "m", vmin=-999, vmax=999, vdefault=0, tooltip="Magnetic quantum number m")
338 : else:
339 0 : self.items[key] = QnItemHalfInt(self, key, vmin=0.5, vdefault=self.i, tooltip=description)
340 0 : self.items["m"] = QnItemHalfInt(
341 : self, "m", vmin=-999.5, vmax=999.5, vdefault=0.5, tooltip="Magnetic quantum number m"
342 : )
343 :
344 0 : self.items["l_ryd"] = QnItemDouble(
345 : self,
346 : "l_ryd",
347 : vmin=0,
348 : vstep=1,
349 : tooltip="Orbital angular momentum l_ryd of the Rydberg electron",
350 : checked=False,
351 : )
|