Loading...
Searching...
No Matches
loader.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Pseudopotential loader — resolves paths and reads dataset files.
5 *
6 * Provides a single entry point that, given a list of species and a
7 * pseudopotential directory, returns a fully populated PseudopotentialSet.
8 * Also patches each species' valence_charge from the loaded PP data, and
9 * reports the functional the datasets were generated with for an input that
10 * names none.
11 */
12
13#include <filesystem>
14#include <stdexcept>
15#include <string>
16#include <vector>
17
18#include "io/logger.hpp"
23
24namespace q3 {
25
26/**
27 * Resolve the full filesystem path for a pseudopotential file.
28 *
29 * Handles three cases:
30 * 1. Absolute path — use as-is
31 * 2. Already prepended with the PP directory — use as-is
32 * 3. Relative path — prepend the PP directory
33 */
34inline std::string resolve_pseudopotential_path(const std::string& pseudopotential_file,
35 const std::string& pseudopotential_directory) {
36
37 if (std::filesystem::path(pseudopotential_file).is_absolute()) {
38 return pseudopotential_file;
39 }
40
41 bool already_prepended = false;
42 if (!pseudopotential_directory.empty() && pseudopotential_directory != "." &&
43 pseudopotential_directory != "./") {
44 std::string normalized_directory = pseudopotential_directory;
45 if (normalized_directory.back() == '/') {
46 normalized_directory.pop_back();
47 }
48 if (pseudopotential_file.rfind(normalized_directory, 0) == 0) {
49 already_prepended = true;
50 }
51 }
52
53 if (already_prepended || std::filesystem::exists(pseudopotential_file)) {
54 return pseudopotential_file;
55 }
56
57 std::string full_path = pseudopotential_directory;
58 if (!full_path.empty() && full_path.back() != '/') {
59 full_path += '/';
60 }
61 full_path += pseudopotential_file;
62 return full_path;
63}
64
65/**
66 * Which dataset inside its file a species is.
67 *
68 * An input format may point every species at one file of concatenated
69 * datasets, joined in species order. So the Nth species naming a given path
70 * takes the Nth dataset in it, which needs no extra field on AtomSpecies.
71 * Every other format names one
72 * file per species, so this stays 0 and nothing changes.
73 */
74inline int dataset_index_within_file(const std::vector<AtomSpecies>& species,
75 int species_index,
76 const std::string& pseudopotential_directory) {
77 const std::string pseudopotential_path = resolve_pseudopotential_path(
78 species[species_index].pseudopotential_file, pseudopotential_directory);
79 int dataset_index = 0;
80 for (int earlier = 0; earlier < species_index; ++earlier) {
81 if (resolve_pseudopotential_path(species[earlier].pseudopotential_file,
82 pseudopotential_directory) == pseudopotential_path)
83 dataset_index++;
84 }
85 return dataset_index;
86}
87
88/**
89 * The functional the pseudopotentials were generated with: what a run whose
90 * input names no functional uses.
91 *
92 * This is pw.x's documented rule, where `input_dft` overrides the functional
93 * read from the pseudopotential files and without it the files decide. Q3 used
94 * to fall back on a default of its own, Perdew-Zunger LDA, which ran PBE
95 * datasets as LDA: 127 meV/atom away on the silicon example, converged, and
96 * silent.
97 *
98 * Every dataset must state a functional that functional_from_pseudopotential_label
99 * maps, and all of them must agree. Otherwise this throws and names the
100 * dataset, because a total energy from datasets of different functionals
101 * belongs to none of them. An input can always settle the question by naming
102 * the functional itself.
103 *
104 * Returns empty when some species names no dataset at all, such as a deck
105 * whose dataset file is missing: nothing states a functional, and the
106 * missing dataset is reported where pseudopotentials are loaded. Nothing runs
107 * on an empty name, since the exchange-correlation factory refuses it.
108 */
109inline std::string functional_from_pseudopotentials(const std::vector<AtomSpecies>& species,
110 const std::string& pseudopotential_directory) {
111 const std::string remedy = " Name the functional in the input: model.functional in a YAML "
112 "deck, input_dft in a QE-format deck, or the functional tag "
113 "of the format in use.";
114 if (species.empty())
115 return {};
116 for (const auto& one_species : species) {
117 if (one_species.pseudopotential_file.empty())
118 return {};
119 }
120
121 std::string resolved_functional;
122 std::string resolved_from;
123 for (int species_index = 0; species_index < static_cast<int>(species.size()); ++species_index) {
124 const std::string pseudopotential_path = resolve_pseudopotential_path(
125 species[species_index].pseudopotential_file, pseudopotential_directory);
126 const std::string source =
127 pseudopotential_path + " (" + species[species_index].symbol + ")";
128
129 std::string label;
130 try {
131 label =
133 pseudopotential_path,
134 dataset_index_within_file(species, species_index, pseudopotential_directory))
135 ->xc_functional;
136 } catch (const std::exception& error) {
137 throw std::runtime_error(
138 "The input names no exchange-correlation functional, so Q3 takes it from the "
139 "pseudopotentials, and " +
140 source + " cannot be read: " + error.what());
141 }
142
143 const std::string functional = functional_from_pseudopotential_label(label);
144 if (functional.empty())
145 throw std::runtime_error(
146 "The input names no exchange-correlation functional, and the pseudopotential " +
147 source +
148 (label.empty()
149 ? std::string(" states none")
150 : " states '" + label + "', which Q3 does not map to a functional it runs") +
151 "." + remedy);
152
153 if (resolved_functional.empty()) {
154 resolved_functional = functional;
155 resolved_from = source;
156 } else if (functional != resolved_functional) {
157 throw std::runtime_error(
158 "The input names no exchange-correlation functional, and its pseudopotentials "
159 "were generated with different ones: " +
160 resolved_from + " states " + resolved_functional + ", " + source + " states " +
161 functional + ". Q3 will not pick one, because the result would belong to neither." +
162 remedy);
163 }
164 }
165
166 log::info(" Exchange-correlation functional: {}, as the pseudopotentials state (the "
167 "input names none)",
168 resolved_functional);
169 return resolved_functional;
170}
171
172/**
173 * Load pseudopotentials for all species and patch their valence_charge.
174 *
175 * Resolves pseudopotential file paths, reads each dataset, and updates
176 * each species' valence_charge from the loaded pseudopotential data.
177 *
178 * Reads every registered dataset format alike — the format is chosen from the
179 * file's own first line (PseudopotentialFactory::read), so a dataset named in
180 * an input works without the user saying which format it is.
181 *
182 * This is the one place pseudopotentials enter a calculation. It used to
183 * refuse PAW datasets, because the downstream factories dispatched on
184 * `pseudopotential_type_enum() == ultrasoft` and a PAW file that got this far
185 * would have been computed as if it were norm-conserving. That guard came out
186 * when the PAW SCF iterator landed: the factories now dispatch on capability,
187 * and PAW has its own iterator branch.
188 *
189 * Returns the loaded PseudopotentialSet.
190 */
191inline PseudopotentialSet load_pseudopotentials(std::vector<AtomSpecies>& species,
192 const std::string& pseudopotential_directory) {
193
194 PseudopotentialSet pseudopotential_set;
195
196 for (int species_index = 0; species_index < static_cast<int>(species.size()); ++species_index) {
197 std::string pseudopotential_path = resolve_pseudopotential_path(
198 species[species_index].pseudopotential_file, pseudopotential_directory);
199 int dataset_index =
200 dataset_index_within_file(species, species_index, pseudopotential_directory);
201
202 log::info(" Reading PP: {}{}",
203 pseudopotential_path,
204 dataset_index > 0 ? " (dataset " + std::to_string(dataset_index + 1) + ")" : "");
205 auto pseudopotential = PseudopotentialFactory::read(pseudopotential_path, dataset_index);
206
207 const auto* paw_pseudopotential =
208 dynamic_cast<const PawPseudopotential*>(pseudopotential.get());
209 if (paw_pseudopotential != nullptr && paw_pseudopotential->has_core_energy) {
210 throw std::runtime_error(
211 "PAW UPF dataset " + pseudopotential_path +
212 " declares a core_energy reference constant, which Q3 does not yet apply. "
213 "Use a supported PAW dataset without core_energy; refusing "
214 "this dataset prevents a silently wrong absolute energy and unstable SCF.");
215 }
216
217 // Patch valence_charge from loaded pseudopotential data
218 species[species_index].valence_charge = pseudopotential->valence_charge;
219
220 pseudopotential_set.push_back(std::move(pseudopotential));
221 }
222
223 return pseudopotential_set;
224}
225
226} // namespace q3
PAW pseudopotential — extends ultrasoft with one-center (on-site) data.
Definition init.hpp:33
static std::unique_ptr< Pseudopotential > read(const std::string &filename, int dataset_index=0)
Definition factory.hpp:38
void push_back(std::unique_ptr< Pseudopotential > pseudopotential)
Move a unique_ptr into the set.
Definition set.hpp:39
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
PseudopotentialSet load_pseudopotentials(std::vector< AtomSpecies > &species, const std::string &pseudopotential_directory)
Definition loader.hpp:191
int dataset_index_within_file(const std::vector< AtomSpecies > &species, int species_index, const std::string &pseudopotential_directory)
Definition loader.hpp:74
std::string functional_from_pseudopotential_label(const std::string &label)
The functional a pseudopotential label names, or empty when it names none Q3 maps.
std::string functional_from_pseudopotentials(const std::vector< AtomSpecies > &species, const std::string &pseudopotential_directory)
Definition loader.hpp:109
std::string resolve_pseudopotential_path(const std::string &pseudopotential_file, const std::string &pseudopotential_directory)
Definition loader.hpp:34