LCOV - code coverage report
Current view: top level - src/basis - BasisPairCreator.test.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 368 369 99.7 %
Date: 2026-08-17 11:38:34 Functions: 9 9 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/BasisAtomCreator.hpp"
       8             : #include "pairinteraction/basis/BasisPair.hpp"
       9             : #include "pairinteraction/database/Database.hpp"
      10             : #include "pairinteraction/diagonalize/DiagonalizerEigen.hpp"
      11             : #include "pairinteraction/enums/OperatorType.hpp"
      12             : #include "pairinteraction/enums/Parity.hpp"
      13             : #include "pairinteraction/enums/TransformationType.hpp"
      14             : #include "pairinteraction/ket/KetAtom.hpp"
      15             : #include "pairinteraction/ket/KetAtomCreator.hpp"
      16             : #include "pairinteraction/ket/KetPair.hpp"
      17             : #include "pairinteraction/system/SystemAtom.hpp"
      18             : #include "pairinteraction/system/SystemPair.hpp"
      19             : #include "pairinteraction/utils/hash.hpp"
      20             : #include "pairinteraction/utils/streamed.hpp"
      21             : 
      22             : #include <algorithm>
      23             : #include <array>
      24             : #include <cmath>
      25             : #include <doctest/doctest.h>
      26             : #include <unordered_map>
      27             : #include <utility>
      28             : #include <vector>
      29             : 
      30             : namespace pairinteraction {
      31             : 
      32             : constexpr double HARTREE_IN_GHZ = 6579683.920501762;
      33             : constexpr double VOLT_PER_CM_IN_ATOMIC_UNITS = 1 / 5.14220675112e9;
      34             : constexpr double UM_IN_ATOMIC_UNITS = 1 / 5.29177210544e-5;
      35             : 
      36             : namespace {
      37             : template <typename Scalar>
      38             : std::shared_ptr<const typename BasisAtom<Scalar>::ket_t>
      39         864 : get_dominant_ket(const std::shared_ptr<const BasisAtom<Scalar>> &basis, size_t state_index) {
      40         864 :     Eigen::MatrixX<Scalar> column(
      41         864 :         basis->get_coefficients().col(static_cast<Eigen::Index>(state_index)));
      42         864 :     Eigen::Index ket_index = 0;
      43         864 :     column.cwiseAbs().col(0).maxCoeff(&ket_index);
      44        1728 :     return basis->get_ket(static_cast<size_t>(ket_index));
      45         864 : }
      46             : 
      47             : template <typename Scalar>
      48             : Eigen::SparseMatrix<Scalar, Eigen::RowMajor>
      49           2 : build_manual_symmetrizer(const std::shared_ptr<const BasisPair<Scalar>> &basis,
      50             :                          Parity parity_under_inversion, Parity parity_under_permutation) {
      51             :     using real_t = typename BasisPair<Scalar>::real_t;
      52             : 
      53           2 :     const auto basis1 = basis->get_basis1();
      54           2 :     const auto basis2 = basis->get_basis2();
      55           2 :     const auto inv_sqrt_two = static_cast<real_t>(1 / std::sqrt(2.0));
      56             : 
      57           2 :     std::vector<Eigen::Triplet<Scalar>> triplets;
      58           2 :     triplets.reserve(2 * basis->get_number_of_states());
      59             : 
      60           2 :     Eigen::Index state_index = 0;
      61             :     std::unordered_map<std::array<size_t, 2>, Eigen::Index, utils::hash<std::array<size_t, 2>>>
      62           2 :         ket_ids_to_state_index;
      63          26 :     for (size_t idx1 = 0; idx1 < basis1->get_number_of_states(); ++idx1) {
      64         312 :         for (size_t idx2 = 0; idx2 < basis2->get_number_of_states(); ++idx2) {
      65         288 :             int ket_index = basis->get_ket_index_from_tuple(idx1, idx2);
      66         288 :             if (ket_index < 0) {
      67          24 :                 continue;
      68             :             }
      69             : 
      70         288 :             size_t id1 = get_dominant_ket(basis1, idx1)->get_id_in_database();
      71         288 :             size_t id2 = get_dominant_ket(basis2, idx2)->get_id_in_database();
      72             : 
      73         288 :             if (id1 == id2) {
      74          24 :                 if (parity_under_inversion == Parity::EVEN ||
      75             :                     parity_under_permutation == Parity::EVEN) {
      76           0 :                     continue;
      77             :                 }
      78          24 :                 triplets.emplace_back(ket_index, state_index++, Scalar{1});
      79          24 :                 continue;
      80             :             }
      81             : 
      82         264 :             std::array<size_t, 2> ordered_ids{std::max(id1, id2), std::min(id1, id2)};
      83         264 :             auto [it, inserted] = ket_ids_to_state_index.try_emplace(ordered_ids, state_index);
      84         264 :             if (inserted) {
      85         132 :                 ++state_index;
      86             :             }
      87         264 :             Eigen::Index column_index = it->second;
      88             : 
      89         264 :             if (id1 > id2) {
      90         132 :                 triplets.emplace_back(ket_index, column_index, static_cast<Scalar>(inv_sqrt_two));
      91             :             } else {
      92         132 :                 int swapped_sign = 0;
      93         132 :                 if (parity_under_inversion != Parity::UNKNOWN &&
      94             :                     parity_under_permutation == Parity::UNKNOWN) {
      95         198 :                     swapped_sign = -static_cast<int>(parity_under_inversion) *
      96          66 :                         static_cast<int>(basis1->get_parity(idx1)) *
      97          66 :                         static_cast<int>(basis2->get_parity(idx2));
      98             :                 } else {
      99          66 :                     swapped_sign = -static_cast<int>(parity_under_permutation);
     100             :                 }
     101         132 :                 triplets.emplace_back(ket_index, column_index,
     102         132 :                                       static_cast<Scalar>(swapped_sign * inv_sqrt_two));
     103             :             }
     104             :         }
     105             :     }
     106             : 
     107           4 :     Eigen::SparseMatrix<Scalar, Eigen::RowMajor> transformation(
     108           2 :         static_cast<Eigen::Index>(basis->get_number_of_states()), state_index);
     109           2 :     transformation.setFromTriplets(triplets.begin(), triplets.end());
     110           4 :     return transformation;
     111           2 : }
     112             : 
     113             : template <typename Scalar>
     114             : std::shared_ptr<const BasisPair<Scalar>>
     115           4 : build_pair_basis(std::shared_ptr<const BasisAtom<Scalar>> basis1,
     116             :                  std::shared_ptr<const BasisAtom<Scalar>> basis2) {
     117           4 :     auto system1 = SystemAtom<Scalar>(std::move(basis1));
     118           4 :     auto system2 = SystemAtom<Scalar>(std::move(basis2));
     119          12 :     return BasisPairCreator<Scalar>().add(system1).add(system2).create();
     120           4 : }
     121             : 
     122             : template <typename Scalar>
     123           2 : void check_same_pair_eigenenergies(const std::shared_ptr<const BasisPair<Scalar>> &basis1,
     124             :                                    const std::shared_ptr<const BasisPair<Scalar>> &basis2,
     125             :                                    const DiagonalizerEigen<Scalar> &diagonalizer) {
     126           2 :     auto system_pair_1 = SystemPair<Scalar>(basis1).set_distance_vector(
     127             :         std::array<typename BasisPair<Scalar>::real_t, 3>{0, 0, 1 * UM_IN_ATOMIC_UNITS});
     128           2 :     auto system_pair_2 = SystemPair<Scalar>(basis2).set_distance_vector(
     129             :         std::array<typename BasisPair<Scalar>::real_t, 3>{0, 0, 1 * UM_IN_ATOMIC_UNITS});
     130             : 
     131           2 :     system_pair_1.diagonalize(diagonalizer);
     132           2 :     system_pair_2.diagonalize(diagonalizer);
     133             : 
     134           2 :     auto eigenenergies_1 = system_pair_1.get_eigenenergies();
     135           2 :     auto eigenenergies_2 = system_pair_2.get_eigenenergies();
     136             : 
     137           2 :     DOCTEST_REQUIRE(eigenenergies_1.size() == eigenenergies_2.size());
     138           2 :     DOCTEST_CHECK(eigenenergies_1.isApprox(eigenenergies_2, 1e-11));
     139           2 : }
     140             : } // namespace
     141             : 
     142           3 : DOCTEST_TEST_CASE("create a BasisPair") {
     143             :     // Create single-atom system
     144           3 :     Database &database = Database::get_global_instance();
     145           3 :     auto basis = BasisAtomCreator<double>()
     146           6 :                      .set_species("Rb")
     147           6 :                      .restrict_quantum_number("n", 58, 62)
     148           6 :                      .restrict_quantum_number("l", 0, 2)
     149           3 :                      .create(database);
     150           3 :     SystemAtom<double> system(basis);
     151           3 :     system.set_electric_field({0, 0, 1 * VOLT_PER_CM_IN_ATOMIC_UNITS});
     152             : 
     153           3 :     DiagonalizerEigen<double> diagonalizer;
     154           3 :     system.diagonalize(diagonalizer);
     155             : 
     156             :     // Get energy window for a two-atom basis
     157           3 :     auto ket = KetAtomCreator()
     158           6 :                    .set_species("Rb")
     159           6 :                    .set_quantum_number("n", 60)
     160           6 :                    .set_quantum_number("l", 0)
     161           6 :                    .set_quantum_number("m", 0.5)
     162           3 :                    .create(database);
     163           3 :     double min_energy = 2 * ket->get_energy() - 3 / HARTREE_IN_GHZ;
     164           3 :     double max_energy = 2 * ket->get_energy() + 3 / HARTREE_IN_GHZ;
     165             : 
     166             :     // Create two-atom bases
     167           3 :     auto basis_pair_a = pairinteraction::BasisPairCreator<double>()
     168           3 :                             .add(system)
     169           3 :                             .add(system)
     170           3 :                             .restrict_energy(min_energy, max_energy)
     171           3 :                             .restrict_quantum_number_m(1, 1)
     172           3 :                             .create();
     173           3 :     auto basis_pair_b = pairinteraction::BasisPairCreator<double>()
     174           3 :                             .add(system)
     175           3 :                             .add(system)
     176           3 :                             .restrict_energy(min_energy, max_energy)
     177           3 :                             .restrict_quantum_number_m(1, 1)
     178           3 :                             .create();
     179             : 
     180           3 :     DOCTEST_SUBCASE("check equality of kets") {
     181             :         // Obtain kets from the two-atom bases and check for equality
     182           1 :         auto ket1a = basis_pair_a->get_kets()[0];
     183           1 :         auto ket1b = basis_pair_b->get_kets()[0];
     184           1 :         auto ket2a = basis_pair_a->get_kets()[1];
     185           1 :         auto ket2b = basis_pair_b->get_kets()[1];
     186           1 :         DOCTEST_CHECK(*ket1a == *ket1a);
     187           1 :         DOCTEST_CHECK(*ket2a == *ket2a);
     188           1 :         DOCTEST_CHECK(*ket1a != *ket2b);
     189           1 :         DOCTEST_CHECK(*ket2a != *ket1b);
     190           1 :         DOCTEST_CHECK(*ket1a == *ket1b);
     191           1 :         DOCTEST_CHECK(*ket2a == *ket2b);
     192           4 :     }
     193             : 
     194           3 :     DOCTEST_SUBCASE("check overlap") {
     195           1 :         auto basis_ket = BasisAtomCreator<double>().add_ket(ket).create(database);
     196           1 :         auto basis_pair_ket = build_pair_basis<double>(basis_ket, basis_ket);
     197             :         Eigen::RowVectorXd amplitudes =
     198             :             basis_pair_a
     199           2 :                 ->get_matrix_elements(basis_pair_ket, OperatorType::IDENTITY,
     200             :                                       OperatorType::IDENTITY, 0, 0)
     201           1 :                 .row(0);
     202           1 :         auto overlaps = amplitudes.cwiseAbs2().eval();
     203             : 
     204             :         // The total overlap is less than 1 because of the restricted energy window
     205           1 :         DOCTEST_CHECK(overlaps.sum() == doctest::Approx(0.9107819201));
     206           4 :     }
     207             : 
     208           3 :     DOCTEST_SUBCASE("get the atomic states constituting a ket of the basis_pair") {
     209           1 :         auto atomic_states = basis_pair_a->get_kets()[0]->get_atomic_states();
     210           1 :         DOCTEST_CHECK(atomic_states.size() == 2);
     211           1 :         DOCTEST_CHECK(atomic_states[0]->get_number_of_states() == 1);
     212           1 :         DOCTEST_CHECK(atomic_states[0]->get_number_of_kets() == basis->get_number_of_kets());
     213           4 :     }
     214           3 : }
     215             : 
     216           1 : DOCTEST_TEST_CASE("merge rejects differently transformed atomic bases") {
     217           1 :     auto &database = Database::get_global_instance();
     218           1 :     auto atomic_basis = BasisAtomCreator<double>()
     219           2 :                             .set_species("Rb")
     220           2 :                             .restrict_quantum_number("n", 60, 60)
     221           2 :                             .restrict_quantum_number("l", 0, 1)
     222           2 :                             .restrict_quantum_number("m", 0.5, 0.5)
     223           1 :                             .create(database);
     224             : 
     225           1 :     auto system_a = SystemAtom<double>(atomic_basis);
     226           1 :     system_a.set_electric_field({0, 0, 1 * VOLT_PER_CM_IN_ATOMIC_UNITS});
     227           1 :     system_a.diagonalize(DiagonalizerEigen<double>());
     228             : 
     229           1 :     auto system_b = SystemAtom<double>(atomic_basis);
     230           1 :     system_b.set_electric_field({0, 0, 2 * VOLT_PER_CM_IN_ATOMIC_UNITS});
     231           1 :     system_b.diagonalize(DiagonalizerEigen<double>());
     232             : 
     233           1 :     auto pair_basis_a = BasisPairCreator<double>().add(system_a).add(system_a).create();
     234           1 :     auto pair_basis_b = BasisPairCreator<double>().add(system_b).add(system_b).create();
     235             : 
     236           2 :     DOCTEST_CHECK_THROWS_WITH_AS(pair_basis_a->merge(pair_basis_b),
     237             :                                  doctest::Contains("Cannot merge two pair bases"),
     238             :                                  std::invalid_argument);
     239           1 : }
     240             : 
     241           2 : DOCTEST_TEST_CASE("get matrix elements in the pair basis") {
     242           2 :     DiagonalizerEigen<double> diagonalizer;
     243             : 
     244             :     // Create single-atom system
     245           2 :     Database &database = Database::get_global_instance();
     246           2 :     auto basis = BasisAtomCreator<double>()
     247           4 :                      .set_species("Rb")
     248           4 :                      .restrict_quantum_number("n", 58, 62)
     249           4 :                      .restrict_quantum_number("l", 0, 2)
     250           2 :                      .create(database);
     251           2 :     SystemAtom<double> system(basis);
     252           2 :     system.set_electric_field({0, 0, 10 * VOLT_PER_CM_IN_ATOMIC_UNITS});
     253           2 :     system.diagonalize(diagonalizer);
     254             : 
     255             :     // Get energy window for a two-atom basis
     256           2 :     auto ket = KetAtomCreator()
     257           4 :                    .set_species("Rb")
     258           4 :                    .set_quantum_number("n", 60)
     259           4 :                    .set_quantum_number("l", 0)
     260           4 :                    .set_quantum_number("m", 0.5)
     261           2 :                    .create(database);
     262           2 :     double min_energy = 2 * ket->get_energy() - 3 / HARTREE_IN_GHZ;
     263           2 :     double max_energy = 2 * ket->get_energy() + 3 / HARTREE_IN_GHZ;
     264             : 
     265             :     // Create two-atom system
     266           2 :     auto basis_pair_unperturbed = pairinteraction::BasisPairCreator<double>()
     267           2 :                                       .add(system)
     268           2 :                                       .add(system)
     269           2 :                                       .restrict_energy(min_energy, max_energy)
     270           2 :                                       .restrict_quantum_number_m(1, 1)
     271           2 :                                       .create();
     272           4 :     auto system_pair = SystemPair<double>(basis_pair_unperturbed)
     273           2 :                            .set_distance_vector({0, 0, 1 * UM_IN_ATOMIC_UNITS});
     274           2 :     system_pair.diagonalize(diagonalizer);
     275             : 
     276           2 :     auto basis_pair = system_pair.get_eigenbasis();
     277             : 
     278           2 :     DOCTEST_SUBCASE("check dimensions") {
     279             :         // <basis_pair_unperturbed|d0d0|basis_pair_unperturbed>
     280             :         auto matrix_elements_all = basis_pair_unperturbed->get_matrix_elements(
     281             :             basis_pair_unperturbed, OperatorType::ELECTRIC_DIPOLE, OperatorType::ELECTRIC_DIPOLE, 0,
     282           1 :             0);
     283           1 :         DOCTEST_CHECK(matrix_elements_all.rows() == basis_pair_unperturbed->get_number_of_states());
     284           1 :         DOCTEST_CHECK(matrix_elements_all.cols() == basis_pair_unperturbed->get_number_of_states());
     285             : 
     286             :         // <ket_pair|d0d0|basis_pair_unperturbed>
     287           1 :         auto atomic_states = basis_pair_unperturbed->get_kets()[0]->get_atomic_states();
     288           1 :         auto basis_pair_ket_pair = build_pair_basis<double>(atomic_states[0], atomic_states[1]);
     289             :         Eigen::RowVectorXd matrix_elements_ket_pair =
     290             :             basis_pair_unperturbed
     291           2 :                 ->get_matrix_elements(basis_pair_ket_pair, OperatorType::ELECTRIC_DIPOLE,
     292             :                                       OperatorType::ELECTRIC_DIPOLE, 0, 0)
     293           1 :                 .row(0);
     294           1 :         DOCTEST_CHECK(matrix_elements_ket_pair.size() ==
     295             :                       basis_pair_unperturbed->get_number_of_states());
     296             : 
     297             :         {
     298           1 :             Eigen::RowVectorXd ref = matrix_elements_all.row(0);
     299           1 :             DOCTEST_CHECK(ref.isApprox(matrix_elements_ket_pair, 1e-11));
     300           1 :         }
     301             : 
     302             :         // <basis x basis|d0d0|basis_pair>
     303           1 :         auto basis_pair_product = build_pair_basis<double>(basis, basis);
     304             :         auto matrix_elements_product = basis_pair->get_matrix_elements(
     305           1 :             basis_pair_product, OperatorType::ELECTRIC_DIPOLE, OperatorType::ELECTRIC_DIPOLE, 0, 0);
     306           1 :         DOCTEST_CHECK(matrix_elements_product.rows() ==
     307             :                       basis->get_number_of_states() * basis->get_number_of_states());
     308           1 :         DOCTEST_CHECK(matrix_elements_product.cols() == basis_pair->get_number_of_states());
     309             : 
     310             :         // <ket,ket|d0d0|basis_pair>
     311           1 :         auto basis_ket = BasisAtomCreator<double>().add_ket(ket).create(database);
     312           1 :         auto basis_pair_ket = build_pair_basis<double>(basis_ket, basis_ket);
     313             :         auto matrix_elements_ket = basis_pair->get_matrix_elements(
     314           1 :             basis_pair_ket, OperatorType::ELECTRIC_DIPOLE, OperatorType::ELECTRIC_DIPOLE, 0, 0);
     315           1 :         DOCTEST_CHECK(matrix_elements_ket.rows() == 1);
     316           1 :         DOCTEST_CHECK(matrix_elements_ket.cols() == basis_pair->get_number_of_states());
     317           3 :     }
     318             : 
     319           2 :     DOCTEST_SUBCASE("check matrix elements") {
     320             :         // energy
     321             :         auto hamiltonian = basis_pair->get_matrix_elements(basis_pair, OperatorType::ENERGY,
     322           1 :                                                            OperatorType::IDENTITY, 0, 0);
     323           2 :         hamiltonian += basis_pair->get_matrix_elements(basis_pair, OperatorType::IDENTITY,
     324           1 :                                                        OperatorType::ENERGY, 0, 0);
     325             : 
     326             :         // interaction with electric field
     327             :         {
     328           2 :             Eigen::SparseMatrix<double, Eigen::RowMajor> tmp = -basis_pair->get_matrix_elements(
     329           1 :                 basis_pair, OperatorType::ELECTRIC_DIPOLE, OperatorType::IDENTITY, 0, 0);
     330           2 :             tmp += -basis_pair->get_matrix_elements(basis_pair, OperatorType::IDENTITY,
     331           1 :                                                     OperatorType::ELECTRIC_DIPOLE, 0, 0);
     332           1 :             hamiltonian += 10 * VOLT_PER_CM_IN_ATOMIC_UNITS * tmp;
     333           1 :         }
     334             : 
     335             :         // dipole-dipole interaction
     336             :         {
     337           1 :             Eigen::SparseMatrix<double, Eigen::RowMajor> tmp = -2 *
     338           2 :                 basis_pair->get_matrix_elements(basis_pair, OperatorType::ELECTRIC_DIPOLE,
     339           1 :                                                 OperatorType::ELECTRIC_DIPOLE, 0, 0);
     340           2 :             tmp += -basis_pair->get_matrix_elements(basis_pair, OperatorType::ELECTRIC_DIPOLE,
     341           1 :                                                     OperatorType::ELECTRIC_DIPOLE, 1, -1);
     342           2 :             tmp += -basis_pair->get_matrix_elements(basis_pair, OperatorType::ELECTRIC_DIPOLE,
     343           1 :                                                     OperatorType::ELECTRIC_DIPOLE, -1, 1);
     344           1 :             hamiltonian += std::pow(UM_IN_ATOMIC_UNITS, -3) * tmp;
     345           1 :         }
     346             : 
     347             :         // compare to reference
     348           1 :         const auto &ref = system_pair.get_matrix();
     349           1 :         DOCTEST_CHECK(ref.isApprox(hamiltonian, 1e-11));
     350           3 :     }
     351           2 : }
     352             : 
     353           1 : DOCTEST_TEST_CASE("get amplitudes (via matrix elements) between different pair basis") {
     354           1 :     DiagonalizerEigen<double> diagonalizer;
     355             : 
     356           1 :     Database &database = Database::get_global_instance();
     357           1 :     auto atomic_basis = BasisAtomCreator<double>()
     358           2 :                             .set_species("Rb")
     359           2 :                             .restrict_quantum_number("n", 58, 62)
     360           2 :                             .restrict_quantum_number("l", 0, 2)
     361           2 :                             .restrict_quantum_number("m", 0.5, 0.5)
     362           1 :                             .create(database);
     363             : 
     364           1 :     auto ket = KetAtomCreator()
     365           2 :                    .set_species("Rb")
     366           2 :                    .set_quantum_number("n", 60)
     367           2 :                    .set_quantum_number("l", 0)
     368           2 :                    .set_quantum_number("m", 0.5)
     369           1 :                    .create(database);
     370             : 
     371           1 :     auto perturbed_system1 = SystemAtom<double>(atomic_basis);
     372           1 :     perturbed_system1.set_electric_field({0, 0, 1 * VOLT_PER_CM_IN_ATOMIC_UNITS});
     373           1 :     perturbed_system1.diagonalize(diagonalizer);
     374             : 
     375           1 :     auto perturbed_system2 = SystemAtom<double>(atomic_basis);
     376           1 :     perturbed_system2.set_electric_field({0, 0, 2 * VOLT_PER_CM_IN_ATOMIC_UNITS});
     377           1 :     perturbed_system2.diagonalize(diagonalizer);
     378             : 
     379           1 :     double min_energy = 2 * ket->get_energy() - 20 / HARTREE_IN_GHZ;
     380           1 :     double max_energy = 2 * ket->get_energy() + 20 / HARTREE_IN_GHZ;
     381           1 :     auto perturbed_basis = BasisPairCreator<double>()
     382           1 :                                .add(perturbed_system1)
     383           1 :                                .add(perturbed_system2)
     384           1 :                                .restrict_energy(min_energy, max_energy)
     385           1 :                                .create();
     386           1 :     auto state_in_perturbed_basis = perturbed_basis->get_state(42);
     387             : 
     388           1 :     auto unperturbed_system1 = SystemAtom<double>(atomic_basis);
     389           1 :     auto unperturbed_system2 = SystemAtom<double>(atomic_basis);
     390           1 :     min_energy = 2 * ket->get_energy() - 10 / HARTREE_IN_GHZ;
     391           1 :     max_energy = 2 * ket->get_energy() + 10 / HARTREE_IN_GHZ;
     392           1 :     auto small_unperturbed_basis = BasisPairCreator<double>()
     393           1 :                                        .add(unperturbed_system1)
     394           1 :                                        .add(unperturbed_system2)
     395           1 :                                        .restrict_energy(min_energy, max_energy)
     396           1 :                                        .create();
     397           1 :     DOCTEST_CHECK(perturbed_basis->get_number_of_states() !=
     398             :                   small_unperturbed_basis->get_number_of_states());
     399             : 
     400             :     auto amplitudes = state_in_perturbed_basis->get_matrix_elements(
     401           1 :         small_unperturbed_basis, OperatorType::IDENTITY, OperatorType::IDENTITY, 0, 0);
     402           1 :     DOCTEST_CHECK(amplitudes.rows() == small_unperturbed_basis->get_number_of_states());
     403           1 :     DOCTEST_CHECK(amplitudes.cols() == state_in_perturbed_basis->get_number_of_states());
     404           1 : }
     405             : 
     406           5 : DOCTEST_TEST_CASE("create a symmetrized BasisPair") {
     407           5 :     auto &database = Database::get_global_instance();
     408           5 :     auto diagonalizer = DiagonalizerEigen<double>();
     409             : 
     410           5 :     auto basis = BasisAtomCreator<double>()
     411          10 :                      .set_species("Rb")
     412          10 :                      .restrict_quantum_number("n", 60, 61)
     413          10 :                      .restrict_quantum_number("l", 0, 1)
     414          10 :                      .restrict_quantum_number("m", -0.5, 0.5)
     415           5 :                      .create(database);
     416             : 
     417           5 :     SystemAtom<double> system(basis);
     418           5 :     system.diagonalize(diagonalizer);
     419             : 
     420           5 :     auto canonical_basis = BasisPairCreator<double>().add(system).add(system).create();
     421             : 
     422           5 :     DOCTEST_SUBCASE("restrict permutation parity") {
     423           1 :         auto symmetrized_basis = BasisPairCreator<double>()
     424           1 :                                      .add(system)
     425           1 :                                      .add(system)
     426           1 :                                      .restrict_parity_under_permutation(Parity::ODD)
     427           1 :                                      .create();
     428             : 
     429           3 :         auto expected_basis = canonical_basis->transformed(Transformation<double>(
     430           3 :             build_manual_symmetrizer(canonical_basis, Parity::UNKNOWN, Parity::ODD)));
     431             : 
     432           1 :         check_same_pair_eigenenergies(symmetrized_basis, expected_basis, diagonalizer);
     433           1 :         DOCTEST_CHECK(symmetrized_basis->get_number_of_states() <
     434             :                       canonical_basis->get_number_of_states());
     435           6 :     }
     436             : 
     437           5 :     DOCTEST_SUBCASE("restrict inversion parity") {
     438           1 :         auto symmetrized_basis = BasisPairCreator<double>()
     439           1 :                                      .add(system)
     440           1 :                                      .add(system)
     441           1 :                                      .restrict_parity_under_inversion(Parity::ODD)
     442           1 :                                      .create();
     443             : 
     444           3 :         auto expected_basis = canonical_basis->transformed(Transformation<double>(
     445           3 :             build_manual_symmetrizer(canonical_basis, Parity::ODD, Parity::UNKNOWN)));
     446             : 
     447           1 :         check_same_pair_eigenenergies(symmetrized_basis, expected_basis, diagonalizer);
     448           1 :         DOCTEST_CHECK(symmetrized_basis->get_number_of_states() <
     449             :                       canonical_basis->get_number_of_states());
     450           6 :     }
     451             : 
     452           5 :     DOCTEST_SUBCASE("restrict permutation parity to EVEN includes identical-state kets") {
     453           1 :         auto symmetrized_basis_even = BasisPairCreator<double>()
     454           1 :                                           .add(system)
     455           1 :                                           .add(system)
     456           1 :                                           .restrict_parity_under_permutation(Parity::EVEN)
     457           1 :                                           .create();
     458             : 
     459           1 :         auto symmetrized_basis_odd = BasisPairCreator<double>()
     460           1 :                                          .add(system)
     461           1 :                                          .add(system)
     462           1 :                                          .restrict_parity_under_permutation(Parity::ODD)
     463           1 :                                          .create();
     464             : 
     465             :         // Count kets in the canonical basis where both atoms are in the same state (id1 == id2).
     466             :         // Such kets are always permutation-symmetric and must appear in EVEN but not ODD.
     467           1 :         size_t num_diagonal_kets = 0;
     468         145 :         for (const auto &ket : *canonical_basis) {
     469         144 :             auto atomic_states = ket->get_atomic_states();
     470         144 :             DOCTEST_REQUIRE(atomic_states.size() == 2);
     471         144 :             size_t id1 = get_dominant_ket(atomic_states[0], 0)->get_id_in_database();
     472         144 :             size_t id2 = get_dominant_ket(atomic_states[1], 0)->get_id_in_database();
     473         144 :             if (id1 == id2) {
     474          12 :                 ++num_diagonal_kets;
     475             :             }
     476         144 :         }
     477           1 :         DOCTEST_REQUIRE(num_diagonal_kets > 0);
     478             : 
     479             :         // EVEN and ODD together partition all canonical kets: off-diagonal pairs contribute one
     480             :         // state to each sector, diagonal kets only contribute to EVEN.
     481           1 :         DOCTEST_CHECK(symmetrized_basis_even->get_number_of_states() +
     482             :                           symmetrized_basis_odd->get_number_of_states() ==
     483             :                       canonical_basis->get_number_of_states());
     484           1 :         DOCTEST_CHECK(symmetrized_basis_odd->get_number_of_states() -
     485             :                           symmetrized_basis_even->get_number_of_states() ==
     486             :                       num_diagonal_kets);
     487             : 
     488             :         // The eigenenergies of the EVEN and ODD bases together must reproduce the
     489             :         // eigenenergies of the canonical (non-symmetrized) basis.
     490             :         auto system_pair_canonical =
     491           1 :             SystemPair<double>(canonical_basis).set_distance_vector({0, 0, 1 * UM_IN_ATOMIC_UNITS});
     492           2 :         auto system_pair_even = SystemPair<double>(symmetrized_basis_even)
     493           1 :                                     .set_distance_vector({0, 0, 1 * UM_IN_ATOMIC_UNITS});
     494           2 :         auto system_pair_odd = SystemPair<double>(symmetrized_basis_odd)
     495           1 :                                    .set_distance_vector({0, 0, 1 * UM_IN_ATOMIC_UNITS});
     496             : 
     497           1 :         system_pair_canonical.diagonalize(diagonalizer);
     498           1 :         system_pair_even.diagonalize(diagonalizer);
     499           1 :         system_pair_odd.diagonalize(diagonalizer);
     500             : 
     501           1 :         auto canonical_eigenenergies = system_pair_canonical.get_eigenenergies();
     502           1 :         auto even_eigenenergies = system_pair_even.get_eigenenergies();
     503           1 :         auto odd_eigenenergies = system_pair_odd.get_eigenenergies();
     504             : 
     505           1 :         DOCTEST_REQUIRE(canonical_eigenenergies.size() ==
     506             :                         even_eigenenergies.size() + odd_eigenenergies.size());
     507             : 
     508           1 :         Eigen::VectorXd combined_eigenenergies(canonical_eigenenergies.size());
     509           1 :         combined_eigenenergies << even_eigenenergies, odd_eigenenergies;
     510           1 :         std::sort(combined_eigenenergies.data(),
     511           1 :                   combined_eigenenergies.data() + combined_eigenenergies.size());
     512           1 :         std::sort(canonical_eigenenergies.data(),
     513           1 :                   canonical_eigenenergies.data() + canonical_eigenenergies.size());
     514             : 
     515           1 :         DOCTEST_CHECK(combined_eigenenergies.isApprox(canonical_eigenenergies, 1e-11));
     516           6 :     }
     517             : 
     518           5 :     DOCTEST_SUBCASE("combine inversion and permutation parity") {
     519           1 :         auto symmetrized_basis = BasisPairCreator<double>()
     520           1 :                                      .add(system)
     521           1 :                                      .add(system)
     522           1 :                                      .restrict_parity_under_inversion(Parity::ODD)
     523           1 :                                      .restrict_parity_under_permutation(Parity::ODD)
     524           1 :                                      .create();
     525             : 
     526           1 :         DOCTEST_CHECK(symmetrized_basis->get_number_of_states() <
     527             :                       canonical_basis->get_number_of_states());
     528             : 
     529             :         Eigen::SparseMatrix<double, Eigen::ColMajor> coefficients =
     530           1 :             symmetrized_basis->get_coefficients();
     531           1 :         const double inv_sqrt_two = 1 / std::sqrt(2.0);
     532             : 
     533          47 :         for (int state_index = 0; state_index < coefficients.outerSize(); ++state_index) {
     534          46 :             std::vector<std::pair<int, double>> entries;
     535          46 :             for (Eigen::SparseMatrix<double, Eigen::ColMajor>::InnerIterator it(coefficients,
     536          46 :                                                                                 state_index);
     537         126 :                  it; ++it) {
     538          80 :                 auto atomic_states = symmetrized_basis->get_kets()[it.row()]->get_atomic_states();
     539          80 :                 DOCTEST_REQUIRE(atomic_states.size() == 2);
     540          80 :                 DOCTEST_CHECK(static_cast<int>(atomic_states[0]->get_parity(0)) *
     541             :                                   static_cast<int>(atomic_states[1]->get_parity(0)) ==
     542             :                               static_cast<int>(Parity::EVEN));
     543          80 :                 entries.emplace_back(it.row(), it.value());
     544          80 :             }
     545             : 
     546          46 :             DOCTEST_CHECK(entries.size() >= 1);
     547          46 :             DOCTEST_CHECK(entries.size() <= 2);
     548          46 :             if (entries.size() == 1) {
     549          12 :                 DOCTEST_CHECK(entries[0].second == doctest::Approx(1));
     550             :             } else {
     551          34 :                 DOCTEST_CHECK(std::abs(entries[0].second) == doctest::Approx(inv_sqrt_two));
     552          34 :                 DOCTEST_CHECK(std::abs(entries[1].second) == doctest::Approx(inv_sqrt_two));
     553          34 :                 DOCTEST_CHECK(entries[0].second == doctest::Approx(entries[1].second));
     554             :             }
     555          46 :         }
     556           6 :     }
     557             : 
     558           5 :     DOCTEST_SUBCASE("parity restrictions require the same SystemAtom twice") {
     559             :         // A second, independently constructed system represents a different atom. Even though it
     560             :         // is built from the same basis, it is a distinct object, so symmetrization is rejected.
     561           1 :         SystemAtom<double> system_other(basis);
     562           1 :         system_other.diagonalize(diagonalizer);
     563             : 
     564           2 :         DOCTEST_CHECK_THROWS_AS(BasisPairCreator<double>()
     565             :                                     .add(system)
     566             :                                     .add(system_other)
     567             :                                     .restrict_parity_under_permutation(Parity::ODD)
     568             :                                     .create(),
     569             :                                 std::invalid_argument);
     570             : 
     571           2 :         DOCTEST_CHECK_THROWS_AS(BasisPairCreator<double>()
     572             :                                     .add(system)
     573             :                                     .add(system_other)
     574             :                                     .restrict_parity_under_inversion(Parity::ODD)
     575             :                                     .create(),
     576             :                                 std::invalid_argument);
     577             : 
     578             :         // Without a parity restriction, two different systems remain allowed.
     579           1 :         DOCTEST_CHECK_NOTHROW(BasisPairCreator<double>().add(system).add(system_other).create());
     580           6 :     }
     581           5 : }
     582             : 
     583             : } // namespace pairinteraction

Generated by: LCOV version 1.16