Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
compensation_charge.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Compensation-charge synthesis for datasets that do not ship one.
5 *
6 * A UPF PAW dataset tabulates Q_ij^L(r) directly (`PP_QIJL`). Some formats do
7 * not: they state only the augmentation *charges* q_ij and expect the code to
8 * rebuild the radial functions itself. Without them such a dataset's pseudo
9 * density carries no compensation at all, its multipole moments do not match the
10 * all-electron ones, and the two-grid split of a PAW calculation is invalid.
11 *
12 * The shape is the Kresse–Joubert construction (PRB 59, 1758 (1999)): a
13 * combination of spherical Bessel functions whose arguments are chosen so
14 * each vanishes at the augmentation radius,
15 * fixed by two conditions —
16 *
17 * 1. the L-th moment is exactly the value the partial waves require, so the
18 * compensation is correct by construction rather than by convergence;
19 * 2. the radial derivative vanishes at the augmentation radius, so the
20 * function joins zero smoothly instead of with a kink that would need an
21 * unreasonable plane-wave cutoff to represent.
22 *
23 * Adopting the published shape is not cosmetic. A converged total energy is
24 * independent of the shape — any function with the right moment will do — but
25 * the *rate* of convergence in the augmentation cutoff is not, so building the
26 * shape Kresse & Joubert published removes a finite-augmentation-cutoff
27 * discrepancy from parity comparisons with codes that use it, rather than
28 * leaving it to be explained later; the parity runs confirm the agreement.
29 *
30 * In equation numbers: the shape is the two-Bessel form of Eq. (61) of
31 * G. Kresse & D. Joubert, Phys. Rev. B 59, 1758 (1999), normalized to unit
32 * moment as in their Eq. (62); the moments it is scaled by are their Eq. (26),
33 * and the charge it builds is their Eq. (27). The two wavenumbers here are
34 * taken from the zeros of j_L, with the coefficients fixing the unit moment
35 * and the zero slope; their Eq. (63) takes the wavenumbers from the zeros of
36 * j_L′ instead and uses the coefficients to make g_L vanish at the radius.
37 *
38 * Everything here is r-weighted, matching the partial waves and the rest of
39 * the one-center module: Q_ij^L(r) as stored includes the r² of the volume
40 * element, so `∫ Q_ij^L(r) r^L dr` is the moment directly.
41 */
42
43#include <cmath>
44#include <vector>
45
50
51namespace q3 {
52namespace paw_compensation {
53
54/**
55 * The first `count` positive zeros of j_L, by scan-and-bisect.
56 *
57 * A closed form exists only for L = 0 (multiples of π); for higher L the
58 * zeros are found numerically, which is cheap because only two are ever
59 * needed per channel.
60 */
61inline std::vector<Real> spherical_bessel_zeros(int degree, int count) {
62 std::vector<Real> zeros;
63 const Real scan_step = 0.05;
64 Real previous_argument = 1e-6;
65 Real previous_value = spherical_bessel(degree, previous_argument);
66
67 for (Real argument = previous_argument + scan_step;
68 zeros.size() < static_cast<size_t>(count) && argument < 500.0;
69 argument += scan_step) {
70 Real value = spherical_bessel(degree, argument);
71 if (previous_value == 0.0 || (previous_value < 0.0) != (value < 0.0)) {
72 Real low = previous_argument;
73 Real high = argument;
74 for (int iteration = 0; iteration < 200; iteration++) {
75 Real middle = 0.5 * (low + high);
76 Real middle_value = spherical_bessel(degree, middle);
77 if ((spherical_bessel(degree, low) < 0.0) != (middle_value < 0.0))
78 high = middle;
79 else
80 low = middle;
81 }
82 zeros.push_back(0.5 * (low + high));
83 }
84 previous_argument = argument;
85 previous_value = value;
86 }
87 return zeros;
88}
89
90/// d/dx j_L(x), from the standard recurrence.
91inline Real spherical_bessel_derivative(int degree, Real argument) {
92 if (std::abs(argument) < 1e-12)
93 return degree == 1 ? (1.0 / 3.0) : 0.0;
94 if (degree == 0)
95 return -spherical_bessel(1, argument);
96 return spherical_bessel(degree - 1, argument) -
97 (static_cast<Real>(degree + 1) / argument) * spherical_bessel(degree, argument);
98}
99
100/**
101 * A unit-moment compensation shape for one angular momentum.
102 *
103 * Returns G_L(r), r-weighted and confined to r ≤ r_aug, satisfying
104 * ∫ G_L(r) r^L dr = 1. Multiply by a channel's moment to obtain its
105 * Q_ij^L(r).
106 */
107inline std::vector<Real> build_compensation_shape(int angular_momentum,
108 Real augmentation_radius,
109 const std::vector<Real>& radial_grid,
110 int mesh_size) {
111 std::vector<Real> shape(mesh_size, 0.0);
112 auto zeros = spherical_bessel_zeros(angular_momentum, 2);
113 if (zeros.size() < 2 || augmentation_radius <= 0.0)
114 return shape;
115
116 const Real first_argument = zeros[0] / augmentation_radius;
117 const Real second_argument = zeros[1] / augmentation_radius;
118
119 /**
120 * Moment of each Bessel component, and its slope at the boundary.
121 *
122 * The integral is normalized in the continuum, the modern default; a
123 * legacy compatibility mode some codes keep normalizes on the discrete
124 * mesh instead. Use the exact identity
125 *
126 * integral_0^R j_L(q r) r^(L+2) dr
127 * = R^(L+2) j_(L+1)(q R) / q
128 *
129 * so the construction follows the continuous convention without adding
130 * another quadrature error. The old mesh-normalized form matched the
131 * legacy mode instead and was most visible for Cu d channels in the
132 * absolute parity gate.
133 */
134 auto moment_of = [&](Real wavenumber) {
135 return std::pow(augmentation_radius, angular_momentum + 2) *
136 spherical_bessel(angular_momentum + 1, wavenumber * augmentation_radius) /
137 wavenumber;
138 };
139
140 Real first_moment = moment_of(first_argument);
141 Real second_moment = moment_of(second_argument);
142 Real first_slope = first_argument * spherical_bessel_derivative(
143 angular_momentum, first_argument * augmentation_radius);
144 Real second_slope =
145 second_argument *
146 spherical_bessel_derivative(angular_momentum, second_argument * augmentation_radius);
147
148 /**
149 * Solve
150 * a₁·m₁ + a₂·m₂ = 1 (unit moment)
151 * a₁·s₁ + a₂·s₂ = 0 (no kink at the boundary)
152 */
153 Real determinant = first_moment * second_slope - second_moment * first_slope;
154 if (std::abs(determinant) < 1e-30)
155 return shape;
156 Real first_coefficient = second_slope / determinant;
157 Real second_coefficient = -first_slope / determinant;
158
159 for (int point = 0; point < mesh_size; point++) {
160 Real radius = radial_grid[point];
161 if (radius > augmentation_radius)
162 break;
163 Real value =
164 first_coefficient * spherical_bessel(angular_momentum, first_argument * radius) +
165 second_coefficient * spherical_bessel(angular_momentum, second_argument * radius);
166 shape[point] = value * radius * radius;
167 }
168 return shape;
169}
170
171/**
172 * Multipole moment ∫(φ_i φ_j − φ̃_i φ̃_j) r^L dr for one radial channel pair.
173 *
174 * The quantity the compensation charge must reproduce. Identical to the check
175 * in `paw/validation.hpp`, promoted here because it is now production input
176 * rather than only a dataset sanity test.
177 */
178inline Real partial_wave_moment(const PawPseudopotential& pseudopotential,
179 int first_channel,
180 int second_channel,
181 int angular_momentum) {
182 int mesh_size = pseudopotential.mesh_size;
183 std::vector<Real> integrand(mesh_size);
184 const auto& all_electron_first =
185 pseudopotential.partial_waves_all_electron[first_channel].radial_values;
186 const auto& all_electron_second =
187 pseudopotential.partial_waves_all_electron[second_channel].radial_values;
188 const auto& pseudo_first = pseudopotential.partial_waves_pseudo[first_channel].radial_values;
189 const auto& pseudo_second = pseudopotential.partial_waves_pseudo[second_channel].radial_values;
190
191 for (int point = 0; point < mesh_size; point++) {
192 Real difference = all_electron_first[point] * all_electron_second[point] -
193 pseudo_first[point] * pseudo_second[point];
194 integrand[point] =
195 difference * std::pow(pseudopotential.radial_grid[point], angular_momentum);
196 }
197 return simpson_rule(integrand, pseudopotential.radial_grid_derivative, mesh_size);
198}
199
200/**
201 * Fill `augmentation_function_by_angular_momentum` for a dataset that does not
202 * ship it.
203 *
204 * A no-op when the dataset already has them: a UPF PAW file's own Q_ij^L are
205 * the generator's, and replacing them with a synthesized shape would discard
206 * information the file actually contains.
207 */
209 int number_of_channels = pseudopotential.number_of_projectors;
210 if (number_of_channels == 0)
211 return;
212
213 bool already_present = false;
214 for (const auto& pair : pseudopotential.augmentation_function_by_angular_momentum) {
215 for (const auto& channel : pair) {
216 if (!channel.empty())
217 already_present = true;
218 }
219 }
220 if (already_present)
221 return;
222
223 int maximum_angular_momentum = 0;
224 for (const auto& wave : pseudopotential.partial_waves_pseudo)
225 maximum_angular_momentum = std::max(maximum_angular_momentum, wave.angular_momentum);
226 int number_of_angular_momenta = 2 * maximum_angular_momentum + 1;
227
228 Real augmentation_radius = pseudopotential.augmentation_cutoff_radius;
229 if (augmentation_radius <= 0.0)
230 return;
231
232 // One shape per L, reused across every channel pair that needs it.
233 std::vector<std::vector<Real>> shapes(number_of_angular_momenta);
234 for (int angular_momentum = 0; angular_momentum < number_of_angular_momenta;
235 angular_momentum++) {
236 shapes[angular_momentum] = build_compensation_shape(angular_momentum,
237 augmentation_radius,
238 pseudopotential.radial_grid,
239 pseudopotential.mesh_size);
240 }
241
242 pseudopotential.augmentation_function_by_angular_momentum.assign(
243 static_cast<size_t>(number_of_channels) * number_of_channels,
244 std::vector<std::vector<Real>>(number_of_angular_momenta));
245 pseudopotential.number_of_q_angular_momenta = number_of_angular_momenta;
246
247 for (int first = 0; first < number_of_channels; first++) {
248 for (int second = 0; second < number_of_channels; second++) {
249 int first_l = pseudopotential.partial_waves_pseudo[first].angular_momentum;
250 int second_l = pseudopotential.partial_waves_pseudo[second].angular_momentum;
251 size_t pair_index = static_cast<size_t>(first) * number_of_channels + second;
252
253 for (int angular_momentum = 0; angular_momentum < number_of_angular_momenta;
254 angular_momentum++) {
255 /**
256 * Channels the triangle condition forbids carry no
257 * compensation charge — the same selection rule the UPF
258 * datasets encode as exact zeros.
259 */
260 bool allowed = angular_momentum >= std::abs(first_l - second_l) &&
261 angular_momentum <= first_l + second_l &&
262 (first_l + second_l + angular_momentum) % 2 == 0;
263 if (!allowed)
264 continue;
265
266 Real moment = partial_wave_moment(pseudopotential, first, second, angular_momentum);
267 auto& destination =
268 pseudopotential
269 .augmentation_function_by_angular_momentum[pair_index][angular_momentum];
270 destination.assign(pseudopotential.mesh_size, 0.0);
271 for (int point = 0; point < pseudopotential.mesh_size; point++)
272 destination[point] = moment * shapes[angular_momentum][point];
273 }
274 }
275 }
276}
277
278} // namespace paw_compensation
279} // 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
Real augmentation_cutoff_radius
Radius beyond which the augmentation charges vanish (bohr).
Definition init.hpp:106
std::vector< Real > radial_grid_derivative
Definition init.hpp:77
int number_of_projectors
Definition init.hpp:72
std::vector< Real > radial_grid
Definition init.hpp:76
std::vector< std::vector< std::vector< Real > > > augmentation_function_by_angular_momentum
Definition init.hpp:47
Real spherical_bessel_derivative(int degree, Real argument)
d/dx j_L(x), from the standard recurrence.
Real partial_wave_moment(const PawPseudopotential &pseudopotential, int first_channel, int second_channel, int angular_momentum)
std::vector< Real > build_compensation_shape(int angular_momentum, Real augmentation_radius, const std::vector< Real > &radial_grid, int mesh_size)
void synthesize_augmentation_functions(PawPseudopotential &pseudopotential)
std::vector< Real > spherical_bessel_zeros(int degree, int count)
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
Real spherical_bessel(int degree, Real argument)
Definition bessel.hpp:30