Loading...
Searching...
No Matches
yaml.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * YAMLInputParser — YAML/JSON manifest file parser.
5 *
6 * Reads ESSE-aligned YAML input files with optional include_yaml() directives.
7 * Supports both inline material definitions and references to external JSON files.
8 */
9
10#include <algorithm>
11#include <array>
12#include <cmath>
13#include <filesystem>
14#include <fstream>
15#include <map>
16#include <nlohmann/json.hpp>
17#include <stdexcept>
18#include <string>
19#include <vector>
20#include <yaml-cpp/yaml.h>
21
22#include "generated/q3/input.hpp"
24#include "io/input/utils.hpp"
25#include "io/logger.hpp"
27
28namespace q3 {
29
31 public:
32 InputData parse(const std::string& filename) const override {
33 InputData data;
34
35 std::string manifest_directory = std::filesystem::path(filename).parent_path().string();
36 if (manifest_directory.empty())
37 manifest_directory = ".";
38
39 YAML::Node root = YAML::LoadFile(filename);
40
41 resolve_includes(root, manifest_directory);
42
43 reject_unknown_keys(root, "");
44
45 if (!root["material"])
46 throw std::runtime_error("input.yml must contain 'material' key");
47
48 const auto& material_node = root["material"];
49 if (material_node.IsScalar()) {
50 std::string material_path =
51 detail::resolve_path(manifest_directory, material_node.as<std::string>());
52 nlohmann::json material_json = detail::read_json_file(material_path);
53 parse_material(material_json, data);
54 } else if (material_node.IsMap()) {
55 nlohmann::json material_json;
56
57 if (material_node["lattice"]) {
58 nlohmann::json lattice;
59 const auto& lattice_node = material_node["lattice"];
60 if (lattice_node["type"])
61 lattice["type"] = lattice_node["type"].as<std::string>();
62 for (const char* length_key : {"a", "b", "c", "alpha", "beta", "gamma"}) {
63 if (lattice_node[length_key])
64 lattice[length_key] = lattice_node[length_key].as<Real>();
65 }
66 if (lattice_node["units"]) {
67 // units may be a plain string or {length: ..., angle: ...}.
68 if (lattice_node["units"].IsMap()) {
69 nlohmann::json units_json;
70 if (lattice_node["units"]["length"])
71 units_json["length"] =
72 lattice_node["units"]["length"].as<std::string>();
73 if (lattice_node["units"]["angle"])
74 units_json["angle"] = lattice_node["units"]["angle"].as<std::string>();
75 lattice["units"] = units_json;
76 } else {
77 lattice["units"] = lattice_node["units"].as<std::string>();
78 }
79 }
80 if (lattice_node["vectors"]) {
81 nlohmann::json vectors;
82 const auto& vectors_node = lattice_node["vectors"];
83 for (const char* row_key : {"a", "b", "c"}) {
84 if (vectors_node[row_key]) {
85 nlohmann::json row = nlohmann::json::array();
86 for (const auto& value : vectors_node[row_key])
87 row.push_back(value.as<Real>());
88 vectors[row_key] = row;
89 }
90 }
91 if (vectors_node["alat"])
92 vectors["alat"] = vectors_node["alat"].as<Real>();
93 if (vectors_node["units"]) {
94 /**
95 * Same string-or-{length: ...} forms as the lattice-level
96 * units key, so inline materials parse identically to
97 * file-referenced material JSON.
98 */
99 if (vectors_node["units"].IsMap()) {
100 nlohmann::json units_json;
101 if (vectors_node["units"]["length"])
102 units_json["length"] =
103 vectors_node["units"]["length"].as<std::string>();
104 vectors["units"] = units_json;
105 } else {
106 vectors["units"] = vectors_node["units"].as<std::string>();
107 }
108 }
109 lattice["vectors"] = vectors;
110 }
111 material_json["lattice"] = lattice;
112 }
113
114 if (material_node["basis"]) {
115 nlohmann::json basis;
116 const auto& basis_node = material_node["basis"];
117 if (basis_node["elements"]) {
118 nlohmann::json elements = nlohmann::json::array();
119 for (const auto& element_node : basis_node["elements"]) {
120 elements.push_back({{"id", element_node["id"].as<int>()},
121 {"value", element_node["value"].as<std::string>()}});
122 }
123 basis["elements"] = elements;
124 }
125 if (basis_node["coordinates"]) {
126 nlohmann::json coordinates = nlohmann::json::array();
127 for (const auto& coordinate_node : basis_node["coordinates"]) {
128 nlohmann::json coordinate_value = nlohmann::json::array();
129 for (const auto& value : coordinate_node["value"])
130 coordinate_value.push_back(value.as<Real>());
131 coordinates.push_back(
132 {{"id", coordinate_node["id"].as<int>()}, {"value", coordinate_value}});
133 }
134 basis["coordinates"] = coordinates;
135 }
136 if (basis_node["units"])
137 basis["units"] = basis_node["units"].as<std::string>();
138 material_json["basis"] = basis;
139 }
140
141 parse_material(material_json, data);
142 } else {
143 throw std::runtime_error("'material' must be a file path (string) or inline object");
144 }
145
146 if (root["model"]) {
147 parse_model(root["model"], data);
148 }
149
150 if (root["method"]) {
151 parse_method(root["method"], manifest_directory, data);
152 }
153
154 if (root["control"]) {
155 parse_control(root["control"], data);
156 }
157
158 return data;
159 }
160
161 private:
162 /**
163 * Reject any key in a YAML map that the schema doesn't list for this
164 * manifest level, so a misspelled or unsupported field fails fast instead
165 * of silently falling back to a default (the field it was meant to
166 * override never applies).
167 *
168 * The allow-lists are not written here: `schema_path` indexes the ones
169 * generated from schemas/q3/input.json into
170 * `q3::generated::input_schema`, so the schema is the single place a key
171 * is added and the parser cannot drift away from it. Asking for a path
172 * the schema doesn't close throws -- that is a bug in this file, not bad
173 * input.
174 *
175 * Does nothing if node isn't a map (absent/scalar/sequence nodes have no
176 * keys to check). Only the levels Q3 owns end to end are closed in the
177 * schema: material is deliberately left open there, since the broader
178 * ESSE material schema legitimately carries fields Q3 doesn't consume.
179 */
180 static void reject_unknown_keys(const YAML::Node& node, const std::string& schema_path) {
181 if (!node.IsMap())
182 return;
183 const std::vector<std::string>& known_keys =
184 generated::input_schema::allowed_keys(schema_path);
185 const std::string context = describe_schema_path(schema_path);
186 for (auto iterator = node.begin(); iterator != node.end(); ++iterator) {
187 std::string key = iterator->first.as<std::string>();
188 if (std::find(known_keys.begin(), known_keys.end(), key) == known_keys.end()) {
189 throw std::runtime_error("Unknown key '" + key + "' in " + context +
190 " -- check for a typo");
191 }
192 }
193 }
194
195 /**
196 * How a schema path is named in an error message: the document root is
197 * "manifest root", an array's items are an "... entry", and every other
198 * path already reads as the dotted manifest key it is.
199 */
200 static std::string describe_schema_path(const std::string& schema_path) {
201 if (schema_path.empty())
202 return "manifest root";
203 const std::string items_suffix = "[]";
204 if (schema_path.size() > items_suffix.size() &&
205 schema_path.compare(
206 schema_path.size() - items_suffix.size(), items_suffix.size(), items_suffix) == 0)
207 return schema_path.substr(0, schema_path.size() - items_suffix.size()) + " entry";
208 return schema_path;
209 }
210
211 /**
212 * Try to parse an include_yaml("path") directive from a scalar string.
213 * Returns the path if matched, empty string otherwise.
214 */
215 static std::string parse_include_directive(const std::string& value) {
216 const std::string prefix = "include_yaml(";
217 if (value.size() < prefix.size() + 3)
218 return "";
219 if (value.substr(0, prefix.size()) != prefix)
220 return "";
221 if (value.back() != ')')
222 return "";
223
224 std::string inner = value.substr(prefix.size(), value.size() - prefix.size() - 1);
225 if (inner.size() >= 2 && ((inner.front() == '"' && inner.back() == '"') ||
226 (inner.front() == '\'' && inner.back() == '\''))) {
227 inner = inner.substr(1, inner.size() - 2);
228 }
229 return inner;
230 }
231
232 /**
233 * Recursively resolve include_yaml() directives in a YAML tree.
234 */
235 static void resolve_includes(YAML::Node node, const std::string& base_directory) {
236 if (node.IsScalar()) {
237 std::string include_path = parse_include_directive(node.as<std::string>());
238 if (!include_path.empty()) {
239 std::string full_path = detail::resolve_path(base_directory, include_path);
240 YAML::Node included = YAML::LoadFile(full_path);
241 std::string include_directory =
242 std::filesystem::path(full_path).parent_path().string();
243 if (include_directory.empty())
244 include_directory = ".";
245 resolve_includes(included, include_directory);
246 node = included;
247 }
248 } else if (node.IsMap()) {
249 for (auto iterator = node.begin(); iterator != node.end(); ++iterator) {
250 resolve_includes(iterator->second, base_directory);
251 }
252 } else if (node.IsSequence()) {
253 for (size_t index = 0; index < node.size(); index++) {
254 resolve_includes(node[index], base_directory);
255 }
256 }
257 }
258
259 /**
260 * Length unit ("bohr" default) from an ESSE lattice `units` entry, which
261 * may be a plain string or an object like {"length": "angstrom", "angle": "degree"}.
262 */
263 static std::string read_lattice_length_units(const nlohmann::json& lattice_node) {
264 if (lattice_node.contains("units")) {
265 if (lattice_node["units"].is_string()) {
266 return lattice_node["units"].get<std::string>();
267 }
268 if (lattice_node["units"].is_object() && lattice_node["units"].contains("length")) {
269 return lattice_node["units"]["length"].get<std::string>();
270 }
271 }
272 return "bohr";
273 }
274
275 public:
276 /// Parse ESSE material JSON into InputData fields; parse_input() also reads `--material` here.
277 static void parse_material(const nlohmann::json& material_json, InputData& data) {
278 if (!material_json.contains("lattice"))
279 throw std::runtime_error("Material JSON must contain 'lattice'");
280
281 const auto& lattice_node = material_json["lattice"];
282 std::string lattice_units = read_lattice_length_units(lattice_node);
283 Real length_to_bohr =
284 (lattice_units == "angstrom") ? 1.0 / constants::BOHR_RADIUS_ANGSTROM : 1.0;
285
286 if (lattice_node.contains("vectors")) {
287 /**
288 * Explicit vectors: {"a": [ax,ay,az], "b": [...], "c": [...],
289 * "alat": 1.0, "units": "angstrom"} (ESSE shape).
290 */
291 const auto& vectors_node = lattice_node["vectors"];
292 if (!vectors_node.contains("a") || !vectors_node.contains("b") ||
293 !vectors_node.contains("c"))
294 throw std::runtime_error("lattice.vectors must contain 'a', 'b' and 'c' rows");
295 /**
296 * The annotation on the vectors themselves is more specific than the
297 * enclosing lattice-level default, so it takes precedence.
298 */
299 std::string vector_units = vectors_node.contains("units")
300 ? read_lattice_length_units(vectors_node)
301 : lattice_units;
302 Real vector_to_bohr =
303 (vector_units == "angstrom") ? 1.0 / constants::BOHR_RADIUS_ANGSTROM : 1.0;
304 Real alat_multiplier =
305 vectors_node.contains("alat") ? vectors_node["alat"].get<Real>() : 1.0;
306
307 Matrix3x3 lattice_vectors_bohr;
308 const char* row_keys[3] = {"a", "b", "c"};
309 for (int row = 0; row < 3; ++row) {
310 const auto& row_node = vectors_node[row_keys[row]];
311 if (!row_node.is_array() || row_node.size() != 3)
312 throw std::runtime_error(std::string("lattice.vectors.") + row_keys[row] +
313 " must be an array of 3 numbers");
314 for (int column = 0; column < 3; ++column) {
315 lattice_vectors_bohr[row][column] =
316 row_node[column].get<Real>() * alat_multiplier * vector_to_bohr;
317 }
318 }
319 data.set_free_lattice_vectors(lattice_vectors_bohr);
320 } else if (lattice_node.contains("type") && lattice_node.contains("a")) {
321 std::string lattice_type = lattice_node["type"].get<std::string>();
322 for (auto& character : lattice_type)
323 character = static_cast<char>(toupper(character));
324 Real lattice_parameter_a = lattice_node["a"].get<Real>();
325
326 if (lattice_type == "FCC" || lattice_type == "BCC" || lattice_type == "SC" ||
327 lattice_type == "CUB") {
328 /**
329 * Cubic lattices keep the Bravais-index construction (the ESSE
330 * "a" is the primitive vector length; convert to the conventional
331 * lattice constant alat).
332 */
333 if (lattice_type == "FCC") {
334 data.bravais_lattice_index = 2;
335 } else if (lattice_type == "BCC") {
336 data.bravais_lattice_index = 3;
337 } else {
338 data.bravais_lattice_index = 1;
339 }
340
341 data.cell_dimension_1 = lattice_parameter_a * length_to_bohr;
342 if (data.bravais_lattice_index == 2) {
343 data.cell_dimension_1 *= std::sqrt(2.0);
344 } else if (data.bravais_lattice_index == 3 || data.bravais_lattice_index == -3) {
345 data.cell_dimension_1 *= 2.0 / std::sqrt(3.0);
346 }
347 } else {
348 /**
349 * General lattice from cell parameters a, b, c, alpha, beta, gamma
350 * (degrees). Missing lengths default to a; missing angles default
351 * to 90°, except gamma = 120° for HEX.
352 */
353 Real lattice_parameter_b = lattice_node.contains("b")
354 ? lattice_node["b"].get<Real>()
355 : lattice_parameter_a;
356 Real lattice_parameter_c = lattice_node.contains("c")
357 ? lattice_node["c"].get<Real>()
358 : lattice_parameter_a;
359 Real alpha_degrees =
360 lattice_node.contains("alpha") ? lattice_node["alpha"].get<Real>() : 90.0;
361 Real beta_degrees =
362 lattice_node.contains("beta") ? lattice_node["beta"].get<Real>() : 90.0;
363 Real default_gamma = (lattice_type == "HEX") ? 120.0 : 90.0;
364 Real gamma_degrees = lattice_node.contains("gamma")
365 ? lattice_node["gamma"].get<Real>()
366 : default_gamma;
367
368 Matrix3x3 lattice_vectors_bohr =
369 get_lattice_vectors_from_cell_parameters(lattice_parameter_a * length_to_bohr,
370 lattice_parameter_b * length_to_bohr,
371 lattice_parameter_c * length_to_bohr,
372 alpha_degrees,
373 beta_degrees,
374 gamma_degrees);
375 data.set_free_lattice_vectors(lattice_vectors_bohr);
376 }
377 } else {
378 throw std::runtime_error("Lattice must specify either 'type' + 'a' or 'vectors'");
379 }
380
381 if (!material_json.contains("basis"))
382 throw std::runtime_error("Material JSON must contain 'basis'");
383
384 const auto& basis_node = material_json["basis"];
385 if (!basis_node.contains("elements") || !basis_node.contains("coordinates"))
386 throw std::runtime_error("Basis must contain 'elements' and 'coordinates'");
387
388 const auto& elements_node = basis_node["elements"];
389 const auto& coordinates_node = basis_node["coordinates"];
390
391 if (elements_node.size() != coordinates_node.size())
392 throw std::runtime_error("elements and coordinates arrays must have same length");
393
394 std::map<std::string, int> species_map;
395 for (const auto& element_node : elements_node) {
396 std::string element_symbol = element_node["value"].get<std::string>();
397 if (species_map.find(element_symbol) == species_map.end()) {
398 int species_index = static_cast<int>(species_map.size());
399 species_map[element_symbol] = species_index;
400 AtomSpecies species_data;
401 species_data.symbol = element_symbol;
402 species_data.mass = 0.0;
403 species_data.valence_charge = 0.0;
404 data.species.push_back(species_data);
405 }
406 }
407 data.number_of_species = static_cast<int>(species_map.size());
408
409 for (size_t element_index = 0; element_index < elements_node.size(); element_index++) {
410 std::string element_symbol = elements_node[element_index]["value"].get<std::string>();
411 const auto& coordinate_value = coordinates_node[element_index]["value"];
412 Atom atom_data;
413 atom_data.species_index = species_map[element_symbol];
414 atom_data.position = {coordinate_value[0].get<Real>(),
415 coordinate_value[1].get<Real>(),
416 coordinate_value[2].get<Real>()};
417 data.atoms.push_back(atom_data);
418 }
419 data.number_of_atoms = static_cast<int>(data.atoms.size());
420
421 /**
422 * ESSE basis.constraints, as mat3ra-made writes them: per site id, `true`
423 * where a Cartesian component may move (pw.x if_pos 1).
424 */
425 if (basis_node.contains("constraints")) {
426 for (const auto& constraint_node : basis_node["constraints"]) {
427 for (size_t atom_index = 0; atom_index < coordinates_node.size(); ++atom_index) {
428 if (coordinates_node[atom_index]["id"] != constraint_node["id"])
429 continue;
430 for (int axis = 0; axis < 3; ++axis)
431 data.atoms[atom_index].movable_components[axis] =
432 constraint_node["value"][axis].get<bool>();
433 }
434 }
435 }
436 }
437
438 private:
439 static void parse_model(const YAML::Node& model_node, InputData& data) {
440 /**
441 * "type"/"subtype" (e.g. "dft"/"gga") are accepted but not yet consumed:
442 * DFT/Kohn-Sham is Q3's only supported model, so there is nothing to
443 * branch on yet. Every example in this repo sets them, self-documenting
444 * the calculation even though Q3 doesn't act on them today.
445 */
446 reject_unknown_keys(model_node, "model");
447
448 // Kept as written; parse_input() lower-cases every reader's functional.
449 if (model_node["functional"])
450 data.exchange_correlation_functional = model_node["functional"].as<std::string>();
451 if (model_node["dispersion"]) {
452 std::string dispersion_name = model_node["dispersion"].as<std::string>();
453 for (auto& character : dispersion_name)
454 character = static_cast<char>(tolower(character));
455 /**
456 * Validate here (in addition to create_dispersion_calculator's own
457 * run-time throw) so a typo fails at parse time, before any SCF
458 * work starts -- previously an unrecognized name was silently
459 * inert, running without the correction the user asked for.
460 */
461 if (dispersion_name != "none" && dispersion_name != "d3bj")
462 throw std::runtime_error("model.dispersion must be 'none' or 'd3bj', got '" +
463 dispersion_name + "'");
464 data.dispersion_correction = dispersion_name;
465 }
466 }
467
468 static void parse_method(const YAML::Node& method_node,
469 const std::string& manifest_directory,
470 InputData& data) {
471 /**
472 * "type" (e.g. "pseudopotential") is accepted but not yet consumed:
473 * planewave-pseudopotential is Q3's only supported method.
474 */
475 reject_unknown_keys(method_node, "method");
476
477 if (method_node["pseudopotentials"]) {
478 const auto& pseudopotentials_node = method_node["pseudopotentials"];
479 for (auto iterator = pseudopotentials_node.begin();
480 iterator != pseudopotentials_node.end();
481 ++iterator) {
482 std::string element = iterator->first.as<std::string>();
483 std::string pseudopotential_filename = iterator->second.as<std::string>();
484 std::string pseudopotential_path =
485 detail::resolve_path(manifest_directory, pseudopotential_filename);
486
487 bool found = false;
488 for (auto& species_data : data.species) {
489 if (species_data.symbol == element) {
490 species_data.pseudopotential_file = pseudopotential_path;
491 found = true;
492 }
493 }
494 if (!found) {
495 log::warn(
496 "Pseudopotential specified for element '{}' but not found in material "
497 "basis",
498 element);
499 }
500 }
501
502 if (!data.species.empty() && !data.species[0].pseudopotential_file.empty()) {
503 data.pseudopotential_directory =
504 std::filesystem::path(data.species[0].pseudopotential_file)
505 .parent_path()
506 .string();
507 if (data.pseudopotential_directory.empty())
508 data.pseudopotential_directory = ".";
509 }
510 }
511
512 if (method_node["cutoffs"]) {
513 const auto& cutoffs_node = method_node["cutoffs"];
514 reject_unknown_keys(cutoffs_node, "method.cutoffs");
515 if (cutoffs_node["wavefunction"])
516 data.wavefunction_energy_cutoff = cutoffs_node["wavefunction"].as<Real>();
517 if (cutoffs_node["density"])
518 data.charge_density_energy_cutoff = cutoffs_node["density"].as<Real>();
519 }
520
521 YAML::Node self_consistent_field_node;
522 if (method_node["self_consistent_field"]) {
523 self_consistent_field_node = method_node["self_consistent_field"];
524 } else if (method_node["scf"]) {
525 self_consistent_field_node = method_node["scf"];
526 }
527
528 if (self_consistent_field_node) {
529 /**
530 * The QE abbreviation `nspin` belongs to the QE-namelist parser
531 * only, so the manifest schema does not list it. Catch it before
532 * the generic rejection below, to answer a mistyped manifest with
533 * the key it should have used rather than a bare "unknown key" --
534 * and so it can never silently run unpolarized.
535 */
536 if (self_consistent_field_node["nspin"])
537 throw std::runtime_error(
538 "'nspin' is not a manifest key; use method.scf.number_of_spin_components");
539
540 /**
541 * Both spellings of this block share one schema definition, so
542 * they share one allow-list; "method.scf" names it in errors
543 * either way.
544 */
545 reject_unknown_keys(self_consistent_field_node, "method.scf");
546
547 if (self_consistent_field_node["convergence_threshold"])
548 data.convergence_threshold =
549 self_consistent_field_node["convergence_threshold"].as<Real>();
550 if (self_consistent_field_node["max_iterations"])
551 data.maximum_iterations = self_consistent_field_node["max_iterations"].as<int>();
552 if (self_consistent_field_node["diagonalization"])
553 data.diagonalization_method =
554 self_consistent_field_node["diagonalization"].as<std::string>();
555 if (self_consistent_field_node["number_of_bands"])
556 data.number_of_bands = self_consistent_field_node["number_of_bands"].as<int>();
557 if (self_consistent_field_node["total_charge"])
558 data.total_charge = self_consistent_field_node["total_charge"].as<Real>();
559
560 if (self_consistent_field_node["hubbard"]) {
561 const auto& hubbard_node = self_consistent_field_node["hubbard"];
562 if (!hubbard_node.IsSequence())
563 throw std::runtime_error(
564 "method.scf.hubbard must be a list of {species, orbital_label, u_ev}");
565 for (const auto& entry : hubbard_node) {
566 reject_unknown_keys(entry, "method.scf.hubbard[]");
567 if (!entry["species"] || !entry["orbital_label"] || !entry["u_ev"])
568 throw std::runtime_error(
569 "each method.scf.hubbard entry needs species, orbital_label, and "
570 "u_ev");
571 Real u_ev = entry["u_ev"].as<Real>();
572 data.hubbard.push_back({entry["species"].as<std::string>(),
573 entry["orbital_label"].as<std::string>(),
574 u_ev * 2.0 / constants::HARTREE_TO_EV});
575 }
576 }
577
578 if (self_consistent_field_node["hubbard_projectors"]) {
579 std::string hubbard_projector_type =
580 self_consistent_field_node["hubbard_projectors"].as<std::string>();
581 if (hubbard_projector_type != "atomic" && hubbard_projector_type != "ortho-atomic")
582 throw std::runtime_error(
583 "method.scf.hubbard_projectors must be 'atomic' or 'ortho-atomic', got '" +
584 hubbard_projector_type + "'");
585 data.hubbard_projector_type = hubbard_projector_type;
586 }
587
588 if (self_consistent_field_node["hubbard_fixed_occupation_iterations"]) {
589 int fixed_iterations =
590 self_consistent_field_node["hubbard_fixed_occupation_iterations"].as<int>();
591 if (fixed_iterations < 0)
592 throw std::runtime_error(
593 "method.scf.hubbard_fixed_occupation_iterations must be >= 0, got " +
594 std::to_string(fixed_iterations));
595 data.hubbard_fixed_occupation_iterations = fixed_iterations;
596 }
597
598 if (self_consistent_field_node["hubbard_starting_eigenvalues"]) {
599 const auto& seeding_node =
600 self_consistent_field_node["hubbard_starting_eigenvalues"];
601 if (!seeding_node.IsSequence())
602 throw std::runtime_error("method.scf.hubbard_starting_eigenvalues must be a "
603 "list of {species, eigenvalue, spin, value}");
604 for (const auto& entry : seeding_node) {
605 reject_unknown_keys(entry, "method.scf.hubbard_starting_eigenvalues[]");
606 if (!entry["species"] || !entry["eigenvalue"] || !entry["spin"] ||
607 !entry["value"])
608 throw std::runtime_error(
609 "each method.scf.hubbard_starting_eigenvalues entry needs species, "
610 "eigenvalue, spin, and value");
611 /**
612 * 1-based on the user side, matching the drop-in
613 * `starting_ns_eigenvalue(m,ispin,ityp)` numbering so a
614 * deck translated between the two formats keeps its
615 * indices verbatim. Eigenvalues are ascending: the largest
616 * m is the most occupied orbital. Range rules live in the
617 * shared factory so both readers enforce one copy of them.
618 */
619 data.hubbard_starting_eigenvalues.push_back(make_hubbard_starting_eigenvalue(
620 entry["species"].as<std::string>(),
621 entry["eigenvalue"].as<int>(),
622 entry["spin"].as<int>(),
623 entry["value"].as<Real>(),
624 "method.scf.hubbard_starting_eigenvalues"));
625 }
626 }
627
628 if (self_consistent_field_node["davidson"]) {
629 const auto& davidson_node = self_consistent_field_node["davidson"];
630 reject_unknown_keys(davidson_node, "method.scf.davidson");
631 if (davidson_node["basis_expansion_factor"]) {
632 int factor = davidson_node["basis_expansion_factor"].as<int>();
633 if (factor < 2) {
634 throw std::runtime_error(
635 "method.scf.davidson.basis_expansion_factor must be at least 2: the "
636 "working basis has to be able to grow past the bands it is solving "
637 "for, got " +
638 std::to_string(factor));
639 }
640 data.davidson_basis_expansion_factor = factor;
641 data.davidson_basis_expansion_factor_set_by_deck = true;
642 }
643 }
644
645 if (self_consistent_field_node["mixing"]) {
646 const auto& mixing_node = self_consistent_field_node["mixing"];
647 reject_unknown_keys(mixing_node, "method.scf.mixing");
648 if (mixing_node["mode"])
649 data.mixing_mode = mixing_node["mode"].as<std::string>();
650 if (mixing_node["beta"])
651 data.mixing_beta = mixing_node["beta"].as<Real>();
652 if (mixing_node["ndim"])
653 data.mixing_history_size = mixing_node["ndim"].as<int>();
654 if (mixing_node["preconditioning"])
655 data.mixing_preconditioning = mixing_node["preconditioning"].as<std::string>();
656 if (mixing_node["q0"])
657 data.mixing_q0 = mixing_node["q0"].as<Real>();
658 }
659
660 /**
661 * Collinear spin polarization:
662 * method.scf.number_of_spin_components: 2
663 * method.scf.starting_magnetization: # per species, in [-1, 1]
664 * Fe: 0.4
665 * Cr2: -0.5 # AFM sublattice label
666 * (The QE abbreviation `nspin` is rejected above.)
667 */
668 if (self_consistent_field_node["number_of_spin_components"]) {
669 data.number_of_spin_components =
670 self_consistent_field_node["number_of_spin_components"].as<int>();
671 if (data.number_of_spin_components != 1 && data.number_of_spin_components != 2)
672 throw std::runtime_error(
673 "number_of_spin_components must be 1 (unpolarized) or 2 (collinear "
674 "LSDA), got " +
675 std::to_string(data.number_of_spin_components));
676 }
677
678 if (self_consistent_field_node["starting_magnetization"]) {
679 const auto& magnetization_node =
680 self_consistent_field_node["starting_magnetization"];
681 if (!magnetization_node.IsMap())
682 throw std::runtime_error(
683 "starting_magnetization must be a map of species symbol to value, "
684 "e.g. {Fe: 0.4}");
685 data.starting_magnetization.assign(data.species.size(), 0.0);
686 for (auto iterator = magnetization_node.begin();
687 iterator != magnetization_node.end();
688 ++iterator) {
689 std::string species_symbol = iterator->first.as<std::string>();
690 Real magnetization_value = iterator->second.as<Real>();
691 bool found = false;
692 for (size_t species_index = 0; species_index < data.species.size();
693 ++species_index) {
694 if (data.species[species_index].symbol == species_symbol) {
695 data.starting_magnetization[species_index] = magnetization_value;
696 found = true;
697 }
698 }
699 if (!found)
700 throw std::runtime_error("starting_magnetization specified for species '" +
701 species_symbol +
702 "' which is not in the material basis");
703 }
704 }
705
706 /**
707 * Per-atom starting moments, one per atom of the material basis in
708 * order (same [-1, 1] convention):
709 * method.scf.atomic_starting_magnetization: [0.5, -0.5]
710 * Lets atoms of one species start antiparallel without relabeling
711 * them as separate species. Exclusive with the species-keyed map,
712 * since giving both would leave the precedence implicit.
713 */
714 if (self_consistent_field_node["atomic_starting_magnetization"]) {
715 const auto& atomic_node =
716 self_consistent_field_node["atomic_starting_magnetization"];
717 if (!atomic_node.IsSequence())
718 throw std::runtime_error(
719 "atomic_starting_magnetization must be a list with one value per "
720 "atom of the material basis, e.g. [0.5, -0.5]");
721 if (self_consistent_field_node["starting_magnetization"])
722 throw std::runtime_error("give either starting_magnetization (per species) or "
723 "atomic_starting_magnetization (per atom), not both");
724 if (atomic_node.size() != data.atoms.size())
725 throw std::runtime_error("atomic_starting_magnetization lists " +
726 std::to_string(atomic_node.size()) +
727 " values but the material basis has " +
728 std::to_string(data.atoms.size()) + " atoms");
729 data.atomic_starting_magnetization.clear();
730 for (const auto& value_node : atomic_node) {
731 Real magnetization_value = value_node.as<Real>();
732 if (magnetization_value < -1.0 || magnetization_value > 1.0)
733 throw std::runtime_error(
734 "atomic_starting_magnetization values must be in [-1, 1], got " +
735 std::to_string(magnetization_value));
736 data.atomic_starting_magnetization.push_back(magnetization_value);
737 }
738 }
739
740 if (self_consistent_field_node["smearing"]) {
741 const auto& smearing_node = self_consistent_field_node["smearing"];
742 reject_unknown_keys(smearing_node, "method.scf.smearing");
743 if (smearing_node["type"])
744 data.smearing = smearing_node["type"].as<std::string>();
745 if (smearing_node["degauss"])
746 data.smearing_width = smearing_node["degauss"].as<Real>();
747 if (smearing_node["order"]) {
748 data.methfessel_paxton_order = smearing_node["order"].as<int>();
749 const std::string& kind = data.smearing;
750 if (kind != "methfessel-paxton" && kind != "m-p" && kind != "mp")
751 throw std::runtime_error(
752 "method.scf.smearing.order applies to Methfessel-Paxton smearing "
753 "only; smearing type is '" +
754 kind + "'");
755 if (data.methfessel_paxton_order < 1)
756 throw std::runtime_error("method.scf.smearing.order must be >= 1, got " +
757 std::to_string(data.methfessel_paxton_order));
758 }
759 }
760
761 if (self_consistent_field_node["kgrid"]) {
762 const auto& kpoint_grid_node = self_consistent_field_node["kgrid"];
763 reject_unknown_keys(kpoint_grid_node, "method.scf.kgrid");
764 if (kpoint_grid_node["dimensions"]) {
765 auto dimensions_node = kpoint_grid_node["dimensions"];
766 data.kpoint_grid_dimension_1 = dimensions_node[0].as<int>();
767 data.kpoint_grid_dimension_2 = dimensions_node[1].as<int>();
768 data.kpoint_grid_dimension_3 = dimensions_node[2].as<int>();
769 }
770 if (kpoint_grid_node["shifts"]) {
771 auto shifts_node = kpoint_grid_node["shifts"];
772 data.kpoint_grid_shift_1 = shifts_node[0].as<int>();
773 data.kpoint_grid_shift_2 = shifts_node[1].as<int>();
774 data.kpoint_grid_shift_3 = shifts_node[2].as<int>();
775 }
776 if (kpoint_grid_node["ibz_reduction"]) {
777 data.ibz_reduction_method = kpoint_grid_node["ibz_reduction"].as<std::string>();
778 }
779 }
780 }
781
782 if (method_node["band_structure"]) {
783 const auto& bs_node = method_node["band_structure"];
784 reject_unknown_keys(bs_node, "method.band_structure");
785 if (bs_node["number_of_bands"])
786 data.number_of_bands = bs_node["number_of_bands"].as<int>();
787 }
788 }
789
790 /**
791 * `control.interpolation` (see InterpolationSpecification for what the
792 * block means on each calculation type).
793 *
794 * Two refusals live here rather than downstream. A block on a calculation
795 * that never reads it (an SCF, or a Python-orchestrated workflow) would
796 * let the run exit cleanly having built nothing -- the silently-ignored
797 * control field q3.workflows.* once suffered from. And a build step
798 * without `grid` cannot resolve k+b onto the mesh, so it is refused with
799 * the reason instead of failing inside the neighbour arithmetic.
800 */
801 /// control.interpolation.<key> as a positive number; an absent key leaves `value` as it was.
802 template <typename T>
803 static void
804 read_positive_interpolation_value(const YAML::Node& block, const char* key, T& value) {
805 if (!block[key]) {
806 return;
807 }
808 value = block[key].as<T>();
809 if (!(value > T{0})) {
810 throw std::runtime_error(std::string("control.interpolation.") + key +
811 " must be positive.");
812 }
813 }
814
815 /// control.interpolation.<key> as one of `choices`; an absent key leaves `value` as it was.
816 static void read_interpolation_choice(const YAML::Node& block,
817 const char* key,
818 const std::vector<std::string>& choices,
819 std::string& value) {
820 if (!block[key]) {
821 return;
822 }
823 value = block[key].as<std::string>();
824 if (std::find(choices.begin(), choices.end(), value) != choices.end()) {
825 return;
826 }
827 /// "'a', 'b' or 'c'": the choices as a sentence names them.
828 std::string named;
829 for (std::size_t index = 0; index < choices.size(); index++) {
830 named += (index == 0 ? "" : (index + 1 == choices.size() ? " or " : ", "));
831 named += "'" + choices[index] + "'";
832 }
833 throw std::runtime_error(std::string("control.interpolation.") + key + " must be " + named +
834 ", got '" + value + "'.");
835 }
836
837 /**
838 * control.interpolation.<key> as three positive integers; an absent key leaves `mesh` as it
839 * was.
840 */
841 static void
842 read_interpolation_mesh(const YAML::Node& block, const char* key, std::array<int, 3>& mesh) {
843 const YAML::Node node = block[key];
844 if (!node) {
845 return;
846 }
847 const std::string refusal = std::string("control.interpolation.") + key +
848 " must be three positive integers [n1, n2, n3].";
849 if (!node.IsSequence() || node.size() != 3) {
850 throw std::runtime_error(refusal);
851 }
852 for (std::size_t axis = 0; axis < 3; axis++) {
853 mesh[axis] = node[axis].as<int>();
854 if (mesh[axis] <= 0) {
855 throw std::runtime_error(refusal);
856 }
857 }
858 }
859
860 /// The keys an NSCF build request reads, and the keys `q3_utility interpolate` reads.
861 static constexpr std::array<const char*, 6> interpolation_build_step_keys = {
862 "grid",
863 "bands",
864 "projection_centers",
865 "projection_decay_bohr",
866 "trial_shells",
867 "export_formats"};
868 static constexpr std::array<const char*, 9> interpolation_evaluation_step_keys = {
869 "dos_mesh",
870 "disentanglement",
871 "disentanglement_maximum_iterations",
872 "disentanglement_convergence_tolerance",
873 "localization",
874 "localization_maximum_iterations",
875 "projectability_minimum",
876 "projectability_maximum",
877 "frozen_window_above_fermi_ev"};
878
879 /// What the capture is built from: the grid, the band range and the trial orbitals.
880 static void parse_interpolation_build_step(const YAML::Node& interpolation_node,
881 InterpolationSpecification& specification) {
882 read_interpolation_mesh(interpolation_node, "grid", specification.grid);
883 if (interpolation_node["bands"]) {
884 const YAML::Node node = interpolation_node["bands"];
885 if (!node.IsSequence() || node.size() != 2) {
886 throw std::runtime_error(
887 "control.interpolation.bands must be [first, last], two 1-based band indices.");
888 }
889 specification.bands[0] = node[0].as<int>();
890 specification.bands[1] = node[1].as<int>();
891 if (specification.bands[0] < 1 || specification.bands[1] < specification.bands[0]) {
892 throw std::runtime_error(
893 "control.interpolation.bands must be [first, last] with 1 <= first <= last, "
894 "got [" +
895 std::to_string(specification.bands[0]) + ", " +
896 std::to_string(specification.bands[1]) + "].");
897 }
898 }
899 read_positive_interpolation_value(
900 interpolation_node, "projection_decay_bohr", specification.projection_decay_bohr);
901 if (interpolation_node["projection_centers"]) {
902 for (const auto& center_node : interpolation_node["projection_centers"]) {
903 if (!center_node.IsSequence() || center_node.size() != 3) {
904 throw std::runtime_error(
905 "control.interpolation.projection_centers: each entry must be three "
906 "crystal (fractional) coordinates.");
907 }
908 specification.projection_centers.emplace_back(center_node[0].as<Real>(),
909 center_node[1].as<Real>(),
910 center_node[2].as<Real>());
911 }
912 }
913 if (interpolation_node["trial_shells"]) {
914 const YAML::Node node = interpolation_node["trial_shells"];
915 if (!node.IsMap()) {
916 throw std::runtime_error(
917 "control.interpolation.trial_shells must map each species to a list of "
918 "0-based shell indices, e.g. {Si: [0, 2]}.");
919 }
920 for (const auto& entry : node) {
921 const std::string species = entry.first.as<std::string>();
922 if (!entry.second.IsSequence() || entry.second.size() == 0) {
923 throw std::runtime_error("control.interpolation.trial_shells." + species +
924 " must be a non-empty list of 0-based shell indices.");
925 }
926 std::vector<int> shells;
927 for (const auto& index_node : entry.second) {
928 const int index = index_node.as<int>();
929 if (index < 0 ||
930 std::find(shells.begin(), shells.end(), index) != shells.end()) {
931 throw std::runtime_error(
932 "control.interpolation.trial_shells." + species +
933 " must list distinct, non-negative shell indices.");
934 }
935 shells.push_back(index);
936 }
937 specification.trial_shells[species] = shells;
938 }
939 }
940 if (!specification.trial_shells.empty() && !specification.projection_centers.empty()) {
941 throw std::runtime_error(
942 "control.interpolation.trial_shells chooses among the pseudo-atomic orbitals and "
943 "projection_centers replaces them with hydrogenic ones; give one or the other.");
944 }
945 if (interpolation_node["export_formats"]) {
946 for (const auto& format_node : interpolation_node["export_formats"]) {
947 const std::string format = format_node.as<std::string>();
948 if (format != "wannier90") {
949 throw std::runtime_error(
950 "control.interpolation.export_formats entries must be 'wannier90', got "
951 "'" +
952 format + "' (refused here rather than after the NSCF has run).");
953 }
954 specification.export_formats.push_back(format);
955 }
956 }
957 }
958
959 /// How the saved record is evaluated: the subspace, the gauge, and the density of states.
960 static void parse_interpolation_evaluation_step(const YAML::Node& interpolation_node,
961 InterpolationSpecification& specification) {
962 read_interpolation_mesh(interpolation_node, "dos_mesh", specification.dos_mesh);
963 read_interpolation_choice(interpolation_node,
964 "disentanglement",
965 {"none", "projectability"},
966 specification.disentanglement);
967 if (interpolation_node["projectability_minimum"]) {
968 specification.projectability_minimum =
969 interpolation_node["projectability_minimum"].as<Real>();
970 }
971 if (interpolation_node["projectability_maximum"]) {
972 specification.projectability_maximum =
973 interpolation_node["projectability_maximum"].as<Real>();
974 }
975 if (interpolation_node["frozen_window_above_fermi_ev"]) {
976 /**
977 * A number in eV, or the word "auto" (the default): the classifier
978 * then applies the published window, E_F + 2 eV, to metal and
979 * insulator alike.
980 */
981 const YAML::Node node = interpolation_node["frozen_window_above_fermi_ev"];
982 const std::string text = node.IsScalar() ? node.as<std::string>() : "";
983 if (text == "auto") {
984 specification.frozen_window_is_automatic = true;
985 } else {
986 try {
987 specification.frozen_window_above_fermi_ev = node.as<Real>();
988 } catch (const YAML::Exception&) {
989 throw std::runtime_error(
990 "control.interpolation.frozen_window_above_fermi_ev must be a number in "
991 "electronvolts or 'auto', got '" +
992 text + "'.");
993 }
994 specification.frozen_window_is_automatic = false;
995 }
996 }
997 read_positive_interpolation_value(interpolation_node,
998 "localization_maximum_iterations",
999 specification.localization_maximum_iterations);
1000 read_positive_interpolation_value(interpolation_node,
1001 "disentanglement_maximum_iterations",
1002 specification.disentanglement_maximum_iterations);
1003 read_positive_interpolation_value(interpolation_node,
1004 "disentanglement_convergence_tolerance",
1005 specification.disentanglement_convergence_tolerance);
1006 if (!(specification.projectability_minimum < specification.projectability_maximum)) {
1007 throw std::runtime_error("control.interpolation.projectability_minimum must be below "
1008 "projectability_maximum, got " +
1009 std::to_string(specification.projectability_minimum) +
1010 " and " +
1011 std::to_string(specification.projectability_maximum) + ".");
1012 }
1013 read_interpolation_choice(interpolation_node,
1014 "localization",
1015 {"auto", "maximal", "none"},
1016 specification.localization);
1017 }
1018
1019 /**
1020 * One block, two steps, and a key written on the wrong one used to be
1021 * ignored in silence. `bands` cannot be honoured at evaluation because the
1022 * record holds only what was captured, and `disentanglement` cannot be
1023 * honoured at capture because the record deliberately stores the
1024 * untouched window. So neither can be quietly accepted where it does
1025 * nothing.
1026 */
1027 template <std::size_t Count>
1028 static void refuse_misplaced_interpolation_keys(const YAML::Node& interpolation_node,
1029 const std::array<const char*, Count>& keys,
1030 const std::string& calculation,
1031 const char* where_it_belongs) {
1032 for (const char* key : keys) {
1033 if (interpolation_node[key]) {
1034 throw std::runtime_error(std::string("control.interpolation.") + key +
1035 " has no effect on calculation: " + calculation +
1036 ", so it is refused rather than ignored. " +
1037 where_it_belongs);
1038 }
1039 }
1040 }
1041
1042 static void parse_interpolation(const YAML::Node& interpolation_node, InputData& data) {
1043 reject_unknown_keys(interpolation_node, "control.interpolation");
1044 InterpolationSpecification& specification = data.interpolation;
1045
1046 if (!interpolation_node["method"]) {
1047 throw std::runtime_error(
1048 "control.interpolation.method is required (supported: wannier).");
1049 }
1050 specification.method = interpolation_node["method"].as<std::string>();
1051 if (specification.method != "wannier") {
1052 throw std::runtime_error("control.interpolation.method must be 'wannier', got '" +
1053 specification.method + "'.");
1054 }
1055 parse_interpolation_build_step(interpolation_node, specification);
1056 parse_interpolation_evaluation_step(interpolation_node, specification);
1057
1058 const bool builds = data.calculation == "non_self_consistent_field";
1059 const bool evaluates = data.calculation == "interpolation";
1060 if (!builds && !evaluates) {
1061 throw std::runtime_error(
1062 "control.interpolation is set on calculation: " + data.calculation +
1063 ", which would ignore it. It is built by calculation: nscf "
1064 "(non_self_consistent_field) and evaluated by calculation: interpolation "
1065 "(run with q3_utility interpolate).");
1066 }
1067 if (builds) {
1068 refuse_misplaced_interpolation_keys(
1069 interpolation_node,
1070 interpolation_evaluation_step_keys,
1071 data.calculation,
1072 "It is read when the model is evaluated -- put it in the manifest you pass to "
1073 "q3_utility interpolate.");
1074 } else {
1075 refuse_misplaced_interpolation_keys(
1076 interpolation_node,
1077 interpolation_build_step_keys,
1078 data.calculation,
1079 "It is read when the record is captured -- put it in the NSCF manifest that "
1080 "builds the record.");
1081 }
1082
1083 if (builds && !specification.has_grid()) {
1084 throw std::runtime_error(
1085 "control.interpolation.grid is required on the NSCF build step: the k-points "
1086 "arrive as an explicit control.kpoints_list, which carries no grid shape of its "
1087 "own, and k+b is resolved onto the grid by exact integer indexing.");
1088 }
1089 }
1090
1091 static void parse_control(const YAML::Node& control_node, InputData& data) {
1092 reject_unknown_keys(control_node, "control");
1093
1094 if (control_node["calculation"]) {
1095 std::string calculation_type = control_node["calculation"].as<std::string>();
1096 if (calculation_type == "scf")
1097 calculation_type = "self_consistent_field";
1098 if (calculation_type == "nscf")
1099 calculation_type = "non_self_consistent_field";
1100 if (calculation_type == "bands")
1101 calculation_type = "band_structure";
1102 if (calculation_type == "relax")
1103 calculation_type = "relaxation";
1104 if (calculation_type == "dos")
1105 calculation_type = "density_of_states";
1106 data.calculation = calculation_type;
1107 }
1108 if (control_node["outdir"])
1109 data.output_directory = control_node["outdir"].as<std::string>();
1110 if (control_node["prefix"])
1111 data.prefix = control_node["prefix"].as<std::string>();
1112 if (control_node["verbosity"]) {
1113 /**
1114 * Validated here rather than handed to spdlog's level parser,
1115 * which maps anything it does not recognize to "off" -- so a
1116 * misspelled verbosity would have silenced the run instead of
1117 * making it louder, and said nothing about why.
1118 */
1119 std::string verbosity = control_node["verbosity"].as<std::string>();
1120 if (verbosity != "default" && verbosity != "high")
1121 throw std::runtime_error("control.verbosity must be 'default' or 'high', got '" +
1122 verbosity + "'");
1123 data.verbosity = verbosity;
1124 }
1125 if (control_node["disable_symmetry"])
1126 data.disable_symmetry = control_node["disable_symmetry"].as<bool>();
1127 if (control_node["nonlocal_stress_derivative_method"])
1128 data.nonlocal_stress_derivative_method =
1129 control_node["nonlocal_stress_derivative_method"].as<std::string>();
1130 if (control_node["number_of_kpoint_pools"])
1131 data.number_of_kpoint_pools = control_node["number_of_kpoint_pools"].as<int>();
1132 if (control_node["use_deterministic_fft_planning"])
1133 data.use_deterministic_fft_planning =
1134 control_node["use_deterministic_fft_planning"].as<bool>();
1135 if (control_node["use_exact_wavefunction_grid"])
1136 data.use_exact_wavefunction_grid =
1137 control_node["use_exact_wavefunction_grid"].as<bool>();
1138 if (control_node["isolated_system_correction"]) {
1139 std::string isolated_system_correction =
1140 control_node["isolated_system_correction"].as<std::string>();
1141 if (isolated_system_correction != "none" &&
1142 isolated_system_correction != "makov-payne" &&
1143 isolated_system_correction != "martyna-tuckerman")
1144 throw std::runtime_error(
1145 "control.isolated_system_correction must be 'none', 'makov-payne', or "
1146 "'martyna-tuckerman', got '" +
1147 isolated_system_correction + "'");
1148 data.isolated_system_correction = isolated_system_correction;
1149 }
1150 if (control_node["mean_field_export"]) {
1151 std::string mean_field_export = control_node["mean_field_export"].as<std::string>();
1152 if (mean_field_export != "none" && mean_field_export != "berkeleygw")
1153 throw std::runtime_error(
1154 "control.mean_field_export must be 'none' or 'berkeleygw', got '" +
1155 mean_field_export + "'");
1156 data.mean_field_export = mean_field_export;
1157 }
1158 if (control_node["mean_field_export_kleinman_bylander_projectors"])
1159 data.mean_field_export_kleinman_bylander_projectors =
1160 control_node["mean_field_export_kleinman_bylander_projectors"].as<bool>();
1161
1162 /**
1163 * Band path specification: list of high-symmetry point labels.
1164 * Accepted formats:
1165 * band_path: ["Γ", "X", "W", "L", "Γ", "K"] # labels only
1166 * band_path:
1167 * - label: "Γ"
1168 * coordinates: [0.0, 0.0, 0.0] # explicit fractional coords
1169 * - label: "X"
1170 * band_path_points_per_segment: 50
1171 */
1172 if (control_node["band_path"]) {
1173 const auto& band_path_node = control_node["band_path"];
1174 data.band_path.clear();
1175
1176 if (band_path_node.IsSequence()) {
1177 for (const auto& vertex_node : band_path_node) {
1178 std::string label;
1179 Vector3D coordinates{0.0, 0.0, 0.0};
1180 bool has_coordinates = false;
1181
1182 if (vertex_node.IsScalar()) {
1183 // Simple label string: "Γ"
1184 label = vertex_node.as<std::string>();
1185 } else if (vertex_node.IsMap()) {
1186 // Map with optional explicit coordinates.
1187 reject_unknown_keys(vertex_node, "control.band_path[]");
1188 if (vertex_node["label"])
1189 label = vertex_node["label"].as<std::string>();
1190 if (vertex_node["coordinates"]) {
1191 has_coordinates = true;
1192 const auto& coords_node = vertex_node["coordinates"];
1193 coordinates.x = coords_node[0].as<Real>();
1194 coordinates.y = coords_node[1].as<Real>();
1195 coordinates.z = coords_node[2].as<Real>();
1196 }
1197 }
1198
1199 if (!label.empty()) {
1200 /**
1201 * The engine's band-structure flavor only needs labels
1202 * (Python resolved the path into kpoints_list already);
1203 * q3_utility interpolate walks the vertices itself and has
1204 * no high-symmetry table, and a vertex defaulted to the
1205 * origin is indistinguishable from a genuine Gamma
1206 * downstream -- so the refusal has to happen here.
1207 */
1208 if (!has_coordinates && data.calculation == "interpolation") {
1209 throw std::runtime_error(
1210 "control.band_path vertex '" + label +
1211 "' has no coordinates. On calculation: interpolation every "
1212 "vertex needs `coordinates: [k1, k2, k3]` (crystal), or let "
1213 "q3.workflows.interpolation resolve the labels into an explicit "
1214 "control.kpoints_list.");
1215 }
1216 data.band_path.emplace_back(label, coordinates);
1217 }
1218 }
1219 }
1220 }
1221
1222 if (control_node["band_path_points_per_segment"])
1223 data.band_path_points_per_segment =
1224 control_node["band_path_points_per_segment"].as<int>();
1225
1226 /**
1227 * Explicit k-point list for NSCF calculations.
1228 * Format (crystal coordinates + weight):
1229 * kpoints_list:
1230 * - [0.0, 0.0, 0.0, 1.0]
1231 * - [0.5, 0.0, 0.0, 1.0]
1232 */
1233 if (control_node["kpoints_list"]) {
1234 const auto& kpoints_list_node = control_node["kpoints_list"];
1235 data.kpoints_list.clear();
1236
1237 if (kpoints_list_node.IsSequence()) {
1238 for (const auto& kpoint_node : kpoints_list_node) {
1239 if (kpoint_node.IsSequence() && kpoint_node.size() == 4) {
1240 Vector3D coordinates{kpoint_node[0].as<Real>(),
1241 kpoint_node[1].as<Real>(),
1242 kpoint_node[2].as<Real>()};
1243 Real weight = kpoint_node[3].as<Real>();
1244 data.kpoints_list.emplace_back(coordinates, weight);
1245 }
1246 }
1247 }
1248 }
1249
1250 // DOS parameters
1251 if (control_node["dos_broadening"])
1252 data.dos_broadening = control_node["dos_broadening"].as<Real>();
1253 if (control_node["dos_energy_min"])
1254 data.dos_energy_min = control_node["dos_energy_min"].as<Real>();
1255 if (control_node["dos_energy_max"])
1256 data.dos_energy_max = control_node["dos_energy_max"].as<Real>();
1257 if (control_node["dos_energy_steps"])
1258 data.dos_energy_steps = control_node["dos_energy_steps"].as<int>();
1259
1260 // Atomic-orbital (ortho-atomic/Löwdin) projections for NSCF runs.
1261 if (control_node["compute_projections"])
1262 data.compute_projections = control_node["compute_projections"].as<bool>();
1263
1264 if (control_node["interpolation"])
1265 parse_interpolation(control_node["interpolation"], data);
1266
1267 // Relaxation parameters
1268 if (control_node["relax_max_steps"])
1269 data.relax_maximum_steps = control_node["relax_max_steps"].as<int>();
1270 if (control_node["relax_force_threshold"])
1271 data.relax_force_threshold = control_node["relax_force_threshold"].as<Real>();
1272 if (control_node["relax_energy_threshold"])
1273 data.relax_energy_threshold = control_node["relax_energy_threshold"].as<Real>();
1274 }
1275};
1276
1277} // namespace q3
static void parse_material(const nlohmann::json &material_json, InputData &data)
Parse ESSE material JSON into InputData fields; parse_input() also reads --material here.
Definition yaml.hpp:277
InputData parse(const std::string &filename) const override
Definition yaml.hpp:32
constexpr Real HARTREE_TO_EV
Hartree → eV (AUTOEV)
constexpr Real BOHR_RADIUS_ANGSTROM
Bohr radius in Ångströms (BOHR_RADIUS_SI * 1e10; BOHR_RADIUS_ANGSTROM)
nlohmann::json read_json_file(const std::string &path)
Definition utils.hpp:34
std::string resolve_path(const std::string &base_directory, const std::string &path)
Definition utils.hpp:24
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
Matrix3x3 get_lattice_vectors_from_cell_parameters(Real length_a, Real length_b, Real length_c, Real alpha_degrees, Real beta_degrees, Real gamma_degrees)
Definition lattice.hpp:32
double Real
HubbardStartingEigenvalue make_hubbard_starting_eigenvalue(const std::string &species_symbol, int one_based_eigenvalue_index, int one_based_spin_index, Real target_occupation, const std::string &context)
Definition hubbard.hpp:65
std::string symbol
int species_index
Definition atom.hpp:20
Vector3D position
Definition atom.hpp:21
Parsed input data — holds raw values read from input files.
Definition data.hpp:27
std::string dispersion_correction
Definition data.hpp:78
std::vector< AtomSpecies > species
Definition data.hpp:153
std::vector< Atom > atoms
Definition data.hpp:156
void set_free_lattice_vectors(const Matrix3x3 &vectors_bohr, Real preset_lattice_parameter=0.0)
Definition data.hpp:317
Real cell_dimension_1
Definition data.hpp:129
int number_of_atoms
Definition data.hpp:149
std::string exchange_correlation_functional
Definition data.hpp:41
int bravais_lattice_index
Definition data.hpp:128
int number_of_species
Definition data.hpp:150
3×3 matrix stored row-major: row i = mat[i]