Loading...
Searching...
No Matches
self_consistent_field.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * SelfConsistentFieldFlavor — SCF calculation flavor.
5 *
6 * Runs a full self-consistent field calculation: builds the iterator,
7 * iterates to convergence, computes Hellmann-Feynman forces and stress tensor,
8 * writes binary checkpoints when the execution topology owns complete state,
9 * and returns its results as results.json's own struct.
10 *
11 * The Python layer reads results.json and is responsible for any further
12 * per-property formatting or output. No per-property JSON files are written
13 * from C++ anymore.
14 */
15
16#include <chrono>
17#include <cstddef>
18#include <map>
19#include <optional>
20#include <stdexcept>
21#include <string>
22#include <vector>
23
24#include "generated/esse/properties/scalar/magnetization_absolute.hpp"
25#include "generated/esse/properties/scalar/magnetization_total.hpp"
26#include "generated/esse/properties/scalar/pressure.hpp"
27#include "generated/esse/properties/workflow/convergence_electronic.hpp"
31#include "io/output/results.hpp"
41
42namespace q3 {
43
45 public:
46 /**
47 * `iterator_factory` carries the entry point's execution policy (see
48 * IteratorFactory); empty runs serially on the CPU.
49 */
50 explicit SelfConsistentFieldFlavor(IteratorFactory iterator_factory = {})
51 : iterator_factory_(std::move(iterator_factory)) {}
52
53 protected:
54 generated::Results run_calculation(const InputData& input) override {
55 std::unique_ptr<PlanewaveKohnShamSCFIterator> owned_iterator =
56 create_iterator(input, iterator_factory_);
57 PlanewaveKohnShamSCFIterator* iterator = owned_iterator.get();
58 auto* planewave_iterator =
59 dynamic_cast<PlanewavePseudopotentialKohnShamSCFIterator*>(iterator);
60 if (!planewave_iterator) {
61 throw std::runtime_error(
62 "SelfConsistentFieldFlavor: expected a planewave pseudopotential iterator.");
63 }
65 offer_saved_charge_density(*iterator, input);
66 offer_saved_wavefunctions(*planewave_iterator, input);
67 }
68 iterator->run();
69
70 const bool converged = iterator->is_converged();
71 auto scf_state = iterator->result();
72
73 // Write binary checkpoints.
74 planewave_iterator->write_checkpoints(input.output_directory, input.prefix);
75
76 /**
77 * ─── Mean-field export (BerkeleyGW, etc.) ──────────────────────────────
78 * Format selection lives in the factory ("none" resolves to the Null
79 * object), so there is no branching on the format name here -- see
80 * io/mean_field_export/init.hpp. BerkeleyGW normally consumes an NSCF
81 * run with many empty (conduction) bands rather than the SCF run
82 * itself (see non_self_consistent_field.hpp), but the export is wired
83 * into both flavors so either can be used directly. Which rank (if
84 * any) writes is decided by the execution topology -- see
85 * export_and_record_mean_field_under_topology()'s comment. The exported
86 * Vxc is the valence one, not the state's own, which includes any core
87 * charge -- see valence_exchange_correlation_potential_g_of().
88 */
90 planewave_iterator->execution_policy(),
91 [&scf_state, &input, planewave_iterator] {
92 auto record = build_mean_field_record(scf_state);
93 record.exchange_correlation_potential_g =
94 planewave_iterator->valence_exchange_correlation_potential_g_of(
95 scf_state.charge_density_real);
96 if (input.mean_field_export_kleinman_bylander_projectors)
97 add_kleinman_bylander_projectors_to_mean_field_record(record, scf_state);
98 return record;
99 },
100 input,
101 input.output_directory);
102
103 // ─── Results ──────────────────────────────────────────────────────────
104 generated::Results results = new_results("self_consistent_field");
105 results.runtime_log.converged = converged;
106 results.runtime_log.self_consistent_field_iterations = iterator->iteration();
107 /**
108 * Called on every rank, not just the writing one: the memory reduction
109 * inside is collective, and guarding it behind the root would hang the
110 * others.
111 */
112 results.runtime_log.execution = iterator->execution_policy().execution_record();
113 /**
114 * Carry the timing breakdown into the result, so a measurement does not
115 * have to be scraped back out of the log. Counts go alongside the
116 * stage times: a stage that got slower because it ran more often is a
117 * different finding from one that got slower per call, and the times
118 * alone cannot tell them apart.
119 */
120 {
121 const auto& counters = ProfilingCounters::instance();
122 nlohmann::json timings = counters.davidson_timing.as_map();
123 timings["fft_calls"] = static_cast<double>(counters.fft_call_count.load());
124 timings["davidson_iterations"] =
125 static_cast<double>(counters.davidson_iteration_count.load());
126 timings["hamiltonian_applications"] =
127 static_cast<double>(counters.hamiltonian_application_count.load());
128 if (!timings.empty()) {
129 results.runtime_log.timings_ms = timings;
130 }
131 /**
132 * total_solve_calls is deliberately not emitted: it is declared in
133 * DavidsonTimingAccumulator but never incremented anywhere, so it
134 * would publish a constant zero that reads as "no solves happened".
135 */
136 }
137 const auto& energies = scf_state.energies();
138 set_energy_records(results, energies.total(), energies);
139 set_fermi_energy(results, planewave_iterator->quantum_system().fermi_energy());
140 record_convergence(results, iterator->scf_history());
141
142 /**
143 * Magnetization and the DFT+U occupation traces come from the iterator
144 * rather than being re-derived here: both are integrated over state the
145 * SCF releases a few lines below, and both are already printed, so a
146 * second derivation would be a second definition of the same number.
147 */
148 record_magnetization(results, planewave_iterator->magnetization());
150 results, planewave_iterator->hubbard_occupation_traces(), scf_state.hubbard_manifolds);
151
152 /**
153 * ─── Isolated-system correction (if SCF converged) ─────────────────────
154 * Scheme selection lives in the factory ("none" resolves to the Null
155 * object), so there is no branching on the scheme name here -- see
156 * isolated_system_correction/planewave/init.hpp.
157 */
158 if (converged) {
159 auto correction_calculator =
161 auto correction = correction_calculator->compute(scf_state);
162 if (correction.applied) {
163 generated::ResultsIsolatedSystemCorrection block;
164 block.method = correction.method;
165 for (const auto& [label, value_in_ry] : correction.terms_in_ry) {
166 block.terms[label] = energy(label, value_in_ry);
167 }
168 block.total = energy("total", correction.total_in_ry());
169 block.corrected_total_energy =
170 energy("corrected_total_energy", energies.total() + correction.total_in_ry());
171 results.properties.isolated_system_correction = block;
172 log::info("{} isolated-system correction:", correction.method);
173 for (const auto& [label, value] : correction.terms_in_ry) {
174 log::info(" {:<24} = {:.8f} Ry", label, value);
175 }
176 log::info(" {:<24} = {:.8f} Ry",
177 "corrected total energy",
178 energies.total() + correction.total_in_ry());
179 }
180 }
181
182 /**
183 * The SCF is over: drop its working basis, projector cache and mixing
184 * history before the properties allocate their own, so that the run's
185 * peak is the SCF's rather than the SCF's with the forces' on top.
186 */
187 planewave_iterator->release_scf_workspace();
188
189 // ─── Forces (if SCF converged) ────────────────────────────────────────
190 if (converged) {
191 auto timer_forces_start = std::chrono::high_resolution_clock::now();
192 auto [total_forces, forces_contributions] = ForceCalculator::compute_forces(
193 scf_state, input.dispersion_correction, input.disable_symmetry);
194 auto timer_forces_end = std::chrono::high_resolution_clock::now();
195
196 record_forces(results, total_forces, forces_contributions);
197
198 /**
199 * Record the derivative terms Q3 knows it left out, in the result
200 * rather than only in the log. A relaxation driven by forces with
201 * the Hubbard term missing converges to the wrong geometry without
202 * anything in its output saying so; the Python relax and vc-relax
203 * workflows read these and stop.
204 *
205 * Atomic-projector and ortho-atomic force and stress include their complete
206 * orbital, overlap-operator, and cell-wide Lowdin-transform derivatives for
207 * every dataset family. Keep this metadata guard for future projector types.
208 */
211 results,
212 "no DFT+U stress term is implemented for Hubbard projector type '" +
213 scf_state.hubbard_projector_type + "'");
214 }
215
216 /**
217 * ─── Stress tensor ────────────────────────────────────────────────
218 * Stress computation is not yet G-vector-distribution-aware.
219 * Skip when band_size > 1 to avoid out-of-bounds access on
220 * distributed wavefunctions/projectors.
221 */
222 bool skip_stress = (iterator->execution_policy().band_size() > 1);
223 auto timer_stress_start = std::chrono::high_resolution_clock::now();
224 if (!skip_stress) {
225 auto [stress_total, stress_contributions] =
226 compute_stress(scf_state,
229 input.disable_symmetry);
230
231 record_stress(results, stress_total, stress_contributions);
232 } else {
233 log::warn(
234 " Stress tensor skipped (not yet supported with G-vector distribution)");
236 results,
237 "stress is not implemented when wavefunctions are distributed across an "
238 "MPI band group, so the reported zero tensor is not a physical result");
239 }
240 auto timer_stress_end = std::chrono::high_resolution_clock::now();
241
242 log::debug(" Force/Stress timing:");
244 " forces: {:.1f} ms",
245 std::chrono::duration<double, std::milli>(timer_forces_end - timer_forces_start)
246 .count());
248 " stress: {:.1f} ms",
249 std::chrono::duration<double, std::milli>(timer_stress_end - timer_stress_start)
250 .count());
251 } else {
252 log::warn(
253 "SelfConsistentFieldFlavor: skipping forces and stress — SCF did not converge");
254 }
255
256 // ─── Eigenvalues ──────────────────────────────────────────────────────
257 set_record(results, assemble_global_eigenvalues_at_kpoints(*planewave_iterator));
258
259 return results;
260 }
261
262 public:
263 /**
264 * ─── The SCF's results, shared with the bindings' SCFSolver ──────────────
265 *
266 * The bindings run the same calculation without this flavor, so what an SCF
267 * reports is filled here once, and both report it the same way.
268 */
269
270 /**
271 * The total energy after each iteration, as ESSE's convergence_electronic
272 * record (one row: this run's only ionic step), and the density residual
273 * after each, as runtime_log.density_residuals.
274 */
275 template <typename History>
276 static void record_convergence(generated::Results& results, const History& history) {
277 if (history.empty()) {
278 return;
279 }
280 std::vector<double> energies_in_ry;
281 std::vector<double> density_residuals;
282 for (const auto& iteration : history) {
283 energies_in_ry.push_back(iteration.total_energy_in_ry);
284 density_residuals.push_back(iteration.density_residual);
285 }
286 set_record(results,
287 generated::ConvergenceElectronic{
288 generated::ConvergenceElectronicNameEnum::CONVERGENCE_ELECTRONIC,
289 generated::ConvergenceElectronicUnitsEnum::RY,
290 {energies_in_ry}});
291 results.runtime_log.density_residuals = density_residuals;
292 }
293
294 /**
295 * Both of the numbers the convergence report prints, ∫(ρ↑−ρ↓) and
296 * ∫|ρ↑−ρ↓|, as ESSE's magnetization_total and magnetization_absolute
297 * records. The total alone cannot describe an antiferromagnet -- it is zero
298 * both for a converged AFM state and for one that collapsed to
299 * non-magnetic -- so both are listed, and neither for a run without them
300 * (spin-unpolarized, or one that never reached the convergence block):
301 * absent rather than a zero that reads as a collapsed moment.
302 */
303 static void record_magnetization(generated::Results& results,
304 const std::optional<Magnetization>& magnetization) {
305 if (!magnetization) {
306 return;
307 }
308 set_record(results,
309 generated::MagnetizationTotal{
310 generated::MagnetizationTotalNameEnum::MAGNETIZATION_TOTAL,
311 generated::MagnetizationTotalUnitsEnum::UB,
312 magnetization->total_in_bohr_magneton});
313 set_record(results,
314 generated::MagnetizationAbsolute{
315 generated::MagnetizationAbsoluteNameEnum::MAGNETIZATION_ABSOLUTE,
316 generated::MagnetizationAbsoluteUnitsEnum::UB,
317 magnetization->absolute_in_bohr_magneton});
318 }
319
320 /**
321 * Tr[n] per Hubbard manifold, per spin channel: the number of electrons the
322 * manifold's projectors see, and the first thing to look at when a DFT+U
323 * result looks wrong. Nothing when the run has no Hubbard manifolds.
324 * `atom_index` is 0-based, matching the orbital projections' labels.
325 */
326 template <typename Manifolds>
327 static void record_hubbard_occupations(generated::Results& results,
328 const std::vector<std::vector<Real>>& traces,
329 const Manifolds& manifolds) {
330 std::vector<generated::ResultsPropertiesHubbardOccupationsItem> occupations;
331 for (std::size_t manifold_index = 0;
332 manifold_index < traces.size() && manifold_index < manifolds.size();
333 ++manifold_index) {
334 occupations.push_back({manifolds[manifold_index].atom_index,
335 std::vector<double>(traces[manifold_index].begin(),
336 traces[manifold_index].end())});
337 }
338 if (!occupations.empty()) {
339 results.properties.hubbard_occupations = occupations;
340 }
341 }
342
343 /// ESSE's atomic_forces record, and beside it the share of each mechanism, in Ry/bohr.
344 static void record_forces(generated::Results& results,
345 const std::vector<Vector3D>& total_forces,
346 const AtomicForcesContributions& contributions) {
347 set_record(results, atomic_forces(total_forces));
348 results.properties.force_contributions = std::map<std::string, generated::AtomicForces>{
349 {"local", atomic_forces(contributions.local)},
350 {"ewald", atomic_forces(contributions.ewald)},
351 {"nonlocal", atomic_forces(contributions.nonlocal)},
352 {"core_correction", atomic_forces(contributions.core_correction)},
353 {"augmentation", atomic_forces(contributions.augmentation)},
354 {"dispersion", atomic_forces(contributions.dispersion)},
355 {"hubbard", atomic_forces(contributions.hubbard)},
356 };
357 }
358
359 /**
360 * ESSE's stress_tensor and pressure records, and beside them the share of
361 * each mechanism, in kbar. Listed only by a run that computed the stress,
362 * so absent is never read as a measured zero.
363 */
364 static void record_stress(generated::Results& results,
365 const StressTensorTotal& total,
366 const StressTensorContributions& contributions) {
367 set_record(results, stress_tensor(total.values));
368 set_record(results,
369 generated::Pressure{generated::PressureNameEnum::PRESSURE,
370 generated::PressureUnitsEnum::KBAR,
371 total.pressure});
372 results.properties.stress_contributions = std::map<std::string, generated::StressTensor>{
373 {"kinetic", stress_tensor(contributions.kinetic)},
374 {"hartree", stress_tensor(contributions.hartree)},
375 {"exchange_correlation", stress_tensor(contributions.exchange_correlation)},
376 {"ewald", stress_tensor(contributions.ewald)},
377 {"local_pseudopotential", stress_tensor(contributions.local)},
378 {"nonlocal_pseudopotential", stress_tensor(contributions.nonlocal)},
379 {"core_correction", stress_tensor(contributions.core_correction)},
380 {"augmentation", stress_tensor(contributions.augmentation)},
381 {"hubbard", stress_tensor(contributions.hubbard)},
382 {"dispersion", stress_tensor(contributions.dispersion)},
383 };
384 }
385
386 /**
387 * Name a term the stress leaves out, in runtime_log.incomplete, rather than
388 * only in the log: a variable-cell relaxation driven by a stress with a
389 * term missing converges to the wrong cell without anything in its output
390 * saying so, and the Python vc-relax workflow reads this and stops.
391 */
392 static void record_incomplete_stress(generated::Results& results, const std::string& reason) {
393 auto& incomplete = results.runtime_log.incomplete;
394 if (!incomplete) {
395 incomplete.emplace();
396 }
397 incomplete->stress = incomplete->stress ? *incomplete->stress + "; " + reason : reason;
398 }
399
400 private:
401 /**
402 * Offer the density a previous run saved under this output directory and
403 * prefix (startingpot = 'file'), as each geometry of a relaxation starts
404 * from the last one's. A missing or unreadable save starts from
405 * overlapping atoms, as pw.x does.
406 */
407 static void offer_saved_charge_density(PlanewaveKohnShamSCFIterator& iterator,
408 const InputData& input) {
409 const std::string checkpoint_path =
410 input.output_directory + "/" + input.prefix + ".save/effective-potential.q3cp";
413 if (!checkpoint.load(checkpoint_path, snapshot) || snapshot.charge_density_real.empty()) {
414 log::info(" No saved density at '{}'; starting from overlapping atoms",
415 checkpoint_path);
416 return;
417 }
418 log::info(" Starting from the density saved at '{}'", checkpoint_path);
419 iterator.offer_starting_charge_density(std::move(snapshot.charge_density_real));
420 }
421
422 /**
423 * Offer the bands the same previous run saved (pw.x keeps the last ionic
424 * step's wavefunctions as the next one's guess). Without them every
425 * geometry of a relaxation starts the eigensolver from the atomic +
426 * random guess -- for a dataset with no atomic orbitals, from random
427 * vectors alone. The iterator takes each k-point's file only if it fits.
428 */
429 static void offer_saved_wavefunctions(PlanewavePseudopotentialKohnShamSCFIterator& iterator,
430 const InputData& input) {
431 iterator.offer_starting_wavefunctions(input.output_directory + "/" + input.prefix +
432 ".save");
433 }
434
435 IteratorFactory iterator_factory_;
436};
437
438} // namespace q3
static bool supports(const NormConservingPlanewaveKohnShamSCFState &scf_state)
Definition hubbard.hpp:119
void offer_starting_charge_density(std::vector< std::vector< Real > > charge_density)
Definition init.hpp:249
const std::vector< ScfIterationRecord > & scf_history() const
Definition init.hpp:180
const execution::PlanewaveExecutionPolicy & execution_policy() const
Definition init.hpp:119
virtual NormConservingPlanewaveKohnShamSCFState result() const =0
bool load(const std::string &filename, KohnShamQuantumSystemSnapshot &snapshot) const override
Definition q3.hpp:56
void run()
Definition init.hpp:82
int iteration() const
Definition init.hpp:39
bool is_converged() const
Definition init.hpp:42
static void record_stress(generated::Results &results, const StressTensorTotal &total, const StressTensorContributions &contributions)
generated::Results run_calculation(const InputData &input) override
static void record_incomplete_stress(generated::Results &results, const std::string &reason)
static void record_convergence(generated::Results &results, const History &history)
static void record_forces(generated::Results &results, const std::vector< Vector3D > &total_forces, const AtomicForcesContributions &contributions)
ESSE's atomic_forces record, and beside it the share of each mechanism, in Ry/bohr.
static void record_hubbard_occupations(generated::Results &results, const std::vector< std::vector< Real > > &traces, const Manifolds &manifolds)
SelfConsistentFieldFlavor(IteratorFactory iterator_factory={})
static void record_magnetization(generated::Results &results, const std::optional< Magnetization > &magnetization)
generated::ResultsExecution execution_record() const
Definition init.hpp:258
void debug(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:179
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
generated::EigenvaluesAtKpoints assemble_global_eigenvalues_at_kpoints(const PlanewavePseudopotentialKohnShamSCFIterator &iterator)
void set_record(generated::Results &results, const Record &record)
List record in the results, in place of any record of the same name.
Definition results.hpp:75
generated::Results new_results(const std::string &flavor)
Definition results.hpp:46
std::pair< StressTensorTotal, StressTensorContributions > compute_stress(const NormConservingPlanewaveKohnShamSCFState &scf_state, const std::string &nonlocal_stress_derivative_method="analytical", const std::string &dispersion_correction="none", bool disable_symmetry=false)
Definition init.hpp:211
void export_and_record_mean_field_under_topology(const execution::PlanewaveExecutionPolicy &execution_policy, RecordBuilder &&build_record, const InputData &input, const std::string &directory)
generated::AtomicForces atomic_forces(const std::vector< Vector3D > &values_in_ry_per_bohr)
Forces in Ry/bohr as ESSE's atomic_forces, atoms by 1-based id: the total or one contribution.
Definition results.hpp:109
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
void set_fermi_energy(generated::Results &results, Real fermi_energy_in_ry)
ESSE's fermi_energy record, in rydberg.
Definition results.hpp:174
std::unique_ptr< IsolatedSystemCorrectionCalculator > create_isolated_system_correction_calculator(const std::string &isolated_system_correction)
Definition factory.hpp:36
std::function< std::unique_ptr< PlanewaveKohnShamSCFIterator >(const InputData &)> IteratorFactory
Definition factory.hpp:59
generated::StressTensor stress_tensor(const Real(&values_in_kbar)[3][3])
A stress in kbar as ESSE's stress_tensor: the total or one contribution to it.
Definition results.hpp:121
std::unique_ptr< PlanewaveKohnShamSCFIterator > create_iterator(const InputData &input)
Definition factory.hpp:37
void set_energy_records(generated::Results &results, Real total_energy_in_ry, const PlanewaveKohnShamEnergies &energies)
Definition results.hpp:135
Per-contribution force breakdown (Ry/bohr).
std::vector< Vector3D > nonlocal
std::vector< Vector3D > local
std::vector< Vector3D > augmentation
std::vector< Vector3D > dispersion
std::vector< Vector3D > hubbard
std::vector< Vector3D > core_correction
std::vector< Vector3D > ewald
static std::pair< std::vector< Vector3D >, AtomicForcesContributions > compute_forces(const NormConservingPlanewaveKohnShamSCFState &scf_state, const std::string &dispersion_correction="none", bool disable_symmetry=false)
Definition init.hpp:57
Parsed input data — holds raw values read from input files.
Definition data.hpp:27
std::string dispersion_correction
Definition data.hpp:78
std::string prefix
Definition data.hpp:178
bool disable_symmetry
Definition data.hpp:192
std::string isolated_system_correction
Definition data.hpp:209
std::string nonlocal_stress_derivative_method
Definition data.hpp:200
bool start_from_saved_charge_density
Definition data.hpp:260
std::string output_directory
Definition data.hpp:177
std::vector< std::vector< Real > > charge_density_real
Definition snapshot.hpp:26
static ProfilingCounters & instance()
Definition counters.hpp:220
Per-contribution stress tensor breakdown (kbar after conversion).