LCOV - code coverage report
Current view: top level - src/pairinteraction/state - state_base.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 75 80 93.8 %
Date: 2026-08-14 15:26:44 Functions: 12 13 92.3 %

          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 : from abc import ABC, abstractmethod
       7           1 : from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
       8             : 
       9           1 : import numpy as np
      10             : 
      11             : if TYPE_CHECKING:
      12             :     from typing_extensions import Self
      13             : 
      14             :     from pairinteraction import _backend
      15             :     from pairinteraction.ket import KetBase
      16             :     from pairinteraction.units import NDArray
      17             : 
      18           1 : logger = logging.getLogger(__name__)
      19             : 
      20           1 : KetType = TypeVar("KetType", bound="KetBase")
      21           1 : UnionCPPBasis: TypeAlias = "_backend.BasisAtomComplex | _backend.BasisPairComplex"
      22             : 
      23             : 
      24           1 : class StateBase(ABC, Generic[KetType]):
      25             :     """Base class for all State objects.
      26             : 
      27             :     The state objects are meant to represent a set of kets, that span a Hilbert space
      28             :     and store a coefficient vector, that describes the state in terms of the kets.
      29             : 
      30             :     Basically just a wrapper around the python Basis classes, but with more convenience functions specific to a state.
      31             : 
      32             :     Note, in the cpp code we dont have an explicit state class, but use a Basis object, which has only one state
      33             : 
      34             :     All state objects share a few common attributes and methods, that are defined in this base class, e.g.:
      35             :         - the number of kets,
      36             :         - the kets of the state,
      37             :         - the coefficient vector as 1d-array,
      38             :         - ...
      39             :     """
      40             : 
      41           1 :     _cpp: UnionCPPBasis
      42           1 :     _ket_class: type[KetType]  # should be ClassVar, but cannot be nested yet
      43             : 
      44           1 :     def __init__(self) -> None:
      45           1 :         self._kets_cache: dict[int, KetType] = {}
      46             : 
      47           1 :     @classmethod
      48           1 :     def _from_cpp_object(cls: type[Self], cpp_obj: UnionCPPBasis) -> Self:
      49           1 :         obj = cls.__new__(cls)
      50           1 :         obj._cpp = cpp_obj
      51           1 :         obj._kets_cache = {}
      52           1 :         return obj
      53             : 
      54           1 :     def __repr__(self) -> str:
      55           0 :         return f"{type(self).__name__}({self.get_label()})"
      56             : 
      57           1 :     def __str__(self) -> str:
      58           1 :         return self.get_label()
      59             : 
      60           1 :     def get_label(self, stop_after_num_kets: int = 3, stop_after_accumulated_overlap: float = 0.95) -> str:
      61             :         """Label representing the state.
      62             : 
      63             :         Args:
      64             :             stop_after_num_kets: Maximum number of kets to include in the label.
      65             :             stop_after_accumulated_overlap: Stop including kets in the label,
      66             :                 if the accumulated overlap of the included kets exceeds this value.
      67             : 
      68             :         Returns:
      69             :             The label of the ket in the given format.
      70             : 
      71             :         """
      72           1 :         coefficients = self.get_coefficients()
      73           1 :         sorted_inds = np.argsort(np.abs(coefficients))[::-1]
      74           1 :         norm_squared = self.norm**2
      75           1 :         label = ""
      76           1 :         overlap = 0
      77           1 :         for i, ind in enumerate(sorted_inds, 1):
      78           1 :             label += f"{np.real_if_close(coefficients[ind]):.2f} {self.get_ket(ind).get_label('ket')}"
      79           1 :             overlap += abs(coefficients[ind]) ** 2
      80           1 :             if overlap > (stop_after_accumulated_overlap * norm_squared) or i >= stop_after_num_kets:
      81           1 :                 break
      82           1 :             label += " + "
      83           1 :         if overlap <= norm_squared - 100 * np.finfo(float).eps:
      84           0 :             label += " + ... "
      85             : 
      86           1 :         return label.replace("+ -", "- ")
      87             : 
      88           1 :     @property
      89           1 :     def kets(self) -> list[KetType]:
      90             :         """Return a list containing the kets of the basis."""
      91           1 :         return [self.get_ket(i) for i in range(self.number_of_kets)]
      92             : 
      93           1 :     def get_ket(self, index: int) -> KetType:
      94             :         """Return the ket at the given index."""
      95           1 :         if index not in self._kets_cache:
      96           1 :             if index < 0 or index >= self.number_of_kets:
      97           0 :                 raise IndexError(f"Ket index {index} out of range (number of kets: {self.number_of_kets}).")
      98           1 :             ket_cpp = self._cpp.get_ket(index)
      99           1 :             self._kets_cache[index] = self._ket_class._from_cpp_object(ket_cpp)
     100           1 :         return self._kets_cache[index]
     101             : 
     102           1 :     @property
     103           1 :     def number_of_kets(self) -> int:
     104             :         """Return the number of kets in the basis."""
     105           1 :         return self._cpp.get_number_of_kets()
     106             : 
     107           1 :     @property
     108           1 :     def norm(self) -> np.floating:
     109             :         """Return the norm of the state."""
     110           1 :         return np.linalg.norm(self.get_coefficients())
     111             : 
     112           1 :     def get_coefficients(self) -> NDArray:
     113             :         """Return the coefficients of the state as a 1d-array.
     114             : 
     115             :         The coefficients are stored in a numpy.array with shape (number_of_kets,).
     116             : 
     117             :         The coefficients are normalized, i.e. the sum of the absolute values of the coefficients is equal to 1.
     118             : 
     119             :         """
     120           1 :         coefficients = self._cpp.get_coefficients().toarray().ravel()
     121           1 :         return np.real_if_close(coefficients)
     122             : 
     123           1 :     def get_corresponding_ket(self) -> KetType:
     124             :         """Return the ket with the maximal overlap with self."""
     125           1 :         return self.get_ket(self.get_corresponding_ket_index())
     126             : 
     127           1 :     def get_corresponding_ket_index(self) -> int:
     128             :         """Return the ket index with the maximal overlap with self."""
     129           1 :         overlaps = np.abs(self.get_coefficients()) ** 2
     130           1 :         err_msg = "ket for the state"
     131           1 :         return get_index_with_largest_overlap(overlaps, err_msg=err_msg)
     132             : 
     133             :     @abstractmethod
     134             :     def get_amplitude(self, other: Any) -> Any: ...
     135             : 
     136             :     @abstractmethod
     137             :     def get_overlap(self, other: Any) -> Any: ...
     138             : 
     139             :     @abstractmethod
     140             :     def get_matrix_element(self, other: Any, *args: Any, **kwargs: Any) -> Any: ...
     141             : 
     142             : 
     143           1 : def get_index_with_largest_overlap(overlaps: NDArray, err_msg: str) -> int:
     144           1 :     if len(overlaps) == 0:
     145           0 :         raise ValueError(f"Cannot find the corresponding {err_msg}: No overlaps, this should not happen.")
     146           1 :     if len(overlaps) == 1:
     147           1 :         largest_id = 0
     148           1 :         largest_overlap = overlaps[0]
     149           1 :         second_largest_overlap = 0
     150             :     else:
     151             :         # Find the indices of the two largest overlaps
     152             :         # this is more efficient than sorting the entire array
     153           1 :         ids = np.argpartition(overlaps, -2)[-2:]
     154           1 :         ids = ids[np.argsort(overlaps[ids])[::-1]]
     155           1 :         largest_id = int(ids[0])
     156           1 :         largest_overlap = overlaps[ids[0]]
     157           1 :         second_largest_overlap = overlaps[ids[1]]
     158             : 
     159           1 :     if largest_overlap == 0:
     160           0 :         raise ValueError(f"Cannot find the corresponding {err_msg}: All overlaps are 0.")
     161           1 :     if largest_overlap < 0.5 + 100 * np.finfo(float).eps:
     162           1 :         logger.warning(
     163             :             "Cannot find the uniquely corresponding %s: "
     164             :             "Largest overlap=%.3f <= 0.5, the second largest overlap is %.3f. "
     165             :             "Still returning the result with the largest overlap.",
     166             :             *(err_msg, largest_overlap, second_largest_overlap),
     167             :         )
     168             : 
     169           1 :     return largest_id

Generated by: LCOV version 1.16