LCOV - code coverage report
Current view: top level - src/pairinteraction/perturbative - perturbation_theory.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 60 68 88.2 %
Date: 2026-08-14 15:26:44 Functions: 2 2 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 typing import TYPE_CHECKING
       7             : 
       8           1 : import numpy as np
       9           1 : from scipy import sparse
      10           1 : from scipy.sparse import csr_matrix
      11             : 
      12             : if TYPE_CHECKING:
      13             :     from scipy.sparse import csr_matrix
      14             : 
      15             :     from pairinteraction.units import NDArray
      16             : 
      17             : 
      18           1 : logger = logging.getLogger(__name__)
      19             : 
      20             : 
      21           1 : def calculate_perturbative_hamiltonian(
      22             :     hamiltonian: csr_matrix,
      23             :     model_inds: list[int],
      24             :     perturbation_order: int,
      25             : ) -> tuple[dict[int, NDArray], csr_matrix]:
      26             :     r"""Calculate the perturbative Hamiltonian up to a given order.
      27             : 
      28             :     This function calculates both the effective Hamiltonian, spanned up by the states of the model space,
      29             :     as well as the effective (perturbed) basisvectors due to interactions with the exterior space
      30             :     up to the desired order of perturbation theory.
      31             : 
      32             :     Args:
      33             :         hamiltonian: Hermitian matrix.
      34             :         model_inds: List of indices corresponding to the states that span up the model space.
      35             :         perturbation_order: Order up to which the perturbation theory is expanded.
      36             :             Support up to third order. Default is second order.
      37             : 
      38             :     Returns:
      39             :         Effective hamiltonians as dictionaries mapping perturbation orders to :math:`m \times m` matrices.
      40             :         Effective basisvectors in perturbation theory, returned as a sparse matrix. Each row represents one basisvector.
      41             : 
      42             :     """
      43           1 :     m_inds = np.asarray(model_inds, dtype=int)
      44           1 :     o_inds = np.setdiff1d(np.arange(hamiltonian.shape[0]), m_inds)
      45           1 :     eff_h_dict, eff_vecs = _calculate_unsorted_perturbative_hamiltonian(hamiltonian, m_inds, o_inds, perturbation_order)
      46             : 
      47             :     # resort eigvec to original order
      48           1 :     all_inds = np.append(m_inds, o_inds)
      49           1 :     all_inds_positions = np.argsort(all_inds)
      50           1 :     eff_vecs = eff_vecs[:, all_inds_positions]
      51             : 
      52             :     # include the hermitian conjugate part of the effective Hamiltonian
      53           1 :     for order, h_eff in eff_h_dict.items():
      54           1 :         eff_h_dict[order] = 0.5 * (h_eff + h_eff.conj().T)
      55             : 
      56           1 :     return eff_h_dict, eff_vecs
      57             : 
      58             : 
      59           1 : def _calculate_unsorted_perturbative_hamiltonian(
      60             :     hamiltonian: csr_matrix,
      61             :     m_inds: NDArray,
      62             :     o_inds: NDArray,
      63             :     perturbation_order: int,
      64             : ) -> tuple[dict[int, NDArray], csr_matrix]:
      65             :     # This function is outsourced from calculate_perturbative_hamiltonian to allow for better type checking
      66           1 :     energies = np.real_if_close(hamiltonian.diagonal())
      67           1 :     if any(np.iscomplex(energies)):
      68           0 :         logger.error("The Hamiltonian has complex entries on the diagonal, this might lead to unexpected results.")
      69             : 
      70           1 :     energies_m = energies[m_inds]
      71             : 
      72           1 :     eff_h_dict: dict[int, NDArray] = {}  # perturbation_order -> h_eff
      73             : 
      74           1 :     eff_h_dict[0] = np.diag(energies_m)
      75           1 :     eff_vecs = sparse.csr_matrix(
      76             :         sparse.hstack(
      77             :             [sparse.eye(len(m_inds), len(m_inds), format="csr"), sparse.csr_matrix((len(m_inds), len(o_inds)))]
      78             :         )
      79             :     )
      80             : 
      81           1 :     if perturbation_order == 0:
      82           1 :         return eff_h_dict, eff_vecs
      83             : 
      84           1 :     v_offdiag = hamiltonian - sparse.diags(energies, dtype=float)
      85           1 :     v_mm = v_offdiag[np.ix_(m_inds, m_inds)]  # type: ignore [index]
      86           1 :     eff_h_dict[1] = v_mm.toarray()
      87             : 
      88           1 :     if perturbation_order == 1:
      89           1 :         return eff_h_dict, eff_vecs
      90             : 
      91           1 :     energies_diff = energies_m[np.newaxis, :] - energies[o_inds, np.newaxis]
      92           1 :     with np.errstate(divide="ignore"):
      93           1 :         delta_e_em = 1 / energies_diff
      94           1 :     v_me = v_offdiag[np.ix_(m_inds, o_inds)]  # type: ignore [index]
      95           1 :     eff_h_dict[2] = (v_me @ ((v_me.conj().T).multiply(delta_e_em))).toarray()
      96             : 
      97           1 :     addition_mm = sparse.csr_matrix((len(m_inds), len(m_inds)))
      98           1 :     addition_me = sparse.csr_matrix(((v_me.conj().T).multiply(delta_e_em)).T)
      99           1 :     if np.isinf(np.isinf(addition_me.data)).any():
     100           0 :         logger.critical(
     101             :             "Detected 'inf' entries in the effective basisvectors. "
     102             :             "This might happen, if you forgot to include a degenerate state in the model space. "
     103             :         )
     104             : 
     105           1 :     nan_idx = np.where(np.isnan(addition_me.data))[0]
     106           1 :     nonzero_inds = addition_me.nonzero()
     107           1 :     for i in nan_idx:
     108           0 :         value = addition_me.data[i]
     109           0 :         if np.isinf(value.real):
     110           0 :             row, col = nonzero_inds[0][i], nonzero_inds[1][i]
     111           0 :             addition_me[row, col] = value.real
     112             :         else:
     113           0 :             logger.error("Detected unexpected 'nan' entries in the effective basisvectors.")
     114             : 
     115           1 :     eff_vecs = eff_vecs + sparse.hstack([addition_mm, addition_me])
     116             : 
     117           1 :     if perturbation_order == 2:
     118           1 :         return eff_h_dict, eff_vecs
     119             : 
     120           1 :     diff = energies_m[np.newaxis, :] - energies_m[:, np.newaxis]
     121           1 :     diff = np.where(diff == 0, np.inf, diff)
     122           1 :     delta_e_mm = 1 / diff
     123           1 :     v_ee = v_offdiag[np.ix_(o_inds, o_inds)]  # type: ignore [index]
     124           1 :     if len(m_inds) > 1:
     125           1 :         logger.debug(
     126             :             "At third order, the effective basisvectors are currently only valid, "
     127             :             "when only one state is in the model space. "
     128             :             "Take care with interpretation of the effective basisvectors."
     129             :         )
     130           1 :     _intermediate_term = v_ee @ ((v_me.conj().T).multiply(delta_e_em)) - ((v_me.conj().T).multiply(delta_e_em)) @ v_mm
     131           1 :     eff_h_dict[3] = (v_me @ (_intermediate_term.multiply(delta_e_em))).toarray()
     132             : 
     133           1 :     addition_mm_diag = -0.5 * sparse.csr_matrix(
     134             :         sparse.diags((v_me @ ((v_me.conj().T).multiply(np.square(delta_e_em)))).diagonal())
     135             :     )
     136           1 :     addition_mm_offdiag = sparse.csr_matrix(((v_me @ (v_me.conj().T).multiply(delta_e_em)).multiply(delta_e_mm)).T)
     137           1 :     addition_me = sparse.csr_matrix(((v_ee @ ((v_me.conj().T).multiply(delta_e_em))).multiply(delta_e_em)).T)
     138           1 :     addition_me_2 = sparse.csr_matrix(((v_me.conj().T @ ((v_mm.conj().T).multiply(delta_e_mm))).multiply(delta_e_em)).T)
     139           1 :     eff_vecs = eff_vecs + sparse.hstack([addition_mm_diag + addition_mm_offdiag, addition_me + addition_me_2])
     140             : 
     141           1 :     if perturbation_order == 3:
     142           1 :         return eff_h_dict, eff_vecs
     143             : 
     144           0 :     raise ValueError("Perturbation theory is only implemented for orders [0, 1, 2, 3].")

Generated by: LCOV version 1.16