Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
wavefunction.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * BerkeleyGWWavefunctionFile — writes BerkeleyGW's `WFN` mean-field file.
5 *
6 * Layout (the BerkeleyGW specification, as pw2bgw.x's `write_wfng` writes it
7 * -- see header.hpp for both citations; shapes in brackets, first index fastest):
8 * records 1-8: BerkeleyGWCommonHeader (include_kpoint_fields=true)
9 * record 9: ngk[nk] -- plane waves per k-point
10 * record 10: kw[nk] -- weights, normalized to sum to 1
11 * record 11: kpt[3,nk] -- crystal (fractional) coordinates
12 * record 12: ifmin[nk*ns] -- lowest occupied band (1-based)
13 * record 13: ifmax[nk*ns] -- highest occupied band (1-based, 0 if none)
14 * record 14: energies[nbands,nk*ns] -- Rydberg
15 * record 15: occupations[nbands,nk*ns]
16 * record 16-18: nrecord=1, ng, gvec[3,ng] -- charge-density G-set
17 * body: per k-point, gvec_k then per-band coefficients (see below)
18 *
19 * Always written as flavor 2 ("WFN-Complex"): Q3 stores wavefunction
20 * coefficients as Complex regardless of gamma-only status, so there is no
21 * Q3-side "real" storage to map onto pw2bgw.x's flavor-1 (real_or_complex=1)
22 * path, which additionally reorders bands/k-points into time-reversal pairs
23 * (pw2bgw.x's real_wfng subroutine) -- out of scope here.
24 *
25 * SCOPE: spin-unpolarized (ns=1) only. write_to_file() rejects ns>1 up
26 * front with a clear error rather than writing a silently-wrong file.
27 * The WFN body has two layouts, selected by the file's own flavor tag and
28 * spin count (write_wfng chooses between them). For flavor 2 with ns=1 --
29 * our case -- each band is one record of exactly ngk coefficients, which is what write_body()
30 * below implements. For flavor 1, or for ns>1 with either flavor, each BAND
31 * is instead a SINGLE record holding all ns spins' coefficients concatenated,
32 * G index fastest and spin index slowest (write_wfng; this is the
33 * specification's ng-values-per-spin data layout), not one record
34 * per (k,spin,band) as write_body() produces. Implementing that interleaving
35 * correctly, with no BerkeleyGW reference file available in this environment to
36 * validate against, was judged too risky to guess at silently -- rejecting is
37 * the honest choice (see also exchange_correlation.hpp's vxc.dat gap).
38 */
39
40#include <cstdint>
41#include <fstream>
42#include <stdexcept>
43#include <string>
44#include <vector>
45
46#include "io/checkpoint/qe.hpp"
47#include "io/logger.hpp"
53
54namespace q3 {
55
57 public:
58 explicit BerkeleyGWWavefunctionFile(const MeanFieldRecord& record) : record_(record) {}
59 /**
60 * These writers keep a reference to the record, so binding one to a
61 * temporary would dangle the moment the full expression ends. Deleting the
62 * rvalue overload turns that into a compile error instead of a silent
63 * read of freed memory -- callers must keep the record alive themselves,
64 * which every real call site already does.
65 */
66 explicit BerkeleyGWWavefunctionFile(const MeanFieldRecord&&) = delete;
67
68 void write_to_file(const std::string& file_path) const {
69 if (record_.number_of_spin_components > 1) {
70 throw std::runtime_error(
71 "BerkeleyGWWavefunctionFile: spin-polarized (nspin>1) WFN export is not yet "
72 "supported. pw2bgw writes each band as a single record with all spins' "
73 "coefficients concatenated (G fastest, spin slower) rather than one record per "
74 "(k,spin,band); implementing that interleaving without a BerkeleyGW/QE reference "
75 "to validate against was judged too risky to guess at silently. Use "
76 "number_of_spin_components: 1, or omit mean_field_export, for a spin-polarized "
77 "run.");
78 }
79
80 std::ofstream output_file(file_path, std::ios::binary);
81 if (!output_file.is_open()) {
82 log::warn("BerkeleyGWWavefunctionFile: cannot open {} for writing", file_path);
83 return;
84 }
85
86 BerkeleyGWCommonHeader header(record_, "WFN-Complex", /*include_kpoint_fields=*/true);
87 header.write(output_file);
88
89 BerkeleyGWRecordWriter record_writer(record_);
90 record_writer.write_plane_wave_counts(output_file); // record 9
91 record_writer.write_normalized_kpoint_weights(output_file); // record 10
92 record_writer.write_kpoint_coordinates(output_file); // record 11
93 write_occupied_band_bounds_records(output_file);
94 write_eigenvalues_record(output_file);
95 write_occupations_record(output_file);
96 record_writer.write_charge_density_gvectors(output_file); // records 16-18
97 write_body(output_file, record_writer);
98
99 log::info(" Wrote BerkeleyGW wavefunction file: {}", file_path);
100 }
101
102 private:
103 const MeanFieldRecord& record_;
104 QEBinaryRecordFormat record_format_;
105
106 /**
107 * A band is "occupied" for ifmin/ifmax purposes when its occupation
108 * exceeds this threshold -- the same 0.5 cutoff write_wfng applies to
109 * its own per-band occupations, which are QE's band weights divided by
110 * the k-point weight, i.e. a per-band occupation number on a
111 * max-1-per-(k,spin)-state scale; Q3's own
112 * occupation_by_kpoint_state_index_and_band_index() is on the identical
113 * max-1 scale (the fixed-occupation path returns exactly 1.0 or 0.0; the
114 * smeared path's total_electrons accumulation in
115 * fermi_energy/planewave/init.hpp multiplies the smearing function's
116 * [0,1] output by the k-point weight, which already carries the ×2
117 * spin-degeneracy factor for the unpolarized case) -- so the same
118 * absolute 0.5 cutoff applies unchanged.
119 */
120 static constexpr Real occupied_band_occupation_threshold = 0.5;
121
122 /// Records 12-13: ifmin[nk*ns], ifmax[nk*ns], both 1-based (write_wfng records 12-13).
123 void write_occupied_band_bounds_records(std::ofstream& output_file) const {
124 int number_of_kpoints = record_.number_of_kpoints();
125 int number_of_spin_components = record_.number_of_spin_components;
126 int number_of_bands = record_.number_of_bands;
127 int total_kpoint_states = number_of_kpoints * number_of_spin_components;
128
129 std::vector<int32_t> lowest_occupied_band(total_kpoint_states, 0);
130 std::vector<int32_t> highest_occupied_band(total_kpoint_states, 0);
131
132 for (int state_index = 0; state_index < total_kpoint_states; state_index++) {
133 for (int band_index = 0; band_index < number_of_bands; band_index++) {
134 Real occupation = record_.occupations[state_index][band_index];
135 if (occupation > occupied_band_occupation_threshold) {
136 if (lowest_occupied_band[state_index] == 0) {
137 lowest_occupied_band[state_index] = band_index + 1;
138 }
139 highest_occupied_band[state_index] = band_index + 1;
140 }
141 }
142 }
143
144 record_format_.write_record(
145 output_file,
146 lowest_occupied_band.data(),
147 static_cast<int32_t>(lowest_occupied_band.size() * sizeof(int32_t)));
148 record_format_.write_record(
149 output_file,
150 highest_occupied_band.data(),
151 static_cast<int32_t>(highest_occupied_band.size() * sizeof(int32_t)));
152 }
153
154 /**
155 * Record 14: energies[nbands,nk*ns], Rydberg, as the BerkeleyGW
156 * specification requires. write_wfng writes QE's eigenvalues with NO
157 * unit conversion (unlike vxc.dat's explicit conversion to eV) -- QE's
158 * eigenvalues are already Rydberg internally, matching Q3's own convention (AGENTS.md § Units:
159 * "use the same units as Quantum ESPRESSO"; results.json's "Ry" units), so
160 * eigenvalue() is copied as-is. Band is the fast (first) Fortran index, so for each
161 * k-point-state all nbands values are written contiguously before moving to the next state --
162 * same spin-major state ordering as records 12-13 (see write_occupied_band_bounds_records).
163 */
164 void write_eigenvalues_record(std::ofstream& output_file) const {
165 write_band_major_matrix_record(output_file, [this](int state_index, int band_index) {
166 return record_.eigenvalues_rydberg[state_index][band_index];
167 });
168 }
169
170 /// Record 15: occupations(nbands,nk*ns), same shape as record 14.
171 void write_occupations_record(std::ofstream& output_file) const {
172 write_band_major_matrix_record(output_file, [this](int state_index, int band_index) {
173 return record_.occupations[state_index][band_index];
174 });
175 }
176
177 template <typename ValueFunction>
178 void write_band_major_matrix_record(std::ofstream& output_file, ValueFunction value_of) const {
179 int number_of_kpoints = record_.number_of_kpoints();
180 int number_of_spin_components = record_.number_of_spin_components;
181 int number_of_bands = record_.number_of_bands;
182 int total_kpoint_states = number_of_kpoints * number_of_spin_components;
183
184 std::vector<double> values(static_cast<size_t>(number_of_bands) * total_kpoint_states);
185 for (int state_index = 0; state_index < total_kpoint_states; state_index++) {
186 for (int band_index = 0; band_index < number_of_bands; band_index++) {
187 values[static_cast<size_t>(state_index) * number_of_bands + band_index] =
188 value_of(state_index, band_index);
189 }
190 }
191 record_format_.write_record(
192 output_file, values.data(), static_cast<int32_t>(values.size() * sizeof(double)));
193 }
194
195 /**
196 * Body (the BerkeleyGW specification's per-k-point G-vector list, then
197 * the coefficients of each band, as write_wfng writes them -- the
198 * flavor-2, ns=1 layout, the only one
199 * implemented here; see the class comment's SCOPE note): per k-point, in
200 * physical-k-point order (0..nk-1), one [nrecord][ngk][gvec_k] triple,
201 * then per band one [nrecord][ngk][coefficients] triple. write_to_file()
202 * has already rejected ns>1 before this is ever called, so
203 * kpoint_state_index == kpoint_index unconditionally (spin_index is
204 * always 0).
205 */
206 void write_body(std::ofstream& output_file, const BerkeleyGWRecordWriter& record_writer) const {
207 int number_of_kpoints = record_.number_of_kpoints();
208 int number_of_bands = record_.number_of_bands;
209
210 for (int kpoint_index = 0; kpoint_index < number_of_kpoints; kpoint_index++) {
211 record_writer.write_kpoint_gvectors(output_file, kpoint_index);
212
213 int32_t number_of_plane_waves = record_.plane_waves_per_kpoint[kpoint_index];
214 for (int band_index = 0; band_index < number_of_bands; band_index++) {
215 int kpoint_state_index = kpoint_index; // spin_index is always 0 here (ns=1)
216 record_writer.write_complex_block(
217 output_file,
218 number_of_plane_waves,
219 record_.band_source->coefficients(kpoint_state_index, band_index),
220 number_of_plane_waves);
221 }
222 }
223 }
224};
225
226} // namespace q3
void write(std::ofstream &output_file) const
Write records 1 through 8 to output_file.
Definition header.hpp:256
Writes the records BerkeleyGW's files share; see the file comment.
void write_plane_wave_counts(std::ofstream &output_file) const
ngk(nk) – plane waves per k-point.
void write_charge_density_gvectors(std::ofstream &output_file) const
[nrecord=1][ng][gvec(3,ng)] – the charge-density G-set, in the record's order.
void write_kpoint_coordinates(std::ofstream &output_file) const
void write_normalized_kpoint_weights(std::ofstream &output_file) const
BerkeleyGWWavefunctionFile(const MeanFieldRecord &record)
void write_to_file(const std::string &file_path) const
BerkeleyGWWavefunctionFile(const MeanFieldRecord &&)=delete
void write_record(std::ofstream &output_file, const void *data, int32_t number_of_bytes) const override
Definition qe.hpp:16
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
double Real
std::vector< std::vector< Real > > occupations
Indexed [kpoint_state_index][band_index]; max 1 per (k, spin) state.
Definition init.hpp:131
int number_of_spin_components
Definition init.hpp:127
int number_of_kpoints() const
Definition init.hpp:184
std::vector< std::vector< Real > > eigenvalues_rydberg
Indexed [kpoint_state_index][band_index]; Rydberg.
Definition init.hpp:129
std::vector< int > plane_waves_per_kpoint
Definition init.hpp:121
std::shared_ptr< const MeanFieldBandSource > band_source
Definition init.hpp:132