LCOV - code coverage report
Current view: top level - src/pairinteraction/perturbative - perturbative.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 113 146 77.4 %
Date: 2026-08-14 15:26:44 Functions: 7 7 100.0 %

          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 : import logging
       6           1 : from collections.abc import Iterable
       7           1 : from typing import TYPE_CHECKING, overload
       8             : 
       9           1 : import numpy as np
      10           1 : from scipy import sparse
      11           1 : from scipy.sparse import csr_matrix
      12           1 : from typing_extensions import deprecated
      13             : 
      14           1 : import pairinteraction as pi_complex
      15           1 : import pairinteraction.real as pi_real
      16           1 : from pairinteraction.perturbative.perturbation_theory import calculate_perturbative_hamiltonian
      17           1 : from pairinteraction.units import QuantityArray, QuantityScalar, ureg
      18             : 
      19             : if TYPE_CHECKING:
      20             :     from collections.abc import Sequence
      21             : 
      22             :     from scipy.sparse import csr_matrix
      23             : 
      24             :     from pairinteraction.ket import KetAtomTuple, KetPairLike
      25             :     from pairinteraction.units import NDArray, PintArray, PintFloat
      26             : 
      27             :     SystemPair = pi_real.SystemPair | pi_complex.SystemPair
      28             : 
      29           1 : logger = logging.getLogger(__name__)
      30             : 
      31             : 
      32             : @overload
      33             : def get_effective_hamiltonian_from_system(
      34             :     ket_tuple_list: Sequence[KetPairLike],
      35             :     system_pair: SystemPair,
      36             :     order: int = 2,
      37             :     required_overlap: float = 0.9,
      38             :     *,
      39             :     return_only_specified_order: bool = False,
      40             :     unit: None = None,
      41             : ) -> tuple[PintArray, csr_matrix]: ...
      42             : 
      43             : 
      44             : @overload
      45             : def get_effective_hamiltonian_from_system(
      46             :     ket_tuple_list: Sequence[KetPairLike],
      47             :     system_pair: SystemPair,
      48             :     order: int = 2,
      49             :     required_overlap: float = 0.9,
      50             :     return_only_specified_order: bool = False,
      51             :     *,
      52             :     unit: str,
      53             : ) -> tuple[NDArray, csr_matrix]: ...
      54             : 
      55             : 
      56           1 : @deprecated("Use EffectiveSystemPair(ket_tuples).get_effective_hamiltonian() instead.")
      57           1 : def get_effective_hamiltonian_from_system(
      58             :     ket_tuple_list: Sequence[KetPairLike],
      59             :     system_pair: SystemPair,
      60             :     order: int = 2,
      61             :     required_overlap: float = 0.9,
      62             :     return_only_specified_order: bool = False,
      63             :     unit: str | None = None,
      64             : ) -> tuple[NDArray | PintArray, csr_matrix]:
      65             :     r"""Get the perturbative Hamiltonian at a desired order in Rayleigh-Schrödinger perturbation theory.
      66             : 
      67             :     This function takes a list of tuples of ket states, which forms the basis of the model space in which the effective
      68             :     Hamiltonian is calculated. The whole Hamiltonian is taken from a pair system.
      69             :     The Hamiltonian of the pair system is assumed to be diagonal in the unperturbed Hamiltonian, all off-diagonal
      70             :     elements are assumed to belong to the perturbative term.
      71             :     The function also checks for resonances between all states and states in the model space.
      72             : 
      73             :     Args:
      74             :         ket_tuple_list: List of all pair states that span up the model space.
      75             :             The effective Hamiltonian is calculated for these states.
      76             :         system_pair: Two-Atom-System, diagonal in the basis of the unperturbed Hamiltonian.
      77             :         order: Order up to which the perturbation theory is expanded. Support up to third order.
      78             :             Default is second order.
      79             :         required_overlap: If set, the code checks for validity of a perturbative treatment.
      80             :             Error is thrown if the perturbed eigenstate has less overlap than this value with the
      81             :             unperturbed eigenstate.
      82             :         return_only_specified_order: If True, the returned effective Hamiltonian will only contain the specified order.
      83             :             Default is False, which returns the sum of all orders up to the specified order.
      84             :         unit: The unit to which to convert the result. Default None will return a pint quantity.
      85             : 
      86             :     Returns:
      87             :         - Effective Hamiltonian as a :math:`m \times m` matrix, where m is the length of `ket_tuple_list`.
      88             :         - Eigenvectors in perturbation theory due to interaction with states out of the model space, returned as
      89             :           a sparse matrix in compressed row format. Each row represents the corresponding eigenvector.
      90             : 
      91             :     Raises:
      92             :         ValueError: If a resonance between a state in the model space and a state not in the model space occurs.
      93             : 
      94             :     """
      95           1 :     if np.isinf(system_pair.get_distance().magnitude):
      96           0 :         raise ValueError(
      97             :             "Pair system is initialized without a distance. "
      98             :             "Please set a distance for calculating an effective Hamiltonian."
      99             :         )
     100             : 
     101           1 :     model_inds = _get_model_inds(ket_tuple_list, system_pair)
     102           1 :     h_au = system_pair.get_hamiltonian().to_base_units().magnitude  # Hamiltonian in atomic units
     103           1 :     h_eff_dict_au, eigvec_perturb = calculate_perturbative_hamiltonian(h_au, model_inds, order)
     104           1 :     if not 0 <= required_overlap <= 1:
     105           0 :         raise ValueError("Required overlap has to be a positive real number between zero and one.")
     106           1 :     if required_overlap > 0:
     107           1 :         _check_for_resonances(model_inds, eigvec_perturb, system_pair, required_overlap)
     108             : 
     109           1 :     if return_only_specified_order:
     110           1 :         h_eff_au: NDArray = h_eff_dict_au[order]
     111             :     else:
     112           1 :         h_eff_au = sum(h_eff for h_eff in h_eff_dict_au.values())  # type: ignore [assignment]
     113             : 
     114           1 :     h_eff = QuantityArray.convert_au_to_user(h_eff_au, "energy", unit)
     115           1 :     return h_eff, eigvec_perturb
     116             : 
     117             : 
     118             : @overload
     119             : def get_c3_from_system(
     120             :     ket_tuple_list: Sequence[KetPairLike], system_pair: SystemPair, *, unit: None = None
     121             : ) -> PintFloat: ...
     122             : 
     123             : 
     124             : @overload
     125             : def get_c3_from_system(ket_tuple_list: Sequence[KetPairLike], system_pair: SystemPair, unit: str) -> float: ...
     126             : 
     127             : 
     128           1 : @deprecated("Use C3(ket1, ket2).get() instead.")
     129           1 : def get_c3_from_system(
     130             :     ket_tuple_list: Sequence[KetPairLike], system_pair: SystemPair, unit: str | None = None
     131             : ) -> float | PintFloat:
     132             :     r"""Calculate the :math:`C_3` coefficient for a list of two 2-tuples of single-atom ket states.
     133             : 
     134             :     This function calculates the :math:`C_3` coefficient in the desired unit. The input is a list of two 2-tuples of
     135             :     single-atom ket states. We use the convention :math:`\Delta E = \frac{C_3}{r^3}`.
     136             : 
     137             :     Args:
     138             :         ket_tuple_list: The input as a list of tuples of two states [(a,b),(c,d)],
     139             :             the :math:`C_3` coefficient is calculated for (a,b)->(c,d).
     140             :             If there are not exactly two tuples in the list, a ValueError is raised.
     141             :         system_pair: The pair system that is used for the calculation.
     142             :         unit: The unit to which to convert the result. Default None will return a pint quantity.
     143             : 
     144             :     Returns:
     145             :         The :math:`C_3` coefficient with its unit.
     146             : 
     147             :     Raises:
     148             :         ValueError: If a list of not exactly two tuples of single-atom states is given.
     149             : 
     150             :     """
     151           1 :     if len(ket_tuple_list) != 2:
     152           0 :         raise ValueError("C3 coefficient can be calculated only between two 2-atom states.")
     153             : 
     154           1 :     r = system_pair.get_distance()
     155           1 :     if np.isinf(r.magnitude):
     156           0 :         logger.warning(
     157             :             "Pair system is initialized without a distance. "
     158             :             "Calculating the C3 coefficient at a distance vector of [0, 0, 20] mum."
     159             :         )
     160           0 :         old_distance_vector = system_pair.get_distance_vector()
     161           0 :         system_pair.set_distance_vector([0, 0, 20], "micrometer")
     162           0 :         c3 = get_c3_from_system(ket_tuple_list, system_pair, unit=unit)
     163           0 :         system_pair.set_distance_vector(old_distance_vector)
     164           0 :         return c3
     165             : 
     166           1 :     h_eff, _ = get_effective_hamiltonian_from_system(ket_tuple_list, system_pair, order=1)
     167           1 :     c3_pint = h_eff[0, 1] * r**3  # type: ignore [index] # PintArray does not know it can be indexed
     168           1 :     return QuantityScalar.from_pint(c3_pint, "c3").to_pint_or_unit(unit)
     169             : 
     170             : 
     171             : @overload
     172             : def get_c6_from_system(ket_tuple: KetPairLike, system_pair: SystemPair, *, unit: None = None) -> PintFloat: ...
     173             : 
     174             : 
     175             : @overload
     176             : def get_c6_from_system(ket_tuple: KetPairLike, system_pair: SystemPair, unit: str) -> float: ...
     177             : 
     178             : 
     179           1 : @deprecated("Use C6(ket1, ket2).get() instead.")
     180           1 : def get_c6_from_system(ket_tuple: KetPairLike, system_pair: SystemPair, unit: str | None = None) -> float | PintFloat:
     181             :     r"""Calculate the :math:`C_6` coefficient for a given tuple of ket states.
     182             : 
     183             :     This function calculates the :math:`C_6` coefficient in the desired unit. The input is a 2-tuple of single-atom ket
     184             :     states.
     185             : 
     186             :     Args:
     187             :         ket_tuple: The input is a tuple repeating the same single-atom state in the format (a,a).
     188             :         If a tuple with not exactly two identical states is given, a ValueError is raised.
     189             :         system_pair: The pair system that is used for the calculation.
     190             :         unit: The unit to which to convert the result. Default None will return a pint quantity.
     191             : 
     192             :     Returns:
     193             :         The :math:`C_6` coefficient. If a unit is specified, the value in this unit is returned.
     194             : 
     195             :     Raises:
     196             :         ValueError: If a tuple with more than two single-atom states is given.
     197             : 
     198             :     """
     199           1 :     if isinstance(ket_tuple, Iterable):
     200           1 :         if len(ket_tuple) != 2:
     201           0 :             raise ValueError("C6 coefficient can be calculated only for a single 2-atom state.")
     202           1 :         if ket_tuple[0].species == ket_tuple[1].species and ket_tuple[0] != ket_tuple[1]:
     203           0 :             raise ValueError(
     204             :                 "If you want to calculate 2nd order perturbations of two different states a and b, "
     205             :                 "please use the get_effective_hamiltonian_from_system([(a,b), (b,a)], system_pair) function."
     206             :             )
     207             : 
     208           1 :     r = system_pair.get_distance()
     209           1 :     if np.isinf(r.magnitude):
     210           0 :         logger.warning(
     211             :             "Pair system is initialized without a distance. "
     212             :             "Calculating the C6 coefficient at a distance vector of [0, 0, 20] mum."
     213             :         )
     214           0 :         old_distance_vector = system_pair.get_distance_vector()
     215           0 :         system_pair.set_distance_vector([0, 0, 20], "micrometer")
     216           0 :         c6 = get_c6_from_system(ket_tuple, system_pair, unit=unit)
     217           0 :         system_pair.set_distance_vector(old_distance_vector)
     218           0 :         return c6
     219             : 
     220           1 :     h_eff, _ = get_effective_hamiltonian_from_system(
     221             :         [ket_tuple], system_pair, order=2, return_only_specified_order=True
     222             :     )
     223           1 :     c6_pint = h_eff[0, 0] * r**6  # type: ignore [index] # PintArray does not know it can be indexed
     224           1 :     return QuantityScalar.from_pint(c6_pint, "c6").to_pint_or_unit(unit)
     225             : 
     226             : 
     227           1 : def _get_model_inds(ket_tuple_list: Sequence[KetPairLike], system_pair: SystemPair) -> list[int]:
     228             :     """Get the indices of all ket tuples in the basis of pair system.
     229             : 
     230             :     This function takes a list of 2-tuples of ket states, and a pair system holding the entire basis.
     231             :     It returns an array of indices of the pair system basis in the order of the tuple list.
     232             : 
     233             :     Args:
     234             :         ket_tuple_list: List of all pair states that span up the model space.
     235             :         system_pair: Two-Atom-System, diagonal in the basis of the unperturbed Hamiltonian.
     236             : 
     237             :     Returns:
     238             :         List of indices corresponding to the states that span up the model space.
     239             : 
     240             :     """
     241           1 :     model_inds = []
     242           1 :     for kets in ket_tuple_list:
     243           1 :         overlap = system_pair.basis.get_overlaps(kets)
     244           1 :         index = np.argmax(overlap)
     245           1 :         if overlap[index] == 0:
     246           0 :             raise ValueError(f"The pairstate {kets} is not part of the basis of the pair system.")
     247           1 :         if overlap[index] < 0.5:
     248           0 :             raise ValueError(f"The pairstate {kets} cannot be identified uniquely (max overlap: {overlap[index]}).")
     249           1 :         model_inds.append(int(index))
     250           1 :     return model_inds
     251             : 
     252             : 
     253           1 : def _check_for_resonances(
     254             :     model_inds: list[int],
     255             :     eigvec_perturb: csr_matrix,
     256             :     system_pair: SystemPair,
     257             :     required_overlap: float,
     258             : ) -> None:
     259             :     r"""Check for resonance between the states in the model space and other states.
     260             : 
     261             :     This function takes the perturbed eigenvectors of the perturbation theory as an input.
     262             :     If the overlap of the perturbed eigenstate with its corresponding unperturbed state are too small,
     263             :     this function raises an error, as perturbation theory breaks down.
     264             :     In this case, it also prints all states with a relevant admixture that should therefore be also included in the
     265             :     model space, to allow perturbation theory.
     266             : 
     267             :     Args:
     268             :         model_inds: List of indices corresponding to the states that span up the model space.
     269             :         eigvec_perturb: Sparse representation of the perturbed eigenstates in the desired order of
     270             :             perturbation theory. Each row corresponds to the eigestate according to `state model indices.`
     271             :         system_pair: Two-Atom-System, diagonal in the basis of the unperturbed Hamiltonian.
     272             :         order: Order up to which the perturbation theory is expanded. Support up to third order.
     273             :             Default is second order.
     274             :         required_overlap: If set, the code checks for validity of a perturbative treatment.
     275             :             Error is thrown if the perturbed eigenstate has less overlap than this value with the unperturbed eigenstate
     276             : 
     277             :     Returns:
     278             :         Effective Hamiltonian as a :math:`m \times m` matrix, where m is the length of `ket_tuple_list`
     279             :         Eigenvectors in perturbation theory due to interaction with states out of the model space,
     280             :             returned as a sparse matrix in compressed row format. Each row represent the corresponding eigenvector
     281             : 
     282             :     Raises:
     283             :         ValueError: If a resonance between a state in the model space and a state not in the model space occurs.
     284             : 
     285             :     """
     286           1 :     overlaps = (eigvec_perturb.multiply(eigvec_perturb.conj())).real
     287           1 :     error_flag = False
     288           1 :     for i, j in zip(range(len(model_inds)), model_inds, strict=True):
     289           1 :         vector_norm = sparse.linalg.norm(overlaps[i, :])
     290           1 :         overlap = overlaps[i, j] / vector_norm
     291           1 :         if overlap >= required_overlap:
     292           1 :             continue
     293           0 :         error_flag = True
     294           0 :         print_above_admixture = (1 - required_overlap) * 0.05
     295           0 :         indices = sparse.find(overlaps[i, :] >= print_above_admixture * vector_norm)[1]
     296           0 :         logger.error(
     297             :             "The state %s has resonances with the following states, please consider adding them to your model space:",
     298             :             system_pair.basis.get_ket(j),
     299             :         )
     300           0 :         for index in indices:
     301           0 :             if index == j:
     302           0 :                 continue
     303           0 :             admixture = 1 if np.isinf(overlaps[i, index]) else overlaps[i, index] / vector_norm
     304           0 :             logger.error("  - %s with admixture %.3f", system_pair.basis.get_ket(int(index)), admixture)
     305           1 :     if error_flag:
     306           0 :         raise ValueError(
     307             :             "Error. Perturbative Calculation not possible due to resonances. "
     308             :             "Add more states to the model space or adapt your required overlap."
     309             :         )
     310             : 
     311             : 
     312           1 : @deprecated("Use EffectiveSystemPair(ket_tuples) instead to create a system for perturbative calculations.")
     313           1 : def create_system_for_perturbative(  # noqa: C901, PLR0912, PLR0915
     314             :     ket_tuple_list: Sequence[KetAtomTuple],
     315             :     electric_field: PintArray | None = None,
     316             :     magnetic_field: PintArray | None = None,
     317             :     distance_vector: PintArray | None = None,
     318             :     multipole_order: int = 3,
     319             :     with_diamagnetism: bool = False,
     320             :     perturbation_order: int = 2,
     321             :     number_of_considered_pair_kets: int = 2_000,
     322             : ) -> SystemPair:
     323             :     r"""Create a good estimate for a system in which to perform perturbative calculations.
     324             : 
     325             :     This function takes a list of 2-tuples of ket states and creates a pair system holding a larger basis.
     326             :     The parameters of the basis are adjusted by the electric and magnetic field vectors of the system, as well
     327             :     as the distance and the multipole-order of the interaction. For higher-order perturbation theory, larger
     328             :     systems can be considered. Diamagnetism can be considered as well.
     329             : 
     330             :     Args:
     331             :         ket_tuple_list: List of all pair states that span up the model space. The system is created such that
     332             :             the effective Hamiltonian of the model system can be calculated accurately at a later stage.
     333             :         electric_field: Electric field in the system.
     334             :         magnetic_field: Magnetic field in the system.
     335             :         distance_vector: Distance vector between the atoms.
     336             :         multipole_order: Multipole-order of the interaction. Default is 3 (dipole-dipole).
     337             :         with_diamagnetism: True if diamagnetic term should be considered. Default is False.
     338             :         perturbation_order: Order of perturbative calculation the system shall be used for. Default is 2.
     339             :         number_of_considered_pair_kets: Number of pair kets that are considered in the system. Default is 2000.
     340             : 
     341             :     Returns:
     342             :         Pair system that can be used for perturbative calculations.
     343             : 
     344             :     """
     345           1 :     electric_field = electric_field if electric_field is not None else ureg.Quantity([0, 0, 0], "V/cm")
     346           1 :     magnetic_field = magnetic_field if magnetic_field is not None else ureg.Quantity([0, 0, 0], "G")
     347             : 
     348           1 :     is_real = isinstance(ket_tuple_list[0][0], pi_real.KetAtom)
     349           1 :     pi = pi_real if is_real else pi_complex
     350           1 :     are_fields_along_z = all(x == 0 for x in [*magnetic_field[:2], *electric_field[:2]])  # type: ignore [index]
     351             : 
     352           1 :     system_atoms: list[pi_real.SystemAtom | pi_complex.SystemAtom] = []
     353             : 
     354           1 :     delta_n = 7
     355           1 :     delta_l = perturbation_order * (multipole_order - 2)
     356           1 :     for i in range(2):
     357           1 :         kets = [ket_tuple[i] for ket_tuple in ket_tuple_list]
     358           1 :         nlfm = np.transpose([[ket.n, ket.l, ket.f, ket.m] for ket in kets])
     359           1 :         n_range = (int(np.min(nlfm[0])) - delta_n, int(np.max(nlfm[0])) + delta_n)
     360           1 :         l_range = (np.min(nlfm[1]) - delta_l, np.max(nlfm[1]) + delta_l)
     361           1 :         if any(ket.is_calculated_with_mqdt for ket in kets):
     362             :             # for mqdt we increase delta_l by 1 to take into account the variance ...
     363           0 :             l_range = (np.min(nlfm[1]) - delta_l - 1, np.max(nlfm[1]) + delta_l + 1)
     364           1 :         m_range = None
     365           1 :         if are_fields_along_z:
     366           1 :             m_range = (np.min(nlfm[3]) - delta_l, np.max(nlfm[3]) + delta_l)
     367           1 :         basis = pi.BasisAtom(kets[0].species, n=n_range, l=l_range, m=m_range)
     368           1 :         system = pi.SystemAtom(basis)
     369           1 :         system.set_diamagnetism_enabled(with_diamagnetism)
     370           1 :         system.set_magnetic_field(magnetic_field)
     371           1 :         system.set_electric_field(electric_field)
     372           1 :         system_atoms.append(system)
     373             : 
     374           1 :     pi.diagonalize(system_atoms)
     375             : 
     376           1 :     pair_energies_au = [
     377             :         sum(
     378             :             system.get_corresponding_energy(ket).to_base_units().magnitude
     379             :             for system, ket in zip(system_atoms, ket_tuple, strict=True)
     380             :         )
     381             :         for ket_tuple in ket_tuple_list
     382             :     ]
     383             : 
     384           1 :     def get_basis_pair(delta_energy_au: float) -> pi_real.BasisPair | pi_complex.BasisPair:
     385           1 :         return pi.BasisPair(  # type: ignore [no-any-return]
     386             :             system_atoms,
     387             :             energy=(min(pair_energies_au) - delta_energy_au, max(pair_energies_au) + delta_energy_au),
     388             :             energy_unit="hartree",
     389             :         )
     390             : 
     391           1 :     mhz_au = QuantityScalar.convert_user_to_au(1, "MHz", "energy")
     392           1 :     delta_energy_au = mhz_au
     393           1 :     min_delta, max_delta = None, None
     394             : 
     395             :     # make a bisect search to get a sensible basis size between:
     396             :     # number_of_considered_pair_kets and 1.2 * number_of_considered_pair_kets
     397           1 :     while delta_energy_au < 1:  # stop if delta_energy_au is 1 and the basis is still very small
     398           1 :         basis_pair = get_basis_pair(delta_energy_au)
     399           1 :         if basis_pair.number_of_kets < number_of_considered_pair_kets:
     400           1 :             min_delta = delta_energy_au
     401           1 :             if max_delta is None:
     402           1 :                 delta_energy_au *= 2
     403             :             else:
     404           0 :                 delta_energy_au = (delta_energy_au + max_delta) / 2
     405           1 :         elif basis_pair.number_of_kets > number_of_considered_pair_kets * 1.2:
     406           1 :             max_delta = delta_energy_au
     407           1 :             if min_delta is None:
     408           0 :                 delta_energy_au /= 2
     409             :             else:
     410           1 :                 delta_energy_au = (delta_energy_au + min_delta) / 2
     411             :         else:
     412           1 :             break
     413           1 :         if max_delta is not None and min_delta is not None and max_delta - min_delta < mhz_au:
     414           0 :             break
     415             : 
     416           1 :     logger.debug("The pair basis for the perturbative calculations consists of %d kets.", basis_pair.number_of_kets)
     417             : 
     418           1 :     system_pair = pi.SystemPair(basis_pair)
     419           1 :     if distance_vector is not None:
     420           1 :         system_pair.set_distance_vector(distance_vector)
     421           1 :     system_pair.set_interaction_order(multipole_order)
     422           1 :     return system_pair  # type: ignore [no-any-return]

Generated by: LCOV version 1.16