Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
results_document.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * InterpolationResultsDocument — the results file of `q3_utility interpolate`,
5 * assembled block by block onto the struct generated from its schema, and
6 * written once.
7 *
8 * One contract, one serialisation. schemas/q3/interpolation/results.json is
9 * the source of truth and src/generated/q3/interpolation/results.hpp its
10 * C++ form; this class owns that struct and is the only place the engine's
11 * types are mapped onto it, so nothing builds the file's JSON by hand. The
12 * Python model, q3.io.interpolation.results, reads the same schema, and a
13 * key renamed on one side without the other is caught by the parity test
14 * there rather than by a driver at campaign speed.
15 *
16 * Every block written here is the producer's guarantee the schema states
17 * with `required`. What the workflow adds afterwards -- quality.status,
18 * .reason, .ladder, .band_distance -- is optional in the schema and never
19 * set here.
20 */
21
22#include <array>
23#include <cstdint>
24#include <string>
25#include <utility>
26#include <vector>
27
28#include <nlohmann/json.hpp>
29
30#include "generated/esse/core/reusable/energy.hpp"
31#include "generated/esse/properties/non_scalar/eigenvalues_at_kpoints.hpp"
32#include "generated/esse/properties/scalar/fermi_energy.hpp"
33#include "generated/q3/interpolation/results.hpp"
34#include "io/json_file.hpp"
44
45namespace q3 {
46
48 public:
49 /// A results file starts from what identifies the run it belongs to.
50 InterpolationResultsDocument(nlohmann::json provenance, Real fermi_energy_rydberg) {
51 results_.calculation = "interpolation";
52 results_.provenance = std::move(provenance);
53 results_.properties.fermi_energy =
54 generated::FermiEnergy{generated::FermiEnergyNameEnum::FERMI_ENERGY,
55 generated::FermiEnergyUnitsEnum::RY,
56 fermi_energy_rydberg};
57 }
58
59 /**
60 * The model that was evaluated: its method, where its record is, and
61 * what the interpolator says of itself. The Wigner-Seitz weight identity
62 * has its own key, because the readers and the reference name it;
63 * anything else an interpolator reports goes under `diagnostics`.
64 */
65 void describe_model(const std::string& method,
66 const std::string& record_path,
67 const QuantumStateInterpolator& interpolator) {
68 results_.construction.method = method;
69 results_.construction.description = interpolator.description();
70 results_.construction.record = record_path;
71 results_.construction.number_of_bands = interpolator.number_of_bands();
72 for (const auto& [name, value] : interpolator.diagnostics()) {
74 results_.construction.total_bloch_sum_weight = value;
75 continue;
76 }
77 if (!results_.construction.diagnostics) {
78 results_.construction.diagnostics.emplace();
79 }
80 (*results_.construction.diagnostics)[name] = value;
81 }
82 }
83
84 /**
85 * The Wannier construction: the spread and the centres, reported whether
86 * or not the gauge was localised; the classifier's verdict when the
87 * subspace was disentangled; and what the model says about itself with
88 * no reference bands in hand. The band distance against a direct
89 * calculation is the workflow's to add, since it costs a calculation
90 * the utility does not run; `quality` is where it lands.
91 */
93 const WannierQualityReport& quality) {
94 results_.construction.spread = spread_block(construction.spread);
95 if (construction.disentanglement.performed) {
96 results_.construction.disentanglement =
97 disentanglement_block(construction.disentanglement);
98 }
99 results_.quality = quality_block(quality, construction.spread);
100 }
101
102 /**
103 * The eigenvalues at each evaluated k-point as ESSE's eigenvalues_at_kpoints
104 * record, the one the SCF results file lists: per k-point its crystal
105 * coordinates and one (unlabelled) spin channel of band energies.
106 */
107 void add_bands(const std::vector<Vector3D>& kpoints, const InterpolatedBandStructure& bands) {
108 generated::EigenvaluesAtKpoints block;
109 block.name = generated::EigenvaluesAtKpointsNameEnum::EIGENVALUES_AT_KPOINTS;
110 block.units = generated::EigenvaluesAtKpointsUnitsEnum::RY;
111 block.values.reserve(bands.eigenvalues_rydberg.size());
112 for (size_t kpoint_index = 0; kpoint_index < bands.eigenvalues_rydberg.size();
113 kpoint_index++) {
114 const std::vector<Real>& at_kpoint = bands.eigenvalues_rydberg[kpoint_index];
115 generated::EigenvaluesAtKpointsBandEnergies channel;
116 channel.energies = std::vector<double>(at_kpoint.begin(), at_kpoint.end());
117 generated::EigenvaluesAtKpointsEigenvaluesAtKPoint entry;
118 if (kpoint_index < kpoints.size()) {
119 const Vector3D& kpoint = kpoints[kpoint_index];
120 entry.kpoint = std::vector<double>{kpoint.x, kpoint.y, kpoint.z};
121 }
122 entry.eigenvalues = std::vector{channel};
123 block.values.push_back(std::move(entry));
124 }
125 results_.properties.eigenvalues_at_kpoints = std::move(block);
126 }
127
128 /// The density of states as ESSE's density_of_states record.
130 generated::DensityOfStates block;
131 block.name = generated::DensityOfStatesNameEnum::DENSITY_OF_STATES;
132 block.x_axis.label = generated::DensityOfStatesXAxisLabelEnum::ENERGY;
133 block.x_axis.units = generated::DensityOfStatesXAxisUnitsEnum::RY;
134 block.y_axis.label = generated::DensityOfStatesYAxisLabelEnum::DENSITY_OF_STATES;
135 block.y_axis.units = generated::DensityOfStatesYAxisUnitsEnum::STATES_UNITCELL;
136 block.x_data_array = std::vector<nlohmann::json>(density.energies_rydberg.begin(),
137 density.energies_rydberg.end());
138 block.y_data_series = {density.density_of_states};
139 results_.properties.density_of_states = std::move(block);
140 }
141
142 [[nodiscard]] const generated::InterpolationResults& results() const {
143 return results_;
144 }
145
146 [[nodiscard]] nlohmann::json json() const {
147 return results_;
148 }
149
150 void write(const std::string& path) const {
151 write_json_file(path, json());
152 }
153
154 private:
155 /// Vectors as the file stores them: one [x, y, z] per entry.
156 [[nodiscard]] static std::vector<std::vector<double>>
157 triplets(const std::vector<Vector3D>& vectors) {
158 std::vector<std::vector<double>> rows;
159 rows.reserve(vectors.size());
160 for (const Vector3D& vector : vectors) {
161 rows.push_back({vector.x, vector.y, vector.z});
162 }
163 return rows;
164 }
165
166 /// Zero-based indices as the file's integers.
167 [[nodiscard]] static std::vector<std::int64_t> indices(const std::vector<int>& values) {
168 return std::vector<std::int64_t>(values.begin(), values.end());
169 }
170
171 /// Only what was evaluated is written, and the localisation's numbers only after one.
172 [[nodiscard]] static generated::InterpolationResultsSpread
173 spread_block(const SpreadReport& report) {
174 generated::InterpolationResultsSpread block;
175 block.gauge_invariant_bohr_squared = report.gauge_invariant_bohr_squared;
176 block.localized = report.localized;
177 if (report.evaluated) {
178 block.total_bohr_squared = report.total_bohr_squared;
179 block.guiding_centers =
180 report.guided_by_trial_orbitals ? "trial orbitals" : "projection gauge";
181 block.trial_orbital_centers_bohr = triplets(report.trial_orbital_centers_bohr);
182 block.initial_minimizable_bohr_squared = report.initial_minimizable_bohr_squared;
183 block.centers_bohr = triplets(report.centers_bohr);
184 block.spreads_bohr_squared = report.spreads_bohr_squared;
185 }
186 if (report.localized) {
187 block.localization_iterations = report.localization_iterations;
188 block.localization_converged = report.localization_converged;
189 block.localization_gradient_norm = report.localization_gradient_norm;
190 }
191 return block;
192 }
193
194 /**
195 * What the classifier decided: the window it resolved and which bands
196 * the singular-gauge rule kept out of the freeze -- the one thing about
197 * a model that a dataset without the right shell makes unavoidable, and
198 * that should never be silent. Bands are one-based, as the manifest
199 * counts them; the window is in electronvolts, as the manifest gives it.
200 */
201 [[nodiscard]] static generated::InterpolationResultsDisentanglement
202 disentanglement_block(const DisentanglementReport& report) {
203 const BandClassificationSummary& verdict = report.classification;
204 generated::InterpolationResultsDisentanglement block;
205 block.metallic = verdict.metallic;
206 block.bands_crossing_fermi = verdict.number_of_bands_crossing_fermi;
207 block.frozen_window_above_fermi_ev =
208 verdict.frozen_window_above_fermi_rydberg * constants::RY_TO_EV;
209 block.frozen_window_automatic = verdict.frozen_window_automatic;
210 for (const BandExcludedFromFreeze& band : verdict.bands_excluded_from_freeze) {
211 generated::InterpolationResultsExcludedBand excluded;
212 excluded.band = band.band_index + 1;
213 excluded.kpoints = band.number_of_kpoints;
214 block.bands_excluded_from_freeze.push_back(excluded);
215 }
216 return block;
217 }
218
219 [[nodiscard]] static generated::InterpolationResultsHamiltonianDecay
220 hamiltonian_decay_block(const HamiltonianDecay& decay) {
221 generated::InterpolationResultsHamiltonianDecay block;
222 block.outermost_shell_distance_bohr = decay.outermost_shell_distance;
223 block.outermost_shell_size = decay.outermost_shell_size;
224 block.outermost_shell_maximum_ry = decay.outermost_shell_maximum;
225 block.largest_hopping_ry = decay.largest_hopping;
226 block.ratio = decay.ratio;
227 return block;
228 }
229
230 /// The spread statistics and the centres only when the report measured them; the decay always.
231 [[nodiscard]] static generated::InterpolationResultsQuality
232 quality_block(const WannierQualityReport& report, const SpreadReport& spread) {
233 generated::InterpolationResultsQuality block;
234 if (!report.spreads_bohr_squared.empty()) {
235 generated::InterpolationResultsQualitySpread statistics;
236 statistics.median_bohr_squared = report.spread_median_bohr_squared;
237 statistics.variance_bohr_squared = report.spread_variance_bohr_squared;
238 statistics.maximum_bohr_squared = report.spread_maximum_bohr_squared;
239 statistics.outliers = indices(report.spread_outliers);
240 statistics.above_ten_angstrom_squared =
241 indices(report.spreads_above_ten_angstrom_squared);
242 block.spread = statistics;
243 }
244 if (!report.nearest_atom_distance_bohr.empty()) {
245 generated::InterpolationResultsQualityCenters centers;
246 centers.nearest_atom_distance_bohr = report.nearest_atom_distance_bohr;
247 centers.nearest_bond_midpoint_distance_bohr =
248 report.nearest_bond_midpoint_distance_bohr;
249 centers.shortest_bond_bohr = report.shortest_bond_bohr;
250 block.centers = centers;
251 }
252 block.hamiltonian_decay = hamiltonian_decay_block(report.decay);
253 if (spread.localized) {
254 block.localization_converged = spread.localization_converged;
255 }
256 return block;
257 }
258
259 generated::InterpolationResults results_;
260};
261
262} // namespace q3
InterpolationResultsDocument(nlohmann::json provenance, Real fermi_energy_rydberg)
A results file starts from what identifies the run it belongs to.
void write(const std::string &path) const
void describe_model(const std::string &method, const std::string &record_path, const QuantumStateInterpolator &interpolator)
void add_construction(const WannierConstructionReport &construction, const WannierQualityReport &quality)
const generated::InterpolationResults & results() const
void add_density_of_states(const InterpolatedDensityOfStates &density)
The density of states as ESSE's density_of_states record.
void add_bands(const std::vector< Vector3D > &kpoints, const InterpolatedBandStructure &bands)
static constexpr const char * TOTAL_BLOCH_SUM_WEIGHT
The name that diagnostic goes by, which a results file gives a key of its own.
virtual int number_of_bands() const =0
Number of eigenvalues returned per k-point.
virtual std::string description() const =0
One line for logs: the method and the size of the model.
virtual std::map< std::string, Real > diagnostics() const
Definition init.hpp:57
constexpr Real RY_TO_EV
double Real
void write_json_file(const std::string &path, const nlohmann::json &document)
Definition json_file.hpp:20
bool performed
Whether the subspace was chosen by classification and iteration rather than taken as given.
Eigenvalues along a k-path, one row per k-point.
Definition init.hpp:25
std::vector< std::vector< Real > > eigenvalues_rydberg
[path_index][band_index], Rydberg, ascending within each row.
Definition init.hpp:28
A density of states on a uniform energy grid.
Definition init.hpp:34
std::vector< Real > density_of_states
States per Rydberg per unit cell, spin degeneracy NOT included.
Definition init.hpp:37
std::vector< Real > energies_rydberg
Definition init.hpp:35
DisentanglementReport disentanglement
Definition init.hpp:25