LCOV - code coverage report
Current view: top level - tests - test_gui.py (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 153 153
Test Date: 2026-11-02 09:44:19 Functions: 100.0 % 11 11

            Line data    Source code
       1              : # SPDX-FileCopyrightText: 2024 PairInteraction Developers
       2              : # SPDX-License-Identifier: LGPL-3.0-or-later
       3              : 
       4            1 : from __future__ import annotations
       5              : 
       6            1 : from typing import TYPE_CHECKING, Any, Literal
       7              : 
       8            1 : import numpy as np
       9            1 : import pytest
      10            1 : from pairinteraction_gui.main_window import MainWindow
      11              : 
      12            1 : from .utils import REFERENCE_PATHS, compare_eigensystem_to_reference, no_log_propagation
      13              : 
      14              : if TYPE_CHECKING:
      15              :     from pathlib import Path
      16              : 
      17              :     from pairinteraction_gui.page import LifetimesPage, OneAtomPage
      18              :     from pairinteraction_gui.page.two_atoms_page import TwoAtomsPage
      19              :     from pytestqt.qtbot import QtBot
      20              : 
      21              : 
      22            1 : @pytest.fixture
      23            1 : def base_window(qtbot: QtBot, tmp_path: Path) -> MainWindow:
      24            1 :     window = MainWindow(cache_dir=tmp_path)
      25            1 :     window.show()
      26            1 :     qtbot.addWidget(window)
      27            1 :     return window
      28              : 
      29              : 
      30            1 : @pytest.fixture
      31            1 : def window_starkmap(base_window: MainWindow) -> MainWindow:
      32            1 :     one_atom_page: OneAtomPage = base_window.stacked_pages.getNamedWidget("OneAtomPage")  # type: ignore [assignment]
      33            1 :     one_atom_page.ket_config.species_combo_list[0].setCurrentText("Rb")
      34            1 :     ket_qn = one_atom_page.ket_config.stacked_qn_list[0].currentWidget()
      35            1 :     ket_qn.items["n"].setValue(60)
      36            1 :     ket_qn.items["l"].setValue(0)
      37            1 :     ket_qn.items["m"].setValue(0.5)
      38              : 
      39            1 :     basis_qn = one_atom_page.basis_config.stacked_basis_list[0].currentWidget()
      40            1 :     basis_qn.items["n"].setValue(2)
      41            1 :     basis_qn.items["l"].setValue(2)
      42            1 :     basis_qn.items["m"].setChecked(False)
      43              : 
      44            1 :     calculation_config = one_atom_page.calculation_config
      45            1 :     calculation_config.steps.setValue(11)
      46            1 :     system_config = one_atom_page.system_config
      47            1 :     system_config.Ez.spinboxes[1].setValue(10)
      48              : 
      49            1 :     return base_window
      50              : 
      51              : 
      52            1 : @pytest.fixture
      53            1 : def window_pair_potential(base_window: MainWindow) -> MainWindow:
      54            1 :     two_atoms_page: TwoAtomsPage = base_window.stacked_pages.getNamedWidget("TwoAtomsPage")  # type: ignore [assignment]
      55            1 :     two_atoms_page.ket_config.species_combo_list[0].setCurrentText("Rb")
      56            1 :     for ket_qn_stacked in two_atoms_page.ket_config.stacked_qn_list:
      57            1 :         ket_qn = ket_qn_stacked.currentWidget()
      58            1 :         ket_qn.items["n"].setValue(60)
      59            1 :         ket_qn.items["l"].setValue(0)
      60            1 :         ket_qn.items["m"].setValue(0.5)
      61              : 
      62            1 :     for basis_qn_stacked in two_atoms_page.basis_config.stacked_basis_list:
      63            1 :         basis_qn = basis_qn_stacked.currentWidget()
      64            1 :         basis_qn.items["n"].setValue(2)
      65            1 :         basis_qn.items["l"].setValue(2)
      66            1 :         basis_qn.items["m"].setChecked(False)
      67              : 
      68            1 :     two_atoms_page.basis_config.pair_delta_energy.setValue(3)
      69            1 :     two_atoms_page.basis_config.pair_m_range.setValues(1, 1)
      70              : 
      71            1 :     calculation_config = two_atoms_page.calculation_config
      72            1 :     calculation_config.steps.setValue(5)
      73            1 :     system_config = two_atoms_page.system_config
      74            1 :     system_config.distance.setValues(1, 5)
      75            1 :     return base_window
      76              : 
      77              : 
      78            1 : @pytest.fixture
      79            1 : def window_lifetimes(base_window: MainWindow) -> MainWindow:
      80            1 :     lifetimes_page: LifetimesPage = base_window.stacked_pages.getNamedWidget("LifetimesPage")  # type: ignore [assignment]
      81            1 :     lifetimes_page.ket_config.species_combo_list[0].setCurrentText("Rb")
      82            1 :     ket_qn = lifetimes_page.ket_config.stacked_qn_list[0].currentWidget()
      83            1 :     ket_qn.items["n"].setValue(60)
      84            1 :     ket_qn.items["l"].setValue(0)
      85            1 :     ket_qn.items["m"].setValue(0.5)
      86            1 :     lifetimes_page.ket_config.item_temperature.setValue(300)
      87            1 :     return base_window
      88              : 
      89              : 
      90            1 : def test_main_window_basic(qtbot: QtBot, window_starkmap: MainWindow) -> None:
      91              :     """Test basic main window functionality."""
      92            1 :     one_atom_page: OneAtomPage = window_starkmap.stacked_pages.getNamedWidget("OneAtomPage")  # type: ignore [assignment]
      93            1 :     qn_item = one_atom_page.ket_config.stacked_qn_list[0].currentWidget().items["n"]
      94            1 :     qn_item.setValue(60)
      95              : 
      96            1 :     ket_label = one_atom_page.ket_config.ket_label_list[0].text()
      97            1 :     assert all(x in ket_label for x in ["Rb", "60", "S", "1/2"])
      98            1 :     assert qn_item.label.text() == "n"
      99            1 :     assert qn_item.value() == 60
     100              : 
     101            1 :     qn_item.setValue(61)
     102            1 :     ket_label = one_atom_page.ket_config.ket_label_list[0].text()
     103            1 :     assert qn_item.value() == 61
     104            1 :     assert all(x in ket_label for x in ["Rb", "61", "S", "1/2"])
     105              : 
     106              :     # make the basis smaller for faster test
     107            1 :     basis_qn = one_atom_page.basis_config.stacked_basis_list[0].currentWidget()
     108            1 :     basis_qn.items["n"].setValue(1)
     109            1 :     basis_qn.items["l"].setValue(1)
     110            1 :     basis_qn.items["m"].setValue(0)
     111              : 
     112            1 :     one_atom_page.calculate_and_abort.getNamedWidget("Calculate").click()
     113            1 :     qtbot.waitUntil(lambda: one_atom_page._calculation_finished, timeout=30_000)  # ci macOS-15-intel is very slow
     114            1 :     qtbot.waitUntil(lambda: one_atom_page._plot_finished, timeout=5_000)
     115            1 :     window_starkmap.close()
     116              : 
     117              : 
     118            1 : def test_one_atom_page(window_starkmap: MainWindow) -> None:
     119            1 :     _test_calculate_page(window_starkmap, "OneAtomPage", "stark_map")
     120              : 
     121              : 
     122            1 : def test_two_atoms_page(window_pair_potential: MainWindow) -> None:
     123            1 :     _test_calculate_page(window_pair_potential, "TwoAtomsPage", "pair_potential")
     124              : 
     125              : 
     126            1 : def test_lifetimes_page(window_lifetimes: MainWindow) -> None:
     127            1 :     page: LifetimesPage = window_lifetimes.stacked_pages.getNamedWidget("LifetimesPage")  # type: ignore [assignment]
     128            1 :     with no_log_propagation("cpp"):  # suppress low n state warnings from lifetime calculation
     129            1 :         parameters, results = page.calculate()
     130            1 :     page.plotwidget.plot(parameters, results)
     131              : 
     132            1 :     assert results.lifetime > 0
     133            1 :     assert len(results.kets_sp) == len(results.transition_rates_sp)
     134            1 :     assert len(results.kets_bbr) == len(results.transition_rates_bbr)
     135              : 
     136            1 :     python_code = page._create_python_code()
     137            1 :     python_code = python_code.replace("plt.show()", "plt.close()")  # HACK, otherwise it will block the test
     138              : 
     139            1 :     locals_globals: dict[str, Any] = {}
     140            1 :     with no_log_propagation("cpp"):  # suppress low n state warnings from lifetime calculation
     141            1 :         exec(python_code, locals_globals, locals_globals)  # noqa: S102
     142              : 
     143            1 :     rates_dict = {
     144              :         key: [sum(r for _, r in page.plotwidget.sorted_rates[label][n]) for n in locals_globals["n_list"]]
     145              :         for key, label in [("SP", "Spontaneous Decay"), ("BBR", "Black Body Radiation")]
     146              :     }
     147              : 
     148            1 :     assert np.isclose(locals_globals["lifetime"], results.lifetime)
     149            1 :     for key, rates in rates_dict.items():
     150            1 :         assert np.allclose(locals_globals["rates_summed"][key], rates)
     151              : 
     152              : 
     153            1 : def _test_calculate_page(
     154              :     window: MainWindow,
     155              :     page_name: Literal["OneAtomPage", "TwoAtomsPage"],
     156              :     reference_name: str,
     157              : ) -> None:
     158            1 :     page: OneAtomPage | TwoAtomsPage = window.stacked_pages.getNamedWidget(page_name)  # type: ignore [assignment]
     159            1 :     ket_energy_0 = sum(page.ket_config.get_ket_atom(i).get_energy("GHz") for i in range(page.ket_config.n_atoms))
     160              : 
     161              :     # Test calculation with fast mode off
     162            1 :     page.calculation_config.fast_mode.setChecked(False)
     163            1 :     _parameters, results = page.calculate()
     164            1 :     energies = np.array(results.energies) + ket_energy_0
     165            1 :     compare_eigensystem_to_reference(REFERENCE_PATHS[reference_name], energies, np.array(results.ket_overlaps))
     166              : 
     167              :     # Test calculation with fast mode on and diagonalze energy_range
     168              :     # NOTE: with fast mode, the overlaps are different, so we don't compare them
     169            1 :     page.calculation_config.fast_mode.setChecked(True)
     170            1 :     page.calculation_config.energy_range.setChecked(True)
     171            1 :     page.calculation_config.energy_range.setValues(-200, 200)
     172            1 :     parameters, results = page.calculate()
     173            1 :     energies = np.array(results.energies) + ket_energy_0
     174            1 :     compare_eigensystem_to_reference(REFERENCE_PATHS[reference_name], energies)
     175            1 :     page.plotwidget.plot(parameters, results)
     176              : 
     177            1 :     if page_name == "TwoAtomsPage":
     178              :         # Test fitting of c3/c6/c3+c6 potential curves
     179              :         # We need to pass the data to the plotwidget.
     180            1 :         page.plotwidget.fit("c3")
     181              :         # We run a test fit twice, as it should iterate through the potential curves
     182            1 :         page.plotwidget.fit("c3")
     183            1 :         page.plotwidget.fit("c6")
     184            1 :         page.plotwidget.fit("c3+c6")
     185              :         # One could consider adding some return values to check if the estimated values are reasonable.
     186              :         # Currently fit does not return anything, it just plots and shows the fit parameters in UI
     187              : 
     188              :     # Test export to Python code
     189            1 :     python_code = page._create_python_code()
     190            1 :     python_code = python_code.replace("plt.show()", "plt.close()")  # HACK, otherwise it will block the test
     191              : 
     192              :     # HACK, see also https://stackoverflow.com/questions/45132645/list-comprehension-in-exec-with-empty-locals-nameerror
     193            1 :     locals_globals: dict[str, Any] = {}
     194            1 :     exec(python_code, locals_globals, locals_globals)  # noqa: S102
     195            1 :     energies = np.array(locals_globals["energies_list"]) + ket_energy_0
     196            1 :     compare_eigensystem_to_reference(REFERENCE_PATHS[reference_name], energies)
     197              : 
     198              : 
     199            1 : def test_autosave_skips_unchanged_settings(base_window: MainWindow) -> None:
     200              :     """The periodic autosave must not rewrite the ini file while the user is idle."""
     201            1 :     one_atom_page: OneAtomPage = base_window.stacked_pages.getNamedWidget("OneAtomPage")  # type: ignore [assignment]
     202            1 :     ket_qn = one_atom_page.ket_config.stacked_qn_list[0].currentWidget()
     203              : 
     204            1 :     assert base_window.save_settings()  # the first save writes the initial state
     205              : 
     206            1 :     for _ in range(3):  # further autosave ticks without user interaction must not touch the file
     207            1 :         assert not base_window.save_settings()
     208              : 
     209            1 :     ket_qn.items["n"].setValue(123)
     210            1 :     assert base_window.save_settings()
     211            1 :     assert base_window.settings_manager.value("OneAtomPage/ket_config/atom0_n", value_type=int) == 123
     212              : 
     213            1 :     base_window.close()
     214              : 
     215              : 
     216            1 : def test_save_and_restore_settings(qtbot: QtBot, tmp_path: Path) -> None:
     217            1 :     window = MainWindow(cache_dir=tmp_path)
     218            1 :     window.show()
     219            1 :     qtbot.addWidget(window)
     220            1 :     one_atom_page: OneAtomPage = window.stacked_pages.getNamedWidget("OneAtomPage")  # type: ignore [assignment]
     221            1 :     ket_qn = one_atom_page.ket_config.stacked_qn_list[0].currentWidget()
     222            1 :     ket_qn.items["n"].setValue(222)
     223            1 :     ket_qn.items["l"].setValue(999)
     224            1 :     window.close()
     225              : 
     226            1 :     window = MainWindow(cache_dir=tmp_path)
     227            1 :     window.show()
     228            1 :     qtbot.addWidget(window)
     229            1 :     one_atom_page = window.stacked_pages.getNamedWidget("OneAtomPage")  # type: ignore [assignment]
     230            1 :     ket_qn = one_atom_page.ket_config.stacked_qn_list[0].currentWidget()
     231              : 
     232            1 :     assert ket_qn.items["n"].value() == 222
     233            1 :     assert ket_qn.items["l"].value() == 999
     234              : 
     235            1 :     window.close()
        

Generated by: LCOV version 2.0-1