Loading...
Searching...
No Matches
band_edges.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * The band edges either side of the Fermi energy, and the gap between them,
5 * read off a run's eigenvalues_at_kpoints record.
6 *
7 * Both are found from the eigenvalues and the Fermi energy alone, so they are
8 * worked out once from the records a flavor listed rather than by each
9 * flavor: add_band_edge_records() adds ESSE's band_edge_valence_max,
10 * band_edge_conduction_min and band_gaps records beside them.
11 */
12
13#include <algorithm>
14#include <cmath>
15#include <limits>
16#include <optional>
17#include <vector>
18
19#include "generated/esse/properties/non_scalar/band_gaps.hpp"
20#include "generated/esse/properties/non_scalar/eigenvalues_at_kpoints.hpp"
21#include "generated/esse/properties/scalar/band_edge_conduction_min.hpp"
22#include "generated/esse/properties/scalar/band_edge_valence_max.hpp"
23#include "generated/esse/properties/scalar/fermi_energy.hpp"
24#include "io/output/results.hpp"
26
27namespace q3 {
28
29/**
30 * ESSE's band_edge_valence_max for `eigenvalues`: the highest level at or below
31 * `fermi_energy_in_ry`, at the k-point of the last state holding it when the
32 * record lists k-points; nothing when no level lies there.
33 *
34 * The divider is the Fermi energy itself, not occupation: in an insulator it
35 * sits in the gap, and this is the valence-band maximum; in a metal it lies
36 * inside a band, and with band_edge_conduction_min() it brackets the local
37 * level spacing.
38 */
39[[nodiscard]] inline std::optional<generated::BandEdgeValenceMax>
40band_edge_valence_max(const generated::EigenvaluesAtKpoints& eigenvalues, Real fermi_energy_in_ry) {
41 std::optional<generated::BandEdgeValenceMax> edge;
42 for (const auto& kpoint : eigenvalues.values) {
43 for (const auto& channel : kpoint.eigenvalues) {
44 for (const Real energy_in_ry : channel.energies) {
45 if (energy_in_ry <= fermi_energy_in_ry && (!edge || energy_in_ry >= edge->value)) {
46 edge = generated::BandEdgeValenceMax{
47 generated::BandEdgeValenceMaxNameEnum::BAND_EDGE_VALENCE_MAX,
48 kpoint.kpoint,
49 std::nullopt,
50 generated::BandEdgeValenceMaxUnitsEnum::RY,
51 energy_in_ry};
52 }
53 }
54 }
55 }
56 return edge;
57}
58
59/// ESSE's band_edge_conduction_min: the lowest level above the Fermi energy, as above.
60[[nodiscard]] inline std::optional<generated::BandEdgeConductionMin>
61band_edge_conduction_min(const generated::EigenvaluesAtKpoints& eigenvalues,
62 Real fermi_energy_in_ry) {
63 std::optional<generated::BandEdgeConductionMin> edge;
64 for (const auto& kpoint : eigenvalues.values) {
65 for (const auto& channel : kpoint.eigenvalues) {
66 for (const Real energy_in_ry : channel.energies) {
67 if (energy_in_ry > fermi_energy_in_ry && (!edge || energy_in_ry <= edge->value)) {
68 edge = generated::BandEdgeConductionMin{
69 generated::BandEdgeConductionMinNameEnum::BAND_EDGE_CONDUCTION_MIN,
70 kpoint.kpoint,
71 std::nullopt,
72 generated::BandEdgeConductionMinUnitsEnum::RY,
73 energy_in_ry};
74 }
75 }
76 }
77 }
78 return edge;
79}
80
81/**
82 * Whether any level's occupation lies strictly between empty and full, by more
83 * than the tail a smeared insulator leaves on its band edges: the run's own
84 * statement that it found a metal.
85 */
86[[nodiscard]] inline bool
87is_fractionally_occupied(const generated::EigenvaluesAtKpoints& eigenvalues) {
88 constexpr Real occupation_tolerance = 1.0e-6;
89 for (const auto& kpoint : eigenvalues.values) {
90 for (const auto& channel : kpoint.eigenvalues) {
91 for (const Real occupation : channel.occupations.value_or(std::vector<double>{})) {
92 if (occupation > occupation_tolerance && occupation < 1.0 - occupation_tolerance) {
93 return true;
94 }
95 }
96 }
97 }
98 return false;
99}
100
101/// One ESSE band gap between two levels, with the k-points they lie at where known.
102[[nodiscard]] inline generated::BandGapsBandGap
103band_gap(generated::BandGapsBandGapTypeEnum type,
104 Real valence_in_ry,
105 Real conduction_in_ry,
106 const std::optional<std::vector<double>>& valence_kpoint,
107 const std::optional<std::vector<double>>& conduction_kpoint) {
108 generated::BandGapsBandGap gap;
109 gap.type = type;
110 gap.units = generated::BandGapsBandGapUnitsEnum::RY;
111 gap.value = conduction_in_ry - valence_in_ry;
112 gap.eigenvalue_valence = valence_in_ry;
113 gap.eigenvalue_conduction = conduction_in_ry;
114 gap.kpoint_valence = valence_kpoint;
115 gap.kpoint_conduction = conduction_kpoint;
116 return gap;
117}
118
119/**
120 * ESSE's band_gaps for `eigenvalues` about `fermi_energy_in_ry`: the "indirect"
121 * entry is the fundamental gap between the two band edges, wherever they sit,
122 * and the "direct" entry is the smallest vertical gap at any single k-point.
123 *
124 * Nothing where there is no gap: both edges have to exist, and no level may
125 * be fractionally occupied. In a smeared metal the edges are the local level
126 * spacing at E_F (0.018 Ry on the parity matrix's copper), which a band_gaps
127 * record would state as the material's gap.
128 */
129[[nodiscard]] inline std::optional<generated::BandGaps>
130band_gaps(const generated::EigenvaluesAtKpoints& eigenvalues, Real fermi_energy_in_ry) {
131 const auto valence = band_edge_valence_max(eigenvalues, fermi_energy_in_ry);
132 const auto conduction = band_edge_conduction_min(eigenvalues, fermi_energy_in_ry);
133 if (!valence || !conduction || is_fractionally_occupied(eigenvalues)) {
134 return std::nullopt;
135 }
136
137 std::optional<generated::BandGapsBandGap> direct;
138 for (const auto& kpoint : eigenvalues.values) {
139 Real kpoint_valence_in_ry = -std::numeric_limits<Real>::infinity();
140 Real kpoint_conduction_in_ry = std::numeric_limits<Real>::infinity();
141 for (const auto& channel : kpoint.eigenvalues) {
142 for (const Real energy_in_ry : channel.energies) {
143 if (energy_in_ry <= fermi_energy_in_ry) {
144 kpoint_valence_in_ry = std::max(kpoint_valence_in_ry, energy_in_ry);
145 } else {
146 kpoint_conduction_in_ry = std::min(kpoint_conduction_in_ry, energy_in_ry);
147 }
148 }
149 }
150 const bool has_both_edges =
151 std::isfinite(kpoint_valence_in_ry) && std::isfinite(kpoint_conduction_in_ry);
152 if (has_both_edges &&
153 (!direct || kpoint_conduction_in_ry - kpoint_valence_in_ry < direct->value)) {
154 direct = band_gap(generated::BandGapsBandGapTypeEnum::DIRECT,
155 kpoint_valence_in_ry,
156 kpoint_conduction_in_ry,
157 kpoint.kpoint,
158 kpoint.kpoint);
159 }
160 }
161
162 generated::BandGaps record;
163 record.name = generated::BandGapsNameEnum::BAND_GAPS;
164 if (direct) {
165 record.values.push_back(*direct);
166 }
167 record.values.push_back(band_gap(generated::BandGapsBandGapTypeEnum::INDIRECT,
168 valence->value,
169 conduction->value,
170 valence->kpoint,
171 conduction->kpoint));
172 return record;
173}
174
175/**
176 * Add the band edges and the gap between them to `results`, worked out from
177 * its eigenvalues_at_kpoints and fermi_energy records: each edge where such a
178 * level exists, and band_gaps where there is a gap. Adds nothing when either
179 * record is missing.
180 */
181inline void add_band_edge_records(generated::Results& results) {
182 const auto eigenvalues = find_record<generated::EigenvaluesAtKpoints>(results);
183 const auto fermi_energy = find_record<generated::FermiEnergy>(results);
184 if (!eigenvalues || !fermi_energy) {
185 return;
186 }
187 if (const auto valence = band_edge_valence_max(*eigenvalues, fermi_energy->value)) {
188 set_record(results, *valence);
189 }
190 if (const auto conduction = band_edge_conduction_min(*eigenvalues, fermi_energy->value)) {
191 set_record(results, *conduction);
192 }
193 if (const auto gaps = band_gaps(*eigenvalues, fermi_energy->value)) {
194 set_record(results, *gaps);
195 }
196}
197
198} // namespace q3
void set_record(generated::Results &results, const Record &record)
List record in the results, in place of any record of the same name.
Definition results.hpp:75
std::optional< generated::BandEdgeValenceMax > band_edge_valence_max(const generated::EigenvaluesAtKpoints &eigenvalues, Real fermi_energy_in_ry)
double Real
std::optional< generated::BandEdgeConductionMin > band_edge_conduction_min(const generated::EigenvaluesAtKpoints &eigenvalues, Real fermi_energy_in_ry)
ESSE's band_edge_conduction_min: the lowest level above the Fermi energy, as above.
std::optional< generated::BandGaps > band_gaps(const generated::EigenvaluesAtKpoints &eigenvalues, Real fermi_energy_in_ry)
generated::BandGapsBandGap band_gap(generated::BandGapsBandGapTypeEnum type, Real valence_in_ry, Real conduction_in_ry, const std::optional< std::vector< double > > &valence_kpoint, const std::optional< std::vector< double > > &conduction_kpoint)
One ESSE band gap between two levels, with the k-points they lie at where known.
void add_band_edge_records(generated::Results &results)
bool is_fractionally_occupied(const generated::EigenvaluesAtKpoints &eigenvalues)