Loading...
Searching...
No Matches
cube.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * CubeVolumetricFieldWriter — the Gaussian cube format.
5 *
6 * The format VESTA, XCrySDen, VMD, ASE and pymatgen all read, which is why it
7 * is the first one Q3 writes. Implemented from the published description of
8 * the format (Gaussian's "cubegen" documentation and the many public
9 * restatements of it); no reference implementation was consulted.
10 *
11 * Layout, one line each:
12 *
13 * comment (two free-text lines)
14 * natoms origin_x origin_y origin_z
15 * n1 a1/n1 (three Cartesian components)
16 * n2 a2/n2
17 * n3 a3/n3
18 * Z charge x y z (natoms of these)
19 * values, six per line
20 *
21 * **Conventions this writer chose, and the reasons.**
22 *
23 * - *Bohr, and electrons/bohr^3.* A cube's lengths are bohr when the three
24 * voxel counts are positive and Ångström when they are negative; this
25 * writer always writes them positive and always writes bohr, which is the
26 * near-universal convention and also the unit the engine already works in,
27 * so nothing is converted and nothing can be converted twice. A density's
28 * samples are then electrons/bohr^3 and
29 * `Omega / (n1 n2 n3) * sum(values)` is the electron count —
30 * `tests/unit/io/volumetric/test_cube.cpp` asserts exactly that, reading
31 * it back out of the written file.
32 * - *The third axis varies fastest.* The value loop is `i1` outermost, `i3`
33 * innermost, which is the cube format's own requirement and, as it
34 * happens, the order VolumetricField already stores. A file written in any
35 * other order still opens, still looks like a crystal, and is wrong;
36 * `test_cube.cpp` pins the axis order with an asymmetric field whose value
37 * identifies its own grid point.
38 * - *No duplicated boundary.* The grid is periodic and each point appears
39 * once; the point at fractional coordinate 1 is the point at 0. (XSF
40 * differs — see xsf.hpp.)
41 * - *The atom "charge" column carries the valence charge*, not Z, because
42 * that is what the density in the file actually integrates to for a
43 * pseudopotential calculation. The atomic-number column is Z as usual.
44 *
45 * **Precision is wider than the conventional field widths**, deliberately, and
46 * the reason is the electron count. The traditional layout writes the header
47 * in `%12.6f` and the data in `%13.5E`; both are free-format in the format's
48 * own description, and every reader in use splits on whitespace, so widening
49 * them costs nothing and buys back two errors that are otherwise invisible.
50 * Six decimals on a voxel vector round silicon's 0.1435333800 bohr to
51 * 0.143533, which is a 7.5e-6 relative error in the *cell volume* and
52 * therefore in every quantity integrated out of the file: a density holding
53 * exactly eight electrons reads back as 7.99994. Five significant digits on
54 * the samples would add a comparable error of its own. The header is written
55 * with twelve decimals and the data with ten significant digits, which puts
56 * both below 1e-9 — measured by `tests/regression/test_volumetric_export.py`,
57 * which integrates a real silicon density out of the written file.
58 */
59
60#include <fstream>
61#include <iomanip>
62#include <stdexcept>
63#include <string>
64
67
68namespace q3 {
69
71 public:
72 void write(const std::string& path, const VolumetricField& field) const override {
73 std::ofstream stream(path);
74 if (!stream)
75 throw std::runtime_error("Cannot write cube file: " + path);
76
77 const auto& dimensions = field.grid_dimensions;
78 stream << "Q3 " << field.description << "\n";
79 stream << "units: bohr, " << field.unit << "; grid " << dimensions[0] << "x"
80 << dimensions[1] << "x" << dimensions[2] << ", third axis fastest\n";
81
82 stream << std::fixed << std::setprecision(header_decimals);
83 stream << std::setw(5) << field.number_of_atoms() << std::setw(header_width) << 0.0
84 << std::setw(header_width) << 0.0 << std::setw(header_width) << 0.0 << "\n";
85 for (int axis = 0; axis < 3; axis++) {
86 stream << std::setw(5) << dimensions[axis];
87 for (int component = 0; component < 3; component++) {
88 stream << std::setw(header_width)
89 << field.lattice_vectors_bohr[axis][component] / dimensions[axis];
90 }
91 stream << "\n";
92 }
93 for (int atom_index = 0; atom_index < field.number_of_atoms(); atom_index++) {
94 const Vector3D position = field.cartesian_position_bohr(atom_index);
95 const int species_index = field.atom_species_indices[atom_index];
96 stream << std::setw(5) << field.atomic_number(atom_index) << std::setw(header_width)
97 << field.species_valence_charges[species_index] << std::setw(header_width)
98 << position.x << std::setw(header_width) << position.y << std::setw(header_width)
99 << position.z << "\n";
100 }
101
102 stream << std::scientific << std::setprecision(10);
103 int values_on_line = 0;
104 for (int index_1 = 0; index_1 < dimensions[0]; index_1++) {
105 for (int index_2 = 0; index_2 < dimensions[1]; index_2++) {
106 for (int index_3 = 0; index_3 < dimensions[2]; index_3++) {
107 stream << std::setw(18) << field.value(index_1, index_2, index_3);
108 if (++values_on_line == values_per_line) {
109 stream << "\n";
110 values_on_line = 0;
111 }
112 }
113 /**
114 * A line break after each run along the fastest axis is what
115 * every cube writer does, and what makes a file readable by
116 * eye as a stack of z-columns.
117 */
118 if (values_on_line != 0) {
119 stream << "\n";
120 values_on_line = 0;
121 }
122 }
123 }
124
125 stream.flush();
126 if (!stream)
127 throw std::runtime_error("Failed while writing cube file: " + path);
128 }
129
130 [[nodiscard]] std::string default_file_name(const std::string& stem) const override {
131 return stem + ".cube";
132 }
133
134 private:
135 static constexpr int values_per_line = 6;
136 /// See the header: twelve decimals keep the cell volume exact to 1e-11.
137 static constexpr int header_decimals = 12;
138 static constexpr int header_width = 20;
139};
140
141} // namespace q3
void write(const std::string &path, const VolumetricField &field) const override
Write field to path, throwing std::runtime_error if it cannot be written.
Definition cube.hpp:72
std::string default_file_name(const std::string &stem) const override
Definition cube.hpp:130
Vector3D cartesian_position_bohr(int atom_index) const
Cartesian position of one atom, in bohr.
Definition field.hpp:111
int number_of_atoms() const
Definition field.hpp:78
std::string unit
The unit values is in, e.g. "e/bohr^3".
Definition field.hpp:60
Matrix3x3 lattice_vectors_bohr
Rows are a1, a2, a3, Cartesian, in bohr.
Definition field.hpp:44
std::vector< int > atom_species_indices
Species of each atom, an index into species_symbols.
Definition field.hpp:51
std::vector< Real > species_valence_charges
Valence charge of each species, parallel to species_symbols.
Definition field.hpp:49
std::string description
Free text for a format's comment line, e.g. "charge density".
Definition field.hpp:58
Real value(int index_1, int index_2, int index_3) const
The sample at grid point (i1, i2, i3), third index fastest.
Definition field.hpp:87
int atomic_number(int atom_index) const
Atomic number of one atom, from its element symbol.
Definition field.hpp:123
std::array< int, 3 > grid_dimensions
Grid point counts along a1, a2, a3.
Definition field.hpp:42