Loading...
Searching...
No Matches
subspace_disentangler.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * SubspaceDisentangler — choose, at every k-point, the N-dimensional subspace
5 * of the kept bands that varies most smoothly across the Brillouin zone.
6 *
7 * This is the Souza-Marzari-Vanderbilt iteration (PRB 65, 035109). It is what
8 * makes an entangled manifold interpolable: when the bands worth modelling are
9 * not separated from everything above them at every k -- a transition metal's
10 * d and s bands, where the sp band leaves the manifold and comes back -- no
11 * choice of band range works, because the state sitting at a given index is
12 * not the same state everywhere.
13 *
14 * What is minimised is the gauge-invariant spread
15 *
16 * Omega_I = (1/N_k) sum_k sum_b w_b [ N - sum_mn |M~_mn(k,b)|^2 ]
17 *
18 * with M~ the overlap between the CHOSEN subspaces at k and k+b. Equivalently,
19 * and this is what the iteration does, one maximises sum_k Tr[P(k) Z(k)] with
20 *
21 * Z(k) = sum_b w_b M(k,b) P(k+b) M(k,b)^dagger
22 *
23 * Omega_I comes out in BOHR^2: the record's b-vectors are in inverse bohr, and
24 * the weights that satisfy sum_b w_b b_a b_b = delta_ab therefore carry bohr^2.
25 * Nothing here is in Angstrom.
26 *
27 * Three details decide whether this is correct rather than merely plausible.
28 *
29 * - Discarded bands are gone before anything starts. The working basis at a
30 * k-point is its kept bands, and every overlap block is sliced on BOTH
31 * sides, with the row k-point's keep-list and the column k-point's, because
32 * the two differ. Leave them in and the optimiser selects them back, making
33 * the classification decorative.
34 * - The frozen states are held out by diagonalising in an explicit basis of
35 * their complement, not by projecting them out in place. The projected
36 * matrix carries one exact zero eigenvalue per frozen direction, so a
37 * genuine complement direction near zero can lose to one of them and a
38 * frozen state rotates back in.
39 * - Z is damped between iterations. The undamped iteration oscillates on
40 * metals; 0.5 is the same mixing Wannier90 defaults to.
41 *
42 * The result is one semi-unitary V(k), kept-bands by N, per k-point: the
43 * columns span the chosen subspace. The gauge is then computed from
44 * V(k)^dagger A(k), which is square, and the Hamiltonian rotation receives the
45 * composite V(k) U(k).
46 */
47
48#include <algorithm>
49#include <cmath>
50#include <complex>
51#include <cstddef>
52#include <sstream>
53#include <stdexcept>
54#include <string>
55#include <utility>
56#include <vector>
57
72
73namespace q3 {
74
76 public:
77 /**
78 * `classification` is one verdict per k-point of the stencil: the kept
79 * bands and the frozen ones. `number_of_wannier_functions` is the
80 * dimension of the subspace chosen at every one of them.
81 */
83 const std::vector<KPointBandClassification>& classification,
84 int number_of_wannier_functions,
86 : stencil_(stencil)
87 , classification_(classification)
88 , number_of_wannier_functions_(number_of_wannier_functions)
89 , settings_(settings) {
90 if (number_of_wannier_functions < 1) {
91 throw std::invalid_argument(
92 "SubspaceDisentangler: number_of_wannier_functions must be positive, got " +
93 std::to_string(number_of_wannier_functions));
94 }
95 if (static_cast<int>(classification.size()) != stencil.number_of_kpoints()) {
96 throw std::invalid_argument("SubspaceDisentangler: the classification covers " +
97 std::to_string(classification.size()) +
98 " k-points and the stencil " +
99 std::to_string(stencil.number_of_kpoints()));
100 }
101 if (settings.maximum_iterations < 1) {
102 throw std::invalid_argument(
103 "SubspaceDisentangler: maximum_iterations must be at least 1, got " +
104 std::to_string(settings.maximum_iterations));
105 }
106 if (!(settings.damping > 0.0 && settings.damping <= 1.0)) {
107 throw std::invalid_argument("SubspaceDisentangler: damping must lie in (0, 1], got " +
108 std::to_string(settings.damping));
109 }
110 }
111
112 /**
113 * `overlaps` are M_mn(k,b) over the CAPTURED bands, exactly as the record
114 * stores them; they are sliced to the keep-lists here, once.
115 * `initial_subspaces[k]` is kept(k) x N, the orthonormalised column space
116 * of A(k) restricted to the kept bands.
117 */
118 [[nodiscard]] DisentanglementResult
119 run(const BlochOverlaps& overlaps, std::vector<DenseMatrix<Complex>> initial_subspaces) const {
120 const int number_of_kpoints = stencil_.number_of_kpoints();
121 require_subspace_shapes(initial_subspaces);
122 const BlochOverlaps sliced = overlaps.restricted_to(classification_, stencil_);
123
124 std::vector<DenseMatrix<Complex>> subspaces = std::move(initial_subspaces);
125 std::vector<DenseMatrix<Complex>> damped_z(static_cast<std::size_t>(number_of_kpoints));
127
128 for (int iteration = 1; iteration <= settings_.maximum_iterations; iteration++) {
129 Real spread = 0.0;
130 std::vector<DenseMatrix<Complex>> next_subspaces(
131 static_cast<std::size_t>(number_of_kpoints));
132
133 for (int kpoint = 0; kpoint < number_of_kpoints; kpoint++) {
134 const DenseMatrix<Complex> z =
135 subspace_matrix_at(kpoint, sliced, subspaces, spread);
136 DenseMatrix<Complex>& damped = damped_z[static_cast<std::size_t>(kpoint)];
137 damped = damped.empty() ? z : mixed(damped, z);
138 next_subspaces[static_cast<std::size_t>(kpoint)] =
139 choose_subspace(damped, classification_[static_cast<std::size_t>(kpoint)]);
140 }
141
142 subspaces = std::move(next_subspaces);
143 spread /= static_cast<Real>(number_of_kpoints);
144 require_nonnegative(spread);
145 window.record(spread);
146
147 if (window.has_converged()) {
148 /**
149 * The spread accumulated above belongs to the subspaces this
150 * iteration STARTED from, because those are what Z was built
151 * against; the subspaces about to be returned are one update
152 * further on. At convergence the two differ by less than the
153 * tolerance, but "within tolerance" is not "the same number",
154 * and a caller that prints Omega_I beside a model is entitled
155 * to the Omega_I of THAT model. One extra pass, once.
156 */
157 const Real final_spread = spread_of(sliced, subspaces);
158 log::info("disentanglement: converged in {} iterations, Omega_I = {:.9f} bohr^2",
159 iteration,
160 final_spread);
161 return {std::move(subspaces), final_spread, iteration};
162 }
163 }
164
167 "disentanglement did not converge in " + std::to_string(settings_.maximum_iterations) +
168 " iterations. Last gauge-invariant spreads (bohr^2): " +
169 window.recent_history_text() +
170 ". A subspace that has not converged is not returned as if it had.",
171 window.recent_history());
172 }
173
174 private:
175 /**
176 * Z(k) = sum_b w_b [M(k,b) V(k+b)] [M(k,b) V(k+b)]^dagger over the kept
177 * bands, the matrix whose leading eigenvectors are the next subspace at
178 * `kpoint`; and, added to `spread` on the way, this k-point's part of
179 * Omega_I for the subspaces Z is built against.
180 *
181 * M(k,b) V(k+b) is what both are built from: Z accumulates its outer
182 * product, and the spread's positive term is the part of it V(k) keeps.
183 * Forming it once is not only the cheaper arrangement, it is the only
184 * one in which the two are provably the same quantity -- which is why
185 * one function returns the first and accumulates the second.
186 */
187 [[nodiscard]] DenseMatrix<Complex>
188 subspace_matrix_at(int kpoint,
189 const BlochOverlaps& sliced,
190 const std::vector<DenseMatrix<Complex>>& subspaces,
191 Real& spread) const {
192 const int kept = classification_[static_cast<std::size_t>(kpoint)].number_kept();
193 DenseMatrix<Complex> z(kept, kept, Complex(0.0, 0.0));
194 for (int direction = 0; direction < stencil_.number_of_directions(); direction++) {
195 const Real weight = stencil_.weight(direction);
196 const DenseMatrix<Complex> carried =
198 sliced.at(kpoint, direction),
200 subspaces[static_cast<std::size_t>(stencil_.neighbor(kpoint, direction))]);
202 carried,
204 carried,
205 Complex(weight, 0.0),
206 z);
207 spread +=
208 weight * (static_cast<Real>(number_of_wannier_functions_) -
209 retained_weight(subspaces[static_cast<std::size_t>(kpoint)], carried));
210 }
211 return z;
212 }
213
214 /// damping * fresh + (1 - damping) * previous: undamped, the iteration oscillates on metals.
215 [[nodiscard]] DenseMatrix<Complex> mixed(const DenseMatrix<Complex>& previous,
216 const DenseMatrix<Complex>& fresh) const {
217 DenseMatrix<Complex> result = previous.scaled(1.0 - settings_.damping);
218 result.add_scaled(settings_.damping, fresh);
219 return result;
220 }
221
222 /**
223 * Omega_I cannot be negative. Its positive term is sum_mn |M~_mn|^2 over
224 * an N-dimensional subspace, and for an overlap between two ORTHONORMAL
225 * sets of states that sum is at most N, every singular value of M being
226 * at most one. A negative value therefore does not mean the optimisation
227 * went wrong; it means the overlaps it was given are not overlaps of
228 * orthonormal states -- a corrupt record, or matrices assembled by hand
229 * for a test without that property. Saying so here is worth more than
230 * returning a number whose sign is impossible.
231 */
232 void require_nonnegative(Real spread) const {
234 static_cast<Real>(number_of_wannier_functions_)) {
235 std::ostringstream value;
236 value << std::scientific << spread;
237 throw WannierDiagnosticError(
239 "disentanglement: the gauge-invariant spread came out negative (" + value.str() +
240 " bohr^2), which no overlap between orthonormal states can produce. Some "
241 "M_mn(k,b) has a singular value above one, so the record's overlaps are "
242 "corrupt or were not built from orthonormal Bloch states.");
243 }
244 }
245
246 void require_subspace_shapes(const std::vector<DenseMatrix<Complex>>& subspaces) const {
247 if (static_cast<int>(subspaces.size()) != stencil_.number_of_kpoints()) {
248 throw std::invalid_argument(
249 "SubspaceDisentangler: " + std::to_string(subspaces.size()) +
250 " initial subspaces were given for " +
251 std::to_string(stencil_.number_of_kpoints()) + " k-points");
252 }
253 for (std::size_t kpoint = 0; kpoint < subspaces.size(); kpoint++) {
254 const int kept = classification_[kpoint].number_kept();
255 if (subspaces[kpoint].rows() != kept ||
256 subspaces[kpoint].columns() != number_of_wannier_functions_) {
257 throw std::invalid_argument(
258 "SubspaceDisentangler: the initial subspace at k-point " +
259 std::to_string(kpoint) + " is " + std::to_string(subspaces[kpoint].rows()) +
260 " x " + std::to_string(subspaces[kpoint].columns()) + ", not the " +
261 std::to_string(kept) + " x " + std::to_string(number_of_wannier_functions_) +
262 " its kept bands and the Wannier functions require");
263 }
264 }
265 }
266
267 /// sum_mn |V(k)^dagger M V(k+b)|^2, the part of the carried subspace V(k) keeps.
268 [[nodiscard]] static Real retained_weight(const DenseMatrix<Complex>& subspace,
269 const DenseMatrix<Complex>& carried) {
270 return multiply(
273 }
274
275 /**
276 * Omega_I of a given set of subspaces, over the whole zone. The iteration
277 * gets this for free while it builds Z; this is for the one place that
278 * needs it for subspaces Z was not built from.
279 */
280 [[nodiscard]] Real spread_of(const BlochOverlaps& sliced,
281 const std::vector<DenseMatrix<Complex>>& subspaces) const {
282 Real spread = 0.0;
283 for (int kpoint = 0; kpoint < stencil_.number_of_kpoints(); kpoint++) {
284 for (int direction = 0; direction < stencil_.number_of_directions(); direction++) {
285 const int neighbor = stencil_.neighbor(kpoint, direction);
286 const DenseMatrix<Complex> carried =
288 sliced.at(kpoint, direction),
290 subspaces[static_cast<std::size_t>(neighbor)]);
291 spread += stencil_.weight(direction) *
292 (static_cast<Real>(number_of_wannier_functions_) -
293 retained_weight(subspaces[static_cast<std::size_t>(kpoint)], carried));
294 }
295 }
296 return spread / static_cast<Real>(stencil_.number_of_kpoints());
297 }
298
299 /**
300 * The frozen states, plus the leading eigenvectors of Z restricted to an
301 * explicit orthonormal basis of their complement.
302 */
303 [[nodiscard]] DenseMatrix<Complex>
304 choose_subspace(const DenseMatrix<Complex>& z, const KPointBandClassification& entry) const {
305 const int kept = entry.number_kept();
306 const int frozen = entry.number_frozen();
307 DenseMatrix<Complex> subspace(kept, number_of_wannier_functions_, Complex(0.0, 0.0));
308
309 for (int index = 0; index < frozen; index++) {
310 subspace(entry.frozen_positions[static_cast<std::size_t>(index)], index) =
311 Complex(1.0, 0.0);
312 }
313 const int wanted = number_of_wannier_functions_ - frozen;
314 if (wanted == 0) {
315 return subspace;
316 }
317
318 /**
319 * The frozen states are unit vectors of the working basis, so their
320 * complement is spanned by the remaining unit vectors. No
321 * orthogonalisation is needed, and the restricted matrix is built by
322 * indexing rather than by projecting -- which is the point: the
323 * frozen directions never appear in the eigenproblem at all.
324 */
325 std::vector<int> complement;
326 complement.reserve(static_cast<std::size_t>(kept - frozen));
327 std::vector<bool> is_frozen(static_cast<std::size_t>(kept), false);
328 for (const int position : entry.frozen_positions) {
329 is_frozen[static_cast<std::size_t>(position)] = true;
330 }
331 for (int position = 0; position < kept; position++) {
332 if (!is_frozen[static_cast<std::size_t>(position)]) {
333 complement.push_back(position);
334 }
335 }
336
337 const HermitianEigenDecomposition decomposition =
338 HermitianEigensolver("SubspaceDisentangler: the subspace matrix Z(k)")
339 .solve(z.submatrix(complement, complement));
340
341 /// zheev returns ascending eigenvalues; the largest are the last.
342 const int dimension = static_cast<int>(complement.size());
343 for (int index = 0; index < wanted; index++) {
344 const int source = dimension - 1 - index;
345 const int target = frozen + index;
346 for (int row = 0; row < dimension; row++) {
347 subspace(complement[static_cast<std::size_t>(row)], target) =
348 decomposition.eigenvectors(row, source);
349 }
350 }
351 return subspace;
352 }
353
354 const KPointStencil& stencil_;
355 const std::vector<KPointBandClassification>& classification_;
356 int number_of_wannier_functions_;
357 DisentanglementSettings settings_;
358};
359
360} // namespace q3
BlochOverlaps restricted_to(const std::vector< KPointBandClassification > &classification, const KPointStencil &stencil) const
const DenseMatrix< Complex > & at(int kpoint, int direction) const
std::string recent_history_text(int count=5) const
The last count values, oldest first, as "a, b, c" with six decimals each.
void record(Real value)
The objective at the end of one more step.
std::vector< Real > recent_history(int count=5) const
The last count values, oldest first.
Real frobenius_norm_squared() const
The sum of every element's squared modulus.
bool empty() const
int number_of_directions() const
int number_of_kpoints() const
Real weight(int direction) const
int neighbor(int kpoint, int direction) const
The k-point that k + b lands on.
DisentanglementResult run(const BlochOverlaps &overlaps, std::vector< DenseMatrix< Complex > > initial_subspaces) const
SubspaceDisentangler(const KPointStencil &stencil, const std::vector< KPointBandClassification > &classification, int number_of_wannier_functions, DisentanglementSettings settings)
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
constexpr double wannier_negative_spread_tolerance
double Real
void multiply_add(MatrixOperation left_operation, const DenseMatrix< Complex > &left, MatrixOperation right_operation, const DenseMatrix< Complex > &right, Complex alpha, DenseMatrix< Complex > &result)
std::complex< double > Complex
DenseMatrix< Complex > multiply(MatrixOperation left_operation, const DenseMatrix< Complex > &left, MatrixOperation right_operation, const DenseMatrix< Complex > &right, Complex alpha=Complex(1.0, 0.0))
alpha * op(left) * op(right), as a new matrix of the product's shape.
@ overlaps_not_orthonormal
The gauge-invariant spread came out negative: the overlaps are not of orthonormal states.
@ disentanglement_unconverged
The subspace iteration exhausted its budget; the error carries the spreads it reached.
int convergence_window
Iterations the relative change must stay below the tolerance for.
Real convergence_tolerance
Relative, because an absolute spread threshold does not transfer between systems.
Real damping
Mixing of Z between iterations; 1 is undamped and oscillates on metals.