Loading...
Searching...
No Matches
q3_core_api.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * q3_core_api.hpp — In-process Q3 calculation API for the nanobind module.
5 *
6 * Exposes run_scf() and run_nscf() as free functions that accept a YAML
7 * input file path, run the corresponding flavor in-process, and return a
8 * JSON results string.
9 *
10 * Each call resets the ProfilingCounters singleton so that metrics from
11 * successive invocations do not accumulate.
12 *
13 * Thread safety: NOT thread-safe. The singleton XC functional state and
14 * FFTW plan cache are not protected by mutexes. Callers must serialize
15 * invocations (GIL or external lock).
16 */
17
18#include <cstddef>
19#include <filesystem>
20#include <stdexcept>
21#include <string>
22
23#include "generated/esse/properties/non_scalar/eigenvalues_at_kpoints.hpp"
26#include "io/logger.hpp"
27#include "io/output/results.hpp"
32
33namespace q3 {
34namespace bindings {
35
36/**
37 * Run a self-consistent field calculation and return results as a JSON string.
38 *
39 * Parameters
40 * ----------
41 * input_path:
42 * Path to a Q3 YAML input file with calculation = scf (or equivalent).
43 * write_output_files:
44 * When true (default), also write results.json and checkpoint files to
45 * the outdir specified in the YAML. When false, results are only
46 * returned as the JSON string (useful for in-memory workflows).
47 *
48 * Returns
49 * -------
50 * JSON string with the same schema as results.json.
51 */
52inline std::string run_scf(const std::string& input_path, bool write_output_files = true) {
54
55 InputData input = parse_input(input_path);
56 /**
57 * No command line to defer to in-process, so the deck alone decides -- and
58 * the empty second argument is what makes a "default" deck put the level
59 * back rather than inherit whatever the previous call left it at.
60 */
62 input.calculation = "self_consistent_field";
63
64 generated::Results results = run_flavor(input);
65
66 if (write_output_files) {
67 if (!input.output_directory.empty()) {
68 std::filesystem::create_directories(input.output_directory);
69 }
70 std::string results_path = input.output_directory + "/" + input.prefix + "_results.json";
71 write_results_json(results, results_path);
72 }
73
74 return results_json_string(results);
75}
76
77/**
78 * Run a non-self-consistent field (single-pass diagonalization) and return
79 * results as a JSON string.
80 *
81 * Requires a prior SCF run to have written an effective-potential checkpoint
82 * in <outdir>/<prefix>.save/effective-potential.q3cp.
83 */
84inline std::string run_nscf(const std::string& input_path, bool write_output_files = true) {
86
87 InputData input = parse_input(input_path);
89 input.calculation = "non_self_consistent_field";
90
91 generated::Results results = run_flavor(input);
92
93 if (write_output_files) {
94 if (!input.output_directory.empty()) {
95 std::filesystem::create_directories(input.output_directory);
96 }
97 std::string results_path = input.output_directory + "/" + input.prefix + "_results.json";
98 write_results_json(results, results_path);
99 }
100
101 return results_json_string(results);
102}
103
104/**
105 * Stateful SCF Solver for in-process calculations.
106 *
107 * Caches loaded pseudopotentials to avoid disk I/O between successive
108 * calculations (e.g. during geometry optimization steps).
109 */
111 public:
112 SCFSolver(const SCFSolver&) = delete;
113 SCFSolver& operator=(const SCFSolver&) = delete;
114 SCFSolver(SCFSolver&&) = default;
116
117 explicit SCFSolver(const std::string& input_path) {
118 input_ = parse_input(input_path);
120 patched_species_ = input_.species;
121 pseudopotential_set_ =
122 load_pseudopotentials(patched_species_, input_.pseudopotential_directory);
123 /**
124 * Built once here rather than per solve(): nothing provenance records
125 * varies between steps of a relaxation -- it describes the engine, the
126 * datasets and the requested numerics, never the coordinates -- and
127 * re-reading and re-hashing every dataset on each of a hundred steps
128 * would be pure repetition.
129 */
130 provenance_ = build_provenance_json(input_);
131 }
132
133 /**
134 * Solve the SCF problem for the given atomic coordinates.
135 */
136 std::string solve(const std::vector<std::vector<double>>& coordinates,
137 bool write_output_files = true) {
138 for (std::size_t atom_index = 0;
139 atom_index < coordinates.size() && atom_index < input_.atoms.size();
140 ++atom_index) {
141 if (coordinates[atom_index].size() < 3) {
142 throw std::runtime_error(
143 "SCFSolver::solve: coordinates must have 3 components per atom.");
144 }
145 input_.atoms[atom_index].position.x = coordinates[atom_index][0];
146 input_.atoms[atom_index].position.y = coordinates[atom_index][1];
147 input_.atoms[atom_index].position.z = coordinates[atom_index][2];
148 }
149
150 /**
151 * Shared builders (io/input/system_builder.hpp) — the same crystal and
152 * k-grid construction the iterator factory uses; input semantics must
153 * never drift between the two entry points.
154 */
155 Crystal crystal = build_crystal_from_input(input_, patched_species_);
156
158
159 KPointsGrid kpoint_grid = build_global_kpoint_grid_from_input(input_, crystal);
160
161 KohnShamSCFIteratorConfig scf_config;
162 scf_config.crystal_structure = nullptr;
163 scf_config.pseudopotential_set = &pseudopotential_set_;
166 scf_config.maximum_iterations = input_.maximum_iterations;
168 scf_config.mixing_mode = input_.mixing_mode;
169 scf_config.mixing_beta = input_.mixing_beta;
170 scf_config.mixing_history_size = input_.mixing_history_size;
172 scf_config.mixing_q0 = input_.mixing_q0;
173 scf_config.smearing = input_.smearing;
174 scf_config.smearing_width = input_.smearing_width;
178 scf_config.number_of_bands = input_.number_of_bands;
180 scf_config.calculation = input_.calculation;
183 scf_config.band_path = input_.band_path;
186
187 auto iterator = create_planewave_pseudopotential_iterator(std::move(scf_config));
188
189 iterator->set_owned_crystal(std::move(crystal));
190 iterator->set_owned_kpoint_grid(std::move(kpoint_grid));
191 iterator->set_owned_pseudopotential_set(pseudopotential_set_.clone());
192
193 if (!last_charge_density_.empty()) {
194 log::debug("SCFSolver solve: Injecting cached charge density.");
195 iterator->load_charge_density(last_charge_density_);
196 } else {
197 log::debug("SCFSolver solve: No cached charge density available.");
198 }
199 if (last_quantum_system_ != nullptr) {
200 log::debug("SCFSolver solve: Injecting cached quantum system.");
201 iterator->load_quantum_system(*last_quantum_system_);
202 } else {
203 log::debug("SCFSolver solve: No cached quantum system available.");
204 }
205
207 iterator->run();
208
209 const bool converged = iterator->is_converged();
210 auto scf_state = iterator->result();
211
212 if (converged) {
214 "SCFSolver solve: SCF converged. Caching charge density and quantum states.");
215 last_charge_density_ = scf_state.charge_density_real;
216 last_quantum_system_ =
217 std::make_unique<PlanewaveKohnShamQuantumSystem>(iterator->quantum_system());
218 }
219
220 generated::Results results = new_results("self_consistent_field");
221 results.runtime_log.converged = converged;
222 results.runtime_log.self_consistent_field_iterations = iterator->iteration();
223 if (!provenance_.is_null()) {
224 results.provenance = provenance_;
225 }
226
227 /**
228 * The same quantities SelfConsistentFieldFlavor lists, filled by the
229 * same helpers from the same accessors, so the bindings and the
230 * command line cannot disagree about what a run reported.
231 */
232 const auto& energies = scf_state.energies();
233 set_energy_records(results, energies.total(), energies);
234 set_fermi_energy(results, iterator->quantum_system().fermi_energy());
235 SelfConsistentFieldFlavor::record_convergence(results, iterator->scf_history());
236 SelfConsistentFieldFlavor::record_magnetization(results, iterator->magnetization());
238 results, iterator->hubbard_occupation_traces(), scf_state.hubbard_manifolds);
239
240 if (converged) {
241 auto [total_forces, forces_contributions] = ForceCalculator::compute_forces(
242 scf_state, input_.dispersion_correction, input_.disable_symmetry);
243 SelfConsistentFieldFlavor::record_forces(results, total_forces, forces_contributions);
244
245 auto [stress_total, stress_contributions] =
246 compute_stress(scf_state,
249 input_.disable_symmetry);
250 SelfConsistentFieldFlavor::record_stress(results, stress_total, stress_contributions);
251 }
252
253 const auto& eigenvalue_matrix = iterator->eigenvalues();
254 int number_of_kpoints = eigenvalue_matrix.rows();
255 int number_of_bands = eigenvalue_matrix.cols();
256 /**
257 * kpoint_grid() holds one entry per PHYSICAL k-point; eigenvalue_matrix is
258 * indexed by k-point STATE (physical kpoint * spin) -- see the identical
259 * convention in iterator/kohn_sham/planewave/execution/kpoint_states.hpp.
260 */
261 int number_of_physical_kpoints = iterator->kpoint_grid().number_of_kpoints();
262
263 generated::EigenvaluesAtKpoints eigenvalues;
264 eigenvalues.name = generated::EigenvaluesAtKpointsNameEnum::EIGENVALUES_AT_KPOINTS;
265 eigenvalues.units = generated::EigenvaluesAtKpointsUnitsEnum::RY;
266 for (int kpoint_index = 0; kpoint_index < number_of_kpoints; kpoint_index++) {
267 generated::EigenvaluesAtKpointsBandEnergies channel;
268 for (int band_index = 0; band_index < number_of_bands; band_index++) {
269 channel.energies.push_back(eigenvalue_matrix(kpoint_index, band_index));
270 }
271 generated::EigenvaluesAtKpointsEigenvaluesAtKPoint kpoint;
272 kpoint.weight =
273 iterator->kpoint_grid()[kpoint_index % number_of_physical_kpoints].weight;
274 kpoint.eigenvalues = {channel};
275 eigenvalues.values.push_back(kpoint);
276 }
277 set_record(results, eigenvalues);
278 add_band_edge_records(results);
279
280 if (write_output_files) {
281 if (!input_.output_directory.empty()) {
282 std::filesystem::create_directories(input_.output_directory);
283 }
284 std::string results_path =
285 input_.output_directory + "/" + input_.prefix + "_results.json";
286 write_results_json(results, results_path);
287 }
288
289 return results_json_string(results);
290 }
291
292 private:
293 InputData input_;
294 std::vector<AtomSpecies> patched_species_;
295 PseudopotentialSet pseudopotential_set_;
296 nlohmann::json provenance_;
297 std::vector<std::vector<Real>> last_charge_density_;
298 std::unique_ptr<PlanewaveKohnShamQuantumSystem> last_quantum_system_;
299};
300
301} // namespace bindings
302} // namespace q3
PseudopotentialSet clone() const
Deep-clone the entire set (preserves derived types).
Definition set.hpp:139
static void record_stress(generated::Results &results, const StressTensorTotal &total, const StressTensorContributions &contributions)
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)
static void record_magnetization(generated::Results &results, const std::optional< Magnetization > &magnetization)
static std::string find_spacegroup(const Crystal &crystal, Real symmetry_precision=math::tolerances::default_symmetry_precision)
Definition symmetry.hpp:73
SCFSolver(const std::string &input_path)
std::string solve(const std::vector< std::vector< double > > &coordinates, bool write_output_files=true)
SCFSolver(SCFSolver &&)=default
SCFSolver & operator=(SCFSolver &&)=default
SCFSolver & operator=(const SCFSolver &)=delete
SCFSolver(const SCFSolver &)=delete
std::string run_nscf(const std::string &input_path, bool write_output_files=true)
std::string run_scf(const std::string &input_path, bool write_output_files=true)
void debug(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:179
void apply_verbosity(const std::string &verbosity, const std::string &explicit_log_level)
Definition logger.hpp:113
PseudopotentialSet load_pseudopotentials(std::vector< AtomSpecies > &species, const std::string &pseudopotential_directory)
Definition loader.hpp:191
Crystal build_crystal_from_input(const InputData &input, const std::vector< AtomSpecies > &species)
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 set_fermi_energy(generated::Results &results, Real fermi_energy_in_ry)
ESSE's fermi_energy record, in rydberg.
Definition results.hpp:174
void write_results_json(const generated::Results &results, const std::string &output_path)
Write the results to output_path as results.json.
Definition results.hpp:191
InputData parse_input(const std::string &filename, const std::string &format="")
Definition factory.hpp:122
std::string results_json_string(const generated::Results &results)
Definition results.hpp:186
void add_band_edge_records(generated::Results &results)
generated::Results run_flavor(const InputData &input)
Definition factory.hpp:109
std::unique_ptr< PlanewavePseudopotentialKohnShamSCFIterator > create_planewave_pseudopotential_iterator(KohnShamSCFIteratorConfig config)
Definition factory.hpp:32
nlohmann::json build_provenance_json(const InputData &input)
Build the provenance block for a completed run, from the input it consumed.
Definition factory.hpp:44
void set_energy_records(generated::Results &results, Real total_energy_in_ry, const PlanewaveKohnShamEnergies &energies)
Definition results.hpp:135
KPointsGrid build_global_kpoint_grid_from_input(const InputData &input, const Crystal &crystal)
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::vector< std::pair< std::string, Vector3D > > band_path
Definition data.hpp:117
std::string smearing
Definition data.hpp:75
std::string diagonalization_method
Definition data.hpp:113
Real mixing_beta
Definition data.hpp:59
Real charge_density_energy_cutoff
Definition data.hpp:111
std::vector< AtomSpecies > species
Definition data.hpp:153
std::string prefix
Definition data.hpp:178
int maximum_iterations
Definition data.hpp:33
std::vector< Atom > atoms
Definition data.hpp:156
std::string verbosity
Definition data.hpp:186
bool disable_symmetry
Definition data.hpp:192
std::string calculation
Definition data.hpp:116
int band_path_points_per_segment
Definition data.hpp:118
int methfessel_paxton_order
Definition data.hpp:77
int mixing_history_size
Definition data.hpp:60
Real mixing_q0
Definition data.hpp:74
std::string mixing_preconditioning
Definition data.hpp:73
std::string mixing_mode
Definition data.hpp:58
Real convergence_threshold
Definition data.hpp:32
std::vector< Real > atomic_starting_magnetization
Definition data.hpp:57
std::string pseudopotential_directory
Definition data.hpp:163
Real smearing_width
Definition data.hpp:76
int number_of_bands
Definition data.hpp:112
Real wavefunction_energy_cutoff
Definition data.hpp:110
std::string exchange_correlation_functional
Definition data.hpp:41
std::string nonlocal_stress_derivative_method
Definition data.hpp:200
std::vector< Real > starting_magnetization
Definition data.hpp:49
std::string output_directory
Definition data.hpp:177
int number_of_spin_components
Definition data.hpp:42
std::vector< Real > starting_magnetization
Definition init.hpp:98
std::vector< std::pair< std::string, Vector3D > > band_path
Definition init.hpp:105
std::vector< Real > atomic_starting_magnetization
Definition init.hpp:104
std::string mixing_preconditioning
Definition init.hpp:46
std::string exchange_correlation_functional
Definition init.hpp:42
std::string nonlocal_stress_derivative_method
Definition init.hpp:107
std::string diagonalization_method
Definition init.hpp:69
const PseudopotentialSet * pseudopotential_set
Definition init.hpp:92
static ProfilingCounters & instance()
Definition counters.hpp:220
Crystal * crystal_structure
Definition config.hpp:16