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

Generated by: LCOV version 1.16