Loading...
Searching...
No Matches
volumetric_command.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * VolumetricCommand — `q3_utility volumetric <input.yml>`: a finished run's saved
5 * fields as a file a visualizer reads.
6 *
7 * The density, the effective potential and a single band all live on the same
8 * real-space grid in a run's `<prefix>.save/` directory, and until now nothing
9 * could get them out of it. This turns them into a Gaussian cube, an XCrySDen
10 * `.xsf` or a format an add-on registers, without rerunning anything — Q3's analogue of Quantum
11 * ESPRESSO's `pp.x`, and the same shape as the other `q3_utility` subcommands: a
12 * converter over saved records, never part of the SCF path.
13 *
14 * q3_utility volumetric <input.yml> [--field density|potential|wavefunction]
15 * [--format cube|xsf|<add-on format>]
16 * [--spin 1|2] [--kpoint K] [--band N]
17 * [--outdir <dir>] [--output <file>]
18 *
19 * The input is the manifest the run was launched with: its `control.outdir`
20 * and `control.prefix` say which save directory to read, and its cell and
21 * cutoffs say what grid the saved arrays are on. That is the whole reason the
22 * command takes a manifest rather than a directory — the save directory holds
23 * the numbers but not the cell they belong to, and a volumetric file is
24 * meaningless without one. `q3_utility interpolate` reads its input the same way
25 * and for the same reason.
26 *
27 * Where each field comes from:
28 *
29 * - `density` reads `charge-density.dat` — rho(G) — and transforms it onto
30 * the grid. That file is the one a Quantum ESPRESSO run also writes, so
31 * the same command converts a QE density (see the reader's header for the
32 * two dialects). A spin-polarized Q3 run writes the two channels summed
33 * into that one file, so `--spin 2` has nothing to select there and says
34 * so; the per-channel arrays live in `effective-potential.q3cp`, which is
35 * what `potential` reads.
36 * - `potential` reads `effective-potential.q3cp`, which stores V_eff(r)
37 * directly; no transform is involved.
38 * - `wavefunction` reads `wfc<k>.dat` and writes |psi_nk(r)|^2, normalized so
39 * that its integral over the cell is 1. The band amplitude itself is
40 * complex at a general k-point and has no single real value to plot, so
41 * the probability density is what is written — and its integral is a
42 * number worth checking, which a phase-dependent quantity would not be.
43 */
44
45#include <algorithm>
46#include <array>
47#include <cmath>
48#include <complex>
49#include <cstddef>
50#include <filesystem>
51#include <stdexcept>
52#include <string>
53#include <vector>
54
56#include "io/input/data.hpp"
59#include "io/logger.hpp"
68
69namespace q3 {
70
72 public:
73 static void print_usage() {
74 log::error("Usage: q3_utility volumetric <input.yml> [--field <name>] [--format <name>] "
75 "[--spin <n>] [--kpoint <k>] [--band <n>] [--outdir <dir>] [--output <file>]");
76 log::error(" <input.yml> the manifest the run used; its control.outdir/prefix name the "
77 "save directory, and its cell and cutoffs the grid");
78 log::error(" --field density (default), potential, or wavefunction");
79 log::error(" --format {} (cube is the default)",
80 volumetric_field_writer_registry().canonical_names(", "));
81 log::error(" --spin spin channel, 1-based (default 1)");
82 log::error(" --kpoint which wfc<k>.dat to read, 1-based (default 1)");
83 log::error(" --band which band of it, 1-based (default 1)");
84 log::error(" --outdir where to write (default: the run's own outdir)");
85 log::error(" --output the full output path, overriding --outdir and the default "
86 "file name");
87 }
88
89 /// Returns the process exit code; argument errors print usage, everything else throws.
90 static int run(const std::vector<std::string>& arguments) {
91 std::string input_file;
92 std::string field_name = "density";
93 std::string format = "cube";
94 std::string output_directory;
95 std::string output_file;
96 int spin_channel = 1;
97 int kpoint_number = 1;
98 int band_number = 1;
99
100 for (std::size_t index = 0; index < arguments.size(); index++) {
101 const std::string& argument = arguments[index];
102 auto take_value = [&](const char* flag) -> std::string {
103 if (index + 1 >= arguments.size())
104 throw std::runtime_error(std::string(flag) + " needs a value");
105 return arguments[++index];
106 };
107 if (argument == "--field") {
108 field_name = take_value("--field");
109 } else if (argument == "--format") {
110 format = take_value("--format");
111 } else if (argument == "--spin") {
112 spin_channel = parse_positive_integer("--spin", take_value("--spin"));
113 } else if (argument == "--kpoint") {
114 kpoint_number = parse_positive_integer("--kpoint", take_value("--kpoint"));
115 } else if (argument == "--band") {
116 band_number = parse_positive_integer("--band", take_value("--band"));
117 } else if (argument == "--outdir") {
118 output_directory = take_value("--outdir");
119 } else if (argument == "--output") {
120 output_file = take_value("--output");
121 } else if (argument == "-h" || argument == "--help") {
122 print_usage();
123 return 0;
124 } else if (argument.rfind("-", 0) == 0) {
125 log::error("Unknown option '{}'", argument);
126 print_usage();
127 return 1;
128 } else if (input_file.empty()) {
129 input_file = argument;
130 } else {
131 log::error("Unexpected extra argument '{}'", argument);
132 print_usage();
133 return 1;
134 }
135 }
136 if (input_file.empty()) {
137 print_usage();
138 return 1;
139 }
140
141 const InputData input = parse_input(input_file);
142 const std::string save_directory = input.output_directory + "/" + input.prefix + ".save";
143 if (!std::filesystem::is_directory(save_directory)) {
144 throw std::runtime_error(
145 "no save directory at '" + save_directory +
146 "'. It is written by the run this input describes; point control.outdir and "
147 "control.prefix at a run that has finished.");
148 }
149
150 const Crystal crystal = build_crystal(input);
151 const std::array<int, 3> grid_dimensions = charge_density_grid_dimensions(input, crystal);
152 log::info("Reading {} on a {}x{}x{} grid from {}",
153 field_name,
154 grid_dimensions[0],
155 grid_dimensions[1],
156 grid_dimensions[2],
157 save_directory);
158
159 VolumetricField field = build_field(field_name,
160 save_directory,
161 crystal,
162 grid_dimensions,
163 spin_channel,
164 kpoint_number,
165 band_number);
166
167 auto writer = create_volumetric_field_writer(format);
168 std::string path = output_file;
169 if (path.empty()) {
170 const std::string directory =
171 output_directory.empty() ? input.output_directory : output_directory;
172 std::filesystem::create_directories(directory);
173 path = directory + "/" + writer->default_file_name(input.prefix + "_" + field_name);
174 }
175 writer->write(path, field);
176
177 log::info("Wrote {} ({}) to {}", field.description, format, path);
178 /**
179 * The integral is the cheapest statement that the file is what it
180 * says: for a density it is the valence electron count, and for a
181 * band's probability density it is 1.
182 */
183 log::info(" integral over the cell: {:.8f} {} (cell volume {:.6f} bohr^3)",
184 field.integral(),
185 field.is_a_density ? std::string("electrons") : field.unit + "*bohr^3",
186 field.cell_volume_bohr3());
187 return 0;
188 }
189
190 private:
191 static int parse_positive_integer(const char* flag, const std::string& text) {
192 std::size_t consumed = 0;
193 int value = 0;
194 try {
195 value = std::stoi(text, &consumed);
196 } catch (const std::exception&) {
197 consumed = 0;
198 }
199 if (consumed != text.size() || value <= 0)
200 throw std::runtime_error(std::string(flag) + " expects a positive integer, got '" +
201 text + "'");
202 return value;
203 }
204
205 /**
206 * The crystal the run used, with each species' valence charge filled in
207 * from its pseudopotential.
208 *
209 * The valence charges only decorate the output — a cube's per-atom charge
210 * column, and the electron count the log reports — so a dataset that
211 * cannot be reached leaves them zero and the field is written anyway.
212 * Refusing to convert a density because a pseudopotential moved would be a
213 * poor trade.
214 */
215 static Crystal build_crystal(const InputData& input) {
216 std::vector<AtomSpecies> species = input.species;
217 try {
218 load_pseudopotentials(species, input.pseudopotential_directory);
219 } catch (const std::exception& error) {
220 log::warn("Could not read this run's pseudopotentials ({}); the atoms' valence "
221 "charges will be written as zero.",
222 error.what());
223 species = input.species;
224 }
225 return build_crystal_from_input(input, species);
226 }
227
228 /// The dense (charge-density) FFT grid, sized exactly as the run sized it.
229 static std::array<int, 3> charge_density_grid_dimensions(const InputData& input,
230 const Crystal& crystal) {
231 const Real cutoff = input.charge_density_energy_cutoff > 0.0
232 ? input.charge_density_energy_cutoff
233 : 4.0 * input.wavefunction_energy_cutoff;
234 int fft_factors[3];
235 FFTGridSizer::compute_fft_factors(crystal, fft_factors);
236 std::array<int, 3> dimensions = {0, 0, 0};
238 crystal.two_pi_over_lattice_parameter_squared(),
239 crystal.lattice_vectors(),
240 crystal.reciprocal_lattice_vectors(),
241 dimensions[0],
242 dimensions[1],
243 dimensions[2],
244 fft_factors);
245 return dimensions;
246 }
247
248 /// Everything the formats share: the cell, the atoms and the grid shape.
249 static VolumetricField empty_field(const Crystal& crystal,
250 const std::array<int, 3>& grid_dimensions) {
251 VolumetricField field;
252 field.grid_dimensions = grid_dimensions;
253 field.lattice_vectors_bohr = crystal.lattice_vectors_bohr();
254 for (const auto& species : crystal.species()) {
255 field.species_symbols.push_back(species.symbol);
256 field.species_valence_charges.push_back(species.valence_charge);
257 }
258 for (int atom_index = 0; atom_index < crystal.number_of_atoms(); atom_index++) {
259 field.atom_species_indices.push_back(crystal.species_type_index(atom_index));
260 field.atom_positions_crystal.push_back(crystal.fractional_position(atom_index));
261 }
262 return field;
263 }
264
265 static VolumetricField build_field(const std::string& field_name,
266 const std::string& save_directory,
267 const Crystal& crystal,
268 const std::array<int, 3>& grid_dimensions,
269 int spin_channel,
270 int kpoint_number,
271 int band_number) {
272 if (field_name == "density")
273 return build_density(save_directory, crystal, grid_dimensions, spin_channel);
274 if (field_name == "potential")
275 return build_potential(save_directory, crystal, grid_dimensions, spin_channel);
276 if (field_name == "wavefunction") {
277 return build_band_probability_density(
278 save_directory, crystal, grid_dimensions, kpoint_number, band_number);
279 }
280 throw std::runtime_error("Unknown field '" + field_name +
281 "'. Supported: density, potential, wavefunction");
282 }
283
284 static VolumetricField build_density(const std::string& save_directory,
285 const Crystal& crystal,
286 const std::array<int, 3>& grid_dimensions,
287 int spin_channel) {
288 const std::string path = save_directory + "/charge-density.dat";
289 if (!std::filesystem::exists(path))
290 throw std::runtime_error("no charge density at '" + path + "'.");
291 QEPlanewaveKohnShamChargeDensityReader reader;
292 reader.read(path);
293 if (spin_channel > reader.number_of_spin_components()) {
294 throw std::runtime_error("'" + path + "' holds " +
295 std::to_string(reader.number_of_spin_components()) +
296 " spin channel(s); --spin " + std::to_string(spin_channel) +
297 " asks for one that is not there.");
298 }
299
300 require_grid_holds(reader.miller_indices(), grid_dimensions, path);
301
302 VolumetricField field = empty_field(crystal, grid_dimensions);
303 field.values = real_field_from_reciprocal_space(grid_dimensions,
304 reader.miller_indices(),
305 reader.charge_density_g()[spin_channel - 1],
306 reader.is_gamma_only());
307 field.description = "charge density";
308 field.unit = "e/bohr^3";
309 field.is_a_density = true;
310 return field;
311 }
312
313 static VolumetricField build_potential(const std::string& save_directory,
314 const Crystal& crystal,
315 const std::array<int, 3>& grid_dimensions,
316 int spin_channel) {
317 const std::string path = save_directory + "/effective-potential.q3cp";
318 if (!std::filesystem::exists(path))
319 throw std::runtime_error("no effective potential at '" + path + "'.");
320 KohnShamQuantumSystemSnapshot snapshot;
321 Q3PlanewaveKohnShamQuantumSystemCheckpoint checkpoint;
322 if (!checkpoint.load(path, snapshot))
323 throw std::runtime_error("'" + path + "' could not be read as a Q3 checkpoint.");
324 if (spin_channel > static_cast<int>(snapshot.effective_potential_real.size())) {
325 throw std::runtime_error(
326 "'" + path + "' holds " + std::to_string(snapshot.effective_potential_real.size()) +
327 " potential channel(s); --spin " + std::to_string(spin_channel) +
328 " asks for one that is not there.");
329 }
330
331 VolumetricField field = empty_field(crystal, grid_dimensions);
332 field.values = snapshot.effective_potential_real[spin_channel - 1];
333 require_grid_size(field, path, "effective potential");
334 field.description = "effective potential";
335 field.unit = "Ry";
336 return field;
337 }
338
339 /**
340 * |psi_nk(r)|^2, with `sum_G |c_G|^2 = 1` taken to mean the band is
341 * normalized over the cell.
342 *
343 * `inverse_raw` gives `w(r) = sum_G c_G exp(i G·r)` directly, so the
344 * amplitude is `psi(r) = w(r) / sqrt(Omega)` and the integral of |psi|^2
345 * over the cell comes back as `sum_G |c_G|^2` by Parseval — 1 for a
346 * normalized band, which is what `tests/python/test_volumetric_export.py`
347 * checks out of the written file. The `exp(i k·r)` factor is dropped: it
348 * has modulus one and cancels out of |psi|^2 exactly.
349 */
350 static VolumetricField build_band_probability_density(const std::string& save_directory,
351 const Crystal& crystal,
352 const std::array<int, 3>& grid_dimensions,
353 int kpoint_number,
354 int band_number) {
355 const std::string path = save_directory + "/wfc" + std::to_string(kpoint_number) + ".dat";
356 if (!std::filesystem::exists(path)) {
357 throw std::runtime_error("no wavefunction at '" + path +
358 "'. The save directory holds one wfc<k>.dat per k-point of "
359 "the run's irreducible wedge.");
360 }
361 QEPlanewaveKohnShamWavefunctionReader reader;
362 reader.read(path);
363 if (band_number > reader.number_of_bands()) {
364 throw std::runtime_error("'" + path + "' holds " +
365 std::to_string(reader.number_of_bands()) +
366 " band(s); --band " + std::to_string(band_number) +
367 " asks for one that is not there.");
368 }
369
370 require_grid_holds(reader.miller_indices(), grid_dimensions, path);
371
372 const std::vector<Complex> amplitude =
374 reader.miller_indices(),
375 reader.band_coefficients(band_number - 1),
376 reader.is_gamma_only());
377 VolumetricField field = empty_field(crystal, grid_dimensions);
378 const Real inverse_volume = 1.0 / crystal.cell_volume();
379 field.values.resize(amplitude.size());
380 for (std::size_t index = 0; index < amplitude.size(); index++)
381 field.values[index] = std::norm(amplitude[index]) * inverse_volume;
382 field.description = "band " + std::to_string(band_number) +
383 " probability density at "
384 "k-point " +
385 std::to_string(kpoint_number);
386 field.unit = "e/bohr^3";
387 field.is_a_density = true;
388 return field;
389 }
390
391 /**
392 * A saved G-vector set that does not fit the grid the input describes means
393 * the two do not belong together. The transform drops what does not fit
394 * rather than aliasing it, so the result would be a smooth, plausible field
395 * missing its highest components -- which is why this is checked here
396 * instead of left to look like a converged, slightly soft density.
397 */
398 static void require_grid_holds(const std::vector<IntegerVector3D>& miller_indices,
399 const std::array<int, 3>& grid_dimensions,
400 const std::string& path) {
401 std::array<int, 3> extent = {0, 0, 0};
402 for (const IntegerVector3D& miller : miller_indices) {
403 extent[0] = std::max(extent[0], std::abs(miller.x));
404 extent[1] = std::max(extent[1], std::abs(miller.y));
405 extent[2] = std::max(extent[2], std::abs(miller.z));
406 }
407 for (int axis = 0; axis < 3; axis++) {
408 if (extent[axis] <= grid_dimensions[axis] / 2)
409 continue;
410 throw std::runtime_error(
411 "'" + path + "' reaches Miller index " + std::to_string(extent[axis]) +
412 " along axis " + std::to_string(axis + 1) + ", which a " +
413 std::to_string(grid_dimensions[0]) + "x" + std::to_string(grid_dimensions[1]) +
414 "x" + std::to_string(grid_dimensions[2]) +
415 " grid cannot hold. This input's cell and cutoffs do not describe the run that "
416 "wrote that file.");
417 }
418 }
419
420 /**
421 * A saved real-space array whose length disagrees with the grid the input
422 * describes means the two do not belong together, and every number written
423 * from them would be silently misplaced.
424 */
425 static void require_grid_size(const VolumetricField& field,
426 const std::string& path,
427 const std::string& what) {
428 if (static_cast<int>(field.values.size()) == field.number_of_grid_points())
429 return;
430 throw std::runtime_error(
431 "the " + what + " in '" + path + "' has " + std::to_string(field.values.size()) +
432 " grid points, but this input describes a " + std::to_string(field.grid_dimensions[0]) +
433 "x" + std::to_string(field.grid_dimensions[1]) + "x" +
434 std::to_string(field.grid_dimensions[2]) + " grid (" +
435 std::to_string(field.number_of_grid_points()) +
436 " points). The input and the save directory are from different runs.");
437 }
438};
439
440} // namespace q3
static void compute_fft_factors(const Crystal &crystal_structure, int fft_factors[3])
static void calculate_grid_dimensions(Real energy_cutoff, Real two_pi_over_lattice_parameter_squared, const Matrix3x3 &direct_lattice, const Matrix3x3 &reciprocal_lattice, int &grid_dimension_1, int &grid_dimension_2, int &grid_dimension_3, const int fft_factors[3]=nullptr)
static int run(const std::vector< std::string > &arguments)
Returns the process exit code; argument errors print usage, everything else throws.
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
void error(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:192
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
Crystal build_crystal_from_input(const InputData &input, const std::vector< AtomSpecies > &species)
double Real
std::unique_ptr< VolumetricFieldWriter > create_volumetric_field_writer(const std::string &format)
Definition factory.hpp:41
Registry< NamedFactory< VolumetricFieldWriter > > & volumetric_field_writer_registry()
The registry create_volumetric_field_writer() builds writers from.
Definition factory.hpp:26
InputData parse_input(const std::string &filename, const std::string &format="")
Definition factory.hpp:122
std::vector< Real > real_field_from_reciprocal_space(const std::array< int, 3 > &grid_dimensions, const std::vector< IntegerVector3D > &miller_indices, const std::vector< Complex > &coefficients, bool holds_half_sphere)
The same transform for a field that is real by construction, e.g. a density.
std::vector< Complex > complex_field_from_reciprocal_space(const std::array< int, 3 > &grid_dimensions, const std::vector< IntegerVector3D > &miller_indices, const std::vector< Complex > &coefficients, bool holds_half_sphere)
Parsed input data — holds raw values read from input files.
Definition data.hpp:27
std::string prefix
Definition data.hpp:178
std::string output_directory
Definition data.hpp:177
std::string unit
The unit values is in, e.g. "e/bohr^3".
Definition field.hpp:60
Real integral() const
Definition field.hpp:103
std::string description
Free text for a format's comment line, e.g. "charge density".
Definition field.hpp:58
Real cell_volume_bohr3() const
Definition field.hpp:82