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

Generated by: LCOV version 1.16