Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
densities.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * One-center densities from on-site occupancies.
5 *
6 * Given ρ_ij — the cross-band occupancies the ultrasoft machinery already
7 * accumulates as its projector overlap coefficients — this builds the two
8 * lm-decomposed radial densities a PAW energy needs:
9 *
10 * n¹ = Σ_ij ρ_ij φ_i φ_j (all-electron)
11 * ñ¹ = Σ_ij ρ_ij [ φ̃_i φ̃_j + Q_ij^L(r) ] (pseudo, compensated)
12 *
13 * The compensation charge is part of the pseudo density rather than a separate
14 * term, because that is what it is defined to be: n̂ exists so that ñ¹ + n̂
15 * carries the same multipole moments as n¹ (Blöchl, PRB 50, 17953 (1994)), which
16 * puts it inside the pseudo density and nowhere else. That placement is what
17 * makes the electrostatics cancel between the two grids, and it is exactly
18 * what the gate on this file checks.
19 *
20 * In equation numbers: n¹ and ñ¹ are Eqs. (16)–(17) of P. E. Blöchl, Phys.
21 * Rev. B 50, 17953 (1994), and Eqs. (5)–(7) of G. Kresse & D. Joubert, Phys.
22 * Rev. B 59, 1758 (1999), with ρ_ij their Eq. (7); the compensated ñ¹ above
23 * is their ñ¹ + n̂. The moment condition is Kresse & Joubert's Eq. (24), and
24 * the compensation charge their Eq. (27).
25 * Blöchl's own moments, his Eq. (24), also include the nuclear charge; the
26 * Kresse–Joubert form used here leaves it out and treats the ion-ion term by
27 * an Ewald sum instead.
28 *
29 * Everything is r-weighted: partial waves are stored as r·φ, so a product of
30 * two already carries the r² of the volume element, and the result is the
31 * `u_L(r) = r² ρ_L(r)` form the radial Poisson solve expects. No factor of r²
32 * is inserted or removed anywhere along the chain.
33 *
34 * Pure over dataset + occupancies: no basis, crystal, or iterator types
35 * appear, which is what lets the gate below run without an SCF.
36 */
37
38#include <cmath>
39#include <memory>
40#include <vector>
41
47
48namespace q3 {
49namespace paw_one_center {
50
51/**
52 * The dataset's projector channels expanded over magnetic quantum numbers.
53 *
54 * A dataset states `number_of_projectors` radial channels, each carrying
55 * 2l+1 magnetic components; occupancies are indexed over that expanded set.
56 * The expansion follows the same convention the plane-wave projector mapping
57 * uses, so an occupancy matrix built by one is readable by the other.
58 */
60 std::vector<int> radial_channel; ///< index into partial_waves_*
61 std::vector<int> angular_momentum; ///< l of that channel
62 std::vector<int> lm_index; ///< flat (l,m) index
63
64 [[nodiscard]] int size() const {
65 return static_cast<int>(radial_channel.size());
66 }
67};
68
69inline ChannelExpansion expand_channels(const PawPseudopotential& pseudopotential) {
70 ChannelExpansion expansion;
71 for (int channel = 0; channel < pseudopotential.number_of_projectors; channel++) {
72 int angular_momentum = pseudopotential.partial_waves_pseudo[channel].angular_momentum;
73 for (int magnetic_projection : harmonic_order_sequence(angular_momentum)) {
74 expansion.radial_channel.push_back(channel);
75 expansion.angular_momentum.push_back(angular_momentum);
76 expansion.lm_index.push_back(
77 harmonic_combined_degree_order_index(angular_momentum, magnetic_projection));
78 }
79 }
80 return expansion;
81}
82
83/**
84 * On-site occupancies ρ_ij for one atom.
85 *
86 * Carries a spin index from the outset even though the first validated path is
87 * unpolarized: adding it later would touch every signature the one-center
88 * module exposes.
89 */
92 int number_of_channels{}; ///< expanded channel count (nh)
93 std::vector<Real> values; ///< [spin][i * nh + j], symmetric in (i, j)
94
95 [[nodiscard]] Real at(int spin, int first, int second) const {
96 return values[static_cast<size_t>(spin) * number_of_channels * number_of_channels +
97 static_cast<size_t>(first) * number_of_channels + second];
98 }
99 Real& at(int spin, int first, int second) {
100 return values[static_cast<size_t>(spin) * number_of_channels * number_of_channels +
101 static_cast<size_t>(first) * number_of_channels + second];
102 }
103
105 OnSiteOccupancies occupancies;
108 occupancies.values.assign(static_cast<size_t>(number_of_spin_components) *
110 0.0);
111 return occupancies;
112 }
113};
114
115/// lm-decomposed one-center densities, r-weighted, for one spin channel.
117 std::vector<std::vector<Real>> all_electron; ///< [lm][radial point]
118 std::vector<std::vector<Real>> pseudo; ///< [lm][radial point], compensated
121};
122
123/// Number of lm channels a density needs: products of two l_max harmonics.
124inline int density_lm_count(const PawPseudopotential& pseudopotential) {
125 int maximum = 0;
126 for (const auto& wave : pseudopotential.partial_waves_pseudo)
127 maximum = std::max(maximum, wave.angular_momentum);
128 int product_degree = 2 * maximum;
129 return (product_degree + 1) * (product_degree + 1);
130}
131
132/**
133 * Build n¹ and ñ¹ (+ compensation) for one spin channel.
134 *
135 * @param gaunt coefficients[lm_product][lm_i][lm_j] = ∫ Y Y Y dΩ
136 */
138 const ChannelExpansion& expansion,
139 const OnSiteOccupancies& occupancies,
140 const GauntCoefficientTable& gaunt,
141 int spin) {
142 OneCenterDensities densities;
143 densities.mesh_size = pseudopotential.mesh_size;
144 densities.number_of_lm = density_lm_count(pseudopotential);
145 densities.all_electron.assign(densities.number_of_lm,
146 std::vector<Real>(densities.mesh_size, 0.0));
147 densities.pseudo.assign(densities.number_of_lm, std::vector<Real>(densities.mesh_size, 0.0));
148
149 int number_of_channels = expansion.size();
150 int number_of_radial = pseudopotential.number_of_projectors;
151
152 for (int first = 0; first < number_of_channels; first++) {
153 for (int second = 0; second < number_of_channels; second++) {
154 Real occupancy = occupancies.at(spin, first, second);
155 if (occupancy == 0.0)
156 continue;
157
158 int radial_first = expansion.radial_channel[first];
159 int radial_second = expansion.radial_channel[second];
160 int lm_first = expansion.lm_index[first];
161 int lm_second = expansion.lm_index[second];
162
163 const auto& all_electron_first =
164 pseudopotential.partial_waves_all_electron[radial_first].radial_values;
165 const auto& all_electron_second =
166 pseudopotential.partial_waves_all_electron[radial_second].radial_values;
167 const auto& pseudo_first =
168 pseudopotential.partial_waves_pseudo[radial_first].radial_values;
169 const auto& pseudo_second =
170 pseudopotential.partial_waves_pseudo[radial_second].radial_values;
171
172 int pair_index = radial_first * number_of_radial + radial_second;
173
174 for (int lm = 0; lm < densities.number_of_lm; lm++) {
175 Real angular = gaunt.coefficients[lm][lm_first][lm_second];
176 if (angular == 0.0)
177 continue;
178 Real weight = occupancy * angular;
179
180 // L of this lm channel selects the augmentation function.
181 int angular_momentum = 0;
182 while ((angular_momentum + 1) * (angular_momentum + 1) <= lm)
183 angular_momentum++;
184
185 const std::vector<Real>* augmentation = nullptr;
186 if (pair_index <
187 static_cast<int>(
188 pseudopotential.augmentation_function_by_angular_momentum.size()) &&
189 angular_momentum <
190 static_cast<int>(
191 pseudopotential.augmentation_function_by_angular_momentum[pair_index]
192 .size())) {
193 const auto& candidate =
194 pseudopotential.augmentation_function_by_angular_momentum[pair_index]
195 [angular_momentum];
196 if (!candidate.empty())
197 augmentation = &candidate;
198 }
199
200 for (int point = 0; point < densities.mesh_size; point++) {
201 densities.all_electron[lm][point] +=
202 weight * all_electron_first[point] * all_electron_second[point];
203 Real pseudo_value = pseudo_first[point] * pseudo_second[point];
204 if (augmentation != nullptr)
205 pseudo_value += (*augmentation)[point];
206 densities.pseudo[lm][point] += weight * pseudo_value;
207 }
208 }
209 }
210 }
211 return densities;
212}
213
214/**
215 * Multipole moment ∫ u_LM(r) r^L dr of one density channel.
216 *
217 * The quantity the compensation charge exists to make equal between the
218 * all-electron and pseudo densities — outside the augmentation sphere the two
219 * then produce identical electrostatics, which is what lets a PAW calculation
220 * be split across two grids at all.
221 */
222inline Real density_channel_moment(const std::vector<Real>& density_channel,
223 int angular_momentum,
224 const std::vector<Real>& radial_grid,
225 const std::vector<Real>& radial_grid_derivative,
226 int mesh_size) {
227 std::vector<Real> integrand(mesh_size);
228 for (int point = 0; point < mesh_size; point++) {
229 integrand[point] = density_channel[point] * std::pow(radial_grid[point], angular_momentum);
230 }
231 return simpson_rule(integrand, radial_grid_derivative, mesh_size);
232}
233
234/// The L of a flat lm index.
235inline int angular_momentum_of_lm(int lm_index) {
236 int angular_momentum = 0;
237 while ((angular_momentum + 1) * (angular_momentum + 1) <= lm_index)
238 angular_momentum++;
239 return angular_momentum;
240}
241
242} // namespace paw_one_center
243} // namespace q3
PAW pseudopotential — extends ultrasoft with one-center (on-site) data.
Definition init.hpp:33
std::vector< PartialWave > partial_waves_pseudo
Pseudo partial waves φ̃_i(r), one per projector channel.
Definition init.hpp:76
std::vector< PartialWave > partial_waves_all_electron
All-electron partial waves φ_i(r), one per projector channel.
Definition init.hpp:74
int number_of_projectors
Definition init.hpp:72
std::vector< std::vector< std::vector< Real > > > augmentation_function_by_angular_momentum
Definition init.hpp:47
OneCenterDensities build_one_center_densities(const PawPseudopotential &pseudopotential, const ChannelExpansion &expansion, const OnSiteOccupancies &occupancies, const GauntCoefficientTable &gaunt, int spin)
int angular_momentum_of_lm(int lm_index)
The L of a flat lm index.
ChannelExpansion expand_channels(const PawPseudopotential &pseudopotential)
Definition densities.hpp:69
int density_lm_count(const PawPseudopotential &pseudopotential)
Number of lm channels a density needs: products of two l_max harmonics.
Real density_channel_moment(const std::vector< Real > &density_channel, int angular_momentum, const std::vector< Real > &radial_grid, const std::vector< Real > &radial_grid_derivative, int mesh_size)
int harmonic_combined_degree_order_index(int degree, int order)
Definition harmonics.hpp:61
Real simpson_rule(const std::vector< Real > &function_values, const std::vector< Real > &integration_factors, int number_of_grid_points)
Definition simpson.hpp:23
double Real
std::vector< int > harmonic_order_sequence(int degree)
Definition harmonics.hpp:40
std::vector< std::vector< std::vector< Real > > > coefficients
std::vector< int > lm_index
flat (l,m) index
Definition densities.hpp:62
std::vector< int > radial_channel
index into partial_waves_*
Definition densities.hpp:60
std::vector< int > angular_momentum
l of that channel
Definition densities.hpp:61
std::vector< Real > values
[spin][i * nh + j], symmetric in (i, j)
Definition densities.hpp:93
static OnSiteOccupancies zeros(int number_of_channels, int number_of_spin_components=1)
Real & at(int spin, int first, int second)
Definition densities.hpp:99
int number_of_channels
expanded channel count (nh)
Definition densities.hpp:92
Real at(int spin, int first, int second) const
Definition densities.hpp:95
lm-decomposed one-center densities, r-weighted, for one spin channel.
std::vector< std::vector< Real > > all_electron
[lm][radial point]
std::vector< std::vector< Real > > pseudo
[lm][radial point], compensated