Loading...
Searching...
No Matches
reporter.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * PlanewaveKohnShamSCFReporter — all human-readable SCF output for a plane-wave KS calculation.
5 *
6 * This class (and its companion data structs) is the single location for every
7 * log::info / log::warn call that describes SCF progress, convergence, eigenvalues, and the
8 * final energy breakdown.
9 *
10 * Design: the reporter is completely decoupled from the iterator type. The iterator
11 * populates a plain data struct and passes it to a static report function. No iterator
12 * header is included here, so there is no circular dependency.
13 */
14
15#include <algorithm>
16#include <cmath>
17#include <cstdint>
18#include <cstdio>
19#include <string>
20#include <vector>
21
22#include "io/logger.hpp"
30
31namespace q3 {
32
33// ─── Data-transfer structs ───────────────────────────────────────────────────
34
35/**
36 * All data needed to produce the post-initialization setup summary log block.
37 */
39 // Crystal geometry
43
44 // Plane-wave basis
56
57 // Electronic structure
61
62 // Method / solver choices
63 std::string xc_functional_name;
64 std::string diagonalizer_name;
65 /// Davidson working basis as a multiple of the band count; 0 when not iterative.
67 std::string mixer_name;
69
70 // Energies
72
73 // Symmetry
77
78 /**
79 * Parallel topology, as it ended up rather than as it was requested. Set
80 * only for MPI runs; number_of_processes stays 1 in serial, which is what
81 * suppresses the block below.
82 */
85 /// Ranks sharing a k-point pool, however that pool divides its work.
88 /// True when those ranks divide the bands rather than the reciprocal-space coefficients.
90};
91
92/**
93 * All data needed to produce the post-convergence report block
94 * (magnetization, eigenvalues, and final energies).
95 *
96 * The `energies` pointer is non-owning. The iterator that owns the energies
97 * must outlive this struct.
98 */
101 /// The reporting rank's pool share under MPI, not the irreducible total.
104 /// Set from the execution policy so the report can say whose k-points it lists.
106
108
109 /// eigenvalues[kpoint_state_index][band_index] in Rydberg.
110 std::vector<std::vector<Real>> eigenvalues;
111 /// k-points in the same order as eigenvalues rows.
112 std::vector<KPoint> kpoints;
113
114 /// Globally integrated spin-density difference in Bohr magnetons per cell.
116 /// Globally integrated absolute spin-density difference in Bohr magnetons per cell.
118};
119
120// ─── Reporter ────────────────────────────────────────────────────────────────
121
122/**
123 * Stateless reporter: all methods are static and write to the global logger.
124 */
126 public:
127 /**
128 * Print the estimated per-rank memory footprint, term by term.
129 *
130 * Printed after the pseudopotential summary rather than with the rest of
131 * the setup block, because the projector count it needs does not exist
132 * until the pseudopotential basis has been built.
133 *
134 * The figures are what the code will allocate, not what it ought to: terms
135 * whose labels say "copy" are redundant today and are named so a reader can
136 * see the difference between the footprint and the requirement.
137 *
138 * A dozen lines of arithmetic nobody reads on a run that fits, so it rides
139 * at debug with the rest of the diagnostics; the measured peak at the end
140 * of the run still prints, and is what a reader who did not ask for this
141 * compares against.
142 */
143 static void report_memory_estimate(const PlanewaveMemoryEstimate& estimate) {
144 log::debug(" Estimated memory per rank (arrays only, replicated when bands "
145 "are distributed):");
146 for (const auto& term : estimate.steady_state) {
148 " {:<38} {:<32} = {}", term.label, term.formula, format_bytes(term.bytes));
149 }
150 log::debug(" {:<38} {:<32} = {}",
151 "steady state",
152 "",
153 format_bytes(estimate.steady_state_bytes));
154 for (const auto& term : estimate.transient) {
155 log::debug(" {:<38} {:<32} = {} (transient)",
156 term.label,
157 term.formula,
158 format_bytes(term.bytes));
159 }
160 log::debug(" {:<38} {:<32} ~ {}",
161 "estimated peak per rank",
162 "",
163 format_bytes(estimate.peak_bytes));
164 /**
165 * This is what the run allocates, which is not the same as what it
166 * keeps resident, and the difference goes both ways. Large transient
167 * blocks freed during setup are commonly retained by the allocator
168 * rather than returned, which pushes the resident set above this
169 * figure. Against that, arrays are sized for the largest basis the
170 * solver may reach, and a run that converges before filling one never
171 * touches all of it. Compare with the measured peak at the end of the
172 * run rather than trusting either number alone.
173 */
174 log::debug(" (arrays as allocated; compare with the measured peak at end of run)");
175 }
176
177 /**
178 * Print the one-time setup summary that appears after initialization.
179 */
181 log::info("");
182 log::info(" Q3 - Plane-wave DFT SCF Solver");
183 log::info(" ================================");
184 log::info("");
185 log::info(" lattice parameter (alat) = {:.4f} a.u.", summary.lattice_parameter);
186 log::info(" cell volume = {:.4f} (a.u.)^3", summary.cell_volume);
187 log::info(" ecutwfc = {:.4f} Ry", summary.wavefunction_energy_cutoff);
188 log::info(" ecutrho = {:.4f} Ry",
190 log::info(" FFT grid (rho) = {} x {} x {}",
194 log::info(" FFT grid (wave) = {} x {} x {}",
198 log::info(" number of G-vectors = {}", summary.number_of_g_vectors);
199 // The irreducible total; the iterator assembles it across pools.
200 log::info(" number of k-points = {}", summary.number_of_kpoints);
201 log::info(" number of atoms = {}", summary.number_of_atoms);
202 if (summary.number_of_spin_components == 2) {
203 log::info(" spin polarization = LSDA (number_of_spin_components=2)");
204 }
205 log::info(" number of electrons = {:.4f}", summary.number_of_electrons);
206 log::info(" number of bands = {}", summary.number_of_bands);
207 /**
208 * How the ranks were actually arranged, and whether G-vectors ended up
209 * distributed. Without this the log cannot distinguish a run that used
210 * band groups from one that silently fell back to replicated G-vectors,
211 * which changes both the performance and which properties are available.
212 */
213 if (summary.number_of_processes > 1) {
214 /**
215 * Ranks per pool is derived rather than taken from band_group_size,
216 * because the replicated-G fallback sets the band group size to 1
217 * (see the basis rebuild in the iterator) while the ranks stay in
218 * their pools. Printing that value made the parenthetical fail to
219 * factor the rank count -- "128 (16 pool(s) x 1 rank(s) per pool)"
220 * -- and contradict the topology line logged at startup. The
221 * G-vector line below is what carries the fallback.
222 */
223 const int ranks_per_pool =
224 (summary.number_of_kpoint_pools > 0)
226 : summary.number_of_processes;
227 log::info(" MPI ranks = {} ({} pool(s) x {} rank(s) per pool)",
228 summary.number_of_processes,
230 ranks_per_pool);
231 if (summary.bands_are_distributed) {
232 /**
233 * Replicated here is the point rather than a fallback: each rank
234 * holds every G-vector precisely so that it can own whole bands,
235 * which is what keeps its transforms local and leaves stress and
236 * export available.
237 */
238 log::info(" G-vector distribution = replicated (bands divided across the "
239 "pool's ranks)");
240 } else if (summary.replicated_plane_wave_fallback_active) {
241 log::info(" G-vector distribution = replicated (fallback: a rank's FFT "
242 "slab held no plane waves; every rank in a pool repeats "
243 "the whole solve)");
244 } else if (summary.band_group_size > 1) {
245 log::info(
246 " G-vector distribution = slab-distributed across the band group");
247 } else {
248 log::info(" G-vector distribution = replicated (one rank per pool)");
249 }
250 }
251 log::info(" threads per process = {}", execution::threading::maximum_threads());
252 log::info(" linear algebra = {}",
253 *linear_algebra_libraries != '\0' ? linear_algebra_libraries : "(not recorded)");
254 log::info("");
255 log::info(" XC functional = {}", summary.xc_functional_name);
256 log::info(" diagonalization = {}", summary.diagonalizer_name);
257 log::info(" mixing mode = {} (beta={:.2f})",
258 summary.mixer_name,
259 summary.mixing_beta);
260 log::info("");
261 log::info(" max PW per k-point = {}", summary.max_plane_waves_per_kpoint);
262 if (summary.davidson_basis_expansion_factor > 0) {
263 log::info(" Davidson basis = {} x {} bands = {} vectors",
265 summary.number_of_bands,
267 }
268 log::info("");
269 log::info(" Ewald energy = {:14.9f} Ry", summary.ewald_energy);
270 log::info("");
271 if (summary.density_symmetrization_active) {
272 log::info(
273 " Density symmetrization = {} shells from {} G-vectors ({} symmetry ops)",
274 summary.number_of_shells,
275 summary.number_of_g_vectors,
277 }
278 }
279
280 /**
281 * Print the first SCF iteration line, and its energy breakdown.
282 *
283 * Only the first line belongs to the SCF trajectory a reader follows; the
284 * four components below it are a one-off diagnostic for a first iteration
285 * that landed somewhere unexpected, so they ride at debug with the rest of
286 * the diagnostics.
287 */
288 static void report_first_iteration(Real total_energy,
289 const PlanewaveKohnShamEnergies& energies) {
290 log::info(" iteration # {:3d} Etot = {:14.9f} Ry", 1, total_energy);
291 log::debug(" band energy = {:.8f}", energies.band_energy);
292 log::debug(" double counting correction = {:.8f}",
294 log::debug(" Hartree energy = {:.8f}", energies.hartree_energy);
295 log::debug(" XC energy = {:.8f}",
297 }
298
299 /**
300 * Print the full post-convergence block: magnetization (if spin-polarized),
301 * eigenvalues (first ≤10 k-point states), and the final energy breakdown.
302 */
304 report_magnetization(summary);
305 report_eigenvalues(summary);
306 report_final_energies(*summary.energies);
307 }
308
309 private:
310 /**
311 * Render a byte count in the unit a reader can compare against a node.
312 *
313 * MiB below a gibibyte, GiB above it: a 54-atom deck and a 432-atom deck
314 * differ by three orders of magnitude and neither reads well in the
315 * other's unit.
316 */
317 static std::string format_bytes(std::uint64_t bytes) {
318 char buffer[32];
319 double gibibytes = static_cast<double>(bytes) / 1073741824.0;
320 if (gibibytes < 1.0) {
321 std::snprintf(buffer, sizeof(buffer), "%8.1f MiB", gibibytes * 1024.0);
322 } else {
323 std::snprintf(buffer, sizeof(buffer), "%8.2f GiB", gibibytes);
324 }
325 return std::string(buffer);
326 }
327
328 static void report_magnetization(const PlanewaveKohnShamSCFConvergenceSummary& summary) {
329 if (summary.number_of_spin_components != 2) {
330 return;
331 }
332 /**
333 * Both of QE's numbers, because the total alone cannot describe an
334 * antiferromagnet: ∫(ρ↑−ρ↓) is zero for a converged AFM state *and*
335 * for one that collapsed to non-magnetic, so reporting only the total
336 * left "did the moments survive?" unanswerable from Q3's own output.
337 * ∫|ρ↑−ρ↓| separates them — chromium's AFM state carries 2.60 Bohr
338 * mag/cell of absolute magnetization at zero total.
339 */
340 log::info("");
341 log::info(" total magnetization = {:.4f} Bohr mag/cell", summary.total_magnetization);
342 log::info(" absolute magnetization = {:.4f} Bohr mag/cell",
343 summary.absolute_magnetization);
344 }
345
346 static void report_eigenvalues(const PlanewaveKohnShamSCFConvergenceSummary& summary) {
347 int number_of_kpoint_states = summary.number_of_kpoints * summary.number_of_spin_components;
348 /**
349 * Every k-point, every time. This used to print min(n, 10) of whatever
350 * the reporting rank happened to hold, so a 60-k-point run showed ten
351 * in serial and its pool's eight under MPI, with the labels restarting
352 * at k_0 in each pool. The iterator now assembles the global grid
353 * before calling this, so the indices below are the irreducible ones
354 * and the count matches the IBZ reduction reported at startup.
355 */
356 int number_of_states_to_print = number_of_kpoint_states;
357 log::section("Eigenvalues (eV), " + std::to_string(number_of_kpoint_states) +
358 " k-point state(s):");
359 for (int kpoint_index_state = 0; kpoint_index_state < number_of_states_to_print;
360 ++kpoint_index_state) {
361 int spin_index = kpoint_index_state / summary.number_of_kpoints;
362 int kpoint_index = kpoint_index_state % summary.number_of_kpoints;
363 std::string spin_label = (summary.number_of_spin_components == 2)
364 ? ((spin_index == 0) ? " (up)" : " (dn)")
365 : "";
366 std::string line = " E_k = ( ";
367 for (int band_index = 0; band_index < summary.number_of_bands; ++band_index) {
368 char buffer[32];
369 std::snprintf(buffer,
370 sizeof(buffer),
371 "%.4f",
372 summary.eigenvalues[kpoint_index_state][band_index] *
374 line += buffer;
375 if (band_index < summary.number_of_bands - 1) {
376 line += ", ";
377 }
378 }
379 line += " )";
380 const KPoint& kpoint = summary.kpoints[kpoint_index];
381 log::info(" k_{} = ({:.4f}, {:.4f}, {:.4f}){}",
382 kpoint_index,
383 kpoint.coordinates.x,
384 kpoint.coordinates.y,
385 kpoint.coordinates.z,
386 spin_label);
387 log::info("{}", line);
388 if (kpoint_index_state + 1 < number_of_states_to_print) {
389 log::info("");
390 }
391 }
392 }
393
394 static void report_final_energies(const PlanewaveKohnShamEnergies& energies) {
395 log::section("Final Energies (Ry):");
396 log::info(" total energy = {:14.9f} Ry", energies.total());
397 log::info(" band energy = {:14.9f}", energies.band_energy);
398 log::info(" Hartree energy = {:14.9f}", energies.hartree_energy);
399 log::info(" XC energy = {:14.9f}", energies.exchange_correlation_energy);
400 if (energies.ewald_energy != 0.0) {
401 log::info(" Ewald energy = {:14.9f}", energies.ewald_energy);
402 }
403 if (energies.dispersion_energy() != 0.0) {
404 log::info(" Dispersion (D3-BJ) = {:14.9f}", energies.dispersion_energy());
405 }
406 if (energies.hubbard_energy_contribution() != 0.0) {
407 log::info(" DFT+U (Hubbard) = {:14.9f}",
408 energies.hubbard_energy_contribution());
409 }
410 /**
411 * Printed with the same label QE uses, deliberately: it is the first
412 * rung of the PAW failure ladder, and a mismatched total is diagnosed
413 * by putting these two lines side by side before anything else.
414 */
415 if (energies.one_center_energy_contribution() != 0.0) {
416 log::info(" one-center paw contrib. = {:14.9f}",
417 energies.one_center_energy_contribution());
418 }
419 if (std::abs(energies.smearing_entropy_contribution) >
421 log::info(" smearing contribution = {:14.9f}",
422 energies.smearing_entropy_contribution);
423 log::info(" E_one_electron + E_TS = {:14.9f}",
424 energies.band_energy + energies.double_counting_correction);
425 }
426 log::info("");
427 }
428};
429
430} // namespace q3
static void report_convergence_summary(const PlanewaveKohnShamSCFConvergenceSummary &summary)
Definition reporter.hpp:303
static void report_first_iteration(Real total_energy, const PlanewaveKohnShamEnergies &energies)
Definition reporter.hpp:288
static void report_memory_estimate(const PlanewaveMemoryEstimate &estimate)
Definition reporter.hpp:143
static void report_setup_summary(const PlanewaveKohnShamSCFSetupSummary &summary)
Definition reporter.hpp:180
constexpr Real RY_TO_EV
void section(const std::string &title)
Definition logger.hpp:206
void debug(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:179
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
constexpr double zero_vector_magnitude_threshold
double Real
constexpr const char * linear_algebra_libraries
The BLAS/LAPACK libraries this build links, as CMake found them; empty if unknown.
Definition version.hpp:38
Real double_counting_correction
Definition init.hpp:16
Real exchange_correlation_energy
Definition init.hpp:18
int number_of_kpoints
The reporting rank's pool share under MPI, not the irreducible total.
Definition reporter.hpp:102
std::vector< std::vector< Real > > eigenvalues
eigenvalues[kpoint_state_index][band_index] in Rydberg.
Definition reporter.hpp:110
const PlanewaveKohnShamEnergies * energies
Definition reporter.hpp:107
Real total_magnetization
Globally integrated spin-density difference in Bohr magnetons per cell.
Definition reporter.hpp:115
Real absolute_magnetization
Globally integrated absolute spin-density difference in Bohr magnetons per cell.
Definition reporter.hpp:117
int number_of_kpoint_pools
Set from the execution policy so the report can say whose k-points it lists.
Definition reporter.hpp:105
std::vector< KPoint > kpoints
k-points in the same order as eigenvalues rows.
Definition reporter.hpp:112
int davidson_basis_expansion_factor
Davidson working basis as a multiple of the band count; 0 when not iterative.
Definition reporter.hpp:66
bool bands_are_distributed
True when those ranks divide the bands rather than the reciprocal-space coefficients.
Definition reporter.hpp:89
int band_group_size
Ranks sharing a k-point pool, however that pool divides its work.
Definition reporter.hpp:86
std::vector< PlanewaveMemoryEstimateTerm > transient
std::vector< PlanewaveMemoryEstimateTerm > steady_state