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