Loading...
Searching...
No Matches
cube_reader.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * CubeVolumetricFieldReader — read a Gaussian cube file back into a
5 * VolumetricField.
6 *
7 * The other half of cube.hpp. It exists so that what a writer claims can be
8 * checked against what a reader sees rather than against the array the writer
9 * was handed: `tests/unit/io/volumetric/test_cube.cpp` integrates the density
10 * **out of the written file** and compares it against the electron count, and
11 * asserts the grid came back in the same axis order it went out in. A writer
12 * whose only check is its own in-memory input cannot catch a transposed grid
13 * or a missed unit, which are exactly the two defects that produce a file
14 * looking correct in a viewer.
15 *
16 * It also makes a cube from elsewhere readable: the format's two signed fields
17 * are honoured, so a file written in Ångström (negative voxel counts) comes
18 * back in bohr like any other.
19 *
20 * Not honoured, and refused rather than misread: a negative atom count, which
21 * marks a file carrying several datasets (molecular orbitals) with an extra
22 * index record this reader would otherwise swallow as field values.
23 */
24
25#include <fstream>
26#include <sstream>
27#include <stdexcept>
28#include <string>
29#include <vector>
30
34
35namespace q3 {
36
38 public:
39 [[nodiscard]] VolumetricField read(const std::string& path) const {
40 std::ifstream stream(path);
41 if (!stream)
42 throw std::runtime_error("Cannot read cube file: " + path);
43
44 VolumetricField field;
45 std::string first_comment;
46 std::string second_comment;
47 std::getline(stream, first_comment);
48 std::getline(stream, second_comment);
49 field.description = first_comment;
50
51 int number_of_atoms = 0;
52 Vector3D origin;
53 read_line_values(stream, path, number_of_atoms, origin);
54 if (number_of_atoms < 0) {
55 throw std::runtime_error(
56 "Cube file '" + path +
57 "' declares a negative atom count, which marks a multi-dataset (orbital) cube. "
58 "Q3 reads single-field cubes only.");
59 }
60
61 for (int axis = 0; axis < 3; axis++) {
62 int voxel_count = 0;
63 Vector3D voxel;
64 read_line_values(stream, path, voxel_count, voxel);
65 /**
66 * A negative voxel count is the format's way of saying the lengths
67 * on this file's header lines are Ångström rather than bohr.
68 */
69 const Real to_bohr = voxel_count < 0 ? constants::ANGSTROM_TO_BOHR : 1.0;
70 const int dimension = voxel_count < 0 ? -voxel_count : voxel_count;
71 if (dimension <= 0)
72 throw std::runtime_error("Cube file '" + path + "' has a zero grid dimension.");
73 field.grid_dimensions[axis] = dimension;
74 for (int component = 0; component < 3; component++)
75 field.lattice_vectors_bohr[axis][component] =
76 voxel[component] * to_bohr * dimension;
77 }
78
79 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
80 std::istringstream line = next_content_line(stream, path);
81 int atomic_number = 0;
82 Real charge = 0.0;
83 Vector3D position;
84 line >> atomic_number >> charge >> position.x >> position.y >> position.z;
85 if (!line)
86 throw std::runtime_error("Cube file '" + path + "' ends inside its atom list.");
87 field.atom_species_indices.push_back(species_index_for(field, atomic_number, charge));
88 field.atom_positions_crystal.push_back(
89 to_crystal_coordinates(field.lattice_vectors_bohr, position));
90 }
91
92 const int number_of_grid_points = field.number_of_grid_points();
93 field.values.reserve(number_of_grid_points);
94 Real value = 0.0;
95 while (static_cast<int>(field.values.size()) < number_of_grid_points && stream >> value)
96 field.values.push_back(value);
97 if (static_cast<int>(field.values.size()) != number_of_grid_points) {
98 throw std::runtime_error("Cube file '" + path + "' holds " +
99 std::to_string(field.values.size()) + " values but declares " +
100 std::to_string(number_of_grid_points) + ".");
101 }
102 return field;
103 }
104
105 private:
106 /// Read one line as an integer followed by three reals.
107 static void
108 read_line_values(std::ifstream& stream, const std::string& path, int& count, Vector3D& vector) {
109 std::istringstream line = next_content_line(stream, path);
110 line >> count >> vector.x >> vector.y >> vector.z;
111 if (!line)
112 throw std::runtime_error("Cube file '" + path + "' has a malformed header line.");
113 }
114
115 static std::istringstream next_content_line(std::ifstream& stream, const std::string& path) {
116 std::string text;
117 while (std::getline(stream, text)) {
118 if (text.find_first_not_of(" \t\r") != std::string::npos)
119 return std::istringstream(text);
120 }
121 throw std::runtime_error("Cube file '" + path + "' ends before its header does.");
122 }
123
124 /// Find or append the species an atom line names, keeping species order stable.
125 static int species_index_for(VolumetricField& field, int atomic_number, Real valence_charge) {
126 const std::string& symbol = PeriodicTable::element_symbol_by_atomic_number(atomic_number);
127 for (std::size_t index = 0; index < field.species_symbols.size(); index++) {
128 if (field.species_symbols[index] == symbol)
129 return static_cast<int>(index);
130 }
131 field.species_symbols.push_back(symbol);
132 field.species_valence_charges.push_back(valence_charge);
133 return static_cast<int>(field.species_symbols.size()) - 1;
134 }
135
136 static Vector3D to_crystal_coordinates(const Matrix3x3& lattice_vectors_bohr,
137 const Vector3D& cartesian) {
138 /**
139 * Rows of the lattice matrix are a1, a2, a3, so the fractional
140 * coordinates are `r * A^-1` with r a row vector.
141 */
142 const Matrix3x3 inverse = lattice_vectors_bohr.inverse();
143 Vector3D fractional;
144 for (int axis = 0; axis < 3; axis++) {
145 fractional[axis] = cartesian.x * inverse[0][axis] + cartesian.y * inverse[1][axis] +
146 cartesian.z * inverse[2][axis];
147 }
148 return fractional;
149 }
150};
151
152} // namespace q3
VolumetricField read(const std::string &path) const
static const std::string & element_symbol_by_atomic_number(int atomic_number)
constexpr Real ANGSTROM_TO_BOHR
Ångström → Bohr (ANGSTROM_AU)
double Real
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
int number_of_grid_points() const
Definition field.hpp:74
std::vector< Vector3D > atom_positions_crystal
Fractional (crystal) coordinates, parallel to atom_species_indices.
Definition field.hpp:53
std::string description
Free text for a format's comment line, e.g. "charge density".
Definition field.hpp:58
std::vector< Real > values
The samples themselves, number_of_grid_points() of them.
Definition field.hpp:56
std::array< int, 3 > grid_dimensions
Grid point counts along a1, a2, a3.
Definition field.hpp:42