Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
scf_terms.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * The one-center engine's SCF-facing driver.
5 *
6 * Everything below this file works on one atom and one occupancy matrix. This
7 * is what an SCF loop actually needs: given the projector overlap coefficients
8 * the ultrasoft pathway already accumulates each step, produce
9 *
10 * * Σ_a (E¹_a − Ẽ¹_a), the one-center contribution to the total energy, and
11 * * (D¹ − D̃¹)_Hxc per atom, added to the D the ultrasoft pathway built.
12 *
13 * Both come from the same call, because they come from the same potential: the
14 * D matrix integrates against the branch potentials the energy computed, which
15 * is what makes D ≡ ∂E/∂ρ_ij hold at the level the SCF sees rather than only
16 * inside the module's own gate.
17 *
18 * **The projector overlap coefficients are ρ_ij.** The ultrasoft iterator
19 * accumulates Σ_nk f_nk ⟨ψ|p̃_i⟩⟨p̃_j|ψ⟩ per atom over expanded (lm-resolved)
20 * channels, and that array *is* the PAW on-site occupancy matrix — not
21 * something convertible into it. What has to be true for that to be usable is
22 * that both sides expand
23 * radial channels into lm in the same order; they do, both walking channels in
24 * file order and lm in `harmonic_order_sequence`, and
25 * `channel_expansion_matches_projectors` states it as a checkable
26 * predicate rather than a comment, because a silent mismatch here produces a
27 * plausible, converged, wrong answer.
28 *
29 * Per-species work — the angular grid, the harmonics, the Gaunt table, the
30 * channel expansion — is built once at construction. Only the per-atom radial
31 * work repeats each step, which is why the one-center cost stays negligible
32 * next to the plane-wave side.
33 */
34
35#include <algorithm>
36#include <array>
37#include <memory>
38#include <stdexcept>
39#include <vector>
40
54
55namespace q3 {
56namespace paw_one_center {
57
58/**
59 * Whether a dataset's one-center channel expansion agrees with the order the
60 * plane-wave side accumulates projector overlaps in.
61 *
62 * The two are built independently — `expand_channels` from the partial waves,
63 * the projector mapping table from the β projectors — so their agreement is a
64 * property of the dataset, not of the code. A PAW file that paired them
65 * differently would make every ρ_ij element land in the wrong place while
66 * still producing a symmetric, plausible matrix.
67 */
68inline bool channel_expansion_matches_projectors(const PawPseudopotential& pseudopotential) {
69 if (static_cast<int>(pseudopotential.projectors.size()) != pseudopotential.number_of_projectors)
70 return false;
71 for (int channel = 0; channel < pseudopotential.number_of_projectors; channel++) {
72 if (pseudopotential.projectors[channel].angular_momentum !=
73 pseudopotential.partial_waves_pseudo[channel].angular_momentum)
74 return false;
75 }
76 return true;
77}
78
79/// Everything reusable across SCF steps for one species.
84 std::vector<AngularPoint> angular_grid;
85 std::vector<std::vector<Real>> harmonics;
86 std::vector<std::vector<std::array<Real, 3>>> harmonic_gradients;
87 bool gradient_corrected{false};
88
89 /**
90 * How this dataset's one-center numbers are referenced. Owned per species
91 * because it caches that species' reference state.
92 */
93 std::shared_ptr<PawOneCenterConvention> convention;
94
95 [[nodiscard]] int number_of_channels() const {
96 return expansion.size();
97 }
98};
99
100/**
101 * One-center energy and D correction for every atom, refreshed per SCF step.
102 *
103 * Constructed once per calculation; `update()` is the per-step call. Species
104 * that are not PAW are simply absent from the context list and contribute
105 * nothing, so a mixed PAW/ultrasoft cell needs no special case.
106 */
108 public:
109 /**
110 * @param angular_order drives the Gauss(θ)×uniform(φ) grid size; must be
111 * at least the largest density L, and is set from the
112 * dataset rather than fixed here
113 */
114 void build(const Crystal& crystal_structure,
115 const PseudopotentialSet& pseudopotential_set,
116 const XCFunctional& functional,
117 int angular_order = 0) {
118 functional_ = &functional;
119 int number_of_species = pseudopotential_set.size();
120 contexts_.assign(number_of_species, SpeciesOneCenterContext{});
121
122 for (int species_index = 0; species_index < number_of_species; species_index++) {
123 const auto* paw =
124 dynamic_cast<const PawPseudopotential*>(&pseudopotential_set[species_index]);
125 if (paw == nullptr)
126 continue;
128 throw std::runtime_error(
129 "PAW dataset for " + paw->element +
130 " pairs its projectors and partial waves in different orders, so the "
131 "projector overlap coefficients cannot be read as on-site occupancies");
132
133 auto& context = contexts_[species_index];
134 context.pseudopotential = paw;
135 context.expansion = expand_channels(*paw);
136
137 int maximum_angular_momentum = 0;
138 for (const auto& wave : paw->partial_waves_pseudo)
139 maximum_angular_momentum =
140 std::max(maximum_angular_momentum, wave.angular_momentum);
141 context.gaunt = generate_gaunt_coefficients(maximum_angular_momentum);
142
143 int number_of_lm = density_lm_count(*paw);
144 context.gradient_corrected = functional.requires_density_gradient();
145 /**
146 * The grid must integrate products of harmonics up to the density's
147 * own largest L (2·l_max). GGA raises the effective angular order
148 * by one because the density gradient couples neighboring angular
149 * momenta. Without the increment, a d-channel density received
150 * only 15 azimuthal points rather than 18 and its highest gradient
151 * modes could alias. The dataset still drives the order unless the
152 * caller explicitly asks for more.
153 */
154 int density_order = 2 * maximum_angular_momentum;
155 int functional_order = density_order + (context.gradient_corrected ? 1 : 0);
156 int order = std::max(angular_order, functional_order);
157 context.angular_grid = build_angular_grid(order);
158 context.harmonics = tabulate_harmonics(context.angular_grid, number_of_lm);
159
160 if (context.gradient_corrected) {
161 context.harmonic_gradients =
162 tabulate_harmonic_gradients(context.angular_grid, number_of_lm);
163 }
164
165 /**
166 * The convention is prepared last because building its reference
167 * state runs the full one-center engine once, which needs every
168 * table above already in place.
169 */
170 context.convention = create_one_center_convention(*paw);
172 inputs.pseudopotential = paw;
173 inputs.expansion = &context.expansion;
174 inputs.gaunt = &context.gaunt;
175 inputs.angular_grid = &context.angular_grid;
176 inputs.harmonics = &context.harmonics;
177 inputs.harmonic_gradients =
178 context.gradient_corrected ? &context.harmonic_gradients : nullptr;
179 inputs.functional = &functional;
180 context.convention->prepare(inputs);
181 log::info(" PAW: {} one-center convention for {} (atomic Hxc {:.9f} Ry, "
182 "reference double counting {:.9f} Ry)",
183 context.convention->name(),
184 paw->element,
185 context.convention->reference_one_center_energy(),
186 context.convention->reference_double_counting_energy());
187 }
188
189 int number_of_atoms = crystal_structure.number_of_atoms();
190 d_correction_by_spin_.assign(1, std::vector<std::vector<Real>>(number_of_atoms));
191 energy_ = 0.0;
192 }
193
194 [[nodiscard]] bool is_active() const {
195 for (const auto& context : contexts_) {
196 if (context.pseudopotential != nullptr)
197 return true;
198 }
199 return false;
200 }
201
202 /**
203 * Recompute the one-center terms from the current occupancies.
204 *
205 * @param crystal_structure atom → species map
206 * @param projector_overlap_coefficients ρ_ij: [atom][i * stride + j]
207 * @param stride row length of that array, which is the
208 * *maximum* channel count over species,
209 * not this atom's own
210 */
211 void update(const Crystal& crystal_structure,
212 const Matrix<Real>& projector_overlap_coefficients,
213 int stride) {
214 update_by_spin(crystal_structure, {&projector_overlap_coefficients}, stride);
215 }
216
217 /**
218 * The spin-polarized form: one occupancy array per channel.
219 *
220 * Exchange-correlation is nonlinear in the spin density, so an atom's
221 * on-site XC has to see ρ↑ and ρ↓ separately rather than their sum. Passing
222 * a single array reproduces the unpolarized result exactly, which is what
223 * `update` above does.
224 *
225 * @param projector_overlap_coefficients_by_spin ρ_ij per channel, each
226 * [atom][i * channels + j]
227 * @param row_capacity allocated row length, used only to bound the read
228 */
229 void
230 update_by_spin(const Crystal& crystal_structure,
231 const std::vector<const Matrix<Real>*>& projector_overlap_coefficients_by_spin,
232 int row_capacity) {
233 int number_of_spins = static_cast<int>(projector_overlap_coefficients_by_spin.size());
234 energy_ = 0.0;
235 branch_totals_ = BranchTotals{};
236 int number_of_atoms = crystal_structure.number_of_atoms();
237 d_correction_by_spin_.assign(number_of_spins,
238 std::vector<std::vector<Real>>(number_of_atoms));
239
240 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
241 int species_index = crystal_structure.species_type_index(atom_index);
242 const auto& context = contexts_[species_index];
243 if (context.pseudopotential == nullptr)
244 continue;
245
246 int number_of_channels = context.number_of_channels();
247 /**
248 * The row stride is this **atom's own** channel count, not the
249 * maximum over species.
250 *
251 * Rows are *allocated* max_channels² wide so a mixed cell's array
252 * is rectangular, but the accumulation writes at
253 * `i * channels(atom) + j` — and `add_augmentation_charges` reads it back
254 * the same way. Using the allocated width here instead would shear
255 * every row belonging to a species narrower than the widest one:
256 * the one-center energy and D come out wrong for a mixed-species
257 * PAW cell (SiC, NiO, any oxide) and the run still converges.
258 */
259 if (number_of_channels > row_capacity)
260 throw std::runtime_error("PAW one-center channel count exceeds the allocated "
261 "projector overlap row width");
262
263 auto occupancies = OnSiteOccupancies::zeros(number_of_channels, number_of_spins);
264 for (int spin = 0; spin < number_of_spins; spin++) {
265 const auto& coefficients_for_spin = *projector_overlap_coefficients_by_spin[spin];
266 for (int first = 0; first < number_of_channels; first++)
267 for (int second = 0; second < number_of_channels; second++)
268 occupancies.at(spin, first, second) =
269 coefficients_for_spin[atom_index][first * number_of_channels + second];
270 }
271
272 const auto& paw = *context.pseudopotential;
273 if (number_of_spins == 1) {
274 auto densities = build_one_center_densities(
275 paw, context.expansion, occupancies, context.gaunt, 0);
277 paw,
278 densities,
279 context.angular_grid,
280 context.harmonics,
281 *functional_,
282 context.gradient_corrected ? &context.harmonic_gradients : nullptr,
283 context.convention->options());
284 accumulate_atom(context, paw, occupancies, energy, atom_index, 0);
285 continue;
286 }
287
288 if (number_of_spins > 2)
289 throw std::runtime_error(
290 "PAW one-center terms support at most two spin channels; " +
291 std::to_string(number_of_spins) + " were given");
292
293 std::vector<OneCenterDensities> densities_by_spin;
294 densities_by_spin.reserve(number_of_spins);
295 for (int spin = 0; spin < number_of_spins; spin++)
296 densities_by_spin.push_back(build_one_center_densities(
297 paw, context.expansion, occupancies, context.gaunt, spin));
298 auto energies = compute_one_center_energy_spin(
299 paw,
300 densities_by_spin,
301 context.angular_grid,
302 context.harmonics,
303 *functional_,
304 context.gradient_corrected ? &context.harmonic_gradients : nullptr,
305 context.convention->options());
306 for (int spin = 0; spin < number_of_spins; spin++)
307 accumulate_atom(context, paw, occupancies, energies[spin], atom_index, spin);
308 }
309 }
310
311 private:
312 /**
313 * Fold one atom's (one channel's) result into the running totals.
314 *
315 * The convention's energy correction and D correction are applied per
316 * channel because the convention linearizes per channel too: each channel's
317 * D is referenced against the same atomic state, and the energy correction
318 * is a function of that channel's occupancies.
319 */
320 void accumulate_atom(const SpeciesOneCenterContext& context,
321 const PawPseudopotential& paw,
322 const OnSiteOccupancies& occupancies,
323 const OneCenterEnergy& energy,
324 int atom_index,
325 int spin) {
326 energy_ += energy.total();
327 if (spin == 0)
328 energy_ += context.convention->energy_correction(occupancies);
329 branch_totals_.all_electron_hartree += energy.all_electron.hartree_energy;
330 branch_totals_.pseudo_hartree += energy.pseudo.hartree_energy;
331 branch_totals_.all_electron_xc += energy.all_electron.xc_energy;
332 branch_totals_.pseudo_xc += energy.pseudo.xc_energy;
333 d_correction_by_spin_[spin][atom_index] =
334 compute_one_center_d_matrix(paw, context.expansion, context.gaunt, energy);
335 context.convention->correct_d_matrix(d_correction_by_spin_[spin][atom_index]);
336 }
337
338 public:
339 /**
340 * The four pieces the one-center energy is the difference of, summed over
341 * atoms.
342 *
343 * The second rung of the failure ladder: it separates Hartree from XC and
344 * all-electron from pseudo, which between them localize a discrepancy to a
345 * single step of the engine.
346 *
347 * Quantum ESPRESSO prints four quantities under the same names, but its
348 * pseudo-branch lines already carry the minus sign of E¹ − Ẽ¹: its four
349 * **add** to its printed one-center total, where these four are unsigned
350 * branch energies and have to be differenced. Differencing its printed
351 * values instead — which is what gave −3.407 Ry against a printed
352 * −12.6126 Ry on diamond — is the wrong combination, not evidence of a
353 * different decomposition. Its component print is also per-process rather
354 * than reduced, so under MPI the four can be partial sums even when the
355 * total is not.
356 */
363
364 [[nodiscard]] const BranchTotals& branch_totals() const {
365 return branch_totals_;
366 }
367
368 /// Σ_a (E¹_a − Ẽ¹_a), in Rydberg.
369 [[nodiscard]] Real energy() const {
370 return energy_;
371 }
372
373 /**
374 * (D¹ − D̃¹)_Hxc for one atom in one spin channel, as a dense (nh × nh)
375 * matrix; empty if the species is not PAW.
376 *
377 * A channel beyond what was computed falls back to channel 0, so an
378 * unpolarized `update()` followed by a spin-polarized read is consistent
379 * rather than out of bounds.
380 */
381 [[nodiscard]] const std::vector<Real>& d_correction(int atom_index, int spin = 0) const {
382 int channel = spin < static_cast<int>(d_correction_by_spin_.size()) ? spin : 0;
383 return d_correction_by_spin_[channel][atom_index];
384 }
385
386 [[nodiscard]] int number_of_channels(int species_index) const {
387 return contexts_[species_index].number_of_channels();
388 }
389
390 private:
391 std::vector<SpeciesOneCenterContext> contexts_;
392 /// [spin][atom] — one entry for an unpolarized run, two for a polarized one.
393 std::vector<std::vector<std::vector<Real>>> d_correction_by_spin_;
394 const XCFunctional* functional_{nullptr};
395 Real energy_{0.0};
396 BranchTotals branch_totals_;
397};
398
399} // namespace paw_one_center
400} // namespace q3
int species_type_index(int atom_index) const
Definition crystal.hpp:273
int number_of_atoms() const
Definition crystal.hpp:209
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
int size() const
Definition set.hpp:98
std::vector< Projector > projectors
Definition init.hpp:80
int number_of_projectors
Definition init.hpp:72
Abstract base class for XC functionals.
Definition init.hpp:23
virtual bool requires_density_gradient() const
Whether this is a GGA functional (needs gradients).
Definition init.hpp:62
const std::vector< Real > & d_correction(int atom_index, int spin=0) const
const BranchTotals & branch_totals() const
int number_of_channels(int species_index) const
void update(const Crystal &crystal_structure, const Matrix< Real > &projector_overlap_coefficients, int stride)
Real energy() const
Σ_a (E¹_a − Ẽ¹_a), in Rydberg.
void build(const Crystal &crystal_structure, const PseudopotentialSet &pseudopotential_set, const XCFunctional &functional, int angular_order=0)
void update_by_spin(const Crystal &crystal_structure, const std::vector< const Matrix< Real > * > &projector_overlap_coefficients_by_spin, int row_capacity)
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
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
std::vector< std::vector< std::array< Real, 3 > > > tabulate_harmonic_gradients(const std::vector< AngularPoint > &angular_grid, int number_of_lm)
Definition gradient.hpp:84
std::unique_ptr< PawOneCenterConvention > create_one_center_convention(const PawPseudopotential &pseudopotential)
Definition factory.hpp:30
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
OneCenterDensities build_one_center_densities(const PawPseudopotential &pseudopotential, const ChannelExpansion &expansion, const OnSiteOccupancies &occupancies, const GauntCoefficientTable &gaunt, int spin)
std::vector< AngularPoint > build_angular_grid(int maximum_angular_momentum)
ChannelExpansion expand_channels(const PawPseudopotential &pseudopotential)
Definition densities.hpp:69
std::vector< Real > compute_one_center_d_matrix(const PawPseudopotential &pseudopotential, const ChannelExpansion &expansion, const GauntCoefficientTable &gaunt, const OneCenterEnergy &energy)
Definition d_matrix.hpp:57
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
int density_lm_count(const PawPseudopotential &pseudopotential)
Number of lm channels a density needs: products of two l_max harmonics.
bool channel_expansion_matches_projectors(const PawPseudopotential &pseudopotential)
Definition scf_terms.hpp:68
double Real
GauntCoefficientTable generate_gaunt_coefficients(int max_degree)
static OnSiteOccupancies zeros(int number_of_channels, int number_of_spin_components=1)
Both branches, kept separate so a gate failure can be localized.
Definition energy.hpp:137
Everything the engine needs to rebuild a dataset's own reference state.
Definition init.hpp:42
const PawPseudopotential * pseudopotential
Definition init.hpp:43
const std::vector< std::vector< std::array< Real, 3 > > > * harmonic_gradients
Definition init.hpp:48
const XCFunctional * functional
Definition init.hpp:49
const std::vector< std::vector< Real > > * harmonics
Definition init.hpp:47
const GauntCoefficientTable * gaunt
Definition init.hpp:45
const std::vector< AngularPoint > * angular_grid
Definition init.hpp:46
const ChannelExpansion * expansion
Definition init.hpp:44
Everything reusable across SCF steps for one species.
Definition scf_terms.hpp:80
std::shared_ptr< PawOneCenterConvention > convention
Definition scf_terms.hpp:93
std::vector< std::vector< Real > > harmonics
Definition scf_terms.hpp:85
std::vector< AngularPoint > angular_grid
Definition scf_terms.hpp:84
std::vector< std::vector< std::array< Real, 3 > > > harmonic_gradients
Definition scf_terms.hpp:86
const PawPseudopotential * pseudopotential
Definition scf_terms.hpp:81