Loading...
Searching...
No Matches
band_classifier.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * BandClassifier — which bands are kept exactly, which compete for the
5 * remaining room, and which are thrown away, at each k-point.
6 *
7 * The entangled case has more bands in the window than Wannier functions, so
8 * something must choose. This is the first half of that choice, and it is made
9 * from two quantities the record already carries: the projectability
10 * p_nk = sum_m |A_mn(k)|^2, and the eigenvalue.
11 *
12 * frozen p >= projectability_maximum, OR e <= E_F + frozen_window.
13 * Kept exactly; never rotated by the optimisation.
14 * discarded not frozen, and p < projectability_minimum. Removed from the
15 * working basis entirely, so the optimiser cannot select it back.
16 * candidate everything else; offered to the optimisation.
17 *
18 * The energy rule is not redundant with the projectability rule, and a metal
19 * is why. The states at a transition metal's Fermi level are the
20 * free-electron-like ones with the LOWEST projectability -- copper's sp band
21 * at E_F is exactly such a state -- so freezing on projectability alone
22 * discards them and leaves the model with a hole at the Fermi surface. That
23 * failure is invisible to the mesh-point round trip, which is exact for any
24 * subspace, and shows up only in the density of states at E_F.
25 *
26 * The energy window has a published value, E_F + 2 eV (Qiao, Pizzi, Marzari,
27 * npj Comput. Mater. 9, 208 (2023)), and `automatic_frozen_window` applies it
28 * to metal and insulator alike. It is the same window the band distance that
29 * judges the model is measured on, and the two must coincide: a state the
30 * distance weighs has to be in the model exactly, which only freezing
31 * guarantees. Measured on silicon at 12x12x12, freezing the occupied manifold
32 * alone gives a band distance of 220 meV -- every millivolt of it from the
33 * lowest conduction band, which the subspace iteration does not pick on its
34 * own -- against 14 meV at E_F + 2 eV and 5 meV at E_F + 3 eV. For an
35 * insulator the engine's Fermi level sits in the gap, so the window reaches
36 * the conduction states the distance is measured on and no further. An
37 * explicit value overrides it, and a value below the whole manifold freezes
38 * nothing by energy, which is how a test exercises the optimisation on its
39 * own. Whether the system is a metal is still decided here -- a band whose
40 * energies straddle the Fermi level across the mesh, below it at one k-point
41 * and above it at another -- and reported, because a driver reads it.
42 *
43 * THE SINGULAR-GAUGE RULE. The energy rule and the projectability rule can
44 * disagree: a state inside the window with a projectability below
45 * `projectability_minimum` is one the trial orbitals cannot represent, and
46 * freezing it puts a direction in the subspace that the projection A(k) has
47 * no column for. The gauge U = A (A^dagger A)^(-1/2) is then singular by
48 * construction, not by accident -- copper's norm-conserving dataset has no 4p
49 * shell, its sp band above E_F has zero projectability at seventeen k-points,
50 * and a 2 eV window froze it and collapsed the gauge every time. Such a band
51 * is EXCLUDED from the freeze and reported, never frozen: the model has a
52 * hole where the dataset has none of the character the state needs, and the
53 * report says so at which band and how many k-points. It is a fact about the
54 * dataset, and hiding it behind a hand-lowered window was the alternative.
55 *
56 * Feasibility is checked here rather than left to fail deep in the iteration:
57 * at every k-point the frozen count must not exceed the number of Wannier
58 * functions, and frozen plus candidate must reach it. Either violation is a
59 * typed error naming the k-point and the counts, which is what a retry ladder
60 * needs to act on.
61 */
62
63#include <cstddef>
64#include <map>
65#include <stdexcept>
66#include <string>
67#include <utility>
68#include <vector>
69
77
78namespace q3 {
79
81 public:
82 /// Inverted thresholds describe no rule, and are refused here rather than at each k-point.
83 explicit BandClassifier(BandClassificationThresholds thresholds) : thresholds_(thresholds) {
84 if (!(thresholds.projectability_minimum < thresholds.projectability_maximum)) {
85 throw std::invalid_argument("BandClassifier: projectability_minimum (" +
86 std::to_string(thresholds.projectability_minimum) +
87 ") must be below projectability_maximum (" +
88 std::to_string(thresholds.projectability_maximum) + ")");
89 }
90 }
91
92 /**
93 * The classification of `record`'s captured window for as many Wannier
94 * functions as it has trial orbitals. Its projectabilities and
95 * eigenvalues are one row per k-point, both indexed by band.
96 */
97 [[nodiscard]] BandClassification classify(const WannierInputs& record) const {
98 require_classifiable(record);
99 const int number_of_wannier_functions = record.number_of_trial_orbitals;
100
101 BandClassification classification;
102 BandClassificationSummary& verdict = classification.summary;
105 verdict.metallic = verdict.number_of_bands_crossing_fermi > 0;
108 thresholds_.automatic_frozen_window
111 const Real frozen_ceiling =
113 std::map<int, int> kpoints_excluded_from_freeze;
114
115 classification.kpoints.reserve(record.projectabilities.size());
116 for (std::size_t kpoint_index = 0; kpoint_index < record.projectabilities.size();
117 kpoint_index++) {
118 const std::vector<Real>& projectability = record.projectabilities[kpoint_index];
119 const std::vector<Real>& energies = record.eigenvalues_rydberg[kpoint_index];
120
122 entry.roles.reserve(projectability.size());
123 for (std::size_t band = 0; band < projectability.size(); band++) {
124 const bool in_window = energies[band] <= frozen_ceiling;
125 const BandRole role = role_of(projectability[band], in_window);
126 entry.roles.push_back(role);
127 if (role == BandRole::Discarded) {
128 if (in_window) {
129 kpoints_excluded_from_freeze[static_cast<int>(band)]++;
130 }
131 continue;
132 }
133 if (role == BandRole::Frozen) {
134 entry.frozen_positions.push_back(static_cast<int>(entry.kept_bands.size()));
135 }
136 entry.kept_bands.push_back(static_cast<int>(band));
137 }
138 require_feasible(entry, kpoint_index, number_of_wannier_functions);
139 classification.kpoints.push_back(std::move(entry));
140 }
141 for (const auto& [band_index, number_of_kpoints] : kpoints_excluded_from_freeze) {
142 verdict.bands_excluded_from_freeze.push_back({band_index, number_of_kpoints});
143 }
144 return classification;
145 }
146
147 /**
148 * How many bands lie below the Fermi level at one k-point and above it at
149 * another, by more than the crossing margin on each side. Bands are
150 * counted by index across the k-points that carry them, so a window whose
151 * band count varies between k-points is judged on the bands every k-point
152 * has.
153 */
154 [[nodiscard]] static int
155 bands_crossing_fermi(const std::vector<std::vector<Real>>& eigenvalues_rydberg,
156 Real fermi_energy_rydberg) {
157 std::size_t common_bands = eigenvalues_rydberg.empty() ? 0 : eigenvalues_rydberg[0].size();
158 for (const std::vector<Real>& energies : eigenvalues_rydberg) {
159 common_bands = energies.size() < common_bands ? energies.size() : common_bands;
160 }
161 int crossing = 0;
162 for (std::size_t band = 0; band < common_bands; band++) {
163 bool below = false;
164 bool above = false;
165 for (const std::vector<Real>& energies : eigenvalues_rydberg) {
166 below = below ||
167 energies[band] < fermi_energy_rydberg -
169 above = above ||
170 energies[band] > fermi_energy_rydberg +
172 }
173 if (below && above) {
174 crossing++;
175 }
176 }
177 return crossing;
178 }
179
180 private:
181 /**
182 * The rule for one band: frozen by a projectability at the maximum or
183 * above, or by lying inside the energy window while the trial orbitals can
184 * reach it; a candidate when they can reach it and neither freezes it;
185 * discarded otherwise -- which, inside the window, is the singular-gauge
186 * rule keeping a state out of the freeze.
187 */
188 [[nodiscard]] BandRole role_of(Real projectability, bool in_window) const {
189 const bool reachable = projectability >= thresholds_.projectability_minimum;
190 if (projectability >= thresholds_.projectability_maximum || (in_window && reachable)) {
191 return BandRole::Frozen;
192 }
193 return reachable ? BandRole::Candidate : BandRole::Discarded;
194 }
195
196 /// The frozen bands must fit in the subspace, and the kept ones must be able to fill it.
197 static void require_feasible(const KPointBandClassification& entry,
198 std::size_t kpoint_index,
199 int number_of_wannier_functions) {
200 if (entry.number_frozen() > number_of_wannier_functions) {
201 throw WannierDiagnosticError(
203 "disentanglement: k-point " + std::to_string(kpoint_index) + " freezes " +
204 std::to_string(entry.number_frozen()) + " bands but only " +
205 std::to_string(number_of_wannier_functions) +
206 " Wannier functions were asked for. Lower "
207 "control.interpolation.frozen_window_above_fermi_ev, raise "
208 "projectability_maximum, or ask for more Wannier functions.");
209 }
210 if (entry.number_kept() < number_of_wannier_functions) {
211 throw WannierDiagnosticError(
213 "disentanglement: k-point " + std::to_string(kpoint_index) + " keeps only " +
214 std::to_string(entry.number_kept()) + " bands, fewer than the " +
215 std::to_string(number_of_wannier_functions) +
216 " Wannier functions asked for. Lower "
217 "control.interpolation.projectability_minimum, or widen "
218 "control.interpolation.bands so more states are available.");
219 }
220 }
221
222 static void require_classifiable(const WannierInputs& record) {
223 const std::vector<std::vector<Real>>& projectabilities = record.projectabilities;
224 const std::vector<std::vector<Real>>& eigenvalues_rydberg = record.eigenvalues_rydberg;
225 if (projectabilities.size() != eigenvalues_rydberg.size()) {
226 throw std::invalid_argument("BandClassifier::classify: the projectabilities cover " +
227 std::to_string(projectabilities.size()) +
228 " k-points and the eigenvalues " +
229 std::to_string(eigenvalues_rydberg.size()));
230 }
231 if (projectabilities.empty()) {
232 throw std::invalid_argument("BandClassifier::classify: no k-points");
233 }
234 if (record.number_of_trial_orbitals <= 0) {
235 throw std::invalid_argument(
236 "BandClassifier::classify: number_of_wannier_functions must be positive");
237 }
238 for (std::size_t kpoint_index = 0; kpoint_index < projectabilities.size(); kpoint_index++) {
239 if (projectabilities[kpoint_index].size() != eigenvalues_rydberg[kpoint_index].size()) {
240 throw std::invalid_argument(
241 "BandClassifier::classify: k-point " + std::to_string(kpoint_index) + " has " +
242 std::to_string(projectabilities[kpoint_index].size()) +
243 " projectabilities and " +
244 std::to_string(eigenvalues_rydberg[kpoint_index].size()) + " eigenvalues");
245 }
246 }
247 }
248
249 BandClassificationThresholds thresholds_;
250};
251
252} // namespace q3
BandClassifier(BandClassificationThresholds thresholds)
Inverted thresholds describe no rule, and are refused here rather than at each k-point.
BandClassification classify(const WannierInputs &record) const
static int bands_crossing_fermi(const std::vector< std::vector< Real > > &eigenvalues_rydberg, Real fermi_energy_rydberg)
double Real
BandRole
Definition band_role.hpp:11
@ frozen_window_overfull
A k-point freezes more bands than there are Wannier functions.
@ kept_bands_underfull
A k-point keeps fewer bands than there are Wannier functions.
Real frozen_window_above_fermi_rydberg
The window actually applied, in rydberg, after any automatic resolution.
std::vector< BandExcludedFromFreeze > bands_excluded_from_freeze
Ascending by band.
bool metallic
Whether some band straddles the Fermi level across the mesh.
bool automatic_frozen_window
Apply the published window, AUTOMATIC_WINDOW_RYDBERG, instead of the value above.
Real projectability_maximum
p >= this freezes a band regardless of its energy.
static constexpr Real AUTOMATIC_WINDOW_RYDBERG
The published window for atomic projectors, 2 eV above the Fermi level, in rydberg.
Real projectability_minimum
p < this discards a band, whatever its energy: the singular-gauge rule.
Every k-point's verdict, and the summary of the whole.
std::vector< KPointBandClassification > kpoints
BandClassificationSummary summary
std::vector< int > kept_bands
Band indices that survive, frozen first is NOT assumed; ascending.
std::vector< int > frozen_positions
Positions within kept_bands that are frozen, ascending.
std::vector< std::vector< Real > > eigenvalues_rydberg
Eigenvalues in Rydberg, indexed [kpoint_index][band_index].
Definition inputs.hpp:74
std::vector< std::vector< Real > > projectabilities
p_nk = sum_m |A_mn(k)|^2, indexed [kpoint_index][band_index].
Definition inputs.hpp:76
Real fermi_energy_rydberg
Definition inputs.hpp:77
int number_of_trial_orbitals
Definition inputs.hpp:72