LCOV - code coverage report
Current view: top level - src/pairinteraction/state - state_atom.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 95 112 84.8 %
Date: 2026-08-14 15:26:44 Functions: 13 15 86.7 %

          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 : import logging
       6           1 : import warnings
       7           1 : from typing import TYPE_CHECKING, cast, overload
       8             : 
       9           1 : import numpy as np
      10           1 : from scipy.sparse import csr_matrix
      11           1 : from typing_extensions import deprecated
      12             : 
      13           1 : from pairinteraction.enums import get_cpp_operator_type
      14           1 : from pairinteraction.ket import KetAtom, KetAtomReal
      15           1 : from pairinteraction.state.state_base import StateBase
      16           1 : from pairinteraction.units import QuantityScalar
      17             : 
      18             : if TYPE_CHECKING:
      19             :     from collections.abc import Sequence
      20             : 
      21             :     from typing_extensions import Self
      22             : 
      23             :     from pairinteraction import _backend
      24             :     from pairinteraction.basis import BasisAtom
      25             :     from pairinteraction.database import Database
      26             :     from pairinteraction.enums import OperatorType
      27             :     from pairinteraction.units import PintComplex, PintFloat
      28             : 
      29           1 : logger = logging.getLogger(__name__)
      30             : 
      31             : 
      32           1 : class StateAtom(StateBase[KetAtom]):
      33             :     """State of a single atom.
      34             : 
      35             :     A coefficient vector and a list of kets are used to represent an arbitrary single-atom state.
      36             : 
      37             :     Examples:
      38             :         >>> import pairinteraction as pi
      39             :         >>> ket = pi.KetAtom("Rb", n=60, l=0, m=0.5)
      40             :         >>> state = ket.to_state()
      41             :         >>> print(state)
      42             :         1.00 |Rb:60,S_1/2,1/2⟩
      43             :         >>> ket2 = pi.KetAtom("Rb", n=60, l=1, j=0.5, m=0.5)
      44             :         >>> state2 = pi.StateAtom([1], [ket2])
      45             :         >>> print(state2)
      46             :         1.00 |Rb:60,P_1/2,1/2⟩
      47             :         >>> print((2 * state2 - state).normalize())
      48             :         0.89 |Rb:60,P_1/2,1/2⟩ - 0.45 |Rb:60,S_1/2,1/2⟩
      49             :         >>> print(pi.StateAtom([2, 1], [ket, ket2]).normalize())
      50             :         0.89 |Rb:60,S_1/2,1/2⟩ + 0.45 |Rb:60,P_1/2,1/2⟩
      51             : 
      52             :     """
      53             : 
      54           1 :     _cpp: _backend.BasisAtomComplex
      55           1 :     _ket_class = KetAtom
      56             : 
      57             :     @overload
      58             :     def __init__(
      59             :         self, coefficients: Sequence[complex], kets: Sequence[KetAtom], *, basis: BasisAtom | None = None
      60             :     ) -> None: ...
      61             : 
      62             :     @overload
      63             :     @deprecated("Use ket.to_state() instead of StateAtom(ket, basis).")
      64             :     def __init__(self, ket: KetAtom, basis: BasisAtom) -> None: ...
      65             : 
      66           1 :     def __init__(  # type: ignore [misc]
      67             :         self,
      68             :         coefficients: Sequence[complex] | KetAtom,
      69             :         kets: Sequence[KetAtom] | BasisAtom | None = None,
      70             :         *,
      71             :         basis: BasisAtom | None = None,
      72             :     ) -> None:
      73             :         """Initialize a state object from a coefficient vector and the corresponding kets.
      74             : 
      75             :         Args:
      76             :             coefficients: The coefficient of each of the given kets.
      77             :             kets: The kets the state is composed of.
      78             :             basis: The basis in which the state should be expressed.
      79             :                 If None (default), a minimal basis consisting only of the given kets is constructed.
      80             :                 Providing a basis is only relevant if you want the state to already live in a larger Hilbert space;
      81             :                 when adding states, their bases are merged automatically.
      82             :                 All given kets must be part of this basis.
      83             :                 Since the coefficients are always defined with respect to the kets,
      84             :                 only the kets of the given basis are used and the coefficients of the basis are ignored,
      85             :                 i.e. the basis is canonicalized first.
      86             : 
      87             :         """
      88           1 :         super().__init__()
      89             : 
      90           1 :         if isinstance(coefficients, KetAtom):  # deprecated interface StateAtom(ket, basis)
      91           0 :             coefficients, kets, basis = self._unpack_deprecated_args(coefficients, kets, basis)
      92           1 :         if kets is None:
      93           0 :             raise TypeError("StateAtom.__init__() missing 1 required positional argument: 'kets'")
      94           1 :         kets = cast("Sequence[KetAtom]", kets)
      95             : 
      96           1 :         is_real = isinstance(self, StateAtomReal)
      97           1 :         coeffs = np.array(coefficients, dtype=float if is_real else complex).ravel()
      98           1 :         if len(coeffs) != len(kets):
      99           0 :             raise ValueError(f"Got {len(coeffs)} coefficients for {len(kets)} kets, these must match.")
     100           1 :         if len(kets) != len(set(kets)):
     101           1 :             raise ValueError("The given kets must be unique.")
     102             : 
     103           1 :         if basis is None:
     104           1 :             from pairinteraction.basis.basis_atom import get_cpp_basis_atom_from_kets
     105             : 
     106           1 :             cpp_basis = get_cpp_basis_atom_from_kets(kets, real=is_real)
     107             :         else:
     108           1 :             cpp_basis = basis._cpp.canonicalized()
     109             : 
     110           1 :         cpp_ket_to_index = {cpp_ket: i for i, cpp_ket in enumerate(cpp_basis.get_kets())}
     111           1 :         basis_coeffs = np.zeros((len(cpp_ket_to_index), 1), dtype=coeffs.dtype)
     112           1 :         for coeff, ket in zip(coeffs, kets, strict=True):
     113           1 :             if ket._cpp not in cpp_ket_to_index:
     114           1 :                 raise ValueError(f"The ket {ket} is not part of the given basis.")
     115           1 :             basis_coeffs[cpp_ket_to_index[ket._cpp], 0] = coeff
     116             : 
     117           1 :         state_cpp = cpp_basis.get_state(0)  # single-state basis, i.e. shape (n_kets, 1)
     118           1 :         self._cpp = state_cpp.copy_with_coefficients(csr_matrix(basis_coeffs))
     119             : 
     120           1 :     @staticmethod
     121           1 :     def _unpack_deprecated_args(
     122             :         ket: KetAtom, kets: Sequence[KetAtom] | BasisAtom | None, basis: BasisAtom | None
     123             :     ) -> tuple[Sequence[complex], Sequence[KetAtom], BasisAtom]:
     124             :         """Translate the arguments of the deprecated StateAtom(ket, basis) interface into the new interface."""
     125           0 :         from pairinteraction.basis import BasisAtom  # imported here to avoid a circular import
     126             : 
     127           0 :         warnings.warn(
     128             :             "Calling StateAtom(ket, basis) is deprecated use ket.to_state() instead.",
     129             :             DeprecationWarning,
     130             :             stacklevel=3,
     131             :         )
     132           0 :         if kets is not None:  # in the deprecated interface the second argument was the basis
     133           0 :             if basis is not None:
     134           0 :                 raise TypeError("The basis must not be given both as positional and as keyword argument.")
     135           0 :             basis = cast("BasisAtom", kets)
     136           0 :         if not isinstance(basis, BasisAtom):
     137           0 :             raise TypeError("The basis must be given as a BasisAtom object when creating a StateAtom from a ket.")
     138           0 :         return [1], [ket], basis
     139             : 
     140           1 :     def __add__(self, other: Self | KetAtom) -> Self:
     141             :         """Build the superposition of this state and another state or ket.
     142             : 
     143             :         The bases are merged into a common basis, in which the coefficients are re-expressed before adding them.
     144             :         The resulting superposition is in general not normalized, use :meth:`normalize` to normalize it.
     145             :         """
     146           1 :         if isinstance(other, KetAtom):
     147           1 :             other = cast("Self", other.to_state())
     148           1 :         if type(self) is not type(other):
     149           0 :             raise TypeError(f"Cannot add/subtract {type(self)} and {type(other)}.")
     150             : 
     151             :         # merge the (canonical) bases and re-express the coefficients in the merged basis;
     152           1 :         merged_cpp = self._cpp.canonicalized().merge(other._cpp.canonicalized())
     153           1 :         cpp_op = get_cpp_operator_type("identity")
     154           1 :         coeffs1 = self._cpp.get_matrix_elements(merged_cpp, cpp_op, 0)
     155           1 :         coeffs2 = other._cpp.get_matrix_elements(merged_cpp, cpp_op, 0)
     156           1 :         coeffs = coeffs1 + coeffs2
     157           1 :         new_cpp = merged_cpp.get_state(0)  # single-state basis, i.e. shape (n_kets, 1)
     158             : 
     159           1 :         new_cpp = new_cpp.copy_with_coefficients(coeffs)
     160           1 :         return type(self)._from_cpp_object(new_cpp)
     161             : 
     162           1 :     def __sub__(self, other: Self | KetAtom) -> Self:
     163             :         """Build the superposition of this state and the negative of another state or ket.
     164             : 
     165             :         As for :meth:`__add__` the bases are merged first, the resulting superposition is in general not normalized.
     166             :         """
     167           1 :         if isinstance(other, KetAtom):
     168           1 :             other = cast("Self", other.to_state())
     169           1 :         return self.__add__(-1 * other)
     170             : 
     171           1 :     def __mul__(self, factor: complex) -> Self:
     172             :         """Scale the state by a complex amplitude, e.g. to build superpositions like ``2 * state_s - 1j * state_p``."""
     173           1 :         if not isinstance(factor, (int, float, complex)):
     174           0 :             raise TypeError(f"Cannot multiply {type(self)} with {type(factor)}.")
     175           1 :         coeffs = factor * self._cpp.get_coefficients()  # type: ignore [operator]
     176           1 :         new_cpp = self._cpp.copy_with_coefficients(coeffs)
     177           1 :         return type(self)._from_cpp_object(new_cpp)
     178             : 
     179           1 :     def __truediv__(self, factor: complex) -> Self:
     180             :         """Scale the state by the inverse of a complex amplitude."""
     181           1 :         return self.__mul__(1 / factor)
     182             : 
     183           1 :     def __neg__(self) -> Self:
     184             :         """Flip the sign of all amplitudes of the state."""
     185           1 :         return self.__mul__(-1)
     186             : 
     187           1 :     __rmul__ = __mul__  # for reverse multiplication, i.e. scalar * state will use state.__rmul__
     188             : 
     189           1 :     def normalize(self) -> Self:
     190             :         """Normalize the coefficients of the state."""
     191           1 :         coeffs = self._cpp.get_coefficients()
     192           1 :         self._cpp = self._cpp.copy_with_coefficients(coeffs / self.norm)
     193           1 :         return self
     194             : 
     195           1 :     def is_normalized(self, tol: float = 1e-10) -> bool:
     196             :         """Check if the state is normalized within a given tolerance.
     197             : 
     198             :         Args:
     199             :             tol: The tolerance for the normalization check. Default is 1e-10.
     200             : 
     201             :         Returns:
     202             :             True if the state is normalized within the given tolerance, False otherwise.
     203             : 
     204             :         """
     205           1 :         return abs(self.norm - 1) < tol  # type: ignore [return-value] # numpy
     206             : 
     207           1 :     @property
     208           1 :     def database(self) -> Database:
     209             :         """The database used for this object."""
     210           0 :         return self.get_ket(0).database
     211             : 
     212           1 :     @property
     213           1 :     def species(self) -> str:
     214             :         """The atomic species."""
     215           1 :         return self.get_ket(0).species
     216             : 
     217           1 :     @property
     218           1 :     def is_canonical(self) -> bool:
     219           1 :         return np.count_nonzero(self.get_coefficients()) == 1  # type: ignore [no-any-return]
     220             : 
     221           1 :     def get_amplitude(self, other: Self | KetAtom) -> float | complex:
     222             :         """Calculate the amplitude of the state with respect to another state or ket.
     223             : 
     224             :         This means the inner product <self|other>.
     225             : 
     226             :         Args:
     227             :             other: Either a state or a ket for which the amplitude should be calculated.
     228             : 
     229             :         Returns:
     230             :             The amplitude between self and other.
     231             : 
     232             :         """
     233           1 :         return self.get_matrix_element(other, "identity", 0, unit="")
     234             : 
     235           1 :     def get_overlap(self, other: Self | KetAtom) -> float:
     236             :         r"""Calculate the overlap of the state with respect to another state or ket.
     237             : 
     238             :         This means calculate :math:`|\langle \mathrm{self} | \mathrm{other} \rangle|^2`.
     239             : 
     240             :         Args:
     241             :             other: Either a state or a ket for which the overlap should be calculated.
     242             : 
     243             :         Returns:
     244             :             The overlap between self and other.
     245             : 
     246             :         """
     247           1 :         return abs(self.get_amplitude(other)) ** 2
     248             : 
     249             :     @overload
     250             :     def get_matrix_element(
     251             :         self, other: KetAtom | Self, operator: OperatorType, q: int, unit: None = None
     252             :     ) -> PintFloat | PintComplex: ...
     253             : 
     254             :     @overload
     255             :     def get_matrix_element(
     256             :         self, other: KetAtom | Self, operator: OperatorType, q: int, unit: str
     257             :     ) -> float | complex: ...
     258             : 
     259           1 :     def get_matrix_element(
     260             :         self, other: KetAtom | Self, operator: OperatorType, q: int, unit: str | None = None
     261             :     ) -> PintFloat | PintComplex | float | complex:
     262             :         """Calculate the matrix element of the operator with respect to the state and another state or ket.
     263             : 
     264             :         This means the inner product <self|operator|other>.
     265             : 
     266             :         Args:
     267             :             other: Either a state or a ket for which the matrix element should be calculated.
     268             :             operator: The operator for which the matrix element should be calculated.
     269             :             q: The projection quantum number of the operator.
     270             :             unit: The unit in which the result should be returned.
     271             :                 Default None will return a `pint.Quantity`.
     272             : 
     273             :         Returns:
     274             :             The matrix element between self and other.
     275             : 
     276             :         """
     277           1 :         if not self.is_normalized() or (isinstance(other, StateAtom) and not other.is_normalized()):
     278           0 :             logger.warning("get_matrix_element/get_overlap/get_amplitude is called with a non-normalized state.")
     279             : 
     280           1 :         cpp_op = get_cpp_operator_type(operator)
     281             : 
     282           1 :         if isinstance(other, KetAtom):
     283           1 :             other = cast("Self", other.to_state())
     284           1 :         if isinstance(other, StateAtom):
     285           1 :             matrix_elements_au = self._cpp.get_matrix_elements(other._cpp, cpp_op, q).toarray().ravel()
     286           1 :             matrix_element_au = np.real_if_close(matrix_elements_au)[0]
     287           1 :             return QuantityScalar.convert_au_to_user(matrix_element_au, operator, unit)
     288           0 :         raise TypeError(f"Unknown type: {type(other)=}")
     289             : 
     290             : 
     291           1 : class StateAtomReal(StateAtom):
     292           1 :     _cpp: _backend.BasisAtomReal  # type: ignore [assignment]
     293           1 :     _ket_class = KetAtomReal

Generated by: LCOV version 1.16