LCOV - code coverage report
Current view: top level - src/basis - BasisPairCreator.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 123 129 95.3 %
Date: 2026-08-14 15:36:24 Functions: 14 14 100.0 %

          Line data    Source code
       1             : // SPDX-FileCopyrightText: 2024 PairInteraction Developers
       2             : // SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : #include "pairinteraction/basis/BasisPairCreator.hpp"
       5             : 
       6             : #include "pairinteraction/basis/BasisAtom.hpp"
       7             : #include "pairinteraction/basis/BasisPair.hpp"
       8             : #include "pairinteraction/enums/Parity.hpp"
       9             : #include "pairinteraction/ket/KetPair.hpp"
      10             : #include "pairinteraction/system/SystemAtom.hpp"
      11             : #include "pairinteraction/utils/TaskControl.hpp"
      12             : #include "pairinteraction/utils/hash.hpp"
      13             : 
      14             : #include <algorithm>
      15             : #include <array>
      16             : #include <cassert>
      17             : #include <cmath>
      18             : #include <limits>
      19             : #include <memory>
      20             : #include <stdexcept>
      21             : #include <unordered_map>
      22             : 
      23             : namespace pairinteraction {
      24             : template <typename Scalar>
      25        1030 : BasisPairCreator<Scalar> &BasisPairCreator<Scalar>::add(const SystemAtom<Scalar> &system_atom) {
      26             :     // The system must be diagonalized and its eigenstates sorted by energy.
      27             :     // Sorting is required for the binary search of the energetically allowed range in create().
      28             :     // By default, System::diagonalize ensures this.
      29        1030 :     if (!system_atom.is_diagonal_and_sorted_by_energy()) {
      30           0 :         throw std::invalid_argument(
      31             :             "The system must be diagonalized and sorted by energy before it can be added. "
      32             :             "Consider calling diagonalize() on the SystemAtom which also sorts the eigenstates.");
      33             :     }
      34        1030 :     systems_atom.push_back(system_atom);
      35        1030 :     return *this;
      36             : }
      37             : 
      38             : template <typename Scalar>
      39         222 : BasisPairCreator<Scalar> &BasisPairCreator<Scalar>::restrict_energy(real_t min, real_t max) {
      40         222 :     range_energy = {min, max};
      41         222 :     return *this;
      42             : }
      43             : 
      44             : template <typename Scalar>
      45          55 : BasisPairCreator<Scalar> &BasisPairCreator<Scalar>::restrict_quantum_number_m(real_t min,
      46             :                                                                               real_t max) {
      47          55 :     range_quantum_number_m = {min, max};
      48          55 :     return *this;
      49             : }
      50             : 
      51             : template <typename Scalar>
      52           9 : BasisPairCreator<Scalar> &BasisPairCreator<Scalar>::restrict_parity_under_inversion(Parity value) {
      53           9 :     parity_under_inversion = value;
      54           9 :     return *this;
      55             : }
      56             : 
      57             : template <typename Scalar>
      58             : BasisPairCreator<Scalar> &
      59          11 : BasisPairCreator<Scalar>::restrict_parity_under_permutation(Parity value) {
      60          11 :     parity_under_permutation = value;
      61          11 :     return *this;
      62             : }
      63             : 
      64             : template <typename Scalar>
      65         515 : std::shared_ptr<const BasisPair<Scalar>> BasisPairCreator<Scalar>::create() const {
      66         515 :     set_task_status("Constructing pair basis...");
      67             : 
      68         515 :     if (systems_atom.size() != 2) {
      69           0 :         throw std::invalid_argument("Two SystemAtom must be added before creating the BasisPair.");
      70             :     }
      71             : 
      72             :     // Only references to the systems are stored, so a system might have been changed since add()
      73        1545 :     for (const auto &system_atom : systems_atom) {
      74        1030 :         if (!system_atom.get().is_diagonal_and_sorted_by_energy()) {
      75           0 :             throw std::invalid_argument(
      76             :                 "The systems must still be diagonalized and sorted by energy when the BasisPair is "
      77             :                 "created. Do not change a SystemAtom after it has been added.");
      78             :         }
      79             :     }
      80             : 
      81         515 :     constexpr real_t numerical_precision = 100 * std::numeric_limits<real_t>::epsilon();
      82         515 :     const bool has_symmetry_restriction =
      83         515 :         parity_under_inversion != Parity::UNKNOWN || parity_under_permutation != Parity::UNKNOWN;
      84             : 
      85             :     // This ensures that a one-atom state can be identified across both atoms by its state index
      86         515 :     if (has_symmetry_restriction && &systems_atom[0].get() != &systems_atom[1].get()) {
      87           6 :         throw std::invalid_argument(
      88             :             "Parity restrictions require the same SystemAtom to be added twice, because "
      89             :             "symmetrization is only defined for two identical atoms.");
      90             :     }
      91             : 
      92         509 :     Parity inferred_product_of_parities = Parity::UNKNOWN;
      93         509 :     if (parity_under_inversion != Parity::UNKNOWN && parity_under_permutation != Parity::UNKNOWN) {
      94           1 :         inferred_product_of_parities = static_cast<Parity>(
      95           1 :             static_cast<int>(parity_under_inversion) * static_cast<int>(parity_under_permutation));
      96             :     }
      97             : 
      98         509 :     const auto &system1 = systems_atom[0].get();
      99         509 :     const auto &system2 = systems_atom[1].get();
     100             : 
     101             :     // Construct the canonical basis that contains all KetPair objects with allowed energies and
     102             :     // quantum numbers
     103         509 :     auto basis1 = system1.get_basis();
     104         509 :     auto basis2 = system2.get_basis();
     105         509 :     auto eigenenergies1 = system1.get_eigenenergies();
     106         509 :     auto eigenenergies2 = system2.get_eigenenergies();
     107         509 :     real_t *eigenenergies2_begin = eigenenergies2.data();
     108         509 :     real_t *eigenenergies2_end = eigenenergies2_begin + eigenenergies2.size();
     109             : 
     110         509 :     ketvec_t kets;
     111         509 :     kets.reserve(eigenenergies1.size() * eigenenergies2.size());
     112             : 
     113         509 :     typename basis_t::map_range_t map_range_of_state_index2;
     114         509 :     map_range_of_state_index2.reserve(eigenenergies1.size());
     115             : 
     116         509 :     typename basis_t::map_indices_t state_indices_to_ket_index;
     117             : 
     118         509 :     Eigen::Index state_index = 0;
     119             :     std::unordered_map<std::array<size_t, 2>, Eigen::Index, utils::hash<std::array<size_t, 2>>>
     120         509 :         ket_indices2state_index;
     121         509 :     std::vector<Eigen::Triplet<Scalar>> transformation_triplets;
     122         509 :     if (has_symmetry_restriction) {
     123          13 :         transformation_triplets.reserve(eigenenergies1.size() * eigenenergies2.size());
     124             :     }
     125         509 :     const double inverse_sqrt_two = 1 / std::sqrt(2.0);
     126             : 
     127             :     // Construct the symmetry transformation by recording the contribution of the pair state
     128             :     // |idx1, idx2> to the symmetrized basis. Because the two atoms are identical (enforced above),
     129             :     // a one-atom state is uniquely identified across both atoms by its state index alone.
     130      311021 :     auto construct_symmetry_transformation = [&](Eigen::Index row_index, size_t idx1, size_t idx2) {
     131             :         // Following https://doi.org/10.1088/1361-6455/aa743a, pair states |a, a> cannot be of even
     132             :         // parity.
     133       65456 :         if (idx1 == idx2 &&
     134         780 :             (parity_under_inversion == Parity::EVEN || parity_under_permutation == Parity::EVEN)) {
     135       33118 :             return;
     136             :         }
     137             : 
     138             :         // Map the (unordered) pair of one-atom state indices to the column index of the symmetrized
     139             :         // state it contributes to, creating a new column the first time the pair is encountered.
     140       65084 :         std::array<size_t, 2> ordered_indices{std::max(idx1, idx2), std::min(idx1, idx2)};
     141       65084 :         auto [iterator, inserted] =
     142       65084 :             ket_indices2state_index.try_emplace(ordered_indices, state_index);
     143       65084 :         if (inserted) {
     144       32746 :             ++state_index;
     145             :         }
     146       65084 :         Eigen::Index column_index = iterator->second;
     147             : 
     148             :         // A pair state |a, a> contributes with coefficient one.
     149       65084 :         if (idx1 == idx2) {
     150         408 :             transformation_triplets.emplace_back(row_index, column_index, 1);
     151         408 :             return;
     152             :         }
     153             : 
     154             :         // We let pair states with idx1 > idx2 contribute with coefficient 1/sqrt(2) and put the
     155             :         // phase into the contribution of the partner state with idx1 < idx2.
     156       64676 :         if (idx1 > idx2) {
     157       32338 :             transformation_triplets.emplace_back(row_index, column_index, inverse_sqrt_two);
     158       32338 :             return;
     159             :         }
     160             : 
     161             :         // Determine the phase of the contribution of the partner state with idx1 < idx2.
     162       32338 :         int phase = 0;
     163       32338 :         if (parity_under_permutation != Parity::UNKNOWN) {
     164             :             // If both inversion and permutation are restricted, the earlier filter on the product
     165             :             // of parities already guarantees that the phases in the inversion- and
     166             :             // permutation-symmetric states are the same.
     167       16252 :             phase = -static_cast<int>(parity_under_permutation);
     168             :         } else {
     169             :             // If only inversion is restricted, the phase is determined by the product of the
     170             :             // parities of the one-atom states and the specified inversion parity.
     171       48258 :             phase = -static_cast<int>(parity_under_inversion) *
     172       16086 :                 static_cast<int>(basis1->get_parity(idx1)) *
     173       16086 :                 static_cast<int>(basis2->get_parity(idx2));
     174             :         }
     175       32338 :         transformation_triplets.emplace_back(row_index, column_index, phase * inverse_sqrt_two);
     176             :     };
     177             : 
     178             :     // Loop only over states with an allowed energy
     179         509 :     size_t ket_index = 0;
     180       57644 :     for (size_t idx1 = 0; idx1 < static_cast<size_t>(eigenenergies1.size()); ++idx1) {
     181       57135 :         set_task_status("Constructing pair basis...");
     182             : 
     183             :         // Get the energetically allowed range of the second index
     184       57135 :         size_t min = 0;
     185       57135 :         size_t max = eigenenergies2.size();
     186       57135 :         if (range_energy.is_finite()) {
     187       39884 :             real_t min_val2 = range_energy.min() - eigenenergies1[idx1];
     188       39884 :             real_t max_val2 = range_energy.max() - eigenenergies1[idx1];
     189       39884 :             min =
     190       39884 :                 std::distance(eigenenergies2_begin,
     191             :                               std::lower_bound(eigenenergies2_begin, eigenenergies2_end, min_val2));
     192       39884 :             max =
     193       39884 :                 std::distance(eigenenergies2_begin,
     194             :                               std::upper_bound(eigenenergies2_begin, eigenenergies2_end, max_val2));
     195             :         }
     196       57135 :         map_range_of_state_index2.try_emplace(idx1, typename basis_t::range_t(min, max));
     197             : 
     198             :         // Loop over the energetically allowed range of the second index
     199     5988259 :         for (size_t idx2 = min; idx2 < max; ++idx2) {
     200             :             // Get energy
     201     2965594 :             const real_t energy = eigenenergies1[idx1] + eigenenergies2[idx2];
     202     2965594 :             assert(!range_energy.is_finite() ||
     203             :                    (energy >= range_energy.min() && energy <= range_energy.max()));
     204             : 
     205             :             // Check the parity of the product of the parities
     206     2965594 :             if (inferred_product_of_parities != Parity::UNKNOWN) {
     207         144 :                 if (static_cast<int>(basis1->get_parity(idx1)) *
     208         144 :                         static_cast<int>(basis2->get_parity(idx2)) !=
     209             :                     static_cast<int>(inferred_product_of_parities)) {
     210       67385 :                     continue;
     211             :                 }
     212             :             }
     213             : 
     214             :             // Create a KetPair object
     215     8896590 :             auto ket = std::make_shared<ket_t>(
     216     2965530 :                 typename ket_t::Private(), std::initializer_list<size_t>{idx1, idx2},
     217             :                 std::initializer_list<std::shared_ptr<const BasisAtom<Scalar>>>{basis1, basis2},
     218             :                 energy);
     219             : 
     220             :             // Check the quantum number m
     221     2965530 :             if (ket->has_quantum_number_m()) {
     222     3092270 :                 if (range_quantum_number_m.is_finite() &&
     223      126740 :                     (ket->get_quantum_number_m() <
     224      126740 :                          range_quantum_number_m.min() - numerical_precision ||
     225       73169 :                      ket->get_quantum_number_m() >
     226       73169 :                          range_quantum_number_m.max() + numerical_precision)) {
     227       67321 :                     continue;
     228             :                 }
     229           0 :             } else if (range_quantum_number_m.is_finite()) {
     230           0 :                 throw std::invalid_argument(
     231             :                     "The quantum number m must not be restricted because it is not well-defined.");
     232             :             }
     233             : 
     234             :             // Store the KetPair object as a ket
     235     2898209 :             kets.emplace_back(std::move(ket));
     236     2898209 :             state_indices_to_ket_index.try_emplace(std::vector<size_t>{idx1, idx2}, ket_index);
     237             : 
     238     2898209 :             auto row_index = static_cast<Eigen::Index>(ket_index++);
     239     2898209 :             if (has_symmetry_restriction) {
     240       65456 :                 construct_symmetry_transformation(row_index, idx1, idx2);
     241             :             }
     242             :         }
     243             :     }
     244             : 
     245         509 :     kets.shrink_to_fit();
     246             : 
     247         509 :     std::shared_ptr<const basis_t> basis = std::make_shared<basis_t>(
     248         509 :         typename basis_t::Private(), std::move(kets), std::move(map_range_of_state_index2),
     249         509 :         std::move(state_indices_to_ket_index), basis1, basis2);
     250             : 
     251         509 :     if (!has_symmetry_restriction) {
     252         496 :         return basis;
     253             :     }
     254             : 
     255          13 :     transformation_triplets.shrink_to_fit();
     256             : 
     257          26 :     Eigen::SparseMatrix<Scalar, Eigen::RowMajor> transformation_matrix(
     258          13 :         basis->get_number_of_states(), state_index);
     259          13 :     transformation_matrix.setFromTriplets(transformation_triplets.begin(),
     260          13 :                                           transformation_triplets.end());
     261             : 
     262          13 :     Eigen::Matrix<real_t, Eigen::Dynamic, 1> sum_of_squared_coefficients =
     263             :         transformation_matrix.cwiseAbs2().transpose() *
     264             :         Eigen::Matrix<real_t, Eigen::Dynamic, 1>::Ones(transformation_matrix.rows());
     265       32759 :     for (Eigen::Index column_index = 0; column_index < sum_of_squared_coefficients.size();
     266             :          ++column_index) {
     267       32746 :         if (std::abs(sum_of_squared_coefficients[column_index] - 1) > numerical_precision) {
     268           0 :             throw std::invalid_argument(
     269             :                 "The basis could not be symmetrized. This likely means that the specified parity "
     270             :                 "restrictions are invalid for the given one-atom systems.");
     271             :         }
     272             :     }
     273             : 
     274             :     // TODO: on the long run, construct the coefficient matrix directly
     275          13 :     auto transformation = Transformation<Scalar>(std::move(transformation_matrix));
     276          13 :     return basis->transformed(transformation);
     277     2966039 : }
     278             : 
     279             : // Explicit instantiations
     280             : template class BasisPairCreator<double>;
     281             : template class BasisPairCreator<std::complex<double>>;
     282             : } // namespace pairinteraction

Generated by: LCOV version 1.16