LCOV - code coverage report
Current view: top level - src/system - System.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 188 210 89.5 %
Date: 2026-10-14 13:16:16 Functions: 48 52 92.3 %

          Line data    Source code
       1             : // SPDX-FileCopyrightText: 2024 PairInteraction Developers
       2             : // SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : #include "pairinteraction/system/System.hpp"
       5             : 
       6             : #include "pairinteraction/basis/BasisAtom.hpp"
       7             : #include "pairinteraction/basis/BasisPair.hpp"
       8             : #include "pairinteraction/enums/SorterType.hpp"
       9             : #include "pairinteraction/interfaces/DiagonalizerInterface.hpp"
      10             : #include "pairinteraction/ket/KetAtom.hpp"
      11             : #include "pairinteraction/ket/KetPair.hpp"
      12             : #include "pairinteraction/system/SystemAtom.hpp"
      13             : #include "pairinteraction/system/SystemPair.hpp"
      14             : #include "pairinteraction/utils/TaskControl.hpp"
      15             : #include "pairinteraction/utils/eigen_assertion.hpp"
      16             : #include "pairinteraction/utils/eigen_compat.hpp"
      17             : 
      18             : #include <Eigen/SparseCore>
      19             : #include <algorithm>
      20             : #include <complex>
      21             : #include <limits>
      22             : #include <memory>
      23             : #include <numeric>
      24             : #include <oneapi/tbb.h>
      25             : #include <optional>
      26             : #include <spdlog/spdlog.h>
      27             : #include <stdexcept>
      28             : #include <vector>
      29             : 
      30             : namespace pairinteraction {
      31             : template <typename Derived>
      32        1100 : System<Derived>::System(std::shared_ptr<const basis_t> basis)
      33        1100 :     : basis(std::move(basis)),
      34        1100 :       matrix(static_cast<Eigen::Index>(this->basis->get_number_of_states()),
      35        3300 :              static_cast<Eigen::Index>(this->basis->get_number_of_states())) {
      36             :     // The unperturbed Hamiltonian is constructed from the energies of the kets
      37      150031 :     for (const auto &ket : this->basis->get_kets()) {
      38      148931 :         if (static_cast<real_t>(ket->get_energy()) == std::numeric_limits<real_t>::max()) {
      39           0 :             throw std::invalid_argument("The kets of the basis do not have a well-defined energy.");
      40             :         }
      41             :     }
      42        1100 : }
      43             : 
      44             : template <typename Derived>
      45         602 : std::shared_ptr<const typename System<Derived>::basis_t> System<Derived>::get_basis() const {
      46         602 :     if (hamiltonian_requires_construction) {
      47          87 :         construct_hamiltonian();
      48          87 :         hamiltonian_requires_construction = false;
      49             :     }
      50         602 :     return basis;
      51             : }
      52             : 
      53             : template <typename Derived>
      54        1251 : std::shared_ptr<const typename System<Derived>::basis_t> System<Derived>::get_eigenbasis() const {
      55        1251 :     if (hamiltonian_requires_construction) {
      56         134 :         construct_hamiltonian();
      57         134 :         hamiltonian_requires_construction = false;
      58             :     }
      59        1251 :     if (!this->is_diagonal()) {
      60           0 :         throw std::runtime_error("The Hamiltonian has not been diagonalized yet.");
      61             :     }
      62        1251 :     return basis;
      63             : }
      64             : 
      65             : template <typename Derived>
      66         931 : Eigen::VectorX<typename System<Derived>::real_t> System<Derived>::get_eigenenergies() const {
      67         931 :     if (hamiltonian_requires_construction) {
      68           0 :         construct_hamiltonian();
      69           0 :         hamiltonian_requires_construction = false;
      70             :     }
      71         931 :     if (!this->is_diagonal()) {
      72           0 :         throw std::runtime_error("The Hamiltonian has not been diagonalized yet.");
      73             :     }
      74        1404 :     return matrix.diagonal().real();
      75             : }
      76             : 
      77             : template <typename Derived>
      78             : const Eigen::SparseMatrix<typename System<Derived>::scalar_t, Eigen::RowMajor> &
      79         223 : System<Derived>::get_matrix() const {
      80         223 :     if (hamiltonian_requires_construction) {
      81         177 :         construct_hamiltonian();
      82         177 :         hamiltonian_requires_construction = false;
      83             :     }
      84         223 :     return matrix;
      85             : }
      86             : 
      87             : template <typename Derived>
      88             : Eigen::PermutationMatrix<Eigen::Dynamic, Eigen::Dynamic>
      89        1242 : System<Derived>::get_sorter(const std::vector<SorterType> &labels) const {
      90             :     // The first sorter type in labels is supposed to be the primary sorting criterion.
      91        1242 :     if (hamiltonian_requires_construction) {
      92           6 :         construct_hamiltonian();
      93           6 :         hamiltonian_requires_construction = false;
      94             :     }
      95             : 
      96             :     // Drop repeated sorter types. This also ensures that energy occurs at most once.
      97        1242 :     std::vector<SorterType> unique_labels;
      98        1242 :     unique_labels.reserve(labels.size());
      99        2486 :     for (const auto &label : labels) {
     100        1271 :         if (std::find(unique_labels.begin(), unique_labels.end(), label) == unique_labels.end()) {
     101        1241 :             unique_labels.push_back(label);
     102             :         }
     103             :     }
     104             : 
     105        1211 :     auto it = std::find(unique_labels.begin(), unique_labels.end(), SorterType::ENERGY);
     106        1215 :     std::vector<SorterType> before_energy(unique_labels.begin(), it);
     107        1216 :     bool contains_energy = (it != unique_labels.end());
     108        1221 :     std::vector<SorterType> after_energy(contains_energy ? it + 1 : unique_labels.end(),
     109             :                                          unique_labels.end());
     110             : 
     111        1229 :     Eigen::PermutationMatrix<Eigen::Dynamic, Eigen::Dynamic> sorter(matrix.rows());
     112        1217 :     sorter.setIdentity();
     113             : 
     114        1252 :     if (!after_energy.empty()) {
     115           2 :         basis->get_sorter_without_checks(after_energy, sorter);
     116             :     }
     117             : 
     118        1238 :     if (contains_energy) {
     119         647 :         std::vector<real_t> energies_of_states;
     120         647 :         energies_of_states.reserve(matrix.rows());
     121       31087 :         for (int i = 0; i < matrix.rows(); ++i) {
     122       30476 :             energies_of_states.push_back(std::real(matrix.coeff(i, i)));
     123             :         }
     124             : 
     125         652 :         std::stable_sort(
     126         631 :             sorter.indices().data(), sorter.indices().data() + sorter.indices().size(),
     127      128900 :             [&](int i, int j) { return energies_of_states[i] < energies_of_states[j]; });
     128         664 :     }
     129             : 
     130        1248 :     if (!before_energy.empty()) {
     131         588 :         basis->get_sorter_without_checks(before_energy, sorter);
     132             :     }
     133             : 
     134        2451 :     return sorter;
     135        1236 : }
     136             : 
     137             : template <typename Derived>
     138             : std::vector<IndicesOfBlock>
     139         657 : System<Derived>::get_indices_of_blocks(const std::vector<SorterType> &labels) const {
     140         657 :     if (hamiltonian_requires_construction) {
     141           0 :         construct_hamiltonian();
     142           0 :         hamiltonian_requires_construction = false;
     143             :     }
     144             : 
     145         657 :     std::set<SorterType> unique_labels(labels.begin(), labels.end());
     146         649 :     basis->perform_blocks_checks(unique_labels);
     147             : 
     148         650 :     IndicesOfBlocksCreator blocks_creator({0, static_cast<size_t>(matrix.rows())});
     149             : 
     150         645 :     if (!unique_labels.empty()) {
     151         559 :         basis->get_indices_of_blocks_without_checks(unique_labels, blocks_creator);
     152             :     }
     153             : 
     154        1307 :     return blocks_creator.create();
     155         645 : }
     156             : 
     157             : template <typename Derived>
     158             : System<Derived> &
     159           0 : System<Derived>::transform(const Eigen::SparseMatrix<scalar_t, Eigen::RowMajor> &transformation) {
     160           0 :     if (hamiltonian_requires_construction) {
     161           0 :         construct_hamiltonian();
     162           0 :         hamiltonian_requires_construction = false;
     163             :     }
     164             : 
     165           0 :     set_task_status("Applying transformation...");
     166           0 :     if (matrix.cols() != 0) {
     167           0 :         matrix = transformation.adjoint() * matrix * transformation;
     168           0 :         basis = basis->transformed(transformation);
     169             :     }
     170             : 
     171           0 :     hamiltonian_is_diagonal = false;
     172             : 
     173             :     // A transformed system might have lost its block-diagonalizability if the
     174             :     // transformation was not a sorting
     175           0 :     blockdiagonalizing_labels.clear();
     176             : 
     177           0 :     return *this;
     178             : }
     179             : 
     180             : template <typename Derived>
     181             : System<Derived> &
     182         642 : System<Derived>::transform(const Eigen::PermutationMatrix<Eigen::Dynamic, Eigen::Dynamic> &sorter) {
     183         642 :     if (hamiltonian_requires_construction) {
     184           0 :         construct_hamiltonian();
     185           0 :         hamiltonian_requires_construction = false;
     186             :     }
     187             : 
     188         642 :     set_task_status("Applying transformation...");
     189         666 :     if (matrix.cols() != 0) {
     190         664 :         matrix = matrix.twistedBy(sorter.inverse());
     191         661 :         basis = basis->transformed(sorter);
     192             :     }
     193             : 
     194         662 :     return *this;
     195             : }
     196             : 
     197             : template <typename Derived>
     198         688 : System<Derived> &System<Derived>::diagonalize(const DiagonalizerInterface<scalar_t> &diagonalizer,
     199             :                                               std::optional<real_t> min_eigenenergy,
     200             :                                               std::optional<real_t> max_eigenenergy, double rtol,
     201             :                                               bool sort_by_energy) {
     202         688 :     set_task_status("Preparing Hamiltonian...");
     203             : 
     204         699 :     if (hamiltonian_requires_construction) {
     205         695 :         construct_hamiltonian();
     206         695 :         hamiltonian_requires_construction = false;
     207             :     }
     208             : 
     209         699 :     if (this->is_diagonal()) {
     210          40 :         if (sort_by_energy && !basis->is_sorted_by_energy()) {
     211           2 :             transform(get_sorter({SorterType::ENERGY}));
     212             :         }
     213          40 :         return *this;
     214             :     }
     215             : 
     216         641 :     Eigen::SparseMatrix<scalar_t, Eigen::RowMajor> eigenvectors;
     217         632 :     Eigen::SparseMatrix<scalar_t, Eigen::RowMajor> eigenenergies;
     218             : 
     219             :     // Sort the Hamiltonian according to the block structure
     220         652 :     if (!blockdiagonalizing_labels.empty()) {
     221         551 :         auto sorter = get_sorter(blockdiagonalizing_labels);
     222         574 :         matrix = matrix.twistedBy(sorter.inverse());
     223         590 :         basis = basis->transformed(sorter);
     224         577 :     }
     225             : 
     226             :     // Get the indices of the blocks
     227         648 :     auto blocks = get_indices_of_blocks(blockdiagonalizing_labels);
     228             : 
     229         648 :     assert((blockdiagonalizing_labels.empty() && blocks.size() == 1) ||
     230             :            !blockdiagonalizing_labels.empty());
     231             : 
     232         628 :     SPDLOG_DEBUG("Diagonalizing the Hamiltonian with {} blocks.", blocks.size());
     233             : 
     234             :     // Diagonalize the blocks in parallel
     235         659 :     std::vector<Eigen::VectorX<real_t>> eigenenergies_blocks(blocks.size());
     236         659 :     std::vector<Eigen::SparseMatrix<scalar_t, Eigen::RowMajor>> eigenvectors_blocks(blocks.size());
     237         658 :     oneapi::tbb::parallel_for(
     238        2004 :         oneapi::tbb::blocked_range<size_t>(0, blocks.size()), [&](const auto &range) {
     239        2670 :             for (size_t idx = range.begin(); idx != range.end(); ++idx) {
     240        1347 :                 set_task_status("Diagonalizing Hamiltonian blocks...");
     241        4457 :                 auto eigensys = min_eigenenergy.has_value() || max_eigenenergy.has_value()
     242        2095 :                     ? diagonalizer.eigh(matrix.block(blocks[idx].start, blocks[idx].start,
     243         724 :                                                      blocks[idx].size(), blocks[idx].size()),
     244             :                                         min_eigenenergy, max_eigenenergy, rtol)
     245        3367 :                     : diagonalizer.eigh(matrix.block(blocks[idx].start, blocks[idx].start,
     246        2016 :                                                      blocks[idx].size(), blocks[idx].size()),
     247             :                                         rtol);
     248        1365 :                 eigenvectors_blocks[idx] = eigensys.eigenvectors;
     249        1344 :                 eigenenergies_blocks[idx] = eigensys.eigenvalues;
     250             :             }
     251             :         });
     252             : 
     253             :     // Get the number of non-zeros per row of the combined eigenvector matrix
     254         655 :     std::vector<Eigen::Index> non_zeros_per_inner_index;
     255         655 :     non_zeros_per_inner_index.reserve(matrix.rows());
     256         644 :     Eigen::Index num_rows = 0;
     257         644 :     Eigen::Index num_cols = 0;
     258        2017 :     for (const auto &matrix : eigenvectors_blocks) {
     259        1337 :         set_task_status("Collecting eigenvectors...");
     260       37964 :         for (int i = 0; i < matrix.outerSize(); ++i) {
     261       73259 :             non_zeros_per_inner_index.push_back(matrix.outerIndexPtr()[i + 1] -
     262       36628 :                                                 matrix.outerIndexPtr()[i]);
     263             :         }
     264        1295 :         num_rows += matrix.rows();
     265        1372 :         num_cols += matrix.cols();
     266             :     }
     267             : 
     268         653 :     assert(static_cast<size_t>(num_rows) == basis->get_number_of_states());
     269         653 :     assert(static_cast<size_t>(num_cols) <= basis->get_number_of_states());
     270             : 
     271         652 :     eigenvectors.resize(num_rows, num_cols);
     272         650 :     eigenenergies.resize(num_cols, num_cols);
     273             : 
     274         653 :     if (num_cols > 0) {
     275             :         // Get the combined eigenvector matrix (in case of an restricted energy range, it is not
     276             :         // square)
     277         652 :         eigenvectors.reserve(non_zeros_per_inner_index);
     278         648 :         Eigen::Index offset_rows = 0;
     279         648 :         Eigen::Index offset_cols = 0;
     280        2019 :         for (const auto &matrix : eigenvectors_blocks) {
     281        1352 :             set_task_status("Combining eigenvectors...");
     282       38687 :             for (Eigen::Index i = 0; i < matrix.outerSize(); ++i) {
     283       37310 :                 for (typename Eigen::SparseMatrix<scalar_t, Eigen::RowMajor>::InnerIterator it(
     284             :                          matrix, i);
     285      850983 :                      it; ++it) {
     286      815003 :                     eigenvectors.insert(it.row() + offset_rows, it.col() + offset_cols) =
     287      813726 :                         it.value();
     288             :                 }
     289             :             }
     290        1361 :             offset_rows += matrix.rows();
     291        1369 :             offset_cols += matrix.cols();
     292             :         }
     293         653 :         eigenvectors.makeCompressed();
     294             : 
     295         654 :         assert(
     296             :             eigenvectors.nonZeros() ==
     297             :             std::accumulate(non_zeros_per_inner_index.begin(), non_zeros_per_inner_index.end(), 0));
     298             : 
     299             :         // Get the combined eigenenergy matrix
     300         653 :         eigenenergies.reserve(Eigen::VectorXi::Constant(num_cols, 1));
     301         656 :         Eigen::Index offset = 0;
     302        2026 :         for (const auto &matrix : eigenenergies_blocks) {
     303        1346 :             set_task_status("Combining eigenenergies...");
     304       33159 :             for (int i = 0; i < matrix.size(); ++i) {
     305       31909 :                 eigenenergies.insert(i + offset, i + offset) = matrix(i);
     306             :             }
     307        1328 :             offset += matrix.size();
     308             :         }
     309         654 :         eigenenergies.makeCompressed();
     310             : 
     311             :         // Fix phase ambiguity
     312         653 :         std::vector<scalar_t> map_col_to_max(num_cols, 0);
     313       38011 :         for (int row = 0; row < eigenvectors.outerSize(); ++row) {
     314       37358 :             set_task_status("Normalizing eigenvector phases...");
     315       37533 :             for (typename Eigen::SparseMatrix<scalar_t, Eigen::RowMajor>::InnerIterator it(
     316             :                      eigenvectors, row);
     317      849488 :                  it; ++it) {
     318      809183 :                 if (std::abs(it.value()) > std::abs(map_col_to_max[it.col()])) {
     319      164530 :                     map_col_to_max[it.col()] = it.value();
     320             :                 }
     321             :             }
     322             :         }
     323             : 
     324         643 :         Eigen::SparseMatrix<scalar_t, Eigen::RowMajor> phase_matrix;
     325         657 :         phase_matrix.resize(num_cols, num_cols);
     326         657 :         phase_matrix.reserve(Eigen::VectorXi::Constant(num_cols, 1));
     327       32195 :         for (int i = 0; i < num_cols; ++i) {
     328       31553 :             phase_matrix.insert(i, i) = std::abs(map_col_to_max[i]) / map_col_to_max[i];
     329             :         }
     330         642 :         phase_matrix.makeCompressed();
     331             : 
     332         657 :         set_task_status("Applying eigenvector phases...");
     333         657 :         eigenvectors = eigenvectors * phase_matrix;
     334         657 :     }
     335             : 
     336             :     // Store the diagonalized hamiltonian and assign the eigenenergies to the eigenstates
     337         653 :     matrix = eigenenergies;
     338         648 :     std::vector<real_t> energy_of_eigenstates;
     339         648 :     energy_of_eigenstates.reserve(num_cols);
     340        1982 :     for (const auto &eigenenergies_block : eigenenergies_blocks) {
     341        1331 :         energy_of_eigenstates.insert(energy_of_eigenstates.end(), eigenenergies_block.data(),
     342        1338 :                                      eigenenergies_block.data() + eigenenergies_block.size());
     343             :     }
     344         639 :     basis = basis->transformed(eigenvectors, energy_of_eigenstates);
     345             : 
     346         644 :     hamiltonian_is_diagonal = true;
     347         644 :     if (sort_by_energy) {
     348         637 :         transform(get_sorter({SorterType::ENERGY}));
     349             :     }
     350             : 
     351         659 :     return *this;
     352         659 : }
     353             : 
     354             : template <typename Derived>
     355        2882 : bool System<Derived>::is_diagonal() const {
     356        2882 :     if (hamiltonian_requires_construction) {
     357           0 :         construct_hamiltonian();
     358           0 :         hamiltonian_requires_construction = false;
     359             :     }
     360             : 
     361        2882 :     if (!hamiltonian_is_diagonal) {
     362         898 :         real_t numerical_precision = 100 * std::numeric_limits<real_t>::epsilon();
     363             : 
     364       29656 :         for (int row = 0; row < matrix.outerSize(); ++row) {
     365       29403 :             for (typename Eigen::SparseMatrix<scalar_t, Eigen::RowMajor>::InnerIterator it(matrix,
     366             :                                                                                            row);
     367       59329 :                  it; ++it) {
     368       30280 :                 if (it.row() != it.col() && std::abs(it.value()) > numerical_precision) {
     369         629 :                     return false;
     370             :                 }
     371             :             }
     372             :         }
     373             : 
     374             :         // The states of the basis are eigenstates, so assign them the diagonal entries of the
     375             :         // Hamiltonian as energies
     376         237 :         Eigen::VectorX<real_t> diagonal = matrix.diagonal().real();
     377         244 :         Eigen::SparseMatrix<scalar_t, Eigen::RowMajor> identity(matrix.rows(), matrix.cols());
     378         244 :         identity.setIdentity();
     379         487 :         basis = basis->transformed(
     380         486 :             identity, std::vector<real_t>(diagonal.data(), diagonal.data() + diagonal.size()));
     381             : 
     382         244 :         hamiltonian_is_diagonal = true;
     383         244 :     }
     384             : 
     385        2225 :     return true;
     386             : }
     387             : 
     388             : // Explicit instantiation
     389             : template class System<SystemAtom<double>>;
     390             : template class System<SystemAtom<std::complex<double>>>;
     391             : template class System<SystemPair<double>>;
     392             : template class System<SystemPair<std::complex<double>>>;
     393             : } // namespace pairinteraction

Generated by: LCOV version 1.16