LCOV - code coverage report
Current view: top level - src/system - SystemAtom.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 126 138 91.3 %
Date: 2026-04-17 09:20:02 Functions: 18 18 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/system/SystemAtom.hpp"
       5             : 
       6             : #include "pairinteraction/basis/BasisAtom.hpp"
       7             : #include "pairinteraction/database/Database.hpp"
       8             : #include "pairinteraction/enums/OperatorType.hpp"
       9             : #include "pairinteraction/enums/TransformationType.hpp"
      10             : #include "pairinteraction/ket/KetAtom.hpp"
      11             : #include "pairinteraction/utils/eigen_assertion.hpp"
      12             : #include "pairinteraction/utils/eigen_compat.hpp"
      13             : #include "pairinteraction/utils/operator.hpp"
      14             : #include "pairinteraction/utils/spherical.hpp"
      15             : 
      16             : #include <Eigen/Dense>
      17             : #include <algorithm>
      18             : #include <cmath>
      19             : #include <limits>
      20             : #include <memory>
      21             : #include <set>
      22             : #include <spdlog/spdlog.h>
      23             : 
      24             : namespace pairinteraction {
      25             : template <typename Scalar>
      26         284 : SystemAtom<Scalar>::SystemAtom(std::shared_ptr<const basis_t> basis)
      27         284 :     : System<SystemAtom<Scalar>>(std::move(basis)) {}
      28             : 
      29             : template <typename Scalar>
      30         157 : SystemAtom<Scalar> &SystemAtom<Scalar>::set_electric_field(const std::array<real_t, 3> &field) {
      31         157 :     this->hamiltonian_requires_construction = true;
      32             : 
      33         102 :     if (!traits::NumTraits<Scalar>::is_complex_v && field[1] != 0) {
      34           0 :         throw std::invalid_argument(
      35             :             "The field must not have a y-component if the scalar type is real.");
      36             :     }
      37             : 
      38         157 :     electric_field = field;
      39             : 
      40         157 :     return *this;
      41             : }
      42             : 
      43             : template <typename Scalar>
      44         111 : SystemAtom<Scalar> &SystemAtom<Scalar>::set_magnetic_field(const std::array<real_t, 3> &field) {
      45         111 :     this->hamiltonian_requires_construction = true;
      46             : 
      47          77 :     if (!traits::NumTraits<Scalar>::is_complex_v && field[1] != 0) {
      48           0 :         throw std::invalid_argument(
      49             :             "The field must not have a y-component if the scalar type is real.");
      50             :     }
      51             : 
      52         111 :     magnetic_field = field;
      53             : 
      54         111 :     return *this;
      55             : }
      56             : 
      57             : template <typename Scalar>
      58          97 : SystemAtom<Scalar> &SystemAtom<Scalar>::set_diamagnetism_enabled(bool enable) {
      59          97 :     this->hamiltonian_requires_construction = true;
      60          97 :     diamagnetism_enabled = enable;
      61          97 :     return *this;
      62             : }
      63             : 
      64             : template <typename Scalar>
      65             : SystemAtom<Scalar> &
      66          21 : SystemAtom<Scalar>::set_ion_distance_vector(const std::array<real_t, 3> &vector) {
      67          21 :     this->hamiltonian_requires_construction = true;
      68             : 
      69           4 :     if (!traits::NumTraits<Scalar>::is_complex_v && vector[1] != 0) {
      70           0 :         throw std::invalid_argument(
      71             :             "The distance vector must not have a y-component if the scalar type is real.");
      72             :     }
      73             : 
      74          21 :     ion_distance_vector = vector;
      75             : 
      76          21 :     return *this;
      77             : }
      78             : 
      79             : template <typename Scalar>
      80          19 : SystemAtom<Scalar> &SystemAtom<Scalar>::set_ion_charge(real_t charge) {
      81          19 :     this->hamiltonian_requires_construction = true;
      82          19 :     ion_charge = charge;
      83          19 :     return *this;
      84             : }
      85             : 
      86             : template <typename Scalar>
      87          21 : SystemAtom<Scalar> &SystemAtom<Scalar>::set_ion_interaction_order(int value) {
      88          21 :     this->hamiltonian_requires_construction = true;
      89          21 :     if (value < 2 || value > 3) {
      90           0 :         throw std::invalid_argument("The order of the Rydberg-ion interaction must be 2 or 3");
      91             :     }
      92          21 :     ion_interaction_order = value;
      93          21 :     return *this;
      94             : }
      95             : 
      96             : template <typename Scalar>
      97         282 : void SystemAtom<Scalar>::construct_hamiltonian() const {
      98        1299 :     auto get_operator_matrix = [this](OperatorType type, int q = 0) {
      99         339 :         return this->basis->get_database().get_matrix_elements_in_canonical_basis(
     100         339 :             this->basis, this->basis, type, q);
     101             :     };
     102             : 
     103             :     // Construct the unperturbed Hamiltonian in the canonical atomic basis
     104         282 :     this->matrix = utils::get_energies_in_canonical_basis(this->basis);
     105             : 
     106         282 :     this->hamiltonian_is_diagonal = false;
     107         282 :     bool sort_by_quantum_number_f = this->basis->has_quantum_number_f();
     108         282 :     bool sort_by_quantum_number_m = this->basis->has_quantum_number_m();
     109         282 :     bool sort_by_parity = this->basis->has_parity();
     110             : 
     111             :     // Estimate the numerical precision so that we can decide which terms to keep
     112         282 :     Eigen::VectorX<real_t> diag = this->matrix.diagonal().real();
     113         282 :     real_t scale = (diag - diag.mean() * Eigen::VectorX<real_t>::Ones(diag.size())).norm();
     114         282 :     real_t numerical_precision = 100 * scale * std::numeric_limits<real_t>::epsilon();
     115             : 
     116         282 :     real_t typical_magnetic_dipole = 1e2;     // ~n^1
     117         282 :     real_t typical_electric_dipole = 1e4;     // ~n^2
     118         282 :     real_t typical_electric_quadrupole = 1e8; // ~n^4
     119             : 
     120             :     // Add the interaction with the field of an ion (see
     121             :     // https://en.wikipedia.org/wiki/Multipole_expansion#Spherical_form for details)
     122             : 
     123         564 :     Eigen::Map<const Eigen::Vector3<real_t>> vector_map(ion_distance_vector.data(),
     124         282 :                                                         ion_distance_vector.size());
     125         282 :     real_t distance = vector_map.norm();
     126         282 :     SPDLOG_DEBUG("Distance to the ion: {}", distance);
     127             : 
     128             :     // Dipole order
     129         282 :     if (std::isfinite(distance) && ion_interaction_order >= 2) {
     130             :         // Calculate sqrt(4pi/3) * r * Y_1,q with q = -1, 0, 1 for the first three elements. Take
     131             :         // the conjugate of the result and scale it by 1/distance^2.
     132          21 :         Eigen::Vector3<Scalar> vector_dipole_order =
     133          21 :             spherical::get_transformator<Scalar>(1) * vector_map / distance;
     134          21 :         vector_dipole_order = vector_dipole_order.conjugate() / std::pow(distance, 2);
     135             : 
     136          84 :         for (int q = -1; q <= 1; ++q) {
     137          63 :             if (std::abs(vector_dipole_order[q + 1]) * typical_electric_dipole >
     138             :                 numerical_precision) {
     139          33 :                 this->matrix -= ion_charge * vector_dipole_order[q + 1] *
     140             :                     get_operator_matrix(OperatorType::ELECTRIC_DIPOLE, q);
     141          33 :                 sort_by_quantum_number_f = false;
     142          33 :                 sort_by_parity = false;
     143          33 :                 sort_by_quantum_number_m &= (q == 0);
     144             :             }
     145             :         }
     146             :     }
     147             : 
     148             :     // Quadrupole order (the last entry of vector_quadrupole_order would correspond to
     149             :     // ELECTRIC_QUADRUPOLE_ZERO and does not contribute to a *traceless* quadrupole)
     150         282 :     if (std::isfinite(distance) && ion_interaction_order >= 3) {
     151             :         // Calculate sqrt(4pi/5) * r^2 * Y_2,q / 3 with q = -2, -1, 0, 1, 2 for the first five
     152             :         // elements and (x^2 + y^2 + z^2) / 6 as the last element. Take the conjugate of the
     153             :         // result and scale it by 1/distance^3.
     154          20 :         Eigen::Vector<Scalar, 6> vector_quadrupole_order = spherical::get_transformator<Scalar>(2) *
     155          20 :             Eigen::KroneckerProduct(vector_map / distance, vector_map / distance);
     156          20 :         vector_quadrupole_order = 3 * vector_quadrupole_order.conjugate() / std::pow(distance, 3);
     157             : 
     158         120 :         for (int q = -2; q <= 2; ++q) {
     159         100 :             if (std::abs(vector_quadrupole_order[q + 2]) * typical_electric_quadrupole >
     160             :                 numerical_precision) {
     161          44 :                 this->matrix -= ion_charge * vector_quadrupole_order[q + 2] *
     162             :                     get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE, q);
     163          44 :                 sort_by_quantum_number_f = false;
     164          44 :                 sort_by_quantum_number_m &= (q == 0);
     165             :             }
     166             :         }
     167             :     }
     168             : 
     169             :     // Add external fields (see https://arxiv.org/abs/1612.08053 for details)
     170             : 
     171             :     // Transform the electric field to spherical coordinates and take the conjugate
     172         282 :     Eigen::Vector3<Scalar> electric_field_spherical = spherical::get_transformator<Scalar>(1) *
     173         282 :         Eigen::Map<const Eigen::Vector3<real_t>>(electric_field.data(), electric_field.size());
     174         282 :     electric_field_spherical = electric_field_spherical.conjugate();
     175             : 
     176             :     // Transform the magnetic field to spherical coordinates and take the conjugate
     177         282 :     Eigen::Vector3<Scalar> magnetic_field_spherical = spherical::get_transformator<Scalar>(1) *
     178         282 :         Eigen::Map<const Eigen::Vector3<real_t>>(magnetic_field.data(), magnetic_field.size());
     179         282 :     magnetic_field_spherical = magnetic_field_spherical.conjugate();
     180             : 
     181             :     // Stark effect: - \vec{d} \vec{E} = - d_{1,0} E_{0} + d_{1,1} E_{-} + d_{1,-1} E_{+}
     182             :     // = - d_{1,0} E_{0} - d_{1,1} E_{+}^* - d_{1,-1} E_{-}^*
     183             :     // with the electric dipole operator: d_{1,q} = - e r sqrt{4 pi / 3} Y_{1,q}(\theta, \phi)
     184             :     // where electric_field_spherical=[E_{-}^*, E_{0}, E_{+}^*]
     185        1128 :     for (int q = -1; q <= 1; ++q) {
     186         846 :         if (std::abs(electric_field_spherical[q + 1]) * typical_electric_dipole >
     187             :             numerical_precision) {
     188         183 :             this->matrix -= electric_field_spherical[q + 1] *
     189             :                 get_operator_matrix(OperatorType::ELECTRIC_DIPOLE, q);
     190         183 :             sort_by_quantum_number_f = false;
     191         183 :             sort_by_parity = false;
     192         183 :             sort_by_quantum_number_m &= (q == 0);
     193             :         }
     194             :     }
     195             : 
     196             :     // Zeeman effect: - \vec{\mu} \vec{B} = - \mu_{1,0} B_{0} + \mu_{1,1} B_{-} + \mu_{1,-1} B_{+}
     197             :     // = - \mu_{1,0} B_{0} - \mu_{1,1} B_{+}^* - \mu_{1,-1} B_{-}^*
     198             :     // with the magnetic dipole operator: \vec{\mu} = - \mu_B / \hbar (g_l \vec{l} + g_s \vec{s})
     199             :     // where magnetic_field_spherical=[B_{-}^*, B_{0}, B_{+}^*]
     200        1128 :     for (int q = -1; q <= 1; ++q) {
     201         846 :         if (std::abs(magnetic_field_spherical[q + 1]) * typical_magnetic_dipole >
     202             :             numerical_precision) {
     203          57 :             this->matrix -= magnetic_field_spherical[q + 1] *
     204             :                 get_operator_matrix(OperatorType::MAGNETIC_DIPOLE, q);
     205          57 :             sort_by_quantum_number_f = false;
     206          57 :             sort_by_quantum_number_m &= (q == 0);
     207             :         }
     208             :     }
     209             : 
     210             :     // Diamagnetism: 1 / (8 m_e) abs(\vec{d} \times \vec{B})^2
     211             :     // = (1/12) \left[ B_0^2 (q0 - d_{2,0}) +  B_+ B_- (-2 q0 - d_{2,0})
     212             :     // + \sqrt{3} B_0 B_- d_{2,1} + \sqrt{3} B_0 B_+ d_{2,-1}
     213             :     // - \sqrt{3/2} B_-^2 d_{2,2} - \sqrt{3/2} B_+^2 d_{2,-2} \right]
     214             :     // with the operator: q0 = e^2 r^2 sqrt{4 pi} Y_{0,0} = e^2 r^2
     215             :     // and the electric quadrupole operator: d_{2,q} = e^2 r^2 sqrt{4 pi / 5} Y_{2,q}(\theta, \phi)
     216             :     // where magnetic_field_spherical=[B_{-}^*, B_{0}, B_{+}^*]
     217         282 :     if (diamagnetism_enabled) {
     218          59 :         if (std::abs(magnetic_field_spherical[1]) * typical_electric_quadrupole >
     219             :             numerical_precision) {
     220          14 :             this->matrix += static_cast<real_t>(1 / 12.) *
     221          11 :                 static_cast<Scalar>(std::pow(magnetic_field_spherical[1], 2)) *
     222             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE_ZERO, 0);
     223          14 :             this->matrix -= static_cast<real_t>(1 / 12.) *
     224          11 :                 static_cast<Scalar>(std::pow(magnetic_field_spherical[1], 2)) *
     225             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE, 0);
     226           7 :             sort_by_quantum_number_f = false;
     227             :         }
     228          59 :         if (std::abs(magnetic_field_spherical[0]) * typical_electric_quadrupole >
     229          61 :                 numerical_precision &&
     230           2 :             std::abs(magnetic_field_spherical[2]) * typical_electric_quadrupole >
     231             :                 numerical_precision) {
     232           4 :             this->matrix -= static_cast<real_t>(2 / 12.) * magnetic_field_spherical[0] *
     233           2 :                 magnetic_field_spherical[2] *
     234             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE_ZERO, 0);
     235           4 :             this->matrix -= static_cast<real_t>(1 / 12.) * magnetic_field_spherical[0] *
     236           2 :                 magnetic_field_spherical[2] *
     237             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE, 0);
     238           2 :             sort_by_quantum_number_f = false;
     239             :         }
     240          59 :         if (std::abs(magnetic_field_spherical[1]) * typical_electric_quadrupole >
     241          66 :                 numerical_precision &&
     242           7 :             std::abs(magnetic_field_spherical[2]) * typical_electric_quadrupole >
     243             :                 numerical_precision) {
     244           0 :             this->matrix -= static_cast<real_t>(std::sqrt(3.0) / 12.) *
     245           0 :                 magnetic_field_spherical[1] * magnetic_field_spherical[2] *
     246             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE, 1);
     247           0 :             sort_by_quantum_number_f = false;
     248           0 :             sort_by_quantum_number_m = false;
     249             :         }
     250          59 :         if (std::abs(magnetic_field_spherical[1]) * typical_electric_quadrupole >
     251          66 :                 numerical_precision &&
     252           7 :             std::abs(magnetic_field_spherical[0]) * typical_electric_quadrupole >
     253             :                 numerical_precision) {
     254           0 :             this->matrix -= static_cast<real_t>(std::sqrt(3.0) / 12.) *
     255           0 :                 magnetic_field_spherical[1] * magnetic_field_spherical[0] *
     256             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE, -1);
     257           0 :             sort_by_quantum_number_f = false;
     258           0 :             sort_by_quantum_number_m = false;
     259             :         }
     260          59 :         if (std::abs(magnetic_field_spherical[2]) * typical_electric_quadrupole >
     261             :             numerical_precision) {
     262           4 :             this->matrix -= static_cast<real_t>(std::sqrt(1.5) / 12.) *
     263           4 :                 static_cast<Scalar>(std::pow(magnetic_field_spherical[2], 2)) *
     264             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE, 2);
     265           2 :             sort_by_quantum_number_f = false;
     266           2 :             sort_by_quantum_number_m = false;
     267             :         }
     268          59 :         if (std::abs(magnetic_field_spherical[0]) * typical_electric_quadrupole >
     269             :             numerical_precision) {
     270           4 :             this->matrix -= static_cast<real_t>(std::sqrt(1.5) / 12.) *
     271           4 :                 static_cast<Scalar>(std::pow(magnetic_field_spherical[0], 2)) *
     272             :                 get_operator_matrix(OperatorType::ELECTRIC_QUADRUPOLE, -2);
     273           2 :             sort_by_quantum_number_f = false;
     274           2 :             sort_by_quantum_number_m = false;
     275             :         }
     276             :     }
     277             : 
     278             :     // Transform from the canonical basis into the actual basis
     279         282 :     this->matrix =
     280         282 :         this->basis->get_coefficients().adjoint() * this->matrix * this->basis->get_coefficients();
     281             : 
     282             :     // Store which labels can be used to block-diagonalize the Hamiltonian
     283         282 :     this->blockdiagonalizing_labels.clear();
     284         282 :     if (sort_by_quantum_number_f) {
     285          96 :         this->blockdiagonalizing_labels.push_back(TransformationType::SORT_BY_QUANTUM_NUMBER_F);
     286             :     }
     287         282 :     if (sort_by_quantum_number_m) {
     288         236 :         this->blockdiagonalizing_labels.push_back(TransformationType::SORT_BY_QUANTUM_NUMBER_M);
     289             :     }
     290         282 :     if (sort_by_parity) {
     291         142 :         this->blockdiagonalizing_labels.push_back(TransformationType::SORT_BY_PARITY);
     292             :     }
     293         282 : }
     294             : 
     295             : // Explicit instantiations
     296             : template class SystemAtom<double>;
     297             : template class SystemAtom<std::complex<double>>;
     298             : } // namespace pairinteraction

Generated by: LCOV version 1.16