Loading...
Searching...
No Matches
makov_payne.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * MakovPayneCorrectionCalculator — isolated (charged) system electrostatic correction.
5 *
6 * Corrects the spurious electrostatic self-interaction of a net-charged periodic
7 * cell with its own periodic images (Makov, G. and Payne, M.C., "Periodic boundary
8 * conditions in ab initio calculations," Phys. Rev. B 51, 4014 (1995)). The closed-form
9 * monopole and dipole+quadrupole expressions of that paper are implemented here.
10 *
11 * First order (monopole/Madelung) term:
12 * E_1st = alpha_M(ibrav) * q^2 / L
13 * Second order (dipole+quadrupole) term:
14 * E_2nd = -(2*pi/3) * (q*Tr[Q] - |p|^2) / L^3 * e^2
15 * where:
16 * q = Z_tot - N_electrons (net cell charge; +1 for one electron removed)
17 * Tr[Q] = Tr[Q_ion] - Tr[Q_el] (trace of the second-moment tensor about x0,
18 * e*bohr^2)
19 * p = p_ion - p_el (total dipole about x0; |p|^2 in e^2*bohr^2)
20 * x0 = sum_i(Z_i * tau_i) / sum_i(Z_i) (valence-charge-weighted ionic center)
21 * L = alat (see the Madelung-constant note below)
22 * e^2 = 2 in Rydberg units, so the e^2/2 factor of E_1st is folded away and both
23 * terms are returned in Ry.
24 * Both terms already carry the sign such that
25 * corrected_total_energy = total_energy + E_1st + E_2nd,
26 * and they are reported as the "first_order" and "second_order" terms of
27 * IsolatedSystemCorrectionResult.
28 *
29 * Quadrupole sign. The second-order term above carries the quadrupole contribution
30 * with the sign opposite to the one printed in the 1995 paper's Eq. 15. That
31 * discrepancy is not Q3's own reading of the paper: it is an observation also recorded
32 * upstream in Quantum ESPRESSO. The sign actually in force
33 * here is the one pinned by the cross-code reference values in
34 * tests/validation/test_makov_payne.py; re-deriving the Eq. 15 sign from the paper is
35 * an open item.
36 *
37 * Madelung constants. 2.8373 (SC), 2.8883 (FCC), 2.8885 (BCC) are the Madelung
38 * constants of a point charge in a compensating uniform background, normalised to
39 * L = Omega^(1/3) (Omega = cell volume): they are (4*pi/3)^(1/3) = 1.61199 times the
40 * classic per-particle values 1.76012 / 1.79175 / 1.79186 Ry*bohr. They are applied
41 * here with L = alat, which equals Omega^(1/3) only for SC -- for FCC and BCC, alat
42 * exceeds Omega^(1/3) by 4^(1/3) and 2^(1/3) respectively. L = alat is the convention
43 * that reproduces the reference numbers pinned in tests/validation/test_makov_payne.py
44 * (FCC Si: 2.8883 / 10.3344 bohr = 0.2794840 Ry at q = +1); whether the intended
45 * reading is that interoperability convention or a different definition of L is an
46 * open item.
47 *
48 * Cubic lattices only (ibrav 1/2/3 = SC/FCC/BCC). ibrav=-3 (BCC's alternate
49 * primitive-vector axes) and ibrav=0 (explicit/"free" lattice vectors) are both
50 * rejected, because the Madelung-constant lookup is keyed by the ibrav index itself
51 * and neither value lies in 1..3. This is stricter than Quantum ESPRESSO, which first
52 * resolves ibrav=0 to a Bravais index from the lattice vectors and only then applies
53 * the same 1..3 bound.
54 *
55 * Electronic dipole/quadrupole moments are accumulated over the dense (charge
56 * density) real-space grid with minimum-image wrapping of (r - x0) into the cell
57 * before use -- unwrapped ionic positions are NOT wrapped, matching Quantum ESPRESSO's
58 * convention (reproducing QE's number requires reproducing whatever atomic-position
59 * gauge the reference calculation used). NLCC core charge is excluded, since
60 * charge_density_real is the pure valence density (core charge is a separate array
61 * added only where XC needs it) -- the same choice QE makes, moments taken over the
62 * valence density alone and not the core charge.
63 *
64 * Under band parallelism, charge_density_real holds this rank's part of the grid.
65 * Grid coordinates come from FFTGrid::global_real_space_index(), and the seven electronic
66 * moments are reduced within the k-point pool through the injected execution
67 * policy. K-point-pool mixing has already summed the converged density across
68 * pools, so no across-pool reduction belongs here.
69 */
70
71#include <cmath>
72#include <stdexcept>
73#include <string>
74#include <vector>
75
81
82namespace q3 {
83
85 public:
87 compute(const NormConservingPlanewaveKohnShamSCFState& scf_state) const override {
88 const Crystal& crystal_structure = *scf_state.crystal_structure;
89 int bravais_lattice_index = crystal_structure.bravais_lattice_index();
90
91 Real madelung_constant = 0.0;
92 if (bravais_lattice_index == 1) {
93 madelung_constant = 2.8373; // simple cubic
94 } else if (bravais_lattice_index == 2) {
95 madelung_constant = 2.8883; // FCC
96 } else if (bravais_lattice_index == 3) {
97 madelung_constant = 2.8885; // BCC
98 } else {
99 throw std::runtime_error(
100 "Makov-Payne correction is only defined for cubic lattices (QE ibrav "
101 "1/2/3 = SC/FCC/BCC); got bravais_lattice_index=" +
102 std::to_string(bravais_lattice_index) +
103 " (0 = explicit/'free' lattice vectors, -3 = BCC's alternate-axes "
104 "primitive-vector convention -- QE's own bound check rejects -3 too, "
105 "not just non-cubic lattices)");
106 }
107
108 Real alat = crystal_structure.lattice_parameter();
109 int number_of_atoms = crystal_structure.number_of_atoms();
110
111 // x0 = valence-charge-weighted ionic center, in alat units (Cartesian).
112 Real total_valence_charge = 0.0;
113 Vector3D ionic_center_alat{0.0, 0.0, 0.0};
114 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
115 Real valence_charge =
116 crystal_structure.valence_charge(crystal_structure.species_type_index(atom_index));
117 total_valence_charge += valence_charge;
118 ionic_center_alat +=
119 valence_charge * crystal_structure.convert_from_crystal_to_cartesian(atom_index);
120 }
121 ionic_center_alat = ionic_center_alat / total_valence_charge;
122
123 /**
124 * Ionic dipole/second moments about x0 -- NOT minimum-image wrapped, while the
125 * electronic grid sum below is. This unwrapped-ion gauge is part of what the
126 * cross-code reference values in tests/validation/test_makov_payne.py pin:
127 * reproducing them requires the same gauge for the atomic positions.
128 */
129 Vector3D ionic_dipole_alat{0.0, 0.0, 0.0};
130 Vector3D ionic_quadrupole_alat{0.0, 0.0, 0.0};
131 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
132 Real valence_charge =
133 crystal_structure.valence_charge(crystal_structure.species_type_index(atom_index));
134 Vector3D delta =
135 crystal_structure.convert_from_crystal_to_cartesian(atom_index) - ionic_center_alat;
136 ionic_dipole_alat += valence_charge * delta;
137 ionic_quadrupole_alat.x += valence_charge * delta.x * delta.x;
138 ionic_quadrupole_alat.y += valence_charge * delta.y * delta.y;
139 ionic_quadrupole_alat.z += valence_charge * delta.z * delta.z;
140 }
141 Vector3D ionic_dipole = ionic_dipole_alat * alat;
142 Vector3D ionic_quadrupole = ionic_quadrupole_alat * (alat * alat);
143
144 /**
145 * Electronic dipole/quadrupole: dense real-space grid sum, minimum-image
146 * wrapping (r - x0) into the cell via fractional coordinates before use.
147 */
148 const FFTGrid& density_grid = scf_state.planewave_basis->fft_rho();
149 int grid_dimension_1 = density_grid.grid_dimension_1();
150 int grid_dimension_2 = density_grid.grid_dimension_2();
151 int grid_dimension_3 = density_grid.grid_dimension_3();
152 int total_grid_points = density_grid.total_grid_points();
153 int local_grid_points = density_grid.local_grid_points();
154
155 Matrix3x3 lattice_vectors = crystal_structure.lattice_vectors(); // alat units
156 Matrix3x3 lattice_vectors_transpose = lattice_vectors.transpose();
157 Matrix3x3 lattice_inverse_transpose = lattice_vectors.inverse().transpose();
158
159 int number_of_spin_components = static_cast<int>(scf_state.charge_density_real.size());
160
161 Real electron_count_raw = 0.0;
162 Vector3D electronic_dipole_raw{0.0, 0.0, 0.0};
163 Vector3D electronic_quadrupole_raw{0.0, 0.0, 0.0};
164
165 /**
166 * One sweep over this rank's part of the dense grid, run once post-SCF.
167 * The grid translates each local storage index to the global flat index
168 * before the fractional coordinate is derived. Serial execution visits
169 * the complete grid with the two indices equal, preserving its original
170 * arithmetic.
171 */
172 for (int local_flat_index = 0; local_flat_index < local_grid_points; local_flat_index++) {
173 int global_flat_index = density_grid.global_real_space_index(local_flat_index);
174 int index_3 = global_flat_index % grid_dimension_3;
175 int index_2 = (global_flat_index / grid_dimension_3) % grid_dimension_2;
176 int index_1 = global_flat_index / (grid_dimension_3 * grid_dimension_2);
177 Vector3D fractional_grid_point{static_cast<Real>(index_1) / grid_dimension_1,
178 static_cast<Real>(index_2) / grid_dimension_2,
179 static_cast<Real>(index_3) / grid_dimension_3};
180 Vector3D grid_point_alat = lattice_vectors_transpose * fractional_grid_point;
181 Vector3D displacement = grid_point_alat - ionic_center_alat;
182
183 Vector3D fractional_displacement = lattice_inverse_transpose * displacement;
184 fractional_displacement.x -= std::round(fractional_displacement.x);
185 fractional_displacement.y -= std::round(fractional_displacement.y);
186 fractional_displacement.z -= std::round(fractional_displacement.z);
187 Vector3D wrapped_displacement = lattice_vectors_transpose * fractional_displacement;
188
189 Real total_density_at_point = 0.0;
190 for (int spin_index = 0; spin_index < number_of_spin_components; spin_index++) {
191 total_density_at_point +=
192 scf_state.charge_density_real[spin_index][local_flat_index];
193 }
194
195 electron_count_raw += total_density_at_point;
196 electronic_dipole_raw += total_density_at_point * wrapped_displacement;
197 electronic_quadrupole_raw.x +=
198 total_density_at_point * wrapped_displacement.x * wrapped_displacement.x;
199 electronic_quadrupole_raw.y +=
200 total_density_at_point * wrapped_displacement.y * wrapped_displacement.y;
201 electronic_quadrupole_raw.z +=
202 total_density_at_point * wrapped_displacement.z * wrapped_displacement.z;
203 }
204
205 std::vector<Real> electronic_moments = {
206 electron_count_raw,
207 electronic_dipole_raw.x,
208 electronic_dipole_raw.y,
209 electronic_dipole_raw.z,
210 electronic_quadrupole_raw.x,
211 electronic_quadrupole_raw.y,
212 electronic_quadrupole_raw.z,
213 };
214 if (scf_state.execution_policy != nullptr) {
215 scf_state.execution_policy->reduce_within_pool_vector(electronic_moments);
216 }
217 electron_count_raw = electronic_moments[0];
218 electronic_dipole_raw =
219 Vector3D{electronic_moments[1], electronic_moments[2], electronic_moments[3]};
220 electronic_quadrupole_raw =
221 Vector3D{electronic_moments[4], electronic_moments[5], electronic_moments[6]};
222
223 Real grid_sum_to_integral_scale =
224 crystal_structure.cell_volume() / static_cast<Real>(total_grid_points);
225 Real electron_count = electron_count_raw * grid_sum_to_integral_scale;
226 Vector3D electronic_dipole = electronic_dipole_raw * grid_sum_to_integral_scale * alat;
227 Vector3D electronic_quadrupole =
228 electronic_quadrupole_raw * grid_sum_to_integral_scale * (alat * alat);
229
230 Real net_charge = total_valence_charge - electron_count;
231 Vector3D dipole = ionic_dipole - electronic_dipole;
232 Real quadrupole_trace = (ionic_quadrupole.x - electronic_quadrupole.x) +
233 (ionic_quadrupole.y - electronic_quadrupole.y) +
234 (ionic_quadrupole.z - electronic_quadrupole.z);
235 Real dipole_norm_squared = dipole.x * dipole.x + dipole.y * dipole.y + dipole.z * dipole.z;
236
238
239 Real first_order_in_ry = madelung_constant * net_charge * net_charge / alat;
240 Real second_order_in_ry = -(2.0 / 3.0 * constants::pi) *
241 (net_charge * quadrupole_trace - dipole_norm_squared) /
242 (alat * alat * alat) * e2;
243
245 result.applied = true;
246 result.method = name();
247 result.terms_in_ry = {{"first_order", first_order_in_ry},
248 {"second_order", second_order_in_ry}};
249 return result;
250 }
251
252 [[nodiscard]] std::string name() const override {
253 return "makov-payne";
254 }
255};
256
257} // namespace q3
Vector3D convert_from_crystal_to_cartesian(int atom_index) const
Definition crystal.hpp:225
int bravais_lattice_index() const
Definition crystal.hpp:190
Real valence_charge(int species_index) const
Definition crystal.hpp:317
Real cell_volume() const
Definition crystal.hpp:193
const Matrix3x3 & lattice_vectors() const
Definition crystal.hpp:202
int species_type_index(int atom_index) const
Definition crystal.hpp:273
int number_of_atoms() const
Definition crystal.hpp:209
Real lattice_parameter() const
Definition crystal.hpp:180
virtual int grid_dimension_1() const =0
IsolatedSystemCorrectionResult compute(const NormConservingPlanewaveKohnShamSCFState &scf_state) const override
std::string name() const override
Scheme name as accepted by the input ("none", "makov-payne", ...).
FFTGrid & fft_rho()
Definition init.hpp:286
virtual void reduce_within_pool_vector(std::vector< Real > &values) const =0
constexpr Real electron_charge_squared_rydberg
Definition constants.hpp:32
constexpr Real pi
Definition constants.hpp:24
double Real
std::vector< std::pair< std::string, Real > > terms_in_ry
Definition init.hpp:49
3×3 matrix stored row-major: row i = mat[i]
Matrix3x3 transpose() const
Transpose.
Matrix3x3 inverse() const
Inverse (assumes non-singular)
std::vector< std::vector< Real > > charge_density_real
Definition init.hpp:37
const Crystal * crystal_structure
Definition init.hpp:55
const execution::PlanewaveExecutionPolicy * execution_policy
Definition init.hpp:64
const PlanewaveBasis * planewave_basis
Definition init.hpp:53