LCOV - code coverage report
Current view: top level - src/pairinteraction_gui/config - ket_config.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 162 207 78.3 %
Date: 2025-08-29 20:47:05 Functions: 18 46 39.1 %

          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, Literal, Optional, TypedDict
       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           1 : CORE_SPIN_DICT = {"Sr87": 9 / 2, "Sr88": 0, "Yb171": 1 / 2, "Yb173": 5 / 2, "Yb174": 0}
      35             : 
      36             : 
      37           1 : class QuantumNumbers(TypedDict, total=False):
      38           1 :     n: int
      39           1 :     nu: float
      40           1 :     l: float
      41           1 :     s: float
      42           1 :     j: float
      43           1 :     l_ryd: float
      44           1 :     f: float
      45           1 :     m: float
      46             : 
      47             : 
      48           1 : class KetConfig(BaseConfig):
      49             :     """Section for configuring the ket of interest."""
      50             : 
      51           1 :     title = "State of Interest"
      52             : 
      53           1 :     signal_species_changed = Signal(int, str)
      54             : 
      55           1 :     def setupWidget(self) -> None:
      56           1 :         self.species_combo_list: list[QComboBox] = []
      57           1 :         self.stacked_qn_list: list[NamedStackedWidget[QnBase]] = []
      58           1 :         self.ket_label_list: list[QLabel] = []
      59             : 
      60           1 :     @property
      61           1 :     def n_atoms(self) -> int:
      62             :         """Return the number of atoms configured."""
      63           1 :         return len(self.species_combo_list)
      64             : 
      65           1 :     def setupOneKetAtom(self) -> None:
      66             :         """Set up the UI components for a single ket atom."""
      67           1 :         atom = len(self.species_combo_list)
      68             : 
      69             :         # Species selection
      70           1 :         species_widget = WidgetForm()
      71           1 :         species_combo = QComboBox()
      72           1 :         species_combo.addItems(AVAILABLE_SPECIES)  # TODO get available species from pairinteraction
      73           1 :         species_combo.setToolTip("Select the atomic species")
      74           1 :         species_widget.layout().addRow("Species", species_combo)
      75           1 :         species_combo.currentTextChanged.connect(
      76             :             lambda species, atom=atom: self.signal_species_changed.emit(atom, species)
      77             :         )
      78           1 :         self.signal_species_changed.connect(self.on_species_changed)
      79           1 :         self.layout().addWidget(species_widget)
      80             : 
      81           1 :         stacked_qn = NamedStackedWidget[QnBase]()
      82           1 :         self.layout().addWidget(stacked_qn)
      83             : 
      84             :         # Add a label to display the current ket
      85           1 :         ket_label = QLabel()
      86           1 :         ket_label.setStyleSheet(label_theme)
      87           1 :         ket_label.setWordWrap(True)
      88           1 :         self.layout().addWidget(ket_label)
      89             : 
      90             :         # Store the widgets for later access
      91           1 :         self.species_combo_list.append(species_combo)
      92           1 :         self.stacked_qn_list.append(stacked_qn)
      93           1 :         self.ket_label_list.append(ket_label)
      94           1 :         self.on_qnitem_changed(atom)
      95             : 
      96           1 :     def get_species(self, atom: int = 0) -> str:
      97             :         """Return the selected species of the ... atom."""
      98           1 :         return self.species_combo_list[atom].currentText()
      99             : 
     100           1 :     def get_quantum_numbers(self, atom: int = 0) -> QuantumNumbers:
     101             :         """Return the quantum numbers of the ... atom."""
     102           1 :         qn_widget = self.stacked_qn_list[atom].currentWidget()
     103           1 :         return {key: item.value() for key, item in qn_widget.items.items() if item.isChecked()}  # type: ignore [return-value]
     104             : 
     105           1 :     def get_ket_atom(self, atom: int, *, ask_download: bool = False) -> "pi.KetAtom":
     106             :         """Return the ket of interest of the ... atom."""
     107           1 :         species = self.get_species(atom)
     108           1 :         qns = self.get_quantum_numbers(atom)
     109             : 
     110           1 :         try:
     111           1 :             return pi.KetAtom(species, **qns)
     112           1 :         except Exception as err:
     113           1 :             err = get_custom_error(err)
     114           1 :             if ask_download and isinstance(err, DatabaseMissingError):
     115           0 :                 Application.signals.ask_download_database.emit(err.species)
     116           1 :             raise err
     117             : 
     118           1 :     def on_species_changed(self, atom: int, species: str) -> None:
     119             :         """Handle species selection change."""
     120           1 :         if species not in self.stacked_qn_list[atom]._widgets:
     121           1 :             qn_widget = QnBase.from_species(species, parent=self)
     122           1 :             self.stacked_qn_list[atom].addNamedWidget(qn_widget, species)
     123           1 :             for item in qn_widget.items.values():
     124           1 :                 item.connectAll(lambda atom=atom: self.on_qnitem_changed(atom))  # type: ignore [misc]
     125             : 
     126           1 :         self.stacked_qn_list[atom].setCurrentNamedWidget(species)
     127           1 :         self.on_qnitem_changed(atom)
     128             : 
     129           1 :     def on_qnitem_changed(self, atom: int) -> None:
     130             :         """Update the ket label with current values."""
     131           1 :         try:
     132           1 :             ket = self.get_ket_atom(atom, ask_download=True)
     133           1 :             self.ket_label_list[atom].setText(str(ket))
     134           1 :             self.ket_label_list[atom].setStyleSheet(label_theme)
     135           1 :         except Exception as err:
     136           1 :             if isinstance(err, NoStateFoundError):
     137           1 :                 self.ket_label_list[atom].setText("No ket found. Please select different quantum numbers.")
     138           1 :             elif isinstance(err, DatabaseMissingError):
     139           0 :                 self.ket_label_list[atom].setText(
     140             :                     "Database required but not downloaded. Please select a different species."
     141             :                 )
     142             :             else:
     143           1 :                 self.ket_label_list[atom].setText(str(err))
     144           1 :             self.ket_label_list[atom].setStyleSheet(label_error_theme)
     145             : 
     146             : 
     147           1 : class KetConfigOneAtom(KetConfig):
     148           1 :     def setupWidget(self) -> None:
     149           1 :         super().setupWidget()
     150           1 :         self.setupOneKetAtom()
     151             : 
     152             : 
     153           1 : class KetConfigLifetimes(KetConfig):
     154           1 :     worker_label: Optional[MultiThreadWorker] = None
     155           1 :     worker_plot: Optional[MultiThreadWorker] = None
     156             : 
     157           1 :     page: "LifetimesPage"
     158             : 
     159           1 :     def setupWidget(self) -> None:
     160           1 :         super().setupWidget()
     161           1 :         self.setupOneKetAtom()
     162           1 :         self.layout().addSpacing(15)
     163             : 
     164           1 :         self.item_temperature = QnItemDouble(
     165             :             self,
     166             :             "Temperature",
     167             :             vdefault=300,
     168             :             unit="K",
     169             :             tooltip="Temperature in Kelvin (0K considers only spontaneous decay)",
     170             :         )
     171           1 :         self.item_temperature.connectAll(self.update_lifetime_label)
     172           1 :         self.layout().addWidget(self.item_temperature)
     173           1 :         self.layout().addSpacing(15)
     174             : 
     175             :         # Add a label to display the lifetime
     176           1 :         self.lifetime_label = QLabel()
     177           1 :         self.lifetime_label.setStyleSheet(label_theme)
     178           1 :         self.lifetime_label.setWordWrap(True)
     179           1 :         self.layout().addWidget(self.lifetime_label)
     180             : 
     181           1 :         self.update_lifetime_label()
     182             : 
     183           1 :     def get_temperature(self) -> float:
     184           0 :         return self.item_temperature.value(default=0)
     185             : 
     186           1 :     def update_lifetime_label(self) -> None:
     187           1 :         if self.worker_label and self.worker_label.isRunning():
     188           0 :             self.worker_label.quit()
     189           0 :             self.worker_label.wait()
     190             : 
     191           1 :         def get_lifetime() -> float:
     192           0 :             ket = self.get_ket_atom(0, ask_download=True)
     193           0 :             temperature = self.get_temperature()
     194           0 :             return ket.get_lifetime(temperature, temperature_unit="K", unit="mus")
     195             : 
     196           1 :         def update_result(lifetime: float) -> None:
     197           0 :             self.lifetime_label.setText(f"Lifetime: {lifetime:.3f} μs")
     198           0 :             self.lifetime_label.setStyleSheet(label_theme)
     199             : 
     200           1 :         def update_error(err: Exception) -> None:
     201           1 :             self.lifetime_label.setText("Ket not found.")
     202           1 :             self.lifetime_label.setStyleSheet(label_error_theme)
     203           1 :             self.page.plotwidget.clear()
     204             : 
     205           1 :         self.worker_label = MultiThreadWorker(get_lifetime)
     206           1 :         self.worker_label.signals.result.connect(update_result)
     207           1 :         self.worker_label.signals.error.connect(update_error)
     208           1 :         self.worker_label.start()
     209             : 
     210           1 :         if self.worker_plot and self.worker_plot.isRunning():
     211           0 :             self.worker_plot.quit()
     212           0 :             self.worker_plot.wait()
     213             : 
     214           1 :         self.worker_plot = MultiThreadWorker(self.page.calculate)
     215           1 :         self.worker_plot.signals.result.connect(self.page.update_plot)
     216           1 :         self.worker_plot.start()
     217             : 
     218           1 :     def on_qnitem_changed(self, atom: int) -> None:
     219           1 :         super().on_qnitem_changed(atom)
     220           1 :         if hasattr(self, "item_temperature"):  # not yet initialized the first time this method is called
     221           1 :             self.update_lifetime_label()
     222             : 
     223             : 
     224           1 : class KetConfigTwoAtoms(KetConfig):
     225           1 :     def setupWidget(self) -> None:
     226           1 :         super().setupWidget()
     227             : 
     228           1 :         self.layout().addWidget(QLabel("<b>Atom 1</b>"))
     229           1 :         self.setupOneKetAtom()
     230           1 :         self.layout().addSpacing(15)
     231             : 
     232           1 :         self.layout().addWidget(QLabel("<b>Atom 2</b>"))
     233           1 :         self.setupOneKetAtom()
     234             : 
     235             : 
     236           1 : class QnBase(WidgetV):
     237             :     """Base class for quantum number configuration."""
     238             : 
     239           1 :     margin = (10, 0, 10, 0)
     240           1 :     spacing = 5
     241             : 
     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 :     @classmethod
     249           1 :     def from_species(cls, species: str, parent: Optional[QWidget] = None) -> "QnBase":
     250             :         """Create a quantum number configuration from the species name."""
     251           1 :         species_type = get_species_type(species)
     252           1 :         if species_type == "sqdt_duplet":
     253           1 :             return QnSQDT(parent, s_type="halfint", s=0.5)
     254           0 :         if species_type == "sqdt_singlet":
     255           0 :             return QnSQDT(parent, s_type="int", s=0)
     256           0 :         if species_type == "sqdt_triplet":
     257           0 :             return QnSQDT(parent, s_type="int", s=1)
     258             : 
     259           0 :         element = species.split("_")[0]
     260           0 :         if species_type == "mqdt_halfint":
     261           0 :             i = CORE_SPIN_DICT.get(element, 0.5)
     262           0 :             return QnMQDT(parent, f_type="halfint", i=i)
     263           0 :         if species_type == "mqdt_int":
     264           0 :             i = CORE_SPIN_DICT.get(element, 0)
     265           0 :             return QnMQDT(parent, f_type="int", i=i)
     266             : 
     267           0 :         raise ValueError(f"Unknown species type: {species_type}")
     268             : 
     269             : 
     270           1 : class QnSQDT(QnBase):
     271             :     """Configuration for atoms using SQDT."""
     272             : 
     273           1 :     def __init__(
     274             :         self,
     275             :         parent: Optional[QWidget] = None,
     276             :         *,
     277             :         s_type: Literal["int", "halfint"],
     278             :         s: float,
     279             :     ) -> None:
     280           1 :         assert s_type in ("int", "halfint"), "s_type must be int or halfint"
     281           1 :         self.s_type = s_type
     282           1 :         self.s = s
     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=80, tooltip="Principal quantum number n")
     288           1 :         self.items["l"] = QnItemInt(self, "l", vmin=0, tooltip="Orbital angular momentum l")
     289             : 
     290           1 :         s = self.s
     291           1 :         if self.s_type == "int":
     292           0 :             if s != 0:
     293           0 :                 self.items["j"] = QnItemInt(self, "j", vmin=int(s), vdefault=int(s), tooltip="Total angular momentum j")
     294           0 :             self.items["m"] = QnItemInt(self, "m", vmin=-999, vmax=999, vdefault=0, tooltip="Magnetic quantum number m")
     295             :         else:
     296           1 :             self.items["j"] = QnItemHalfInt(self, "j", vmin=s, vdefault=s, tooltip="Total angular momentum j")
     297           1 :             self.items["m"] = QnItemHalfInt(
     298             :                 self, "m", vmin=-999.5, vmax=999.5, vdefault=0.5, tooltip="Magnetic quantum number m"
     299             :             )
     300             : 
     301             : 
     302           1 : class QnMQDT(QnBase):
     303             :     """Configuration for alkaline-earth atoms using MQDT."""
     304             : 
     305           1 :     def __init__(
     306             :         self,
     307             :         parent: Optional[QWidget] = 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.f_type = f_type
     314           0 :         self.i = i
     315           0 :         self.items = {}
     316           0 :         super().__init__(parent)
     317             : 
     318           1 :     def setupWidget(self) -> None:
     319           0 :         self.items["nu"] = QnItemDouble(
     320             :             self, "nu", vmin=1, vdefault=80, vstep=1, tooltip="Effective principal quantum number nu"
     321             :         )
     322           0 :         self.items["s"] = QnItemDouble(self, "s", vmin=0, vmax=1, vstep=0.1, tooltip="Spin s")
     323             : 
     324           0 :         if self.i == 0:
     325           0 :             key = "j"
     326           0 :             description = "Total angular momentum j (j=f for I=0)"
     327             :         else:
     328           0 :             key = "f"
     329           0 :             description = "Total angular momentum f"
     330           0 :         if self.f_type == "int":
     331           0 :             self.items[key] = QnItemInt(self, key, vmin=0, vdefault=int(self.i), tooltip=description)
     332           0 :             self.items["m"] = QnItemInt(self, "m", vmin=-999, vmax=999, vdefault=0, tooltip="Magnetic quantum number m")
     333             :         else:
     334           0 :             self.items[key] = QnItemHalfInt(self, key, vmin=0.5, vdefault=self.i, tooltip=description)
     335           0 :             self.items["m"] = QnItemHalfInt(
     336             :                 self, "m", vmin=-999.5, vmax=999.5, vdefault=0.5, tooltip="Magnetic quantum number m"
     337             :             )
     338             : 
     339           0 :         self.items["l_ryd"] = QnItemDouble(
     340             :             self,
     341             :             "l_ryd",
     342             :             vmin=0,
     343             :             vstep=1,
     344             :             tooltip="Orbital angular momentum l_ryd of the Rydberg electron",
     345             :             checked=False,
     346             :         )

Generated by: LCOV version 1.16