Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
energy.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * One-center energy and potential: E¹ − Ẽ¹ and the potentials D_ij is built
5 * from.
6 *
7 * The two-branch decomposition is the PAW formalism's own (Blöchl, PRB 50,
8 * 17953 (1994)): an all-electron and a pseudo branch, each contributing
9 * Hartree plus exchange-correlation, combined with opposite signs. What
10 * differs between the branches is only which density and which core density
11 * each uses —
12 *
13 * all-electron: n¹, core = the frozen all-electron core
14 * pseudo: ñ¹ + n̂, core = the *pseudized* core
15 *
16 * — and getting the second of those wrong (using one core for both) shifts
17 * every element with a core correction by a smooth, plausible amount.
18 *
19 * In equation numbers: the split E = Ẽ + E¹ − Ẽ¹ is stated with Eq. (18) of
20 * P. E. Blöchl, Phys. Rev. B 50, 17953 (1994), whose Eqs. (20) and (21) are
21 * E¹ and Ẽ¹. The pseudized core in the pseudo branch's exchange-correlation
22 * term is the Kresse–Joubert form, Eqs. (22)–(23) of G. Kresse & D. Joubert,
23 * Phys. Rev. B 59, 1758 (1999).
24 *
25 * The result is reported as four separate numbers rather than one total, on
26 * purpose: they are the same four-way (Hartree|XC x AE|PS) per-atom split
27 * Quantum ESPRESSO reports, so a µRy-gate failure can be bisected against the
28 * reference term by term instead of staring at two totals.
29 *
30 * Hartree separates per lm channel and is solved exactly; XC does not, because
31 * it is nonlinear in the density. So XC is evaluated by summing the channels
32 * onto real directions on the angular grid, calling the functional pointwise
33 * there, and projecting the potential back onto Y_LM — which is what the
34 * quadrature's exactness gate exists to make safe.
35 */
36
37#include <array>
38#include <cmath>
39#include <vector>
40
49
50namespace q3 {
51namespace paw_one_center {
52
53/// Real spherical harmonics at every angular grid point, [point][lm].
54inline std::vector<std::vector<Real>>
55tabulate_harmonics(const std::vector<AngularPoint>& angular_grid, int number_of_lm) {
56 int maximum_angular_momentum = 0;
57 while ((maximum_angular_momentum + 1) * (maximum_angular_momentum + 1) < number_of_lm)
58 maximum_angular_momentum++;
59
60 std::vector<std::vector<Real>> harmonics(angular_grid.size(),
61 std::vector<Real>(number_of_lm, 0.0));
62 for (size_t point = 0; point < angular_grid.size(); point++) {
63 Vector3D direction{angular_grid[point].x, angular_grid[point].y, angular_grid[point].z};
64 for (int degree = 0; degree <= maximum_angular_momentum; degree++) {
65 for (int order : harmonic_order_sequence(degree)) {
66 int index = harmonic_combined_degree_order_index(degree, order);
67 if (index < number_of_lm)
68 harmonics[point][index] =
69 compute_real_spherical_harmonic(degree, order, direction);
70 }
71 }
72 }
73 return harmonics;
74}
75
76/**
77 * The extra terms a dataset's convention asks the one-center engine to
78 * include.
79 *
80 * Default-constructed means "none", which is the absolute formulation UPF-PAW
81 * datasets need. Anything switched on here belongs to an atomic-referenced
82 * convention, whose datasets have had the corresponding piece removed from
83 * their tabulated D_ion at generation time. Switching one on for a UPF dataset
84 * double-counts it: doing so moved diamond from 1.98 to 5221 meV/atom against
85 * QE, which is how the separation was established rather than assumed.
86 *
87 * Filled by `PawOneCenterConvention`, never by callers directly.
88 */
90 /**
91 * Whether E_xc[n_core] is removed from the all-electron branch.
92 *
93 * That branch evaluates E_xc at n_valence + n_core, which necessarily
94 * contains the core's interaction with itself — a property of the frozen
95 * core, not of this calculation. An atomic-referenced convention removes
96 * it as a setup-time constant. A dataset may state a related header value,
97 * but one covering only the remainder from outside the augmentation sphere
98 * cannot stand in for the term, which therefore has to be computed rather
99 * than read.
100 */
102};
103
104/// One branch (all-electron or pseudo) of the one-center terms.
108 /// v_H + v_rho, lm-decomposed — what D_ij's local term integrates against.
109 std::vector<std::vector<Real>> potential_lm;
110
111 /**
112 * Gradient-correction weights, empty for LDA.
113 *
114 * A GGA energy depends on ∇n, so its derivative carries a term
115 * 2∫v_σ ∇n·∇δn. Rather than take the divergence ∇·(2v_σ∇n) — which would
116 * introduce a numerical error the D ≡ ∂E/∂ρ identity cannot absorb — the
117 * term is integrated by parts against the perturbation's own channels:
118 *
119 * 2∫ v_σ ∇n·∇δn dV = Σ_lm ∫ dr [ r² g'_lm P_lm + g_lm Q_lm ]
120 *
121 * with δn = Σ_lm g_lm(r) Y_lm(Ω), and
122 *
123 * P_lm(r) = ∫dΩ 2 v_σ (∂n/∂r) Y_lm
124 * Q_lm(r) = ∫dΩ 2 v_σ (∇_Ω n · ∇_Ω Y_lm)
125 *
126 * so the identity holds by construction rather than by cancellation.
127 */
128 std::vector<std::vector<Real>> gradient_radial_weight_lm; ///< P_lm
129 std::vector<std::vector<Real>> gradient_angular_weight_lm; ///< Q_lm
130
131 [[nodiscard]] bool has_gradient_correction() const {
132 return !gradient_radial_weight_lm.empty();
133 }
134};
135
136/// Both branches, kept separate so a gate failure can be localized.
140
141 /// E¹ − Ẽ¹, the quantity that enters the total energy.
142 [[nodiscard]] Real total() const {
145 }
146};
147
148/**
149 * Exchange-correlation for one branch on the radial × angular grid.
150 *
151 * @param density_channels u_lm(r) = r² ρ_lm(r)
152 * @param core_density spherical ρ_core(r), added to the total before the
153 * functional is evaluated (never to the Hartree term)
154 */
155inline void compute_one_center_xc(const std::vector<std::vector<Real>>& density_channels,
156 const std::vector<Real>& core_density,
157 const std::vector<Real>& radial_grid,
158 const std::vector<Real>& radial_grid_derivative,
159 int mesh_size,
160 const std::vector<AngularPoint>& angular_grid,
161 const std::vector<std::vector<Real>>& harmonics,
162 const XCFunctional& functional,
163 Real& energy,
164 std::vector<std::vector<Real>>& potential_lm) {
165 int number_of_lm = static_cast<int>(density_channels.size());
166 int number_of_angles = static_cast<int>(angular_grid.size());
167
168 potential_lm.assign(number_of_lm, std::vector<Real>(mesh_size, 0.0));
169 energy = 0.0;
170
171 std::vector<Real> density_at_angles(number_of_angles);
172 std::vector<Real> xc_potential(number_of_angles);
173 std::vector<Real> xc_energy_density(number_of_angles);
174 std::vector<Real> radial_energy(mesh_size, 0.0);
175 std::vector<std::vector<Real>> radial_potential(number_of_lm,
176 std::vector<Real>(mesh_size, 0.0));
177
178 for (int point = 0; point < mesh_size; point++) {
179 Real radius = radial_grid[point];
180 Real inverse_r_squared = 1.0 / (radius * radius);
181 Real core = core_density.empty() ? 0.0 : core_density[point];
182
183 for (int angle = 0; angle < number_of_angles; angle++) {
184 Real value = 0.0;
185 for (int lm = 0; lm < number_of_lm; lm++)
186 value += density_channels[lm][point] * harmonics[angle][lm];
187 density_at_angles[angle] = value * inverse_r_squared + core;
188 }
189
190 functional.compute(density_at_angles, xc_potential, xc_energy_density);
191
192 /**
193 * ε_xc is per particle, so the energy density is ε·n; the r² restores
194 * the volume element the r-weighted storage already carries elsewhere.
195 */
196 Real energy_at_radius = 0.0;
197 for (int angle = 0; angle < number_of_angles; angle++) {
198 energy_at_radius +=
199 angular_grid[angle].weight * xc_energy_density[angle] * density_at_angles[angle];
200 }
201 radial_energy[point] = energy_at_radius * radius * radius;
202
203 for (int lm = 0; lm < number_of_lm; lm++) {
204 Real projected = 0.0;
205 for (int angle = 0; angle < number_of_angles; angle++) {
206 projected +=
207 angular_grid[angle].weight * xc_potential[angle] * harmonics[angle][lm];
208 }
209 radial_potential[lm][point] = projected;
210 }
211 }
212
213 auto cumulative = cumulative_integral(radial_energy, radial_grid_derivative, mesh_size);
214 energy = cumulative[mesh_size - 1];
215 potential_lm = radial_potential;
216}
217
218/**
219 * Gradient-corrected exchange-correlation on the radial × angular grid.
220 *
221 * Same shape as the LDA routine, plus σ = |∇n|² and the by-parts weights the
222 * D_ij gradient term needs. The core density is spherical, so it contributes
223 * to the radial derivative through its own slope and nothing to the tangential
224 * part.
225 */
227 const std::vector<std::vector<Real>>& density_channels,
228 const std::vector<Real>& core_density,
229 const std::vector<Real>& radial_grid,
230 const std::vector<Real>& radial_grid_derivative,
231 int mesh_size,
232 const std::vector<AngularPoint>& angular_grid,
233 const std::vector<std::vector<Real>>& harmonics,
234 const std::vector<std::vector<std::array<Real, 3>>>& harmonic_gradients,
235 const XCFunctional& functional,
236 Real& energy,
237 std::vector<std::vector<Real>>& potential_lm,
238 std::vector<std::vector<Real>>& radial_weight_lm,
239 std::vector<std::vector<Real>>& angular_weight_lm) {
240 int number_of_lm = static_cast<int>(density_channels.size());
241 int number_of_angles = static_cast<int>(angular_grid.size());
242
243 /**
244 * The true density channels f_lm = u_lm/r², and their radial derivatives.
245 * The core is spherical, so it enters only the L = 0 channel — through
246 * Y_00, hence the √(4π).
247 */
248 std::vector<std::vector<Real>> channel_values(number_of_lm, std::vector<Real>(mesh_size, 0.0));
249 for (int lm = 0; lm < number_of_lm; lm++) {
250 for (int point = 0; point < mesh_size; point++) {
251 Real radius = radial_grid[point];
252 channel_values[lm][point] = density_channels[lm][point] / (radius * radius);
253 }
254 }
255 if (!core_density.empty()) {
256 const Real inverse_y00 = std::sqrt(constants::four_pi);
257 for (int point = 0; point < mesh_size; point++)
258 channel_values[0][point] += core_density[point] * inverse_y00;
259 }
260
261 std::vector<std::vector<Real>> channel_derivatives(number_of_lm);
262 for (int lm = 0; lm < number_of_lm; lm++)
263 channel_derivatives[lm] =
264 radial_derivative(channel_values[lm], radial_grid_derivative, mesh_size);
265
266 potential_lm.assign(number_of_lm, std::vector<Real>(mesh_size, 0.0));
267 radial_weight_lm.assign(number_of_lm, std::vector<Real>(mesh_size, 0.0));
268 angular_weight_lm.assign(number_of_lm, std::vector<Real>(mesh_size, 0.0));
269
270 std::vector<Real> density_at_angles(number_of_angles);
271 std::vector<Real> xc_potential(number_of_angles);
272 std::vector<Real> gradient_correction(number_of_angles);
273 std::vector<Real> xc_energy_density(number_of_angles);
274 std::vector<Real> radial_energy(mesh_size, 0.0);
275
276 std::vector<Real> values_at_radius(number_of_lm);
277 std::vector<Real> derivatives_at_radius(number_of_lm);
278
279 for (int point = 0; point < mesh_size; point++) {
280 Real radius = radial_grid[point];
281 for (int lm = 0; lm < number_of_lm; lm++) {
282 values_at_radius[lm] = channel_values[lm][point];
283 derivatives_at_radius[lm] = channel_derivatives[lm][point];
284 }
285
286 auto gradient = build_gradient_at_radius(
287 values_at_radius, derivatives_at_radius, radius, harmonics, harmonic_gradients);
288
289 for (int angle = 0; angle < number_of_angles; angle++) {
290 Real value = 0.0;
291 for (int lm = 0; lm < number_of_lm; lm++)
292 value += values_at_radius[lm] * harmonics[angle][lm];
293 density_at_angles[angle] = value;
294 }
295
296 functional.compute_gradient_corrected(density_at_angles,
297 gradient.gradient_squared,
298 xc_potential,
299 gradient_correction,
300 xc_energy_density);
301
302 Real energy_at_radius = 0.0;
303 for (int angle = 0; angle < number_of_angles; angle++) {
304 energy_at_radius +=
305 angular_grid[angle].weight * xc_energy_density[angle] * density_at_angles[angle];
306 }
307 radial_energy[point] = energy_at_radius * radius * radius;
308
309 for (int lm = 0; lm < number_of_lm; lm++) {
310 Real local = 0.0;
311 Real radial_weight = 0.0;
312 Real angular_weight = 0.0;
313 for (int angle = 0; angle < number_of_angles; angle++) {
314 Real weight = angular_grid[angle].weight;
315 local += weight * xc_potential[angle] * harmonics[angle][lm];
316
317 Real two_v_sigma = 2.0 * gradient_correction[angle];
318 radial_weight +=
319 weight * two_v_sigma * gradient.radial_component[angle] * harmonics[angle][lm];
320
321 Real tangential_dot = 0.0;
322 for (int axis = 0; axis < 3; axis++) {
323 tangential_dot +=
324 gradient.tangential[angle][axis] * harmonic_gradients[angle][lm][axis];
325 }
326 /**
327 * Bookkeeping worth spelling out, because getting it wrong is
328 * an error of r⁴. The stored tangential field is ∇_Ω n / r,
329 * and the perturbation's tangential field is
330 * (1/r) Σ g_lm ∇_Ω Y_lm, so their product carries 1/r; the
331 * volume element contributes r². Net, the weight paired with
332 * g_lm under ∫dr is r·S_lm, not S_lm/r.
333 */
334 angular_weight += weight * two_v_sigma * tangential_dot * radius;
335 }
336 potential_lm[lm][point] = local;
337 radial_weight_lm[lm][point] = radial_weight;
338 angular_weight_lm[lm][point] = angular_weight;
339 }
340 }
341
342 auto cumulative = cumulative_integral(radial_energy, radial_grid_derivative, mesh_size);
343 energy = cumulative[mesh_size - 1];
344}
345
346/**
347 * Spin-polarized exchange-correlation for one branch (LDA).
348 *
349 * Same grid and same projection as the unpolarized routine; the difference is
350 * that the functional sees two densities and returns two potentials, so the
351 * D_ij of each channel integrates against its own v_xc. The frozen core is
352 * unpolarized and is split evenly between the channels, which is what makes
353 * n↑ + n↓ reproduce the unpolarized total exactly.
354 *
355 * @param density_channels_by_spin [spin][lm][radial point], r-weighted
356 * @param potential_lm_by_spin out, [spin][lm][radial point]
357 */
359 const std::vector<std::vector<std::vector<Real>>>& density_channels_by_spin,
360 const std::vector<Real>& core_density,
361 const std::vector<Real>& radial_grid,
362 const std::vector<Real>& radial_grid_derivative,
363 int mesh_size,
364 const std::vector<AngularPoint>& angular_grid,
365 const std::vector<std::vector<Real>>& harmonics,
366 const XCFunctional& functional,
367 Real& energy,
368 std::vector<std::vector<std::vector<Real>>>& potential_lm_by_spin) {
369 int number_of_lm = static_cast<int>(density_channels_by_spin[0].size());
370 int number_of_angles = static_cast<int>(angular_grid.size());
371
372 potential_lm_by_spin.assign(
373 2, std::vector<std::vector<Real>>(number_of_lm, std::vector<Real>(mesh_size, 0.0)));
374 energy = 0.0;
375
376 std::vector<Real> density_up(number_of_angles), density_down(number_of_angles);
377 std::vector<Real> potential_up(number_of_angles), potential_down(number_of_angles);
378 std::vector<Real> xc_energy_density(number_of_angles);
379 std::vector<Real> radial_energy(mesh_size, 0.0);
380
381 for (int point = 0; point < mesh_size; point++) {
382 Real radius = radial_grid[point];
383 Real inverse_r_squared = 1.0 / (radius * radius);
384 Real half_core = core_density.empty() ? 0.0 : 0.5 * core_density[point];
385
386 for (int angle = 0; angle < number_of_angles; angle++) {
387 Real up = 0.0;
388 Real down = 0.0;
389 for (int lm = 0; lm < number_of_lm; lm++) {
390 up += density_channels_by_spin[0][lm][point] * harmonics[angle][lm];
391 down += density_channels_by_spin[1][lm][point] * harmonics[angle][lm];
392 }
393 density_up[angle] = up * inverse_r_squared + half_core;
394 density_down[angle] = down * inverse_r_squared + half_core;
395 }
396
397 functional.compute_spin(
398 density_up, density_down, potential_up, potential_down, xc_energy_density);
399
400 /**
401 * The functional returns ε per particle of the *total* density, matching
402 * the unpolarized convention, so the energy weight is n↑ + n↓.
403 */
404 Real energy_at_radius = 0.0;
405 for (int angle = 0; angle < number_of_angles; angle++) {
406 energy_at_radius += angular_grid[angle].weight * xc_energy_density[angle] *
407 (density_up[angle] + density_down[angle]);
408 }
409 radial_energy[point] = energy_at_radius * radius * radius;
410
411 for (int lm = 0; lm < number_of_lm; lm++) {
412 Real projected_up = 0.0;
413 Real projected_down = 0.0;
414 for (int angle = 0; angle < number_of_angles; angle++) {
415 Real weight = angular_grid[angle].weight * harmonics[angle][lm];
416 projected_up += weight * potential_up[angle];
417 projected_down += weight * potential_down[angle];
418 }
419 potential_lm_by_spin[0][lm][point] = projected_up;
420 potential_lm_by_spin[1][lm][point] = projected_down;
421 }
422 }
423
424 auto cumulative = cumulative_integral(radial_energy, radial_grid_derivative, mesh_size);
425 energy = cumulative[mesh_size - 1];
426}
427
428/**
429 * Spin-polarized gradient-corrected exchange-correlation for one branch.
430 *
431 * The by-parts construction of the unpolarized routine carries over with one
432 * change: a GGA's dependence on ∇n_σ runs through both σ_σσ and the cross term
433 * σ_↑↓, so the field conjugate to ∇δn_σ is
434 *
435 * W_σ = 2 v_{σσ} ∇n_σ + v_{↑↓} ∇n_{σ'}
436 *
437 * rather than 2 v_σ ∇n.
438 *
439 * It reduces exactly, not just numerically: at n↑ = n↓ = n/2 each channel's
440 * gradient is half the total, and the functional's own spin scaling supplies
441 * the rest — PBE exchange has no cross term (v_{↑↓} = 0) with
442 * v_{↑↑} = 2 v_σ^unpol, while PBE correlation has v_{↑↓} = 2 v_{↑↑}. Both land
443 * on 2 v_σ^unpol ∇n. `OneCenterEnergySpinTest` gates the resulting energy.
444 */
446 const std::vector<std::vector<std::vector<Real>>>& density_channels_by_spin,
447 const std::vector<Real>& core_density,
448 const std::vector<Real>& radial_grid,
449 const std::vector<Real>& radial_grid_derivative,
450 int mesh_size,
451 const std::vector<AngularPoint>& angular_grid,
452 const std::vector<std::vector<Real>>& harmonics,
453 const std::vector<std::vector<std::array<Real, 3>>>& harmonic_gradients,
454 const XCFunctional& functional,
455 Real& energy,
456 std::vector<std::vector<std::vector<Real>>>& potential_lm_by_spin,
457 std::vector<std::vector<std::vector<Real>>>& radial_weight_lm_by_spin,
458 std::vector<std::vector<std::vector<Real>>>& angular_weight_lm_by_spin) {
459 int number_of_lm = static_cast<int>(density_channels_by_spin[0].size());
460 int number_of_angles = static_cast<int>(angular_grid.size());
461
462 // f_lm = u_lm/r² per spin, with half the spherical core in each L = 0 channel.
463 std::vector<std::vector<std::vector<Real>>> channel_values(
464 2, std::vector<std::vector<Real>>(number_of_lm, std::vector<Real>(mesh_size, 0.0)));
465 for (int spin = 0; spin < 2; spin++) {
466 for (int lm = 0; lm < number_of_lm; lm++) {
467 for (int point = 0; point < mesh_size; point++) {
468 Real radius = radial_grid[point];
469 channel_values[spin][lm][point] =
470 density_channels_by_spin[spin][lm][point] / (radius * radius);
471 }
472 }
473 if (!core_density.empty()) {
474 const Real inverse_y00 = std::sqrt(constants::four_pi);
475 for (int point = 0; point < mesh_size; point++)
476 channel_values[spin][0][point] += 0.5 * core_density[point] * inverse_y00;
477 }
478 }
479
480 std::vector<std::vector<std::vector<Real>>> channel_derivatives(
481 2, std::vector<std::vector<Real>>(number_of_lm));
482 for (int spin = 0; spin < 2; spin++)
483 for (int lm = 0; lm < number_of_lm; lm++)
484 channel_derivatives[spin][lm] =
485 radial_derivative(channel_values[spin][lm], radial_grid_derivative, mesh_size);
486
487 auto empty_by_spin = [&]() {
488 return std::vector<std::vector<std::vector<Real>>>(
489 2, std::vector<std::vector<Real>>(number_of_lm, std::vector<Real>(mesh_size, 0.0)));
490 };
491 potential_lm_by_spin = empty_by_spin();
492 radial_weight_lm_by_spin = empty_by_spin();
493 angular_weight_lm_by_spin = empty_by_spin();
494
495 std::vector<Real> density_up(number_of_angles), density_down(number_of_angles);
496 std::vector<Real> sigma_uu(number_of_angles), sigma_ud(number_of_angles),
497 sigma_dd(number_of_angles);
498 std::vector<Real> potential_up(number_of_angles), potential_down(number_of_angles);
499 std::vector<Real> gradient_uu(number_of_angles), gradient_ud(number_of_angles),
500 gradient_dd(number_of_angles);
501 std::vector<Real> xc_energy_density(number_of_angles);
502 std::vector<Real> radial_energy(mesh_size, 0.0);
503
504 std::vector<Real> values_at_radius(number_of_lm), derivatives_at_radius(number_of_lm);
505
506 for (int point = 0; point < mesh_size; point++) {
507 Real radius = radial_grid[point];
508
509 std::vector<GradientAtRadius> gradient(2);
510 for (int spin = 0; spin < 2; spin++) {
511 for (int lm = 0; lm < number_of_lm; lm++) {
512 values_at_radius[lm] = channel_values[spin][lm][point];
513 derivatives_at_radius[lm] = channel_derivatives[spin][lm][point];
514 }
515 gradient[spin] = build_gradient_at_radius(
516 values_at_radius, derivatives_at_radius, radius, harmonics, harmonic_gradients);
517 }
518
519 for (int angle = 0; angle < number_of_angles; angle++) {
520 Real up = 0.0;
521 Real down = 0.0;
522 for (int lm = 0; lm < number_of_lm; lm++) {
523 up += channel_values[0][lm][point] * harmonics[angle][lm];
524 down += channel_values[1][lm][point] * harmonics[angle][lm];
525 }
526 density_up[angle] = up;
527 density_down[angle] = down;
528
529 sigma_uu[angle] = gradient[0].gradient_squared[angle];
530 sigma_dd[angle] = gradient[1].gradient_squared[angle];
531 Real cross = gradient[0].radial_component[angle] * gradient[1].radial_component[angle];
532 for (int axis = 0; axis < 3; axis++)
533 cross += gradient[0].tangential[angle][axis] * gradient[1].tangential[angle][axis];
534 sigma_ud[angle] = cross;
535 }
536
537 functional.compute_gradient_corrected_spin(density_up,
538 density_down,
539 sigma_uu,
540 sigma_ud,
541 sigma_dd,
542 potential_up,
543 potential_down,
544 gradient_uu,
545 gradient_ud,
546 gradient_dd,
547 xc_energy_density);
548
549 Real energy_at_radius = 0.0;
550 for (int angle = 0; angle < number_of_angles; angle++) {
551 energy_at_radius += angular_grid[angle].weight * xc_energy_density[angle] *
552 (density_up[angle] + density_down[angle]);
553 }
554 radial_energy[point] = energy_at_radius * radius * radius;
555
556 for (int spin = 0; spin < 2; spin++) {
557 const auto& potential = spin == 0 ? potential_up : potential_down;
558 const auto& same_spin_gradient = spin == 0 ? gradient_uu : gradient_dd;
559 const auto& own = gradient[spin];
560 const auto& other = gradient[1 - spin];
561
562 for (int lm = 0; lm < number_of_lm; lm++) {
563 Real local = 0.0;
564 Real radial_weight = 0.0;
565 Real angular_weight = 0.0;
566 for (int angle = 0; angle < number_of_angles; angle++) {
567 Real weight = angular_grid[angle].weight;
568 local += weight * potential[angle] * harmonics[angle][lm];
569
570 // W_σ = 2 v_σσ ∇n_σ + v_↑↓ ∇n_σ'
571 Real conjugate_radial =
572 2.0 * same_spin_gradient[angle] * own.radial_component[angle] +
573 gradient_ud[angle] * other.radial_component[angle];
574 radial_weight += weight * conjugate_radial * harmonics[angle][lm];
575
576 Real tangential_dot = 0.0;
577 for (int axis = 0; axis < 3; axis++) {
578 Real conjugate_tangential =
579 2.0 * same_spin_gradient[angle] * own.tangential[angle][axis] +
580 gradient_ud[angle] * other.tangential[angle][axis];
581 tangential_dot +=
582 conjugate_tangential * harmonic_gradients[angle][lm][axis];
583 }
584 /**
585 * Same r-bookkeeping as the unpolarized routine: the stored
586 * tangential field carries 1/r and the perturbation's own
587 * carries another, against r² from the volume element, so
588 * the weight paired with g_lm under ∫dr is r·S_lm.
589 */
590 angular_weight += weight * tangential_dot * radius;
591 }
592 potential_lm_by_spin[spin][lm][point] = local;
593 radial_weight_lm_by_spin[spin][lm][point] = radial_weight;
594 angular_weight_lm_by_spin[spin][lm][point] = angular_weight;
595 }
596 }
597 }
598
599 auto cumulative = cumulative_integral(radial_energy, radial_grid_derivative, mesh_size);
600 energy = cumulative[mesh_size - 1];
601}
602
603/**
604 * Assemble both branches: Hartree and XC energies, and the summed potential
605 * each branch's D_ij contribution integrates against.
606 */
608 const PawPseudopotential& pseudopotential,
609 const OneCenterDensities& densities,
610 const std::vector<AngularPoint>& angular_grid,
611 const std::vector<std::vector<Real>>& harmonics,
612 const XCFunctional& functional,
613 const std::vector<std::vector<std::array<Real, 3>>>* harmonic_gradients = nullptr,
614 const OneCenterOptions& options = {}) {
615 OneCenterEnergy result;
616 int mesh_size = pseudopotential.mesh_size;
617 int number_of_lm = densities.number_of_lm;
618
619 auto build_branch = [&](const std::vector<std::vector<Real>>& channels,
620 const std::vector<Real>& core_density,
621 bool subtract_core_exchange_correlation) {
622 OneCenterBranch branch;
623
624 std::vector<std::vector<Real>> hartree_potential(number_of_lm);
625 for (int lm = 0; lm < number_of_lm; lm++) {
627 pseudopotential.radial_grid,
628 pseudopotential.radial_grid_derivative,
629 channels[lm],
630 mesh_size,
631 hartree_potential[lm]);
632 }
633 branch.hartree_energy = radial_hartree_energy(
634 hartree_potential, channels, pseudopotential.radial_grid_derivative, mesh_size);
635
636 std::vector<std::vector<Real>> xc_potential;
637 if (harmonic_gradients != nullptr) {
639 core_density,
640 pseudopotential.radial_grid,
641 pseudopotential.radial_grid_derivative,
642 mesh_size,
643 angular_grid,
644 harmonics,
645 *harmonic_gradients,
646 functional,
647 branch.xc_energy,
648 xc_potential,
649 branch.gradient_radial_weight_lm,
650 branch.gradient_angular_weight_lm);
651 } else {
652 compute_one_center_xc(channels,
653 core_density,
654 pseudopotential.radial_grid,
655 pseudopotential.radial_grid_derivative,
656 mesh_size,
657 angular_grid,
658 harmonics,
659 functional,
660 branch.xc_energy,
661 xc_potential);
662 }
663
664 /**
665 * Only the energy is corrected here, never `potential_lm`: E_xc[n_core]
666 * does not depend on the valence occupancies, so its derivative — which
667 * is what D_ij is — is identically zero, and removing it from the
668 * potential too would break D ≡ ∂E/∂ρ_ij.
669 */
670 if (subtract_core_exchange_correlation && !core_density.empty()) {
671 std::vector<std::vector<Real>> no_valence(number_of_lm,
672 std::vector<Real>(mesh_size, 0.0));
673 std::vector<std::vector<Real>> discarded_potential;
674 Real core_only_energy = 0.0;
675 if (harmonic_gradients != nullptr) {
676 std::vector<std::vector<Real>> discarded_radial_weight;
677 std::vector<std::vector<Real>> discarded_angular_weight;
679 core_density,
680 pseudopotential.radial_grid,
681 pseudopotential.radial_grid_derivative,
682 mesh_size,
683 angular_grid,
684 harmonics,
685 *harmonic_gradients,
686 functional,
687 core_only_energy,
688 discarded_potential,
689 discarded_radial_weight,
690 discarded_angular_weight);
691 } else {
692 compute_one_center_xc(no_valence,
693 core_density,
694 pseudopotential.radial_grid,
695 pseudopotential.radial_grid_derivative,
696 mesh_size,
697 angular_grid,
698 harmonics,
699 functional,
700 core_only_energy,
701 discarded_potential);
702 }
703 branch.xc_energy -= core_only_energy;
704 }
705
706 branch.potential_lm.assign(number_of_lm, std::vector<Real>(mesh_size, 0.0));
707 for (int lm = 0; lm < number_of_lm; lm++) {
708 for (int point = 0; point < mesh_size; point++) {
709 branch.potential_lm[lm][point] =
710 hartree_potential[lm][point] + xc_potential[lm][point];
711 }
712 }
713 return branch;
714 };
715
716 result.all_electron = build_branch(densities.all_electron,
718 options.subtract_core_exchange_correlation);
719 /**
720 * The pseudo branch keeps its core-core term: its "core" is the shallow
721 * pseudized partial core, and an atomic-referenced convention removes the
722 * core self-interaction from the all-electron branch alone.
723 */
724 result.pseudo =
725 build_branch(densities.pseudo, pseudopotential.charge_core.radial_values, false);
726 return result;
727}
728
729/**
730 * Both branches, spin-polarized: one `OneCenterEnergy` per spin channel.
731 *
732 * Hartree is a functional of the *total* density alone, so it is computed once
733 * from n↑ + n↓ and its potential handed to both channels — that is the physics,
734 * not an approximation. Only XC splits. To keep `total()` summable over the
735 * returned vector without double counting, the Hartree energy is recorded on
736 * channel 0 and left at zero on channel 1, while both channels' `potential_lm`
737 * carry the full Hartree potential because each channel's D_ij needs it.
738 *
739 * @param densities_by_spin one `OneCenterDensities` per channel, same lm and mesh
740 */
741inline std::vector<OneCenterEnergy> compute_one_center_energy_spin(
742 const PawPseudopotential& pseudopotential,
743 const std::vector<OneCenterDensities>& densities_by_spin,
744 const std::vector<AngularPoint>& angular_grid,
745 const std::vector<std::vector<Real>>& harmonics,
746 const XCFunctional& functional,
747 const std::vector<std::vector<std::array<Real, 3>>>* harmonic_gradients = nullptr,
748 const OneCenterOptions& options = {}) {
749 std::vector<OneCenterEnergy> result(2);
750 int mesh_size = pseudopotential.mesh_size;
751 int number_of_lm = densities_by_spin[0].number_of_lm;
752
753 auto build_branch = [&](const std::vector<std::vector<Real>>& channels_up,
754 const std::vector<std::vector<Real>>& channels_down,
755 const std::vector<Real>& core_density,
756 bool subtract_core_exchange_correlation,
757 OneCenterBranch& branch_up,
758 OneCenterBranch& branch_down) {
759 std::vector<std::vector<Real>> total(number_of_lm, std::vector<Real>(mesh_size, 0.0));
760 for (int lm = 0; lm < number_of_lm; lm++)
761 for (int point = 0; point < mesh_size; point++)
762 total[lm][point] = channels_up[lm][point] + channels_down[lm][point];
763
764 std::vector<std::vector<Real>> hartree_potential(number_of_lm);
765 for (int lm = 0; lm < number_of_lm; lm++) {
767 pseudopotential.radial_grid,
768 pseudopotential.radial_grid_derivative,
769 total[lm],
770 mesh_size,
771 hartree_potential[lm]);
772 }
773 branch_up.hartree_energy = radial_hartree_energy(
774 hartree_potential, total, pseudopotential.radial_grid_derivative, mesh_size);
775 branch_down.hartree_energy = 0.0;
776
777 std::vector<std::vector<std::vector<Real>>> density_by_spin{channels_up, channels_down};
778 std::vector<std::vector<std::vector<Real>>> xc_potential_by_spin;
779 Real xc_energy = 0.0;
780 if (harmonic_gradients != nullptr) {
781 std::vector<std::vector<std::vector<Real>>> radial_weight_by_spin;
782 std::vector<std::vector<std::vector<Real>>> angular_weight_by_spin;
784 core_density,
785 pseudopotential.radial_grid,
786 pseudopotential.radial_grid_derivative,
787 mesh_size,
788 angular_grid,
789 harmonics,
790 *harmonic_gradients,
791 functional,
792 xc_energy,
793 xc_potential_by_spin,
794 radial_weight_by_spin,
795 angular_weight_by_spin);
796 branch_up.gradient_radial_weight_lm = radial_weight_by_spin[0];
797 branch_up.gradient_angular_weight_lm = angular_weight_by_spin[0];
798 branch_down.gradient_radial_weight_lm = radial_weight_by_spin[1];
799 branch_down.gradient_angular_weight_lm = angular_weight_by_spin[1];
800 } else {
801 compute_one_center_xc_spin(density_by_spin,
802 core_density,
803 pseudopotential.radial_grid,
804 pseudopotential.radial_grid_derivative,
805 mesh_size,
806 angular_grid,
807 harmonics,
808 functional,
809 xc_energy,
810 xc_potential_by_spin);
811 }
812 branch_up.xc_energy = xc_energy;
813 branch_down.xc_energy = 0.0;
814
815 /**
816 * The core-only term goes through the spin routines as well.
817 *
818 * The frozen core carries no moment, so this is the same number the
819 * unpolarized routine would give — but only if it is evaluated with an
820 * unpolarized handle, and the handle here is polarized. Nickel's core
821 * exchange-correlation is about 106 Ry, so taking the wrong branch put
822 * the atomic reference at −107.95 Ry instead of −1.58 Ry, which then
823 * appeared as a 113 Ry error in the SCF total.
824 */
825 if (subtract_core_exchange_correlation && !core_density.empty()) {
826 std::vector<std::vector<std::vector<Real>>> no_valence(
827 2, std::vector<std::vector<Real>>(number_of_lm, std::vector<Real>(mesh_size, 0.0)));
828 std::vector<std::vector<std::vector<Real>>> discarded_potential;
829 Real core_only_energy = 0.0;
830 if (harmonic_gradients != nullptr) {
831 std::vector<std::vector<std::vector<Real>>> discarded_radial_weight;
832 std::vector<std::vector<std::vector<Real>>> discarded_angular_weight;
834 no_valence,
835 core_density,
836 pseudopotential.radial_grid,
837 pseudopotential.radial_grid_derivative,
838 mesh_size,
839 angular_grid,
840 harmonics,
841 *harmonic_gradients,
842 functional,
843 core_only_energy,
844 discarded_potential,
845 discarded_radial_weight,
846 discarded_angular_weight);
847 } else {
849 core_density,
850 pseudopotential.radial_grid,
851 pseudopotential.radial_grid_derivative,
852 mesh_size,
853 angular_grid,
854 harmonics,
855 functional,
856 core_only_energy,
857 discarded_potential);
858 }
859 branch_up.xc_energy -= core_only_energy;
860 }
861
862 for (int spin = 0; spin < 2; spin++) {
863 OneCenterBranch& branch = spin == 0 ? branch_up : branch_down;
864 branch.potential_lm.assign(number_of_lm, std::vector<Real>(mesh_size, 0.0));
865 for (int lm = 0; lm < number_of_lm; lm++)
866 for (int point = 0; point < mesh_size; point++)
867 branch.potential_lm[lm][point] =
868 hartree_potential[lm][point] + xc_potential_by_spin[spin][lm][point];
869 }
870 };
871
872 build_branch(densities_by_spin[0].all_electron,
873 densities_by_spin[1].all_electron,
875 options.subtract_core_exchange_correlation,
876 result[0].all_electron,
877 result[1].all_electron);
878 build_branch(densities_by_spin[0].pseudo,
879 densities_by_spin[1].pseudo,
880 pseudopotential.charge_core.radial_values,
881 false,
882 result[0].pseudo,
883 result[1].pseudo);
884 return result;
885}
886
887/**
888 * One-center energy for an unpolarized occupancy, with either functional mode.
889 *
890 * The atomic reference state and every non-magnetic SCF step are unpolarized,
891 * but the functional handle they are evaluated with belongs to the calculation,
892 * which may be spin-polarized. Driving a polarized LibXC handle through the
893 * unpolarized routines does not fail — it misreads the interleaved density
894 * array and returns a plausible wrong number. For nickel that put the atomic
895 * reference at −24.12 Ry instead of −1.58 Ry, and the SCF total 113 Ry low
896 * while the magnetic moment still looked right.
897 *
898 * So the branch is on the functional, not on the caller's density count: a
899 * polarized handle gets the density split evenly and the spin routines, which
900 * `OneCenterEnergySpinTest` gates as identical to the unpolarized answer.
901 */
903 const PawPseudopotential& pseudopotential,
904 const OneCenterDensities& densities,
905 const std::vector<AngularPoint>& angular_grid,
906 const std::vector<std::vector<Real>>& harmonics,
907 const XCFunctional& functional,
908 const std::vector<std::vector<std::array<Real, 3>>>* harmonic_gradients = nullptr,
909 const OneCenterOptions& options = {}) {
910 if (functional.number_of_spin_components() < 2) {
911 return compute_one_center_energy(pseudopotential,
912 densities,
913 angular_grid,
914 harmonics,
915 functional,
916 harmonic_gradients,
917 options);
918 }
919
920 OneCenterDensities half = densities;
921 for (auto* branch : {&half.all_electron, &half.pseudo})
922 for (auto& channel : *branch)
923 for (auto& value : channel)
924 value *= 0.5;
925
926 auto by_spin = compute_one_center_energy_spin(pseudopotential,
927 {half, half},
928 angular_grid,
929 harmonics,
930 functional,
931 harmonic_gradients,
932 options);
933 /**
934 * Channel 0 already carries the whole energy — Hartree comes from the total
935 * density and channel 1's energies are left at zero — and both channels'
936 * potentials are equal here, so channel 0 is the complete answer.
937 */
938 return by_spin[0];
939}
940
941} // namespace paw_one_center
942} // namespace q3
PAW pseudopotential — extends ultrasoft with one-center (on-site) data.
Definition init.hpp:33
ChargeCore charge_core_all_electron
All-electron core density (distinct from the pseudized NLCC core density).
Definition init.hpp:83
std::vector< Real > radial_grid_derivative
Definition init.hpp:77
std::vector< Real > radial_grid
Definition init.hpp:76
ChargeCore charge_core
Definition init.hpp:84
Abstract base class for XC functionals.
Definition init.hpp:23
virtual int number_of_spin_components() const
Definition init.hpp:76
virtual void compute_gradient_corrected_spin(const std::vector< Real > &density_up, const std::vector< Real > &density_down, const std::vector< Real > &grho2_uu, const std::vector< Real > &grho2_ud, const std::vector< Real > &grho2_dd, std::vector< Real > &xc_potential_up, std::vector< Real > &xc_potential_down, std::vector< Real > &gradient_correction_up_up, std::vector< Real > &gradient_correction_up_down, std::vector< Real > &gradient_correction_down_down, std::vector< Real > &xc_energy_density) const
Definition init.hpp:110
virtual void compute(const std::vector< Real > &density, std::vector< Real > &xc_potential, std::vector< Real > &xc_energy_density) const =0
Compute XC potential and energy density (LDA interface, unpolarized).
virtual void compute_spin(const std::vector< Real > &density_up, const std::vector< Real > &density_down, std::vector< Real > &xc_potential_up, std::vector< Real > &xc_potential_down, std::vector< Real > &xc_energy_density) const
Definition init.hpp:38
virtual void compute_gradient_corrected(const std::vector< Real > &density, const std::vector< Real > &gradient_density_squared, std::vector< Real > &xc_potential, std::vector< Real > &gradient_correction_density, std::vector< Real > &xc_energy_density) const
Definition init.hpp:84
constexpr Real four_pi
Definition constants.hpp:26
OneCenterEnergy compute_one_center_energy(const PawPseudopotential &pseudopotential, const OneCenterDensities &densities, const std::vector< AngularPoint > &angular_grid, const std::vector< std::vector< Real > > &harmonics, const XCFunctional &functional, const std::vector< std::vector< std::array< Real, 3 > > > *harmonic_gradients=nullptr, const OneCenterOptions &options={})
Definition energy.hpp:607
std::vector< std::vector< Real > > tabulate_harmonics(const std::vector< AngularPoint > &angular_grid, int number_of_lm)
Real spherical harmonics at every angular grid point, [point][lm].
Definition energy.hpp:55
void compute_one_center_xc_spin(const std::vector< std::vector< std::vector< Real > > > &density_channels_by_spin, const std::vector< Real > &core_density, const std::vector< Real > &radial_grid, const std::vector< Real > &radial_grid_derivative, int mesh_size, const std::vector< AngularPoint > &angular_grid, const std::vector< std::vector< Real > > &harmonics, const XCFunctional &functional, Real &energy, std::vector< std::vector< std::vector< Real > > > &potential_lm_by_spin)
Definition energy.hpp:358
GradientAtRadius build_gradient_at_radius(const std::vector< Real > &channel_values, const std::vector< Real > &channel_derivatives, Real radius, const std::vector< std::vector< Real > > &harmonics, const std::vector< std::vector< std::array< Real, 3 > > > &harmonic_gradients)
Definition gradient.hpp:140
std::vector< OneCenterEnergy > compute_one_center_energy_spin(const PawPseudopotential &pseudopotential, const std::vector< OneCenterDensities > &densities_by_spin, const std::vector< AngularPoint > &angular_grid, const std::vector< std::vector< Real > > &harmonics, const XCFunctional &functional, const std::vector< std::vector< std::array< Real, 3 > > > *harmonic_gradients=nullptr, const OneCenterOptions &options={})
Definition energy.hpp:741
std::vector< Real > radial_derivative(const std::vector< Real > &values, const std::vector< Real > &measures, int mesh_size)
Definition gradient.hpp:45
void compute_one_center_xc_gradient_corrected(const std::vector< std::vector< Real > > &density_channels, const std::vector< Real > &core_density, const std::vector< Real > &radial_grid, const std::vector< Real > &radial_grid_derivative, int mesh_size, const std::vector< AngularPoint > &angular_grid, const std::vector< std::vector< Real > > &harmonics, const std::vector< std::vector< std::array< Real, 3 > > > &harmonic_gradients, const XCFunctional &functional, Real &energy, std::vector< std::vector< Real > > &potential_lm, std::vector< std::vector< Real > > &radial_weight_lm, std::vector< std::vector< Real > > &angular_weight_lm)
Definition energy.hpp:226
void solve_radial_hartree(int angular_momentum, const std::vector< Real > &radial_grid, const std::vector< Real > &radial_grid_derivative, const std::vector< Real > &density_r_squared, int mesh_size, std::vector< Real > &potential)
int angular_momentum_of_lm(int lm_index)
The L of a flat lm index.
Real radial_hartree_energy(const std::vector< std::vector< Real > > &potential_channels, const std::vector< std::vector< Real > > &density_channels, const std::vector< Real > &radial_grid_derivative, int mesh_size)
void compute_one_center_xc_gradient_corrected_spin(const std::vector< std::vector< std::vector< Real > > > &density_channels_by_spin, const std::vector< Real > &core_density, const std::vector< Real > &radial_grid, const std::vector< Real > &radial_grid_derivative, int mesh_size, const std::vector< AngularPoint > &angular_grid, const std::vector< std::vector< Real > > &harmonics, const std::vector< std::vector< std::array< Real, 3 > > > &harmonic_gradients, const XCFunctional &functional, Real &energy, std::vector< std::vector< std::vector< Real > > > &potential_lm_by_spin, std::vector< std::vector< std::vector< Real > > > &radial_weight_lm_by_spin, std::vector< std::vector< std::vector< Real > > > &angular_weight_lm_by_spin)
Definition energy.hpp:445
OneCenterEnergy compute_one_center_energy_either_mode(const PawPseudopotential &pseudopotential, const OneCenterDensities &densities, const std::vector< AngularPoint > &angular_grid, const std::vector< std::vector< Real > > &harmonics, const XCFunctional &functional, const std::vector< std::vector< std::array< Real, 3 > > > *harmonic_gradients=nullptr, const OneCenterOptions &options={})
Definition energy.hpp:902
std::vector< Real > cumulative_integral(const std::vector< Real > &values, const std::vector< Real > &measures, int mesh_size)
void compute_one_center_xc(const std::vector< std::vector< Real > > &density_channels, const std::vector< Real > &core_density, const std::vector< Real > &radial_grid, const std::vector< Real > &radial_grid_derivative, int mesh_size, const std::vector< AngularPoint > &angular_grid, const std::vector< std::vector< Real > > &harmonics, const XCFunctional &functional, Real &energy, std::vector< std::vector< Real > > &potential_lm)
Definition energy.hpp:155
int harmonic_combined_degree_order_index(int degree, int order)
Definition harmonics.hpp:61
double Real
Real compute_real_spherical_harmonic(int degree, int order, const Vector3D &direction)
Definition harmonics.hpp:85
generated::Energy energy(const std::string &name, Real value_in_ry)
One of Q3's energies that ESSE has no property for, as an ESSE energy in rydberg.
Definition results.hpp:103
std::vector< int > harmonic_order_sequence(int degree)
Definition harmonics.hpp:40
std::vector< Real > radial_values
One branch (all-electron or pseudo) of the one-center terms.
Definition energy.hpp:105
std::vector< std::vector< Real > > gradient_angular_weight_lm
Q_lm.
Definition energy.hpp:129
std::vector< std::vector< Real > > potential_lm
v_H + v_rho, lm-decomposed — what D_ij's local term integrates against.
Definition energy.hpp:109
std::vector< std::vector< Real > > gradient_radial_weight_lm
P_lm.
Definition energy.hpp:128
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
Both branches, kept separate so a gate failure can be localized.
Definition energy.hpp:137
Real total() const
E¹ − Ẽ¹, the quantity that enters the total energy.
Definition energy.hpp:142