Loading...
Searching...
No Matches
init.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * PlanewaveFermiEnergyCalculator — planewave Fermi energy and occupation calculator.
5 *
6 * Computes the Fermi energy from eigenvalues on a PlanewaveKohnShamQuantumSystem,
7 * dispatching to fixed (insulator) or smeared (metal) occupations.
8 */
9
10#include <algorithm>
11#include <cassert>
12#include <cmath>
13#include <stdexcept>
14#include <utility>
15#include <vector>
16
17#include "io/logger.hpp"
24
25namespace q3 {
26
27/// Which end of the electron-count plateau a Fermi-energy bisection converges to.
28enum class PlateauEdge { Lower, Upper };
29
30/// Planewave Fermi energy calculator — operates on PlanewaveKohnShamQuantumSystem.
32 public:
33 /**
34 * Default constructor: leaves quantum_system_ unset.
35 * calculate_occupations() asserts that it has been set before use.
36 */
38
40 PlanewaveKohnShamQuantumSystem& quantum_system,
42 const execution::PlanewaveExecutionPolicy* execution_policy = nullptr)
43 : quantum_system_(&quantum_system)
44 , smearing_(smearing)
45 , execution_policy_(execution_policy) {}
46
48 if (quantum_system_ == nullptr) {
49 throw std::logic_error(
50 "PlanewaveFermiEnergyCalculator::calculate_occupations() called before the "
51 "quantum system was assigned. The calculator is wired after construction, so "
52 "there is a window in which it is not yet usable; this reports that window "
53 "instead of dereferencing null, which is what the assert it replaces did in a "
54 "release build.");
55 }
56 if (uses_smearing()) {
57 if (!kpoint_weights_sample_the_brillouin_zone()) {
58 return quantum_system_->fermi_energy();
59 }
60 calculate_smeared_occupations();
61 } else {
62 calculate_fixed_occupations();
63 }
64 return quantum_system_->fermi_energy();
65 }
66
67 /// Check if this calculator uses smearing.
68 bool uses_smearing() const {
69 return smearing_ != nullptr && smearing_->uses_smearing();
70 }
71
72 const SmearingScheme* smearing() const {
73 return smearing_;
74 }
75
76 private:
77 /**
78 * Whether this k-point set is a Brillouin-zone quadrature at all.
79 *
80 * A smeared Fermi energy is found by bisecting sum_k w_k sum_n f(e_nk)
81 * against the electron count, which is a BZ integral and means nothing
82 * unless the weights are the quadrature's. KPointsGrid produces weights
83 * summing to the spin degeneracy -- 2 unpolarized, 1 per channel when the
84 * two spins are enumerated as separate states -- and a symmetry reduction
85 * preserves that sum. A BAND PATH does not: q3.workflows.band_structure
86 * gives every point weight 1, so the sum is the number of points on the
87 * path and the count comes out tens of times too large at every trial
88 * energy. The bisection then never reaches the target and converges to its
89 * own lower bracket, which is the smallest eigenvalue in the run.
90 *
91 * That is not a subtle error. Measured on copper: a band-path NSCF reported
92 * a Fermi energy of -7.046 Ry, BELOW the lowest band it had just computed,
93 * where the SCF on the same density reports 1.243 Ry. It is reported rather
94 * than computed here because a path samples lines through the zone, not the
95 * zone -- no weighting of it can be made into the integral, so the honest
96 * answer is that this run cannot determine one.
97 *
98 * The Fermi energy is left as it stands, which is the same choice the
99 * missing-band-edge path below makes and for the same reason: a zero a
100 * reader can recognize as "not determined" beats a plausible-looking number
101 * that is wrong. Use the SCF's value, which the workflows carry.
102 */
103 [[nodiscard]] bool kpoint_weights_sample_the_brillouin_zone() const {
104 const KPointsGrid& grid = quantum_system_->kpoint_grid();
105 Real total_weight = 0.0;
106 for (int kpoint_index = 0; kpoint_index < grid.number_of_kpoints(); kpoint_index++) {
107 total_weight += grid[kpoint_index].weight;
108 }
109 /**
110 * With pool parallelism this grid holds only the pool's own k-points,
111 * so the local sum is a fraction of the whole and every multi-rank run
112 * would fail this test. Summing across pools first is what makes the
113 * answer -- and therefore the decision every rank takes below -- the
114 * same on one rank and on four, which is exactly the invariance the MPI
115 * topology tests check and which the first version of this guard broke.
116 */
117 if (execution_policy_ != nullptr && execution_policy_->number_of_pools() > 1) {
118 execution_policy_->reduce_scalar(total_weight);
119 }
120 const Real expected = quantum_system_->spin_degeneracy();
121 if (std::abs(total_weight - expected) <= math::tolerances::kpoint_weight_sum) {
122 return true;
123 }
124 warn_once("Fermi energy: the {} k-point weights sum to {:.6f}, not the {:.1f} a "
125 "Brillouin-zone sampling carries, so this k-point set is not one this "
126 "calculation can integrate over -- a band path is the usual reason, since it "
127 "traces lines through the zone rather than filling it. No Fermi energy is "
128 "determined from it; the value stays at {:.8f} Ry. Take the Fermi energy from "
129 "the self-consistent run that produced the density.",
130 grid.number_of_kpoints(),
131 total_weight,
132 expected,
133 quantum_system_->fermi_energy());
134 return false;
135 }
136
137 /**
138 * For fixed occupations (insulators): set HOMO-LUMO midpoint as Fermi energy.
139 *
140 * The midpoint needs both edges to exist, and one of them can be missing. A
141 * run whose band count equals its occupied-band count -- an NSCF asked for
142 * exactly the occupied manifold, as a Wannier workflow does -- has no LUMO
143 * to pair the HOMO with. Reporting the midpoint anyway is how
144 * `fermi_energy_ry: 5e+29` reached a results file: the LUMO search starts
145 * at a +1e30 sentinel that nothing overwrites, and half of it survives into
146 * the answer. Both edges are handled here rather than only that one,
147 * because the HOMO search has the same shape at the other end.
148 *
149 * With only one edge, that edge is the answer: Quantum ESPRESSO reports
150 * "highest occupied level" alone where it would otherwise print "highest
151 * occupied, lowest unoccupied level", and this is the same statement --
152 * everything known about where E_F lies, without inventing the half that
153 * was never computed. A warning says so, since a run asked for a Fermi
154 * energy and got a band edge -- once per calculator, not once per SCF
155 * iteration, which would bury the rest of the log in a repetition of a
156 * fact that cannot change between iterations.
157 */
158 void calculate_fixed_occupations() const {
159 int number_of_kpoint_states = quantum_system_->number_of_kpoint_states();
160 int number_of_bands = quantum_system_->number_of_bands();
161 int occupied_bands = quantum_system_->number_of_occupied_bands_per_spin();
162
163 /**
164 * Both predicates are properties of the band count and the electron
165 * count, which every pool shares, so they cannot make the reductions
166 * below disagree between ranks.
167 */
168 const bool has_occupied_band = occupied_bands > 0 && occupied_bands <= number_of_bands;
169 const bool has_unoccupied_band = occupied_bands < number_of_bands;
170
171 // Fermi energy is midpoint of HOMO-LUMO gap
172 Real highest_occupied_molecular_orbital = -1e30;
173 Real lowest_unoccupied_molecular_orbital = 1e30;
174 for (int kpoint_state_index = 0; kpoint_state_index < number_of_kpoint_states;
175 kpoint_state_index++) {
176 if (has_occupied_band) {
177 highest_occupied_molecular_orbital =
178 std::max(highest_occupied_molecular_orbital,
179 quantum_system_->eigenvalue(kpoint_state_index, occupied_bands - 1));
180 }
181 if (has_unoccupied_band) {
182 lowest_unoccupied_molecular_orbital =
183 std::min(lowest_unoccupied_molecular_orbital,
184 quantum_system_->eigenvalue(kpoint_state_index, occupied_bands));
185 }
186 }
187
188 /**
189 * With pool parallelism, each pool has only local eigenvalues.
190 * Reduce HOMO (max) and LUMO (min) across all pools to find global gap.
191 */
192 if (execution_policy_ != nullptr && execution_policy_->number_of_pools() > 1) {
193 execution_policy_->reduce_scalar_max(highest_occupied_molecular_orbital);
194 execution_policy_->reduce_scalar_min(lowest_unoccupied_molecular_orbital);
195 }
196
197 if (has_occupied_band && has_unoccupied_band) {
198 quantum_system_->set_fermi_energy(
199 0.5 * (highest_occupied_molecular_orbital + lowest_unoccupied_molecular_orbital));
200 return;
201 }
202
203 if (has_occupied_band) {
204 warn_once("Fermi energy: {} band(s) requested for {} occupied band(s), so there is no "
205 "unoccupied level to bound the gap from above; reporting the highest "
206 "occupied level ({:.8f} Ry) instead of a mid-gap Fermi energy.",
207 number_of_bands,
208 occupied_bands,
209 highest_occupied_molecular_orbital);
210 quantum_system_->set_fermi_energy(highest_occupied_molecular_orbital);
211 return;
212 }
213
214 if (has_unoccupied_band) {
215 warn_once("Fermi energy: no occupied bands ({} electron(s)), so there is no occupied "
216 "level to bound the gap from below; reporting the lowest unoccupied level "
217 "({:.8f} Ry).",
218 quantum_system_->number_of_electrons(),
219 lowest_unoccupied_molecular_orbital);
220 quantum_system_->set_fermi_energy(lowest_unoccupied_molecular_orbital);
221 return;
222 }
223
224 /**
225 * Neither edge exists, so there is no eigenvalue to report at all. The
226 * system's own initial value stands rather than a sentinel: a zero that
227 * a reader can recognize as "not determined" beats 5e+29, and a run
228 * with no bands has nothing else to say.
229 */
230 warn_once("Fermi energy: no bands to report one from; leaving it at {:.8f} Ry.",
231 quantum_system_->fermi_energy());
232 }
233
234 /**
235 * Report a missing band edge once for this calculator's lifetime.
236 *
237 * Which edge is missing follows from the band and electron counts, so it
238 * holds for every iteration of a run; saying it each time would add a line
239 * per SCF step and tell the reader nothing new after the first.
240 */
241 template <typename... Arguments>
242 void warn_once(const char* message, Arguments&&... arguments) const {
243 if (has_warned_about_missing_band_edge_) {
244 return;
245 }
246 has_warned_about_missing_band_edge_ = true;
247 log::warn(message, std::forward<Arguments>(arguments)...);
248 }
249
250 /**
251 * For smeared occupations (metals): bisection to find Fermi energy,
252 * then set fractional occupations.
253 */
254 void calculate_smeared_occupations() const {
255 int number_of_kpoint_states = quantum_system_->number_of_kpoint_states();
256 int number_of_bands = quantum_system_->number_of_bands();
257 Real number_of_electrons = quantum_system_->number_of_electrons();
258 Real smearing_width = smearing_->smearing_width();
259
260 /**
261 * Every k-point state's weight and eigenvalues, in global order.
262 *
263 * The bisection below evaluates the electron count ~100 times per
264 * plateau edge. It used to sum each pool's states locally and
265 * Allreduce the partial sum on every trial energy -- two hundred
266 * scalar collectives per SCF iteration, and a count whose rounding
267 * depended on how the k-points were split across pools, so the Fermi
268 * energy itself moved in its last bits with the topology. Gathering
269 * the (tiny) eigenvalue table once and bisecting on it locally removes
270 * both: no collective inside the loop, and every rank sums the same
271 * numbers in the same global order a serial run does, so the count --
272 * and with it E_F -- is bit-identical on 1 rank and on N pools for
273 * identical eigenvalues. Serial runs take the local table unchanged,
274 * which is the order and arithmetic they always had.
275 */
276 const std::vector<Real> state_table = global_kpoint_state_table();
277 const int values_per_state = number_of_bands + 1;
278 const int number_of_global_states = static_cast<int>(state_table.size()) / values_per_state;
279 const auto weight_of = [&](int state_index) {
280 return state_table[static_cast<std::size_t>(state_index) * values_per_state];
281 };
282 const auto eigenvalue_of = [&](int state_index, int band_index) {
283 return state_table[static_cast<std::size_t>(state_index) * values_per_state + 1 +
284 band_index];
285 };
286
287 // Find eigenvalue range
288 Real eigenvalue_minimum = 1e30, eigenvalue_maximum = -1e30;
289 for (int state_index = 0; state_index < number_of_global_states; state_index++) {
290 for (int band_index = 0; band_index < number_of_bands; band_index++) {
291 Real energy = eigenvalue_of(state_index, band_index);
292 eigenvalue_minimum = std::min(eigenvalue_minimum, energy);
293 eigenvalue_maximum = std::max(eigenvalue_maximum, energy);
294 }
295 }
296
297 eigenvalue_minimum -= 1.0;
298 eigenvalue_maximum += 1.0;
299
300 /**
301 * Σ_{k,σ,n} w_k f((ε − E_F)/σ) over the global table. K-point weights
302 * already carry the spin factor: they sum to 2 (spin degeneracy) for
303 * unpolarized runs and are halved per channel for LSDA, so there is
304 * no extra factor. Summed in one fixed order on every rank, so the two
305 * bisections agree across ranks by construction, with no reduction.
306 */
307 auto count_electrons = [&](Real trial_fermi_energy) {
308 Real total_electrons = 0.0;
309 for (int state_index = 0; state_index < number_of_global_states; state_index++) {
310 Real weight = weight_of(state_index);
311 for (int band_index = 0; band_index < number_of_bands; band_index++) {
312 Real energy_difference_ratio =
313 (eigenvalue_of(state_index, band_index) - trial_fermi_energy) /
314 smearing_width;
315 total_electrons += weight * smearing_->occupation(energy_difference_ratio);
316 }
317 }
318 return total_electrons;
319 };
320
321 /**
322 * Bisect for one end of the electron-count plateau and return it.
323 *
324 * `PlateauEdge::Lower` breaks ties toward the bottom (the first E_F
325 * reaching the target count), `Upper` toward the top, within the
326 * count tolerance. On a metal the two differ only by that tolerance
327 * divided by the density of states; across a gap they bracket it.
328 */
329 auto bisect_for_fermi_energy = [&](PlateauEdge edge) {
330 bool take_upper_edge = edge == PlateauEdge::Upper;
331 Real lower_bound = eigenvalue_minimum;
332 Real upper_bound = eigenvalue_maximum;
333 Real trial_fermi_energy = 0.0;
334 for (int iteration = 0; iteration < 100; iteration++) {
335 trial_fermi_energy = 0.5 * (lower_bound + upper_bound);
336 Real total_electrons = count_electrons(trial_fermi_energy);
337 /**
338 * On the plateau the electron count equals the target, and
339 * which way the tie breaks decides which end of the plateau
340 * this converges to: `<` walks down to its bottom, `<=` (with
341 * a tolerance, since the count only equals the target to
342 * within the smearing tails) walks up to its top.
343 */
344 bool below_target =
345 take_upper_edge
346 ? total_electrons <=
348 : total_electrons < number_of_electrons;
349 if (below_target)
350 lower_bound = trial_fermi_energy;
351 else
352 upper_bound = trial_fermi_energy;
353 if (std::abs(upper_bound - lower_bound) <
355 break;
356 }
357 return trial_fermi_energy;
358 };
359
360 /**
361 * Where E_F sits *inside a gap* is not arbitrary once the occupations
362 * are smeared.
363 *
364 * The electron count is flat across a gap -- every E_F in it returns
365 * the same integer count -- so a single bisection converges to
366 * whichever end its tie-breaking favours, which is the bottom: the
367 * valence band edge. That places occupied states directly at E_F, and
368 * the smearing then spills them. Measured on FeO with U on Fe 3d and
369 * degauss = 0.02 Ry: E_F landed 1.06 eV (3.9 sigma) below the value an
370 * independent implementation reports, carrying 4.2 mRy of spurious
371 * entropy where that implementation has exactly zero, and moving the
372 * total energy by 2.4 mRy. The error vanishes below degauss ~ 0.01 Ry,
373 * where the tails no longer reach -- which is why it hid: it is
374 * invisible in the sigma -> 0 limit the tests mostly live in, and grows
375 * with the smearing width users actually pick for hard magnetic cases.
376 *
377 * Taking the midpoint of the bracket puts E_F as far from both band
378 * edges as the gap allows. On a metal there is no plateau, so the two
379 * ends differ only by the count tolerance divided by the density of
380 * states, and the midpoint would leave the cell holding half that
381 * tolerance in extra electrons. The width test below keeps a metal on
382 * the single-bisection answer, where the count is exact. It compares
383 * against the smearing width: a gap the smearing straddles spans
384 * several of them (FeO's lower end alone sat 3.9 sigma below the
385 * independent value above), while a metal's bracket is far narrower --
386 * 7.6e-8 Ry on copper, whose density of states is 13 states/Ry. The
387 * bisection tolerance it once compared against, 1e-12 Ry, took the
388 * midpoint on every metal: copper then carried 19 + 5e-7 electrons, and
389 * its total energy rose by E_F x 5e-7 = 0.62 μRy.
390 *
391 * Refining further to the true mid-gap -- the highest eigenvalue below
392 * the plateau and the lowest above it -- is the sigma-independent
393 * choice and was tried: it improves degauss = 0.02 to within 0.34 mRy,
394 * but destabilises small smearings, where the count stays flat far past
395 * the gap, the bracket spans other bands, and the spectrum gets split
396 * in the wrong place. Identifying the gap robustly needs the
397 * occupied-state count rather than the current E_F, which is a larger
398 * change than this fix.
399 */
400 Real fermi_energy = bisect_for_fermi_energy(PlateauEdge::Lower);
401 Real fermi_energy_plateau_top = bisect_for_fermi_energy(PlateauEdge::Upper);
402 if (fermi_energy_plateau_top - fermi_energy > smearing_width)
403 fermi_energy = 0.5 * (fermi_energy + fermi_energy_plateau_top);
404
405 quantum_system_->set_fermi_energy(fermi_energy);
406
407 // Compute fractional occupations
408 for (int kpoint_state_index = 0; kpoint_state_index < number_of_kpoint_states;
409 kpoint_state_index++) {
410 for (int band_index = 0; band_index < number_of_bands; band_index++) {
411 Real energy_difference_ratio =
412 (quantum_system_->eigenvalue(kpoint_state_index, band_index) - fermi_energy) /
413 smearing_width;
414 quantum_system_->set_occupation(
415 kpoint_state_index, band_index, smearing_->occupation(energy_difference_ratio));
416 }
417 }
418 }
419
420 /**
421 * This pool's k-point states as rows of [w_k, e_1k, ..., e_Nk], assembled
422 * across pools into the global spin-major order (one gather, no per-step
423 * reduction). Without pool parallelism the local rows are the global ones.
424 */
425 [[nodiscard]] std::vector<Real> global_kpoint_state_table() const {
426 const int number_of_kpoint_states = quantum_system_->number_of_kpoint_states();
427 const int number_of_bands = quantum_system_->number_of_bands();
428 const int values_per_state = number_of_bands + 1;
429 const KPointsGrid& grid = quantum_system_->kpoint_grid();
430 const int local_number_of_kpoints = grid.number_of_kpoints();
431 std::vector<Real> table(static_cast<std::size_t>(number_of_kpoint_states) *
432 values_per_state);
433 if (local_number_of_kpoints == 0) {
434 table.clear();
435 }
436 for (int kpoint_state_index = 0;
437 local_number_of_kpoints > 0 && kpoint_state_index < number_of_kpoint_states;
438 kpoint_state_index++) {
439 const std::size_t row = static_cast<std::size_t>(kpoint_state_index) * values_per_state;
440 table[row] = grid[kpoint_state_index % local_number_of_kpoints].weight;
441 for (int band_index = 0; band_index < number_of_bands; band_index++) {
442 table[row + 1 + band_index] =
443 quantum_system_->eigenvalue(kpoint_state_index, band_index);
444 }
445 }
446 if (execution_policy_ == nullptr || execution_policy_->number_of_pools() <= 1) {
447 return table;
448 }
449 /**
450 * The spin-block count comes from the spin setting, not from this
451 * pool's state count, so a pool that owns no k-point still passes the
452 * same dimensions as every other pool into the collective.
453 */
454 const int number_of_spin_blocks = quantum_system_->number_of_spin_components == 2 ? 2 : 1;
455 return execution_policy_->assemble_global_kpoint_state_data(
456 table, local_number_of_kpoints, number_of_spin_blocks, values_per_state);
457 }
458
459 mutable bool has_warned_about_missing_band_edge_ = false;
460 PlanewaveKohnShamQuantumSystem* quantum_system_ = nullptr;
461 const SmearingScheme* smearing_ = nullptr;
462 const execution::PlanewaveExecutionPolicy* execution_policy_ = nullptr;
463};
464
465} // namespace q3
Abstract base for Fermi energy calculators.
Definition init.hpp:15
int number_of_kpoints() const
Planewave Fermi energy calculator — operates on PlanewaveKohnShamQuantumSystem.
Definition init.hpp:31
PlanewaveFermiEnergyCalculator(PlanewaveKohnShamQuantumSystem &quantum_system, const SmearingScheme *smearing, const execution::PlanewaveExecutionPolicy *execution_policy=nullptr)
Definition init.hpp:39
bool uses_smearing() const
Check if this calculator uses smearing.
Definition init.hpp:68
const SmearingScheme * smearing() const
Definition init.hpp:72
Real calculate_occupations() override
Compute occupations and return the Fermi energy.
Definition init.hpp:47
const KPointsGrid & kpoint_grid() const
Definition init.hpp:80
void set_occupation(int kpoint_state_index, int band_index, Real value)
Definition init.hpp:138
int number_of_occupied_bands_per_spin() const
Definition init.hpp:156
Real eigenvalue(int kpoint_state_index, int band_index) const
Definition init.hpp:118
void set_fermi_energy(Real value)
Definition init.hpp:203
Abstract interface for electronic smearing schemes.
Definition init.hpp:19
Real smearing_width() const
Smearing width σ (degauss) in Ry.
Definition init.hpp:33
virtual Real occupation(Real x) const =0
Compute the occupation f(x) where x = (E - E_F) / σ.
bool uses_smearing() const
Whether this scheme uses actual smearing (width > 0).
Definition init.hpp:38
virtual void reduce_scalar_min(Real &value) const =0
virtual void reduce_scalar_max(int &value) const =0
virtual int number_of_pools() const =0
virtual std::vector< Real > assemble_global_kpoint_state_data(const std::vector< Real > &local_data, int local_number_of_physical_kpoints, int number_of_spin_components, int number_of_values_per_state) const =0
virtual void reduce_scalar(Real &value) const =0
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
constexpr double fermi_electron_count_plateau
constexpr double fermi_energy_bisection_convergence
constexpr double kpoint_weight_sum
double Real
generated::Energy energy(const std::string &name, Real value_in_ry)
One of Q3's energies that ESSE has no property for, as an ESSE energy in rydberg.
Definition results.hpp:103
PlateauEdge
Which end of the electron-count plateau a Fermi-energy bisection converges to.
Definition init.hpp:28