Line data Source code
1 : # SPDX-FileCopyrightText: 2025 Pairinteraction Developers
2 : # SPDX-License-Identifier: LGPL-3.0-or-later
3 :
4 1 : from typing import TYPE_CHECKING, Any, Optional
5 :
6 1 : from PySide6.QtCore import Signal
7 1 : from PySide6.QtWidgets import QComboBox, QLabel, QWidget
8 :
9 1 : import pairinteraction.real as pi
10 1 : from pairinteraction_gui.app import Application
11 1 : from pairinteraction_gui.config.base_config import BaseConfig
12 1 : from pairinteraction_gui.qobjects import (
13 : NamedStackedWidget,
14 : QnItemDouble,
15 : QnItemHalfInt,
16 : QnItemInt,
17 : WidgetForm,
18 : WidgetV,
19 : )
20 1 : from pairinteraction_gui.theme import label_error_theme, label_theme
21 1 : from pairinteraction_gui.utils import (
22 : AVAILABLE_SPECIES,
23 : DatabaseMissingError,
24 : NoStateFoundError,
25 : get_custom_error,
26 : get_species_type,
27 : )
28 1 : from pairinteraction_gui.worker import MultiThreadWorker
29 :
30 : if TYPE_CHECKING:
31 : from pairinteraction_gui.page.lifetimes_page import LifetimesPage
32 : from pairinteraction_gui.qobjects.item import _QnItem
33 :
34 :
35 1 : class KetConfig(BaseConfig):
36 : """Section for configuring the ket of interest."""
37 :
38 1 : title = "State of Interest"
39 :
40 1 : signal_species_changed = Signal(int, str)
41 :
42 1 : def setupWidget(self) -> None:
43 1 : self.species_combo: list[QComboBox] = []
44 1 : self.stacked_qn: list[NamedStackedWidget[QnBase]] = []
45 1 : self.ket_label: list[QLabel] = []
46 :
47 1 : @property
48 1 : def n_atoms(self) -> int:
49 : """Return the number of atoms configured."""
50 1 : return len(self.species_combo)
51 :
52 1 : def setupOneKetAtom(self) -> None:
53 : """Set up the UI components for a single ket atom."""
54 1 : atom = len(self.species_combo)
55 :
56 : # Species selection
57 1 : species_widget = WidgetForm()
58 1 : species_combo = QComboBox()
59 1 : species_combo.addItems(AVAILABLE_SPECIES) # TODO get available species from pairinteraction
60 1 : species_combo.setToolTip("Select the atomic species")
61 1 : species_widget.layout().addRow("Species", species_combo)
62 1 : species_combo.currentTextChanged.connect(
63 : lambda species, atom=atom: self.signal_species_changed.emit(atom, species)
64 : )
65 1 : self.signal_species_changed.connect(self.on_species_changed)
66 1 : self.layout().addWidget(species_widget)
67 :
68 : # Create stacked widget for different species configurations
69 1 : stacked_qn = NamedStackedWidget[QnBase]()
70 1 : stacked_qn.addNamedWidget(QnSQDT(s=0.5), "sqdt_duplet")
71 1 : stacked_qn.addNamedWidget(QnSQDT(s=0), "sqdt_singlet")
72 1 : stacked_qn.addNamedWidget(QnSQDT(s=1), "sqdt_triplet")
73 1 : stacked_qn.addNamedWidget(QnMQDT(m_is_int=True), "mqdt_int")
74 1 : stacked_qn.addNamedWidget(QnMQDT(m_is_int=False), "mqdt_halfint")
75 :
76 1 : for _, widget in stacked_qn.items():
77 1 : for item in widget.items.values():
78 1 : item.connectAll(lambda atom=atom: self.on_qnitem_changed(atom)) # type: ignore [misc]
79 1 : self.layout().addWidget(stacked_qn)
80 :
81 : # Add a label to display the current ket
82 1 : ket_label = QLabel()
83 1 : ket_label.setStyleSheet(label_theme)
84 1 : ket_label.setWordWrap(True)
85 1 : self.layout().addWidget(ket_label)
86 :
87 : # Store the widgets for later access
88 1 : self.species_combo.append(species_combo)
89 1 : self.stacked_qn.append(stacked_qn)
90 1 : self.ket_label.append(ket_label)
91 1 : self.on_qnitem_changed(atom)
92 :
93 1 : def get_species(self, atom: int = 0) -> str:
94 : """Return the selected species of the ... atom."""
95 1 : return self.species_combo[atom].currentText()
96 :
97 1 : def get_quantum_numbers(self, atom: int = 0) -> dict[str, float]:
98 : """Return the quantum numbers of the ... atom."""
99 1 : qn_widget = self.stacked_qn[atom].currentWidget()
100 1 : return {key: item.value() for key, item in qn_widget.items.items() if item.isChecked()}
101 :
102 1 : def get_ket_atom(self, atom: int, *, ask_download: bool = False) -> "pi.KetAtom":
103 : """Return the ket of interest of the ... atom."""
104 1 : species = self.get_species(atom)
105 1 : qns = self.get_quantum_numbers(atom)
106 :
107 1 : try:
108 1 : return pi.KetAtom(species, **qns) # type: ignore [arg-type]
109 1 : except Exception as err:
110 1 : err = get_custom_error(err)
111 1 : if ask_download and isinstance(err, DatabaseMissingError):
112 0 : Application.signals.ask_download_database.emit(err.species)
113 1 : raise err
114 :
115 1 : def on_species_changed(self, atom: int, species: str) -> None:
116 : """Handle species selection change."""
117 0 : species_type = get_species_type(species)
118 0 : self.stacked_qn[atom].setCurrentNamedWidget(species_type)
119 0 : self.on_qnitem_changed(atom)
120 :
121 1 : def on_qnitem_changed(self, atom: int) -> None:
122 : """Update the ket label with current values."""
123 1 : try:
124 1 : ket = self.get_ket_atom(atom, ask_download=True)
125 1 : self.ket_label[atom].setText(str(ket))
126 1 : self.ket_label[atom].setStyleSheet(label_theme)
127 1 : except Exception as err:
128 1 : if isinstance(err, NoStateFoundError):
129 1 : self.ket_label[atom].setText("No ket found. Please select different quantum numbers.")
130 0 : elif isinstance(err, DatabaseMissingError):
131 0 : self.ket_label[atom].setText("Database required but not downloaded. Please select a different species.")
132 : else:
133 0 : self.ket_label[atom].setText(str(err))
134 1 : self.ket_label[atom].setStyleSheet(label_error_theme)
135 :
136 :
137 1 : class KetConfigOneAtom(KetConfig):
138 1 : def setupWidget(self) -> None:
139 1 : super().setupWidget()
140 1 : self.setupOneKetAtom()
141 :
142 :
143 1 : class KetConfigLifetimes(KetConfig):
144 1 : worker_label: Optional[MultiThreadWorker] = None
145 1 : worker_plot: Optional[MultiThreadWorker] = None
146 :
147 1 : page: "LifetimesPage"
148 :
149 1 : def setupWidget(self) -> None:
150 1 : super().setupWidget()
151 1 : self.setupOneKetAtom()
152 1 : self.layout().addSpacing(15)
153 :
154 1 : self.item_temperature = QnItemDouble(
155 : self,
156 : "Temperature",
157 : vdefault=300,
158 : unit="K",
159 : tooltip="Temperature in Kelvin (0K considers only spontaneous decay)",
160 : )
161 1 : self.item_temperature.connectAll(self.update_lifetime_label)
162 1 : self.layout().addWidget(self.item_temperature)
163 1 : self.layout().addSpacing(15)
164 :
165 : # Add a label to display the lifetime
166 1 : self.lifetime_label = QLabel()
167 1 : self.lifetime_label.setStyleSheet(label_theme)
168 1 : self.lifetime_label.setWordWrap(True)
169 1 : self.layout().addWidget(self.lifetime_label)
170 :
171 1 : self.update_lifetime_label()
172 :
173 1 : def get_temperature(self) -> float:
174 0 : return self.item_temperature.value(default=0)
175 :
176 1 : def update_lifetime_label(self) -> None:
177 1 : if self.worker_label and self.worker_label.isRunning():
178 0 : self.worker_label.quit()
179 0 : self.worker_label.wait()
180 :
181 1 : def get_lifetime() -> float:
182 0 : ket = self.get_ket_atom(0, ask_download=True)
183 0 : temperature = self.get_temperature()
184 0 : return ket.get_lifetime(temperature, temperature_unit="K", unit="mus")
185 :
186 1 : def update_result(lifetime: float) -> None:
187 0 : self.lifetime_label.setText(f"Lifetime: {lifetime:.3f} μs")
188 0 : self.lifetime_label.setStyleSheet(label_theme)
189 :
190 1 : def update_error(err: Exception) -> None:
191 1 : self.lifetime_label.setText("Ket not found.")
192 1 : self.lifetime_label.setStyleSheet(label_error_theme)
193 1 : self.page.plotwidget.clear()
194 :
195 1 : self.worker_label = MultiThreadWorker(get_lifetime)
196 1 : self.worker_label.signals.result.connect(update_result)
197 1 : self.worker_label.signals.error.connect(update_error)
198 1 : self.worker_label.start()
199 :
200 1 : if self.worker_plot and self.worker_plot.isRunning():
201 0 : self.worker_plot.quit()
202 0 : self.worker_plot.wait()
203 :
204 1 : self.worker_plot = MultiThreadWorker(self.page.calculate)
205 1 : self.worker_plot.signals.result.connect(self.page.update_plot)
206 1 : self.worker_plot.start()
207 :
208 1 : def on_qnitem_changed(self, atom: int) -> None:
209 1 : super().on_qnitem_changed(atom)
210 1 : if hasattr(self, "item_temperature"): # not yet initialized the first time this method is called
211 0 : self.update_lifetime_label()
212 :
213 :
214 1 : class KetConfigTwoAtoms(KetConfig):
215 1 : def setupWidget(self) -> None:
216 1 : super().setupWidget()
217 :
218 1 : self.layout().addWidget(QLabel("<b>Atom 1</b>"))
219 1 : self.setupOneKetAtom()
220 1 : self.layout().addSpacing(15)
221 :
222 1 : self.layout().addWidget(QLabel("<b>Atom 2</b>"))
223 1 : self.setupOneKetAtom()
224 :
225 :
226 1 : class QnBase(WidgetV):
227 : """Base class for quantum number configuration."""
228 :
229 1 : margin = (10, 0, 10, 0)
230 1 : spacing = 5
231 :
232 1 : items: dict[str, "_QnItem[Any]"]
233 :
234 1 : def postSetupWidget(self) -> None:
235 1 : for item in self.items.values():
236 1 : self.layout().addWidget(item)
237 :
238 :
239 1 : class QnSQDT(QnBase):
240 : """Configuration for atoms using SQDT."""
241 :
242 1 : def __init__(self, parent: Optional[QWidget] = None, s: float = 0.5) -> None:
243 1 : self.s = s
244 1 : self.items = {}
245 1 : super().__init__(parent)
246 :
247 1 : def setupWidget(self) -> None:
248 1 : self.items["n"] = QnItemInt(self, "n", vmin=1, vdefault=80, tooltip="Principal quantum number n")
249 1 : self.items["l"] = QnItemInt(self, "l", vmin=0, tooltip="Orbital angular momentum l")
250 :
251 1 : s = self.s
252 1 : if s % 1 == 0:
253 1 : self.items["j"] = QnItemInt(self, "j", vmin=int(s), vdefault=int(s), tooltip="Total angular momentum j")
254 1 : self.items["m"] = QnItemInt(self, "m", vmin=-999, vmax=999, vdefault=0, tooltip="Magnetic quantum number m")
255 : else:
256 1 : self.items["j"] = QnItemHalfInt(self, "j", vmin=s, vdefault=s, tooltip="Total angular momentum j")
257 1 : self.items["m"] = QnItemHalfInt(
258 : self, "m", vmin=-999.5, vmax=999.5, vdefault=0.5, tooltip="Magnetic quantum number m"
259 : )
260 :
261 :
262 1 : class QnMQDT(QnBase):
263 : """Configuration for earth alkali atoms using MQDT."""
264 :
265 1 : def __init__(self, parent: Optional[QWidget] = None, m_is_int: bool = False) -> None:
266 1 : self.m_is_int = m_is_int
267 1 : self.items = {}
268 1 : super().__init__(parent)
269 :
270 1 : def setupWidget(self) -> None:
271 1 : self.items["nu"] = QnItemDouble(
272 : self, "nu", vmin=1, vdefault=80, vstep=1, tooltip="Effective principal quantum number nu"
273 : )
274 1 : self.items["s"] = QnItemDouble(self, "s", vmin=0, vmax=1, vstep=0.1, tooltip="Spin s")
275 1 : self.items["j"] = QnItemDouble(self, "j", vmin=0, vstep=1, tooltip="Total angular momentum j")
276 :
277 1 : if self.m_is_int:
278 1 : self.items["f"] = QnItemInt(self, "f", vmin=0, vdefault=0, tooltip="Total angular momentum f")
279 1 : self.items["m"] = QnItemInt(self, "m", vmin=-999, vmax=999, vdefault=0, tooltip="Magnetic quantum number m")
280 : else:
281 1 : self.items["f"] = QnItemHalfInt(self, "f", vmin=0.5, vdefault=0.5, tooltip="Total angular momentum f")
282 1 : self.items["m"] = QnItemHalfInt(
283 : self, "m", vmin=-999.5, vmax=999.5, vdefault=0.5, tooltip="Magnetic quantum number m"
284 : )
285 :
286 1 : self.items["l_ryd"] = QnItemDouble(
287 : self,
288 : "l_ryd",
289 : vmin=0,
290 : vstep=1,
291 : tooltip="Orbital angular momentum l_ryd of the Rydberg electron",
292 : checked=False,
293 : )
|