LCOV - code coverage report
Current view: top level - src/pairinteraction/basis - basis_atom.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 120 127 94.5 %
Date: 2026-08-14 15:26:44 Functions: 9 10 90.0 %

          Line data    Source code
       1             : # SPDX-FileCopyrightText: 2024 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, overload
       6             : 
       7           1 : import numpy as np
       8           1 : from scipy.sparse import csr_matrix
       9             : 
      10           1 : from pairinteraction import _backend
      11           1 : from pairinteraction.basis.basis_base import BasisBase
      12           1 : from pairinteraction.database import Database
      13           1 : from pairinteraction.enums import get_cpp_operator_type, parity_to_int
      14           1 : from pairinteraction.ket import KetAtom, KetAtomReal
      15           1 : from pairinteraction.state import StateAtom, StateAtomReal
      16           1 : from pairinteraction.units import QuantityArray, QuantityScalar, QuantitySparse
      17             : 
      18             : if TYPE_CHECKING:
      19             :     from collections.abc import Sequence
      20             : 
      21             :     from typing_extensions import Self
      22             : 
      23             :     from pairinteraction.enums import OperatorType, Parity
      24             :     from pairinteraction.units import NDArray, PintArray, PintFloat, PintSparse
      25             : 
      26             : 
      27           1 : class BasisAtom(BasisBase[KetAtom, StateAtom]):
      28             :     """Basis for a single atom.
      29             : 
      30             :     Add all KetAtom objects that match the given quantum numbers to the basis.
      31             :     The initial coefficients matrix is a unit matrix, i.e. the first basis state is the first ket, etc.
      32             :     The BasisAtom coefficients matrix will always be square,
      33             :     i.e. the number of kets is equal to the number of states.
      34             : 
      35             :     Examples:
      36             :         >>> import pairinteraction as pi
      37             :         >>> ket = pi.KetAtom("Rb", n=60, l=0, m=0.5)
      38             :         >>> energy_min, energy_max = ket.get_energy(unit="GHz") - 100, ket.get_energy(unit="GHz") + 100
      39             :         >>> basis = pi.BasisAtom("Rb", n=(57, 63), l=(0, 3), energy=(energy_min, energy_max), energy_unit="GHz")
      40             :         >>> print(basis)
      41             :         BasisAtom('Rb', n=(57, 63), l=(0, 3), energy=(1008911.9216, 1009111.9216), energy_unit='GHz')
      42             : 
      43             :     """
      44             : 
      45           1 :     _cpp: _backend.BasisAtomComplex
      46           1 :     _cpp_creator = _backend.BasisAtomCreatorComplex
      47           1 :     _ket_class = KetAtom
      48           1 :     _state_class = StateAtom
      49             : 
      50           1 :     _args: dict[str, Any] | None = None
      51             : 
      52           1 :     def __init__(  # noqa: C901, PLR0912
      53             :         self,
      54             :         species: str,
      55             :         n: tuple[int, int] | None = None,
      56             :         nu: tuple[float, float] | None = None,
      57             :         nui: tuple[float, float] | None = None,
      58             :         l: tuple[float, float] | None = None,
      59             :         s: tuple[float, float] | None = None,
      60             :         j: tuple[float, float] | None = None,
      61             :         l_ryd: tuple[float, float] | None = None,
      62             :         j_ryd: tuple[float, float] | None = None,
      63             :         f: tuple[float, float] | None = None,
      64             :         m: tuple[float, float] | None = None,
      65             :         energy: tuple[float, float] | tuple[PintFloat, PintFloat] | None = None,
      66             :         energy_unit: str | None = None,
      67             :         parity: Parity | None = None,
      68             :         additional_kets: Sequence[KetAtom] | None = None,
      69             :         *,
      70             :         database: Database | None = None,
      71             :         mode: Literal["exact", "fuzzy"] | float = "fuzzy",
      72             :     ) -> None:
      73             :         """Create a basis for a single atom.
      74             : 
      75             :         Args:
      76             :             species: The species of the atom.
      77             :             n: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      78             :             nu: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      79             :             nui: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      80             :             l: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      81             :             s: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      82             :             j: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      83             :             l_ryd: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      84             :             j_ryd: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      85             :             f: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      86             :             m: tuple of (min, max) values for this quantum number. Default None, i.e. add all available states.
      87             :             energy: tuple of (min, max) value for the energy. Default None, i.e. add all available states.
      88             :             energy_unit: In which unit the energy values are given, e.g. "GHz".
      89             :                 Default None, i.e. energy is provided as pint object.
      90             :             parity: The parity of the states to consider. Default None, i.e. add all available states.
      91             :             additional_kets: List of additional kets to add to the basis. Default None.
      92             :             database: Which database to use. Default None, i.e. use the global database instance.
      93             :             mode: Specifies how restrictions on expectation-value quantum numbers are applied.
      94             :                 ``"fuzzy"`` is equal to 2 and includes states whose expectation-value overlaps the requested range
      95             :                 within two standard deviations.
      96             :                 ``"exact"`` is equal to 0 and includes only states whose expectation-value itself lie in the range.
      97             :                 A non-negative number sets the factor applied to the standard deviation explicitly.
      98             :                 Default ``"fuzzy"``.
      99             : 
     100             :         """
     101           1 :         self._args = {"species": species}
     102             : 
     103           1 :         creator = self._cpp_creator()
     104           1 :         creator.set_species(species)
     105             : 
     106           1 :         if n is not None and not all(isinstance(x, int) or x.is_integer() for x in n):
     107           0 :             raise ValueError("Quantum numbers n must be integers.")
     108             : 
     109           1 :         quantum_numbers = {
     110             :             "n": n,
     111             :             "nu": nu,
     112             :             "nui": nui,
     113             :             "s": s,
     114             :             "l": l,
     115             :             "j": j,
     116             :             "l_ryd": l_ryd,
     117             :             "j_ryd": j_ryd,
     118             :             "f": f,
     119             :             "m": m,
     120             :         }
     121           1 :         for name, value in quantum_numbers.items():
     122           1 :             if value is not None:
     123           1 :                 self._args[name] = value
     124           1 :                 creator.restrict_quantum_number(name, *value)
     125             : 
     126           1 :         if parity is not None:
     127           0 :             self._args["parity"] = parity
     128           0 :             parity_int = parity_to_int(parity)
     129           0 :             creator.restrict_quantum_number("parity", parity_int, parity_int)
     130             : 
     131           1 :         if energy is not None:
     132           1 :             self._args.update({"energy": energy, "energy_unit": energy_unit})
     133           1 :             min_energy_au = QuantityScalar.convert_user_to_au(energy[0], energy_unit, "energy")
     134           1 :             max_energy_au = QuantityScalar.convert_user_to_au(energy[1], energy_unit, "energy")
     135           1 :             creator.restrict_energy(min_energy_au, max_energy_au)
     136             : 
     137           1 :         if database is None:
     138           1 :             if Database.get_global_database() is None:
     139           0 :                 Database.initialize_global_database()
     140           1 :             database = Database.get_global_database()
     141             : 
     142           1 :         if additional_kets is not None:
     143           1 :             self._args["additional_kets"] = additional_kets
     144           1 :             for ket in additional_kets:
     145           1 :                 creator.add_ket(ket._cpp)
     146             : 
     147           1 :         if mode == "fuzzy":
     148           1 :             quantum_number_standard_deviation_factor = 2.0
     149           1 :         elif mode == "exact":
     150           1 :             self._args["mode"] = mode
     151           1 :             quantum_number_standard_deviation_factor = 0.0
     152             :         else:
     153           1 :             self._args["mode"] = mode
     154           1 :             msg = "mode must be 'exact', 'fuzzy', or a non-negative number."
     155           1 :             try:
     156           1 :                 quantum_number_standard_deviation_factor = float(mode)
     157           1 :             except (TypeError, ValueError) as err:
     158           1 :                 raise ValueError(msg) from err
     159           1 :             if quantum_number_standard_deviation_factor < 0:
     160           1 :                 raise ValueError(msg)
     161           1 :         creator.set_quantum_number_standard_deviation_factor(quantum_number_standard_deviation_factor)
     162             : 
     163           1 :         self._cpp = creator.create(database._cpp)
     164           1 :         self._post_init()
     165             : 
     166           1 :     @classmethod
     167           1 :     def from_kets(
     168             :         cls: type[Self],
     169             :         kets: KetAtom | Sequence[KetAtom],
     170             :         delta_n: int | None = None,
     171             :         delta_nu: float | None = None,
     172             :         delta_nui: float | None = None,
     173             :         delta_l: float | None = None,
     174             :         delta_s: float | None = None,
     175             :         delta_j: float | None = None,
     176             :         delta_l_ryd: float | None = None,
     177             :         delta_j_ryd: float | None = None,
     178             :         delta_f: int | None = None,
     179             :         delta_m: int | None = None,
     180             :         delta_energy: float | PintFloat | None = None,
     181             :         delta_energy_unit: str | None = None,
     182             :         parity: Parity | None = None,
     183             :         database: Database | None = None,
     184             :         additional_kets: Sequence[KetAtom] | None = None,
     185             :         *,
     186             :         mode: Literal["exact", "fuzzy"] | float = "fuzzy",
     187             :     ) -> Self:
     188             :         """Create a BasisAtom from one or more kets and quantum number deltas.
     189             : 
     190             :         Currently a single big basis including all kets for the quantum numbers
     191             :         from min_value - delta to max_value + delta is returned.
     192             :         In the future this might change to return a basis including all states around the given kets +/- delta,
     193             :         but not necessarily all states between the given kets.
     194             : 
     195             :         For each quantum number, pass the corresponding ``delta_*`` argument to include
     196             :         all states within ``[min_value - delta, max_value + delta]``, where
     197             :         ``min_value`` / ``max_value`` are the extremes across all provided kets.
     198             :         If no ``delta_*`` is given for a quantum number, that quantum number is left
     199             :         unrestricted.
     200             : 
     201             :         Args:
     202             :             kets: The ket(s) around which the basis should be centered.
     203             :             delta_n: Half-width of the n window (integer steps).
     204             :                 Default None means no restriction on n.
     205             :             delta_nu: Half-width of the nu window.
     206             :                 Default None means no restriction on nu.
     207             :             delta_nui: Half-width of the nui window.
     208             :                 Default None means no restriction on nui.
     209             :             delta_l: Half-width of the l window.
     210             :                 Default None means no restriction on l.
     211             :             delta_s: Half-width of the s window.
     212             :                 Default None means no restriction on s.
     213             :             delta_j: Half-width of the j window.
     214             :                 Default None means no restriction on j.
     215             :             delta_l_ryd: Half-width of the l_ryd window.
     216             :                 Default None means no restriction on l_ryd.
     217             :             delta_j_ryd: Half-width of the j_ryd window.
     218             :                 Default None means no restriction on j_ryd.
     219             :             delta_f: Half-width of the f window (integer steps).
     220             :                 Default None means no restriction on f.
     221             :             delta_m: Half-width of the m window (integer steps).
     222             :                 Default None means no restriction on m.
     223             :             delta_energy: Half-width of the energy window around the energies of the
     224             :                 provided kets. Default None means no energy restriction.
     225             :             delta_energy_unit: Unit for ``delta_energy`` (e.g. ``"GHz"``).
     226             :                 Default None means pint quantities are used.
     227             :             parity: Restrict to states with this parity.
     228             :                 Default None means no parity restriction.
     229             :             database: Database instance to use.
     230             :                 Default None uses the global database.
     231             :             additional_kets: Extra kets to force-include in the basis.
     232             :                 Default None.
     233             :             mode: Passed to :class:`BasisAtom`. Default ``"fuzzy"``.
     234             : 
     235             :         Returns:
     236             :             A new :class:`BasisAtom` centered around the provided kets.
     237             : 
     238             :         Examples:
     239             :             >>> import pairinteraction as pi
     240             :             >>> ket1 = pi.KetAtom("Rb", n=60, l=0, m=0.5)
     241             :             >>> ket2 = pi.KetAtom("Rb", n=59, l=0, m=0.5)
     242             :             >>> basis = pi.BasisAtom.from_kets([ket1, ket2], delta_n=2, delta_l=1)
     243             :             >>> basis.species
     244             :             'Rb'
     245             :             >>> all(57 <= k.n <= 62 for k in basis.kets)
     246             :             True
     247             : 
     248             :         """
     249           1 :         if isinstance(kets, KetAtom):
     250           1 :             kets = [kets]
     251           1 :         kets = list(kets)
     252           1 :         if len(kets) == 0:
     253           1 :             raise ValueError("kets must not be empty.")
     254           1 :         if len({ket.species for ket in kets}) > 1:
     255           1 :             raise ValueError(f"All kets must have the same species, but got: {sorted({ket.species for ket in kets})}.")
     256             : 
     257           1 :         def get_range(name: str, delta: float | None) -> tuple[float, float] | None:
     258           1 :             if delta is None:
     259           1 :                 return None
     260           1 :             if name == "energy":
     261           1 :                 values = [ket.get_energy(unit=delta_energy_unit) for ket in kets]
     262             :             else:
     263           1 :                 values = [getattr(ket, name) for ket in kets]
     264           1 :             return (min(values) - delta, max(values) + delta)
     265             : 
     266           1 :         return cls(
     267             :             species=kets[0].species,
     268             :             n=get_range("n", delta_n),  # type: ignore [arg-type]
     269             :             nu=get_range("nu", delta_nu),
     270             :             nui=get_range("nui", delta_nui),
     271             :             l=get_range("l", delta_l),
     272             :             s=get_range("s", delta_s),
     273             :             j=get_range("j", delta_j),
     274             :             l_ryd=get_range("l_ryd", delta_l_ryd),
     275             :             j_ryd=get_range("j_ryd", delta_j_ryd),
     276             :             f=get_range("f", delta_f),
     277             :             m=get_range("m", delta_m),
     278             :             energy=get_range("energy", delta_energy),  # type: ignore [arg-type]
     279             :             energy_unit=delta_energy_unit,
     280             :             parity=parity,
     281             :             database=database,
     282             :             additional_kets=additional_kets,
     283             :             mode=mode,
     284             :         )
     285             : 
     286           1 :     def __repr__(self) -> str:
     287           1 :         if self._args is None:
     288           1 :             return super().__repr__()
     289             : 
     290           1 :         args_str: list[str] = []
     291           1 :         for k, v in self._args.items():
     292           1 :             if k == "species":
     293           1 :                 args_str.append(repr(v))
     294           1 :             elif k == "energy":
     295           1 :                 args_str.append(f"{k}=({v[0]:.4f}, {v[1]:.4f})")
     296             :             else:
     297           1 :                 args_str.append(f"{k}={v!r}")
     298           1 :         return f"{type(self).__name__}({', '.join(args_str)})"
     299             : 
     300           1 :     @property
     301           1 :     def database(self) -> Database:
     302             :         """The database used for this object."""
     303           0 :         return self.get_ket(0).database
     304             : 
     305           1 :     @property
     306           1 :     def species(self) -> str:
     307             :         """The atomic species."""
     308           1 :         return self.get_ket(0).species
     309             : 
     310             :     @overload
     311             :     def get_amplitudes(self, other: KetAtom | StateAtom) -> NDArray: ...
     312             : 
     313             :     @overload
     314             :     def get_amplitudes(self, other: BasisAtom) -> csr_matrix: ...
     315             : 
     316           1 :     def get_amplitudes(self, other: KetAtom | StateAtom | BasisAtom) -> NDArray | csr_matrix:
     317           1 :         return self.get_matrix_elements(other, "identity", 0, unit="")
     318             : 
     319             :     @overload
     320             :     def get_overlaps(self, other: KetAtom | StateAtom) -> NDArray: ...
     321             : 
     322             :     @overload
     323             :     def get_overlaps(self, other: BasisAtom) -> csr_matrix: ...
     324             : 
     325           1 :     def get_overlaps(self, other: KetAtom | StateAtom | BasisAtom) -> NDArray | csr_matrix:
     326           1 :         amplitudes = self.get_amplitudes(other)
     327           1 :         if isinstance(amplitudes, csr_matrix):
     328           1 :             return amplitudes.multiply(amplitudes.conj()).real  # type: ignore [no-any-return]
     329           1 :         return np.abs(amplitudes) ** 2
     330             : 
     331             :     @overload
     332             :     def get_matrix_elements(
     333             :         self, other: KetAtom | StateAtom, operator: OperatorType, q: int, unit: None = None
     334             :     ) -> PintArray: ...
     335             : 
     336             :     @overload
     337             :     def get_matrix_elements(self, other: KetAtom | StateAtom, operator: OperatorType, q: int, unit: str) -> NDArray: ...
     338             : 
     339             :     @overload
     340             :     def get_matrix_elements(
     341             :         self, other: BasisAtom, operator: OperatorType, q: int, unit: None = None
     342             :     ) -> PintSparse: ...
     343             : 
     344             :     @overload
     345             :     def get_matrix_elements(self, other: BasisAtom, operator: OperatorType, q: int, unit: str) -> csr_matrix: ...
     346             : 
     347           1 :     def get_matrix_elements(
     348             :         self, other: KetAtom | StateAtom | BasisAtom, operator: OperatorType, q: int, unit: str | None = None
     349             :     ) -> NDArray | PintArray | csr_matrix | PintSparse:
     350           1 :         cpp_op = get_cpp_operator_type(operator)
     351             : 
     352             :         matrix_elements_au: NDArray
     353           1 :         if isinstance(other, KetAtom):
     354           1 :             other = other.to_state()
     355           1 :         if isinstance(other, StateAtom):
     356           1 :             matrix_elements_au = self._cpp.get_matrix_elements(other._cpp, cpp_op, q).toarray().ravel()
     357           1 :             matrix_elements_au = np.real_if_close(matrix_elements_au)
     358           1 :             return QuantityArray.convert_au_to_user(matrix_elements_au, operator, unit)
     359           1 :         if isinstance(other, BasisAtom):
     360           1 :             matrix_elements_sparse_au = self._cpp.get_matrix_elements(other._cpp, cpp_op, q)
     361           1 :             matrix_elements_sparse_au.data = np.real_if_close(matrix_elements_sparse_au.data)
     362           1 :             return QuantitySparse.convert_au_to_user(matrix_elements_sparse_au, operator, unit)
     363           1 :         raise TypeError(f"Unknown type: {type(other)=}")
     364             : 
     365             : 
     366           1 : class BasisAtomReal(BasisAtom):
     367           1 :     _cpp: _backend.BasisAtomReal  # type: ignore [assignment]
     368           1 :     _cpp_creator = _backend.BasisAtomCreatorReal  # type: ignore [assignment]
     369           1 :     _ket_class = KetAtomReal
     370           1 :     _state_class = StateAtomReal
     371             : 
     372             : 
     373           1 : def get_cpp_basis_atom_from_kets(kets: Sequence[KetAtom], *, real: bool) -> _backend.BasisAtomComplex:
     374             :     """Create a cpp BasisAtom object containing only the given kets.
     375             : 
     376             :     Like for the _cpp attributes, the return type is annotated as the complex variant,
     377             :     although the real variant is returned if real=True.
     378             :     """
     379           1 :     if len(kets) == 0:
     380           0 :         raise ValueError("Cannot create a basis with zero kets.")
     381           1 :     creator = _backend.BasisAtomCreatorReal() if real else _backend.BasisAtomCreatorComplex()
     382           1 :     for ket in kets:
     383           1 :         creator.add_ket(ket._cpp)
     384           1 :     return creator.create(kets[0].database._cpp)  # type: ignore [return-value]

Generated by: LCOV version 1.16