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

            Line data    Source code
       1              : # SPDX-FileCopyrightText: 2025 PairInteraction Developers
       2              : # SPDX-License-Identifier: LGPL-3.0-or-later
       3              : 
       4            1 : from __future__ import annotations
       5              : 
       6            1 : import logging
       7            1 : from typing import TYPE_CHECKING
       8              : 
       9            1 : import numpy as np
      10            1 : import pytest
      11            1 : from pairinteraction.perturbative.perturbation_theory import calculate_perturbative_hamiltonian
      12            1 : from scipy import sparse
      13              : 
      14            1 : from .utils import no_log_propagation
      15              : 
      16              : if TYPE_CHECKING:
      17              :     from pairinteraction import SystemPair
      18              :     from scipy.sparse import csr_matrix
      19              : 
      20              :     from .utils import PairinteractionModule
      21              : 
      22              : 
      23            1 : def _check_sparse_matrices_equal(matrix_a: csr_matrix, matrix_b: csr_matrix) -> bool:
      24              :     """Check for equality of sparse matrices efficiently.
      25              : 
      26              :     This functions compares two sparse matrices in compressed sparse row format on their equality.
      27              : 
      28              :     Args:
      29              :         matrix_a: A sparse matrix in csr format.
      30              :         matrix_b: A sparse matrix in csr format.
      31              : 
      32              :     Returns:
      33              :         bool: True if matrices are equal, False if not.
      34              : 
      35              :     """
      36            1 :     matrix_a.sort_indices()
      37            1 :     matrix_b.sort_indices()
      38            1 :     if not (
      39              :         matrix_a.format == "csr"
      40              :         and matrix_b.format == "csr"
      41              :         and len(matrix_a.indices) == len(matrix_b.indices)
      42              :         and len(matrix_a.indptr) == len(matrix_b.indptr)
      43              :         and len(matrix_a.data) == len(matrix_b.data)
      44              :     ):
      45            0 :         return False
      46              : 
      47            1 :     return bool(
      48              :         np.all(matrix_a.indices == matrix_b.indices)
      49              :         and np.all(matrix_a.indptr == matrix_b.indptr)
      50              :         and np.allclose(matrix_a.data, matrix_b.data, rtol=0, atol=1e-14)
      51              :     )
      52              : 
      53              : 
      54            1 : @pytest.fixture
      55            1 : def system_pair_sample(pi_module: PairinteractionModule) -> SystemPair:
      56            1 :     basis = pi_module.BasisAtom(
      57              :         species="Rb",
      58              :         n=(59, 63),
      59              :         l=(0, 1),
      60              :         m=(-1.5, 1.5),
      61              :     )
      62            1 :     system = pi_module.SystemAtom(basis=basis)
      63            1 :     system.set_diamagnetism_enabled(False)
      64            1 :     system.set_magnetic_field([0, 0, 1e-3], "gauss")
      65            1 :     pi_module.diagonalize([system], diagonalizer="eigen")
      66            1 :     basis_pair = pi_module.BasisPair([system, system], symmetrize=False)
      67            1 :     system_pair = pi_module.SystemPair(basis_pair)
      68            1 :     theta = 0
      69            1 :     r = 12
      70            1 :     system_pair.set_distance_vector(r * np.array([np.sin(theta), 0, np.cos(theta)]), "micrometer")
      71            1 :     system_pair.set_interaction_order(3)
      72            1 :     return system_pair
      73              : 
      74              : 
      75            1 : def test_perturbative_calculation1(caplog: pytest.LogCaptureFixture) -> None:
      76              :     """Test of mathematic functionality."""
      77            1 :     hamiltonian = sparse.csr_matrix(
      78              :         [[0, 1, 1, 0, 2], [1, 1, 0, 1, 3], [1, 0, 10, 1, 0], [0, 1, 1, 11, 1], [2, 3, 0, 1, 12]]
      79              :     )
      80            1 :     model_space_indices = [0, 1]
      81              : 
      82              :     # Order 0
      83            1 :     h_eff_dict, eig_perturb = calculate_perturbative_hamiltonian(hamiltonian, model_space_indices, perturbation_order=0)
      84            1 :     h_eff = sum(h for h in h_eff_dict.values())
      85            1 :     assert np.any(h_eff == np.array([[0, 0], [0, 1]]))
      86            1 :     assert _check_sparse_matrices_equal(eig_perturb, sparse.csr_matrix(sparse.eye(2, 5, k=0)))
      87              : 
      88              :     # Order 1
      89            1 :     h_eff_dict, eig_perturb = calculate_perturbative_hamiltonian(hamiltonian, model_space_indices, perturbation_order=1)
      90            1 :     h_eff = sum(h for h in h_eff_dict.values())
      91            1 :     assert np.any(h_eff == np.array([[0, 1], [1, 1]]))
      92            1 :     assert _check_sparse_matrices_equal(eig_perturb, sparse.csr_matrix([[1, 0, 0, 0, 0], [0, 1, 0, 0, 0]]))
      93              : 
      94              :     # Order 2
      95            1 :     h_eff_dict, eig_perturb = calculate_perturbative_hamiltonian(hamiltonian, model_space_indices, perturbation_order=2)
      96            1 :     h_eff = sum(h for h in h_eff_dict.values())
      97            1 :     a_00 = 0 + 1 * 1 / (0 - 10) + 2 * 2 / (0 - 12)
      98            1 :     a_01 = 1 + 2 * 3 / (0 - 12)
      99            1 :     a_10 = 1 + 3 * 2 / (1 - 12)
     100            1 :     a_11 = 1 + 1 * 1 / (1 - 11) + 3 * 3 / (1 - 12)
     101            1 :     hamiltonian_new = np.array([[a_00, (a_01 + a_10) / 2], [(a_01 + a_10) / 2, a_11]])
     102            1 :     assert np.any(h_eff == hamiltonian_new)
     103              : 
     104            1 :     v0 = sparse.eye(1, 5, k=0) + 1 / (0 - 10) * sparse.eye(1, 5, k=2) + 2 / (0 - 12) * sparse.eye(1, 5, k=4)
     105            1 :     v1 = sparse.eye(1, 5, k=1) + 1 / (1 - 11) * sparse.eye(1, 5, k=3) + 3 / (1 - 12) * sparse.eye(1, 5, k=4)
     106            1 :     assert _check_sparse_matrices_equal(eig_perturb, sparse.csr_matrix(sparse.vstack([v0, v1])))
     107              : 
     108              :     # Order 3
     109            1 :     with caplog.at_level(logging.ERROR):
     110            1 :         h_eff_dict, eig_perturb = calculate_perturbative_hamiltonian(
     111              :             hamiltonian, model_space_indices, perturbation_order=3
     112              :         )
     113            1 :     h_eff = sum(h for h in h_eff_dict.values())
     114            1 :     a_00 -= 2 * 3 * 1 / ((0 - 12) * (1 - 12))
     115            1 :     a_01 += (
     116              :         1 * 1 * 1 / ((1 - 10) * (1 - 11))
     117              :         + 2 * 1 * 1 / ((1 - 11) * (1 - 12))
     118              :         - 1 * 1 * 1 / ((0 - 10) * (1 - 10))
     119              :         - 2 * 2 * 1 / ((1 - 12) * (0 - 12))
     120              :     )
     121            1 :     a_10 += (
     122              :         1 * 1 * 1 / ((0 - 10) * (0 - 11))
     123              :         + 2 * 1 * 1 / ((0 - 11) * (0 - 12))
     124              :         - 1 * 1 * 1 / ((0 - 11) * (1 - 11))
     125              :         - 3 * 3 * 1 / ((0 - 12) * (1 - 12))
     126              :     )
     127            1 :     a_11 += 1 * 1 * 3 / ((1 - 11) * (1 - 12)) + 3 * 1 * 1 / ((1 - 11) * (1 - 12)) - 3 * 2 * 1 / ((1 - 12) * (0 - 12))
     128            1 :     hamiltonian_new = np.array([[a_00, (a_01 + a_10) / 2], [(a_01 + a_10) / 2, a_11]])
     129            1 :     assert np.any(h_eff == hamiltonian_new)
     130              : 
     131            1 :     v0 = v0 + (
     132              :         -0.5 * (1 * 1 / (0 - 10) ** 2 + 2 * 2 / (0 - 12) ** 2) * sparse.eye(1, 5, k=0)
     133              :         + (1 * 1 / ((0 - 10) * (0 - 11)) + 2 * 1 / ((0 - 11) * (0 - 12))) * sparse.eye(1, 5, k=3)
     134              :         + 1 * 1 / ((0 - 11) * (0 - 1)) * sparse.eye(1, 5, k=3)
     135              :         + 3 * 1 / ((0 - 1) * (0 - 12)) * sparse.eye(1, 5, k=4)
     136              :         + 3 * 2 / ((0 - 1) * (0 - 12)) * sparse.eye(1, 5, k=1)
     137              :     )
     138            1 :     v1 = v1 + (
     139              :         -0.5 * (1 * 1 / (1 - 11) ** 2 + 3 * 3 / (1 - 12) ** 2) * sparse.eye(1, 5, k=1)
     140              :         + 1 * 1 / ((1 - 10) * (1 - 11)) * sparse.eye(1, 5, k=2)
     141              :         + 1 * 3 / ((1 - 11) * (1 - 12)) * sparse.eye(1, 5, k=3)
     142              :         + 1 * 1 / ((1 - 11) * (1 - 12)) * sparse.eye(1, 5, k=4)
     143              :         + 1 * 1 / ((1 - 0) * (1 - 10)) * sparse.eye(1, 5, k=2)
     144              :         + 2 * 1 / ((1 - 0) * (1 - 12)) * sparse.eye(1, 5, k=4)
     145              :         + 3 * 2 / ((1 - 0) * (1 - 12)) * sparse.eye(1, 5, k=0)
     146              :     )
     147            1 :     assert _check_sparse_matrices_equal(eig_perturb, sparse.csr_matrix(sparse.vstack([v0, v1])))
     148              : 
     149              : 
     150            1 : def test_c3_with_sample_system(pi_module: PairinteractionModule, system_pair_sample: SystemPair) -> None:
     151              :     """Test whether the C3 coefficient with a given system is calculated correctly."""
     152            1 :     ket1 = pi_module.KetAtom("Rb", n=61, l=0, j=0.5, m=0.5)
     153            1 :     ket2 = pi_module.KetAtom("Rb", n=61, l=1, j=1.5, m=0.5)
     154            1 :     c3_obj = pi_module.C3(ket1, ket2)
     155            1 :     c3_obj._distance_vector = None  # avoid warning due when setting system pair
     156            1 :     c3_obj.system_pair = system_pair_sample
     157              : 
     158            1 :     c3 = c3_obj.get(unit="planck_constant * gigahertz * micrometer^3")
     159            1 :     assert np.isclose(-0.5 * c3, 3.1515)
     160              : 
     161              : 
     162            1 : def test_c3(pi_module: PairinteractionModule) -> None:
     163              :     """Test whether the C3 coefficient with automatically constructed system is calculated correctly."""
     164            1 :     ket1 = pi_module.KetAtom("Rb", n=61, l=0, j=0.5, m=0.5)
     165            1 :     ket2 = pi_module.KetAtom("Rb", n=61, l=1, j=1.5, m=0.5)
     166            1 :     c3_obj = pi_module.C3(ket1, ket2)
     167              : 
     168            1 :     c3_obj.set_electric_field([0, 0, 0], "volt/cm")
     169            1 :     c3_obj.set_magnetic_field([0, 0, 10], "gauss")
     170              : 
     171            1 :     c3 = c3_obj.get(unit="planck_constant * gigahertz * micrometer^3")
     172            1 :     assert np.isclose(-0.5 * c3, 3.2188)
     173              : 
     174              : 
     175            1 : def test_c6_with_sample_system(pi_module: PairinteractionModule, system_pair_sample: SystemPair) -> None:
     176              :     """Test whether the C6 coefficient with a given system is calculated correctly."""
     177            1 :     ket = pi_module.KetAtom(species="Rb", n=61, l=0, j=0.5, m=0.5)
     178            1 :     c6_obj = pi_module.C6(ket, ket)
     179            1 :     c6_obj._distance_vector = None  # avoid warning due when setting system pair
     180            1 :     c6_obj.system_pair = system_pair_sample
     181              : 
     182            1 :     c6 = c6_obj.get(unit="planck_constant * gigahertz * micrometer^6")
     183            1 :     assert np.isclose(c6, -167.880)
     184              : 
     185              : 
     186            1 : def test_c6(pi_module: PairinteractionModule) -> None:
     187              :     """Test whether the C6 coefficient with automatically constructed system is calculated correctly."""
     188            1 :     ket = pi_module.KetAtom(species="Rb", n=61, l=0, j=0.5, m=0.5)
     189            1 :     c6_obj = pi_module.C6(ket, ket)
     190              : 
     191            1 :     c6_obj.set_electric_field([0, 0, 0], "volt/cm")
     192            1 :     c6_obj.set_magnetic_field([0, 0, 10], "gauss")
     193              : 
     194            1 :     c6 = c6_obj.get(unit="planck_constant * gigahertz * micrometer^6")
     195            1 :     assert np.isclose(c6, -169.135)
     196              : 
     197              : 
     198            1 : def test_symmetrized_basis_pair_is_rejected(pi_module: PairinteractionModule) -> None:
     199              :     """Test that a symmetrized basis pair cannot be used for the effective Hamiltonian."""
     200            1 :     ket1 = pi_module.KetAtom("Rb", n=61, l=0, j=0.5, m=0.5)
     201            1 :     ket2 = pi_module.KetAtom("Rb", n=61, l=1, j=1.5, m=0.5)
     202            1 :     system = pi_module.SystemAtom(pi_module.BasisAtom("Rb", n=(60, 62), l=(0, 1)))
     203            1 :     pi_module.diagonalize([system], diagonalizer="eigen")
     204            1 :     basis_pair = pi_module.BasisPair.from_kets(
     205              :         [(ket1, ket2)], system_atoms=(system, system), delta_energy=3, delta_energy_unit="GHz", symmetrize=True
     206              :     )
     207              : 
     208            1 :     eff_system = pi_module.EffectiveSystemPair([(ket1, ket2), (ket2, ket1)])
     209            1 :     with pytest.raises(ValueError, match="must not be symmetrized"):
     210            1 :         eff_system.basis_pair = basis_pair
     211            1 :     with pytest.raises(ValueError, match="must not be symmetrized"):
     212            1 :         eff_system.system_pair = pi_module.SystemPair(basis_pair)
     213              : 
     214              : 
     215            1 : def test_exact_resonance_detection(
     216              :     pi_module: PairinteractionModule, system_pair_sample: SystemPair, capsys: pytest.CaptureFixture[str]
     217              : ) -> None:
     218              :     """Test whether resonance with infinite admixture is correctly detected."""
     219            1 :     ket1 = pi_module.KetAtom("Rb", n=61, l=0, j=0.5, m=0.5)
     220            1 :     ket2 = pi_module.KetAtom("Rb", n=61, l=1, j=1.5, m=0.5)
     221            1 :     eff_system = pi_module.EffectiveSystemPair([(ket1, ket2)])
     222            1 :     eff_system.system_pair = system_pair_sample
     223              : 
     224              :     # workaround to test for errors, without showing them in the std output
     225            1 :     with no_log_propagation("pairinteraction"), np.errstate(invalid="ignore"):
     226            1 :         eff_system.get_effective_hamiltonian()
     227            1 :     captured = capsys.readouterr()
     228            1 :     assert "gets a large dressing (inf overlap)" in captured.err
     229            1 :     assert "|Rb:61,P_3/2,1/2; Rb:61,S_1/2,1/2⟩" in captured.err
     230              : 
     231              : 
     232            1 : def test_near_resonance_detection(pi_module: PairinteractionModule, capsys: pytest.CaptureFixture[str]) -> None:
     233              :     """Test whether a near resonance is correctly detected."""
     234            1 :     ket1 = pi_module.KetAtom("Rb", n=60, l=0, j=0.5, m=0.5)
     235            1 :     ket2 = pi_module.KetAtom("Rb", n=61, l=0, j=0.5, m=0.5)
     236            1 :     eff_system = pi_module.EffectiveSystemPair([(ket1, ket2), (ket2, ket1)])
     237            1 :     eff_system.set_magnetic_field([0, 0, 245], "gauss")
     238            1 :     eff_system.set_distance(10, 35.1, "micrometer")
     239              : 
     240              :     # workaround to test for errors, without showing them in the std output
     241            1 :     with no_log_propagation("pairinteraction"):
     242            1 :         eff_system.create_basis_pair(number_of_kets=100)
     243            1 :         eff_system.get_effective_hamiltonian()
     244            1 :         eff_system.check_for_resonances(0.01)
     245            1 :     captured = capsys.readouterr()
     246            1 :     assert "The most perturbing states are" in captured.err
     247            1 :     assert "Rb:60,P_3/2,1/2; Rb:60,P_3/2,3/2" in captured.err
        

Generated by: LCOV version 2.0-1