Loading...
Searching...
No Matches
validation.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * PAW dataset self-consistency checks.
5 *
6 * A PAW dataset overdetermines itself: the multipole moments, the augmentation
7 * charges and the partial waves are three views of the same one-center data, so
8 * they can be checked against each other without any external reference. These
9 * checks are what let a reader of an undocumented format be trusted later
10 * on: a dataset that satisfies all of them was almost
11 * certainly parsed correctly, whatever format it arrived in.
12 *
13 * Each check returns the worst absolute deviation it found, so callers can
14 * report a number rather than a bare pass/fail. The tolerances used by the
15 * tests were established by evaluating these identities on the reference
16 * datasets rather than assumed.
17 *
18 * All partial waves are r-weighted (r·φ), so products already carry the r² of
19 * the volume element and integrate against the radial-grid derivative directly.
20 */
21
22#include <cmath>
23#include <vector>
24
27
28namespace q3 {
29namespace paw_validation {
30
31/**
32 * Whether an augmentation channel (l_i, l_j, L) is allowed by the
33 * angular-momentum triangle condition. Forbidden channels carry no
34 * compensation charge and are stored as exact zeros.
35 */
36inline bool is_allowed_augmentation_channel(int angular_momentum_1,
37 int angular_momentum_2,
38 int total_angular_momentum) {
39 if (total_angular_momentum < std::abs(angular_momentum_1 - angular_momentum_2))
40 return false;
41 if (total_angular_momentum > angular_momentum_1 + angular_momentum_2)
42 return false;
43 return (angular_momentum_1 + angular_momentum_2 + total_angular_momentum) % 2 == 0;
44}
45
46/**
47 * Multipole identity: for every triangle-allowed channel,
48 *
49 * multipole_moments[L][i][j] = ∫ (φ_i φ_j − φ̃_i φ̃_j) r^L dr
50 *
51 * This is the tightest available check that the all-electron partial waves,
52 * the pseudo partial waves and the stored moments describe one dataset.
53 */
55 Real worst_deviation = 0.0;
56 int number_of_channels = pseudopotential.number_of_projectors;
57 int mesh_size = pseudopotential.mesh_size;
58
59 for (int angular_momentum = 0;
60 angular_momentum < pseudopotential.number_of_augmentation_angular_momenta;
61 angular_momentum++) {
62 for (int channel_1 = 0; channel_1 < number_of_channels; channel_1++) {
63 for (int channel_2 = 0; channel_2 < number_of_channels; channel_2++) {
64 const auto& all_electron_1 = pseudopotential.partial_waves_all_electron[channel_1];
65 const auto& all_electron_2 = pseudopotential.partial_waves_all_electron[channel_2];
66 if (!is_allowed_augmentation_channel(all_electron_1.angular_momentum,
67 all_electron_2.angular_momentum,
68 angular_momentum))
69 continue;
70
71 const auto& pseudo_1 = pseudopotential.partial_waves_pseudo[channel_1];
72 const auto& pseudo_2 = pseudopotential.partial_waves_pseudo[channel_2];
73
74 std::vector<Real> integrand(mesh_size);
75 for (int radial_index = 0; radial_index < mesh_size; radial_index++) {
76 Real difference =
77 all_electron_1.radial_values[radial_index] *
78 all_electron_2.radial_values[radial_index] -
79 pseudo_1.radial_values[radial_index] * pseudo_2.radial_values[radial_index];
80 integrand[radial_index] =
81 difference *
82 std::pow(pseudopotential.radial_grid[radial_index], angular_momentum);
83 }
84
85 Real computed =
86 simpson_rule(integrand, pseudopotential.radial_grid_derivative, mesh_size);
87 Real stored =
88 pseudopotential.multipole_moments[angular_momentum][channel_1][channel_2];
89 worst_deviation = std::max(worst_deviation, std::abs(computed - stored));
90 }
91 }
92 }
93 return worst_deviation;
94}
95
96/**
97 * The L = 0 multipole block is the augmentation charge matrix: the monopole of
98 * the compensation charge is exactly what the ultrasoft apparatus calls q_ij.
99 * Datasets store both, and they must agree.
100 */
102 Real worst_deviation = 0.0;
103 int number_of_channels = pseudopotential.number_of_projectors;
104 if (pseudopotential.multipole_moments.empty())
105 return worst_deviation;
106
107 for (int channel_1 = 0; channel_1 < number_of_channels; channel_1++) {
108 for (int channel_2 = 0; channel_2 < number_of_channels; channel_2++) {
109 Real monopole = pseudopotential.multipole_moments[0][channel_1][channel_2];
110 Real augmentation_charge =
111 pseudopotential.augmentation_overlap_integrals[channel_1][channel_2];
112 worst_deviation = std::max(worst_deviation, std::abs(monopole - augmentation_charge));
113 }
114 }
115 return worst_deviation;
116}
117
118/**
119 * Projector duality: ⟨p̃_i|φ̃_j⟩ = δ_ij within the same angular-momentum
120 * channel. This is the defining property of the PAW projectors — if it fails,
121 * the projectors and the pseudo partial waves came from different datasets or
122 * were paired up in the wrong order.
123 *
124 * Channels with different l are orthogonal by symmetry and carry no
125 * information about the radial tabulation, so they are skipped.
126 */
128 Real worst_deviation = 0.0;
129 int number_of_channels = pseudopotential.number_of_projectors;
130 int mesh_size = pseudopotential.mesh_size;
131
132 for (int projector_index = 0; projector_index < number_of_channels; projector_index++) {
133 for (int wave_index = 0; wave_index < number_of_channels; wave_index++) {
134 const auto& projector = pseudopotential.projectors[projector_index];
135 const auto& pseudo_wave = pseudopotential.partial_waves_pseudo[wave_index];
136 if (projector.angular_momentum != pseudo_wave.angular_momentum)
137 continue;
138
139 std::vector<Real> integrand(mesh_size);
140 for (int radial_index = 0; radial_index < mesh_size; radial_index++) {
141 integrand[radial_index] =
142 projector.radial_values[radial_index] * pseudo_wave.radial_values[radial_index];
143 }
144 Real overlap =
145 simpson_rule(integrand, pseudopotential.radial_grid_derivative, mesh_size);
146 Real expected = (projector_index == wave_index) ? 1.0 : 0.0;
147 worst_deviation = std::max(worst_deviation, std::abs(overlap - expected));
148 }
149 }
150 return worst_deviation;
151}
152
153/**
154 * Integrated all-electron core charge, 4π ∫ ρ_core(r) r² dr. Equals the number
155 * of core electrons the dataset froze out, Z − Z_valence, which is how a caller
156 * can confirm the all-electron core density was read on the right grid and with
157 * the right normalization without knowing Z beforehand.
158 */
160 int mesh_size = pseudopotential.mesh_size;
161 const auto& core_density = pseudopotential.charge_core_all_electron.radial_values;
162 if (core_density.empty())
163 return 0.0;
164
165 std::vector<Real> integrand(mesh_size);
166 for (int radial_index = 0; radial_index < mesh_size; radial_index++) {
167 Real radius = pseudopotential.radial_grid[radial_index];
168 integrand[radial_index] = core_density[radial_index] * radius * radius;
169 }
170 constexpr Real four_pi = 4.0 * M_PI;
171 return four_pi * simpson_rule(integrand, pseudopotential.radial_grid_derivative, mesh_size);
172}
173
174/**
175 * Nuclear charge implied by the all-electron local potential, read off its
176 * Coulomb tail toward the origin: V_AE(r) → −2Z/r in Rydberg units, so
177 * −r·V(r)/2 → Z. Evaluated at the innermost grid point, which sits close
178 * enough to the origin on a logarithmic mesh to recover Z to a fraction of a
179 * percent.
180 */
182 const auto& potential = pseudopotential.potential_local_all_electron.radial_values;
183 if (potential.empty() || pseudopotential.radial_grid.empty())
184 return 0.0;
185 return -0.5 * pseudopotential.radial_grid[0] * potential[0];
186}
187
188} // namespace paw_validation
189} // 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
PotentialLocal potential_local_all_electron
All-electron local potential (Ry); diverges as -2Z/r toward the origin.
Definition init.hpp:85
int number_of_augmentation_angular_momenta
Number of angular-momentum channels, 2·l_max + 1.
Definition init.hpp:112
std::vector< std::vector< std::vector< Real > > > multipole_moments
Definition init.hpp:99
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< Projector > projectors
Definition init.hpp:80
int number_of_projectors
Definition init.hpp:72
std::vector< Real > radial_grid
Definition init.hpp:76
std::vector< std::vector< Real > > augmentation_overlap_integrals
Definition init.hpp:49
Real worst_projector_duality_deviation(const PawPseudopotential &pseudopotential)
bool is_allowed_augmentation_channel(int angular_momentum_1, int angular_momentum_2, int total_angular_momentum)
Real worst_augmentation_charge_deviation(const PawPseudopotential &pseudopotential)
Real integrated_all_electron_core_charge(const PawPseudopotential &pseudopotential)
Real nuclear_charge_from_all_electron_potential(const PawPseudopotential &pseudopotential)
Real worst_multipole_moment_deviation(const PawPseudopotential &pseudopotential)
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< Real > radial_values
std::vector< Real > radial_values