Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
reader.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Wannier90FormatReader — reads the Wannier90 interface files any plane-wave
5 * code can produce (`.mmn`, `.amn`, `.eig`, `.nnkp`) into a
6 * WannierInputs.
7 *
8 * This is what lets the localization engine be validated on matrices produced
9 * by ANOTHER code before it is ever run on Q3's own: feeding Quantum
10 * ESPRESSO's `pw2wannier90.x` output through this reader and into the
11 * minimizer establishes the minimizer's correctness independently of whether
12 * Q3's own overlap kernels are right. The two failure modes are then
13 * separable, which they would not be if the engine only ever saw Q3 data.
14 *
15 * Formats follow the Wannier90 user guide (v3.1.0, 5 March 2020), chapter
16 * "Interface with underlying electronic structure codes". Three conventions
17 * are load-bearing and are checked rather than assumed:
18 *
19 * - Every index in these files is 1-based (Fortran origin); this reader
20 * converts on the way in and range-checks first, so a writer's off-by-one
21 * surfaces as a thrown error rather than a silently shifted matrix.
22 * - A `.mmn` block stores its num_bands^2 elements with the FIRST index m
23 * varying fastest, i.e. column-major. Reading them row-major would
24 * transpose every block -- for a near-Hermitian overlap that survives
25 * inspection, so it must be got right here rather than noticed later.
26 * - `.eig` holds eigenvalues in ELECTRON VOLTS while Q3 works in Rydberg;
27 * this reader converts on the way in, since WannierInputs's field
28 * is documented as Rydberg.
29 *
30 * The mirror-image writers live in writer.hpp; a round-trip
31 * through both is one of this pair's tests, but the reader is deliberately
32 * written to the format spec rather than to the writer, so a shared
33 * misreading of the spec cannot cancel out between them.
34 */
35
36#include <algorithm>
37#include <cmath>
38#include <cstdint>
39#include <filesystem>
40#include <fstream>
41#include <sstream>
42#include <stdexcept>
43#include <string>
44#include <system_error>
45#include <vector>
46
50
51namespace q3 {
52
53namespace internal::wannier90_format {
54
55/**
56 * A header's declared element count must be reachable from the file's size:
57 * every element is one text line of at least `minimum_bytes_per_element`
58 * bytes, so a header claiming more than the file could hold is corrupt, and
59 * would otherwise become a huge allocation before the first element is read.
60 */
61inline void require_plausible_element_count(const std::string& file_path,
62 long long elements,
63 std::uintmax_t minimum_bytes_per_element) {
64 std::error_code error;
65 const std::uintmax_t size = std::filesystem::file_size(file_path, error);
66 if (!error && static_cast<long double>(elements) * minimum_bytes_per_element >
67 static_cast<long double>(size)) {
68 throw std::runtime_error(file_path + ": the header declares " + std::to_string(elements) +
69 " elements, more than a " + std::to_string(size) +
70 "-byte file can hold");
71 }
72}
73
74} // namespace internal::wannier90_format
75
77 public:
78 /**
79 * One (k, b) pair's neighbour identity, as `.mmn`/`.nnkp` record it: the
80 * physical k-point that is the periodic image of k+b, and the integer
81 * reciprocal-lattice shift carrying that image to the actual k+b.
82 */
87
88 struct OverlapData {
92 /**
93 * [kpoint_index][neighbor_index] -> one column-major M block of
94 * number_of_bands^2 entries, matching WannierInputs's own
95 * storage convention.
96 */
97 std::vector<std::vector<std::vector<Complex>>> overlaps;
98 std::vector<std::vector<NeighborEntry>> neighbors;
99 };
100
105 /// [kpoint_index] -> column-major A block, band index varying fastest.
106 std::vector<std::vector<Complex>> projections;
107 };
108
112 /// [kpoint_index][band_index], converted to Rydberg on the way in.
113 std::vector<std::vector<Real>> eigenvalues_rydberg;
114 };
115
117 /// Rows are a1, a2, a3 in Angstrom, as the `.nnkp` records them.
120 /// Crystal (fractional) coordinates, in the file's own k-point order.
121 std::vector<Vector3D> kpoints;
122 };
123
124 /// eV per Rydberg; `.eig` is eV, WannierInputs is Rydberg.
126
127 /**
128 * Read the `real_lattice`, `recip_lattice` and `kpoints` blocks of a
129 * `.nnkp`. The neighbour table itself is not read here: the interpolation
130 * path needs only the lattice (for the Wigner-Seitz construction) and the
131 * k-point list (for the Fourier transform), while the neighbour table is
132 * already carried by the `.mmn` whose overlaps it labels.
133 */
134 [[nodiscard]] static NeighborListData read_neighbor_list(const std::string& file_path) {
135 std::ifstream stream(file_path);
136 if (!stream) {
137 throw std::runtime_error("cannot open neighbour list file: " + file_path);
138 }
139 std::vector<std::string> lines;
140 std::string line;
141 while (std::getline(stream, line)) {
142 std::string trimmed = trim(line);
143 if (!trimmed.empty()) {
144 lines.push_back(trimmed);
145 }
146 }
147
148 NeighborListData data;
149 data.real_lattice = read_lattice_block(file_path, lines, "real_lattice");
150 data.reciprocal_lattice = read_lattice_block(file_path, lines, "recip_lattice");
151
152 std::vector<std::string> kpoint_body = extract_block(file_path, lines, "kpoints");
153 if (kpoint_body.empty()) {
154 throw std::runtime_error(file_path + ": 'kpoints' block is empty");
155 }
156 int declared_count = 0;
157 {
158 std::istringstream count_stream(kpoint_body[0]);
159 if (!(count_stream >> declared_count) || declared_count <= 0) {
160 throw std::runtime_error(file_path +
161 ": 'kpoints' block must open with a positive count");
162 }
163 }
164 if (static_cast<int>(kpoint_body.size()) - 1 != declared_count) {
165 throw std::runtime_error(file_path + ": 'kpoints' block declares " +
166 std::to_string(declared_count) + " k-points but holds " +
167 std::to_string(kpoint_body.size() - 1) + " rows");
168 }
169 data.kpoints.reserve(declared_count);
170 for (int index = 0; index < declared_count; index++) {
171 std::istringstream row_stream(kpoint_body[index + 1]);
172 Vector3D kpoint;
173 if (!(row_stream >> kpoint.x >> kpoint.y >> kpoint.z)) {
174 throw std::runtime_error(file_path +
175 ": malformed k-point row: " + kpoint_body[index + 1]);
176 }
177 data.kpoints.push_back(kpoint);
178 }
179 return data;
180 }
181
182 [[nodiscard]] static OverlapData read_overlaps(const std::string& file_path) {
183 std::ifstream stream(file_path);
184 if (!stream) {
185 throw std::runtime_error("cannot open overlap file: " + file_path);
186 }
187 std::string comment_line;
188 std::getline(stream, comment_line);
189
190 OverlapData data;
191 if (!(stream >> data.number_of_bands >> data.number_of_kpoints >>
192 data.number_of_neighbors)) {
193 throw std::runtime_error(file_path +
194 ": missing or malformed 'num_bands num_kpts nntot' header");
195 }
196 require_positive(file_path, "num_bands", data.number_of_bands);
197 require_positive(file_path, "num_kpts", data.number_of_kpoints);
198 require_positive(file_path, "nntot", data.number_of_neighbors);
200 file_path,
201 static_cast<long long>(data.number_of_kpoints) * data.number_of_neighbors *
203 10);
204
205 data.overlaps.assign(data.number_of_kpoints,
206 std::vector<std::vector<Complex>>(data.number_of_neighbors));
207 data.neighbors.assign(data.number_of_kpoints,
208 std::vector<NeighborEntry>(data.number_of_neighbors));
209
210 const long long block_element_count =
211 static_cast<long long>(data.number_of_bands) * data.number_of_bands;
212 for (int kpoint_index = 0; kpoint_index < data.number_of_kpoints; kpoint_index++) {
213 for (int neighbor_index = 0; neighbor_index < data.number_of_neighbors;
214 neighbor_index++) {
215 int block_kpoint = 0;
216 int neighbor_kpoint = 0;
217 IntegerVector3D shift;
218 if (!(stream >> block_kpoint >> neighbor_kpoint >> shift.x >> shift.y >> shift.z)) {
219 throw std::runtime_error(
220 file_path + ": file ended before the declared " +
221 std::to_string(static_cast<long long>(data.number_of_kpoints) *
222 data.number_of_neighbors) +
223 " (k,b) blocks were read");
224 }
225 /**
226 * Blocks must sweep k-points in order with nntot neighbours
227 * each -- the file carries no other statement of which block
228 * belongs to which k-point, so a writer that reordered them
229 * would silently mis-associate every overlap.
230 */
231 if (block_kpoint != kpoint_index + 1) {
232 throw std::runtime_error(
233 file_path + ": blocks must sweep k-points in order; expected k-point " +
234 std::to_string(kpoint_index + 1) + ", found " +
235 std::to_string(block_kpoint));
236 }
237 data.neighbors[kpoint_index][neighbor_index].neighbor_kpoint_index = to_zero_based(
238 file_path, "neighbour k-point", neighbor_kpoint, data.number_of_kpoints);
239 data.neighbors[kpoint_index][neighbor_index].periodic_image_shift = shift;
240
241 std::vector<Complex>& block = data.overlaps[kpoint_index][neighbor_index];
242 block.resize(block_element_count);
243 /**
244 * m fastest in the file, and WannierInputs stores M
245 * column-major with the k-side band index fastest -- the same
246 * order, so the elements land in sequence with no transpose.
247 */
248 for (int element_index = 0; element_index < block_element_count; element_index++) {
249 Real real_part = 0.0;
250 Real imaginary_part = 0.0;
251 if (!(stream >> real_part >> imaginary_part)) {
252 throw std::runtime_error(file_path +
253 ": file ended inside a (k,b) overlap block");
254 }
255 block[element_index] = Complex(real_part, imaginary_part);
256 }
257 }
258 }
259 return data;
260 }
261
262 [[nodiscard]] static ProjectionData read_projections(const std::string& file_path) {
263 std::ifstream stream(file_path);
264 if (!stream) {
265 throw std::runtime_error("cannot open projection file: " + file_path);
266 }
267 std::string comment_line;
268 std::getline(stream, comment_line);
269
270 ProjectionData data;
271 if (!(stream >> data.number_of_bands >> data.number_of_kpoints >>
273 throw std::runtime_error(file_path +
274 ": missing or malformed 'num_bands num_kpts num_wann' header");
275 }
276 require_positive(file_path, "num_bands", data.number_of_bands);
277 require_positive(file_path, "num_kpts", data.number_of_kpoints);
278 require_positive(file_path, "num_wann", data.number_of_wannier_functions);
280 file_path,
281 static_cast<long long>(data.number_of_kpoints) * data.number_of_bands *
283 10);
284
285 const std::size_t block_size =
286 static_cast<std::size_t>(data.number_of_bands) * data.number_of_wannier_functions;
287 data.projections.assign(data.number_of_kpoints, std::vector<Complex>(block_size));
288 /**
289 * Every element carries its own (m, n, k) triple, so nothing is
290 * assumed about ordering: fill by index, then require that every slot
291 * was written exactly once. A duplicated triple leaves another slot
292 * silently at zero, which this catches.
293 */
294 std::vector<std::vector<bool>> written(data.number_of_kpoints,
295 std::vector<bool>(block_size, false));
296
297 const long long total_elements = static_cast<long long>(data.number_of_bands) *
299 for (long long element_index = 0; element_index < total_elements; element_index++) {
300 int band = 0;
301 int wannier = 0;
302 int kpoint = 0;
303 Real real_part = 0.0;
304 Real imaginary_part = 0.0;
305 if (!(stream >> band >> wannier >> kpoint >> real_part >> imaginary_part)) {
306 throw std::runtime_error(file_path + ": file ended before the declared " +
307 std::to_string(total_elements) +
308 " projection elements were read");
309 }
310 int band_index = to_zero_based(file_path, "band", band, data.number_of_bands);
311 int wannier_index =
312 to_zero_based(file_path, "Wannier", wannier, data.number_of_wannier_functions);
313 int kpoint_index = to_zero_based(file_path, "k-point", kpoint, data.number_of_kpoints);
314
315 std::size_t slot =
316 static_cast<std::size_t>(wannier_index) * data.number_of_bands + band_index;
317 if (written[kpoint_index][slot]) {
318 throw std::runtime_error(
319 file_path + ": duplicate projection entry for band " + std::to_string(band) +
320 ", Wannier " + std::to_string(wannier) + ", k-point " + std::to_string(kpoint));
321 }
322 data.projections[kpoint_index][slot] = Complex(real_part, imaginary_part);
323 written[kpoint_index][slot] = true;
324 }
325 return data;
326 }
327
328 /**
329 * `.eig` carries no header, so the dimensions come from the indices
330 * present and the index set must then be exactly the full rectangle --
331 * a dropped line is an error, never a hole left at zero.
332 */
333 [[nodiscard]] static EigenvalueData read_eigenvalues(const std::string& file_path) {
334 std::ifstream stream(file_path);
335 if (!stream) {
336 throw std::runtime_error("cannot open eigenvalue file: " + file_path);
337 }
338
339 struct Entry {
340 int band = 0;
341 int kpoint = 0;
342 Real value = 0.0;
343 };
344 std::vector<Entry> entries;
345 int maximum_band = 0;
346 int maximum_kpoint = 0;
347
348 std::string line;
349 while (std::getline(stream, line)) {
350 std::istringstream line_stream(line);
351 Entry entry;
352 if (!(line_stream >> entry.band >> entry.kpoint >> entry.value)) {
353 if (line.find_first_not_of(" \t\r\n") == std::string::npos) {
354 continue;
355 }
356 throw std::runtime_error(file_path +
357 ": expected 'band kpoint eigenvalue', got: " + line);
358 }
359 /**
360 * operator>> stops at the first character it cannot use and
361 * reports success for the prefix it did use. A Fortran
362 * D-exponent value such as -0.557929D+01 therefore reads as
363 * -0.557929 -- wrong by a power of ten, with no error -- unless
364 * the remainder of the line is checked.
365 */
366 std::string trailing;
367 if (line_stream >> trailing) {
368 throw std::runtime_error(
369 file_path + ": unexpected '" + trailing +
370 "' after the eigenvalue (Fortran D exponents are not a valid .eig "
371 "notation; this reader accepts E exponents and plain decimals): " +
372 line);
373 }
374 if (entry.band < 1 || entry.kpoint < 1) {
375 throw std::runtime_error(
376 file_path + ": band and k-point indices are 1-based, got band " +
377 std::to_string(entry.band) + ", k-point " + std::to_string(entry.kpoint));
378 }
379 maximum_band = std::max(maximum_band, entry.band);
380 maximum_kpoint = std::max(maximum_kpoint, entry.kpoint);
381 entries.push_back(entry);
382 }
383 if (entries.empty()) {
384 throw std::runtime_error(file_path + ": contains no eigenvalue lines");
385 }
386
387 /**
388 * The table is sized from the largest indices seen, so check that the
389 * lines present can fill it before allocating it: a single line
390 * "1 2000000000 -5.0" would otherwise ask for a 100 GB table and die
391 * of std::bad_alloc instead of this diagnosis.
392 */
393 if (static_cast<long long>(maximum_band) * maximum_kpoint !=
394 static_cast<long long>(entries.size())) {
395 throw std::runtime_error(
396 file_path + ": " + std::to_string(entries.size()) +
397 " eigenvalue lines cannot fill a " + std::to_string(maximum_kpoint) +
398 " k-point by " + std::to_string(maximum_band) +
399 " band table (the largest indices seen); the file is truncated or an index is "
400 "wrong");
401 }
402 EigenvalueData data;
403 data.number_of_bands = maximum_band;
404 data.number_of_kpoints = maximum_kpoint;
405 data.eigenvalues_rydberg.assign(maximum_kpoint, std::vector<Real>(maximum_band, 0.0));
406 std::vector<std::vector<bool>> written(maximum_kpoint,
407 std::vector<bool>(maximum_band, false));
408 for (const Entry& entry : entries) {
409 if (written[entry.kpoint - 1][entry.band - 1]) {
410 throw std::runtime_error(file_path + ": duplicate eigenvalue for band " +
411 std::to_string(entry.band) + ", k-point " +
412 std::to_string(entry.kpoint));
413 }
414 data.eigenvalues_rydberg[entry.kpoint - 1][entry.band - 1] =
415 entry.value / electron_volts_per_rydberg;
416 written[entry.kpoint - 1][entry.band - 1] = true;
417 }
418 for (int kpoint_index = 0; kpoint_index < maximum_kpoint; kpoint_index++) {
419 for (int band_index = 0; band_index < maximum_band; band_index++) {
420 if (!written[kpoint_index][band_index]) {
421 throw std::runtime_error(
422 file_path + ": index range implies " + std::to_string(maximum_kpoint) +
423 " k-points x " + std::to_string(maximum_band) +
424 " bands, but the entry for band " + std::to_string(band_index + 1) +
425 ", k-point " + std::to_string(kpoint_index + 1) + " is absent");
426 }
427 }
428 }
429 return data;
430 }
431
432 private:
433 static std::string trim(const std::string& text) {
434 std::size_t first = text.find_first_not_of(" \t\r\n");
435 if (first == std::string::npos) {
436 return {};
437 }
438 std::size_t last = text.find_last_not_of(" \t\r\n");
439 return text.substr(first, last - first + 1);
440 }
441
442 static std::string lowercase(std::string text) {
443 for (char& character : text) {
444 character = static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
445 }
446 return text;
447 }
448
449 /// Body lines of a `begin <name>` ... `end <name>` block.
450 static std::vector<std::string> extract_block(const std::string& file_path,
451 const std::vector<std::string>& lines,
452 const std::string& name) {
453 const std::string opening = "begin " + name;
454 const std::string closing = "end " + name;
455 std::size_t start = lines.size();
456 for (std::size_t index = 0; index < lines.size(); index++) {
457 if (lowercase(lines[index]) == opening) {
458 start = index;
459 break;
460 }
461 }
462 if (start == lines.size()) {
463 throw std::runtime_error(file_path + ": no 'begin " + name + "' block found");
464 }
465 for (std::size_t index = start + 1; index < lines.size(); index++) {
466 if (lowercase(lines[index]) == closing) {
467 return std::vector<std::string>(lines.begin() + start + 1, lines.begin() + index);
468 }
469 }
470 throw std::runtime_error(file_path + ": 'begin " + name +
471 "' block is never closed by 'end " + name + "'");
472 }
473
474 static Matrix3x3 read_lattice_block(const std::string& file_path,
475 const std::vector<std::string>& lines,
476 const std::string& name) {
477 std::vector<std::string> body = extract_block(file_path, lines, name);
478 if (body.size() != 3) {
479 throw std::runtime_error(file_path + ": '" + name +
480 "' block must hold exactly 3 rows, found " +
481 std::to_string(body.size()));
482 }
483 Matrix3x3 lattice;
484 for (int row = 0; row < 3; row++) {
485 std::istringstream row_stream(body[row]);
486 if (!(row_stream >> lattice[row].x >> lattice[row].y >> lattice[row].z)) {
487 throw std::runtime_error(file_path + ": malformed '" + name +
488 "' row: " + body[row]);
489 }
490 }
491 return lattice;
492 }
493
494 static void require_positive(const std::string& file_path, const std::string& name, int value) {
495 if (value <= 0) {
496 throw std::runtime_error(file_path + ": " + name + " must be positive, got " +
497 std::to_string(value));
498 }
499 }
500
501 static int to_zero_based(const std::string& file_path,
502 const std::string& name,
503 int value,
504 int upper_bound) {
505 if (value < 1 || value > upper_bound) {
506 throw std::runtime_error(file_path + ": " + name + " index " + std::to_string(value) +
507 " outside declared range 1.." + std::to_string(upper_bound));
508 }
509 return value - 1;
510 }
511};
512
513} // namespace q3
static ProjectionData read_projections(const std::string &file_path)
Definition reader.hpp:262
static EigenvalueData read_eigenvalues(const std::string &file_path)
Definition reader.hpp:333
static NeighborListData read_neighbor_list(const std::string &file_path)
Definition reader.hpp:134
static constexpr Real electron_volts_per_rydberg
eV per Rydberg; .eig is eV, WannierInputs is Rydberg.
Definition reader.hpp:125
static OverlapData read_overlaps(const std::string &file_path)
Definition reader.hpp:182
constexpr Real RY_TO_EV
void require_plausible_element_count(const std::string &file_path, long long elements, std::uintmax_t minimum_bytes_per_element)
Definition reader.hpp:61
double Real
std::complex< double > Complex
Integer 3-vector for Miller indices / FFT grid indices.
3×3 matrix stored row-major: row i = mat[i]
std::vector< std::vector< Real > > eigenvalues_rydberg
[kpoint_index][band_index], converted to Rydberg on the way in.
Definition reader.hpp:113
std::vector< Vector3D > kpoints
Crystal (fractional) coordinates, in the file's own k-point order.
Definition reader.hpp:121
Matrix3x3 real_lattice
Rows are a1, a2, a3 in Angstrom, as the .nnkp records them.
Definition reader.hpp:118
std::vector< std::vector< std::vector< Complex > > > overlaps
Definition reader.hpp:97
std::vector< std::vector< NeighborEntry > > neighbors
Definition reader.hpp:98
std::vector< std::vector< Complex > > projections
[kpoint_index] -> column-major A block, band index varying fastest.
Definition reader.hpp:106