LCOV - code coverage report
Current view: top level - src/pairinteraction_gui/config - ket_config.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 171 207 82.6 %
Date: 2026-10-20 16:32:22 Functions: 21 24 87.5 %

          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             :         )

Generated by: LCOV version 1.16