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

Generated by: LCOV version 1.16