LCOV - code coverage report
Current view: top level - src/pairinteraction_gui/config - ket_config.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 157 175 89.7 %
Date: 2025-06-06 09:09:03 Functions: 18 44 40.9 %

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

Generated by: LCOV version 1.16