Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
spread_functional.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * SpreadFunctional — the Marzari-Vanderbilt spread Omega, its three parts, the
5 * Wannier centres, and the analytic gradient with respect to the gauge.
6 *
7 * The projection gauge fixes a subspace and an orientation inside it in one
8 * shot, and that orientation is arbitrary. What decides whether the model
9 * interpolates is how localised the resulting Wannier functions are, because
10 * the interpolation is a Fourier sum over lattice vectors: functions that decay
11 * quickly give an H(R) that decays quickly, and a truncated sum that is
12 * accurate between the mesh points as well as on them. The one-shot gauge is
13 * exact ON the mesh whatever it does -- which is why a model can look perfect
14 * at every k-point it was built from and be hundreds of meV wrong halfway
15 * between two of them.
16 *
17 * Omega splits into three pieces (PRB 56, 12847, section IV):
18 *
19 * Omega_I = (1/N_k) sum_kb w_b [ N - sum_mn |M_mn|^2 ]
20 * Omega_OD = (1/N_k) sum_kb w_b sum_{m != n} |M_mn|^2
21 * Omega_D = (1/N_k) sum_kb w_b sum_n [ Im ln M_nn + b . r_n ]^2
22 *
23 * with M here always the overlap AFTER the current gauge rotation. Omega_I is
24 * invariant under any k-dependent unitary mixing of the chosen states, which is
25 * exactly why the disentangler minimises it separately and why it is the one
26 * number that must not move while this class works. Only Omega_D + Omega_OD is
27 * minimisable here, and minimising it is what "maximally localised" means.
28 *
29 * Centres come from the same diagonal phases,
30 *
31 * r_n = -(1/N_k) sum_kb w_b b Im ln M_nn,
32 *
33 * which is a finite-difference Berry phase: it is why the b-vectors and their
34 * weights have to be a complete shell set, and why the centres come out in
35 * BOHR when the b-vectors are in inverse bohr. Nothing here is in Angstrom.
36 *
37 * The gradient is MV equation 52. Its two halves pull in different directions:
38 * A[R] drives the off-diagonal overlap weight into the diagonal, S[T] lines the
39 * diagonal phases up with the centres they imply. Both are needed; either alone
40 * has fixed points that are not minima of Omega.
41 *
42 * GUIDING CENTRES. Im ln M_nn is a principal value: std::arg returns it on
43 * (-pi, pi], so it JUMPS by 2pi as the gauge rotates a diagonal element across
44 * the negative real axis. Omega_D squares that phase and is discontinuous
45 * wherever it happens, and no descent direction exists across a jump -- the
46 * line search rejects every step and the minimisation stops with a large
47 * gradient. On a rattled four-atom supercell 8% of the diagonals sat within
48 * 0.64 rad of the cut and the minimiser moved the spread by 0.6% before giving
49 * up.
50 *
51 * Given a guiding centre per Wannier function, each phase is instead taken on
52 * the branch nearest the one that centre implies:
53 *
54 * phase = Im ln M_nn + 2 pi m, m = round[ (-b . r_guide - Im ln M_nn) / 2pi ]
55 *
56 * which is continuous as the gauge rotates through the cut, because m steps by
57 * one exactly when Im ln steps by 2pi the other way.
58 *
59 * What matters is WHERE the shift is applied. Folding the residual
60 * (Im ln M_nn + b . r_n) onto (-pi, pi] is the obvious thing and it is wrong:
61 * MV's gradient relies on sum_kb w_b b q_n = 0, which holds because r_n is
62 * built from the very phases q_n is measured against, and folding q_n breaks
63 * it. Measured, that costs the gradient a third of its value -- a central
64 * difference of 246.6 against a predicted 332.4. Shifting the PHASE keeps the
65 * identity, because the shifted phase is what the centre is then computed from,
66 * so the cancellation survives and equation 52 stays exact.
67 *
68 * The functional is an object over the stencil it sums along and the branch
69 * its phases are read on (PhaseBranch), because both are fixed for the whole
70 * of a minimisation: a functional whose branch choice moved with the gauge
71 * would be a different functional at every step.
72 */
73
74#include <cmath>
75#include <complex>
76#include <cstddef>
77#include <memory>
78#include <stdexcept>
79#include <string>
80#include <utility>
81#include <vector>
82
93
94namespace q3 {
95
97 public:
98 /**
99 * `stencil` must carry b-vectors: the centres are Berry phases along them.
100 * `phase_branch` is the branch every diagonal phase is read on; the
101 * principal one unless another is given.
102 */
105 std::shared_ptr<const PhaseBranch> phase_branch =
106 std::make_shared<const PrincipalPhaseBranch>())
107 : stencil_(stencil)
108 , number_of_wannier_functions_(number_of_wannier_functions)
109 , phase_branch_(std::move(phase_branch)) {
110 if (!stencil.has_b_vectors()) {
111 throw std::invalid_argument("SpreadFunctional: the stencil carries no b-vectors, and "
112 "the spread is a sum over them");
113 }
115 throw std::invalid_argument(
116 "SpreadFunctional: number_of_wannier_functions must be positive");
117 }
118 if (phase_branch_ == nullptr) {
119 throw std::invalid_argument("SpreadFunctional: no phase branch was given");
120 }
121 phase_branch_->require_serves(number_of_wannier_functions);
122 }
123
124 [[nodiscard]] int number_of_wannier_functions() const {
125 return number_of_wannier_functions_;
126 }
127
128 /// `rotated_overlaps`: N x N at every (k, b), between the Wannier-gauge states at k and k+b.
129 [[nodiscard]] SpreadTerms evaluate(const BlochOverlaps& rotated_overlaps) const {
130 require_shape(rotated_overlaps);
131 const int number_of_kpoints = stencil_.number_of_kpoints();
132 const int dimension = number_of_wannier_functions_;
133 const Real inverse_kpoint_count = 1.0 / static_cast<Real>(number_of_kpoints);
134
135 SpreadTerms terms;
136 terms.centers_bohr.assign(static_cast<std::size_t>(dimension), Vector3D{0.0, 0.0, 0.0});
137 /// <r^2>_n, accumulated with the centres and finished into Omega_n below.
138 std::vector<Real> second_moments(static_cast<std::size_t>(dimension), 0.0);
139
140 /**
141 * The centres have to be complete before Omega_D can be evaluated at
142 * all -- its summand measures each diagonal phase against the centre
143 * every k-point agreed on -- so the diagonal phases are swept twice.
144 */
145 for (int kpoint = 0; kpoint < number_of_kpoints; kpoint++) {
146 for (int direction = 0; direction < stencil_.number_of_directions(); direction++) {
147 const DenseMatrix<Complex>& overlap = rotated_overlaps.at(kpoint, direction);
148 const Vector3D& b_vector = stencil_.b_vector(direction);
149 const Real weight = stencil_.weight(direction);
150 for (int wannier = 0; wannier < dimension; wannier++) {
151 const Real phase = diagonal_phase(overlap, wannier, b_vector);
152 for (int axis = 0; axis < 3; axis++) {
153 terms.centers_bohr[static_cast<std::size_t>(wannier)][axis] -=
154 inverse_kpoint_count * weight * b_vector[axis] * phase;
155 }
156 second_moments[static_cast<std::size_t>(wannier)] +=
157 inverse_kpoint_count * weight *
158 ((1.0 - std::norm(overlap(wannier, wannier))) + phase * phase);
159 }
160 }
161 }
162 terms.spreads_bohr_squared.resize(static_cast<std::size_t>(dimension));
163 for (int wannier = 0; wannier < dimension; wannier++) {
164 const Vector3D& center = terms.centers_bohr[static_cast<std::size_t>(wannier)];
165 terms.spreads_bohr_squared[static_cast<std::size_t>(wannier)] =
166 second_moments[static_cast<std::size_t>(wannier)] - center.dot(center);
167 }
168
169 for (int kpoint = 0; kpoint < number_of_kpoints; kpoint++) {
170 for (int direction = 0; direction < stencil_.number_of_directions(); direction++) {
171 const DenseMatrix<Complex>& overlap = rotated_overlaps.at(kpoint, direction);
172 const Vector3D& b_vector = stencil_.b_vector(direction);
173 const Real weight = stencil_.weight(direction) * inverse_kpoint_count;
174
175 const Real total_square_modulus = overlap.frobenius_norm_squared();
176 Real diagonal_square_modulus = 0.0;
177 for (int wannier = 0; wannier < dimension; wannier++) {
178 diagonal_square_modulus += std::norm(overlap(wannier, wannier));
179 }
181 weight * (static_cast<Real>(dimension) - total_square_modulus);
183 weight * (total_square_modulus - diagonal_square_modulus);
184
185 for (int wannier = 0; wannier < dimension; wannier++) {
186 const Real residual =
187 diagonal_phase(overlap, wannier, b_vector) +
188 b_vector.dot(terms.centers_bohr[static_cast<std::size_t>(wannier)]);
189 terms.diagonal_bohr_squared += weight * residual * residual;
190 }
191 }
192 }
193 return terms;
194 }
195
196 /**
197 * dOmega/dW at every k-point, N x N and anti-Hermitian, in the convention
198 * where a step along -gradient lowers Omega by the gradient's square norm
199 * per unit step. `terms` must be the evaluation of these same overlaps:
200 * the gradient depends on the centres, and centres from a different gauge
201 * silently give a gradient of a different function.
202 */
203 [[nodiscard]] std::vector<DenseMatrix<Complex>> gradient(const BlochOverlaps& rotated_overlaps,
204 const SpreadTerms& terms) const {
205 require_shape(rotated_overlaps);
206 const int number_of_kpoints = stencil_.number_of_kpoints();
207 const int dimension = number_of_wannier_functions_;
208 std::vector<DenseMatrix<Complex>> gradients(
209 static_cast<std::size_t>(number_of_kpoints),
210 DenseMatrix<Complex>(dimension, dimension, Complex(0.0, 0.0)));
211
212 /**
213 * Omega carries a 1/N_k and so does its derivative, and the sign is
214 * fixed by the rotation this gradient is meant to be used with:
215 * U(k) -> U(k) exp(t D). Both were established against a central
216 * difference of Omega itself rather than read off the paper, because
217 * the paper's W is not necessarily this code's W -- a convention that
218 * differs by a sign gives a minimiser that climbs, and one that differs
219 * by N_k gives a first step N_k times too long, which backtracking
220 * hides by quietly halving it back.
221 */
222 const Real scale = -1.0 / static_cast<Real>(number_of_kpoints);
223
224 for (int kpoint = 0; kpoint < number_of_kpoints; kpoint++) {
225 DenseMatrix<Complex>& gradient_at_kpoint = gradients[static_cast<std::size_t>(kpoint)];
226 for (int direction = 0; direction < stencil_.number_of_directions(); direction++) {
227 const DenseMatrix<Complex>& overlap = rotated_overlaps.at(kpoint, direction);
228 const Vector3D& b_vector = stencil_.b_vector(direction);
229 const Real weight = stencil_.weight(direction);
230
231 /**
232 * R_mn = M_mn conj(M_nn) and T_mn = (M_mn / M_nn) q_n, with
233 * q_n the diagonal phase measured against the centre. Both
234 * columns are scaled by a property of the DIAGONAL element of
235 * that column, which is what makes them column operations.
236 */
237 for (int column = 0; column < dimension; column++) {
238 const Complex diagonal = overlap(column, column);
239 require_nonzero_diagonal(diagonal, column, kpoint);
240
241 const Real residual =
242 diagonal_phase(overlap, column, b_vector) +
243 b_vector.dot(terms.centers_bohr[static_cast<std::size_t>(column)]);
244 const Complex column_scale = Complex(0.0, residual) / diagonal;
245
246 for (int row = 0; row < dimension; row++) {
247 const Complex element = overlap(row, column);
248 /**
249 * A[R] - S[T], assembled directly into the gradient:
250 * A[B] = (B - B^dagger)/2 contributes B/2 here and
251 * -conj(B)/2 at the transposed index, and
252 * S[B] = (B + B^dagger)/(2i) contributes B/(2i) and
253 * +conj(B)/(2i). Writing both halves as we go avoids
254 * materialising R and T at all.
255 */
256 const Complex r_element = element * std::conj(diagonal);
257 const Complex t_element = element * column_scale;
258 const Complex contribution =
259 scale * 4.0 * weight * (0.5 * r_element + 0.5 * t_element);
260 gradient_at_kpoint(row, column) += contribution;
261 gradient_at_kpoint(column, row) -= std::conj(contribution);
262 }
263 }
264 }
265 }
266 return gradients;
267 }
268
269 /// sum_k ||G^(k)||_F^2, the square norm the descent step is measured in.
270 [[nodiscard]] static Real square_norm(const std::vector<DenseMatrix<Complex>>& gradients) {
271 Real total = 0.0;
272 for (const DenseMatrix<Complex>& gradient_at_kpoint : gradients) {
273 total += gradient_at_kpoint.frobenius_norm_squared();
274 }
275 return total;
276 }
277
278 private:
279 /// The diagonal phase of Wannier function `wannier`, on this functional's branch.
280 [[nodiscard]] Real diagonal_phase(const DenseMatrix<Complex>& overlap,
281 int wannier,
282 const Vector3D& b_vector) const {
283 return phase_branch_->phase_of(overlap(wannier, wannier), wannier, b_vector);
284 }
285
286 /**
287 * A vanishing diagonal overlap is not a small number to be guarded with an
288 * epsilon: Im ln M_nn is the Berry phase this whole construction is built
289 * on, and where M_nn is zero that phase does not exist. It means the gauge
290 * has collapsed -- two Wannier functions have become the same state, or the
291 * mesh is too coarse for the manifold -- and continuing produces centres
292 * and a spread that are noise wearing the shape of an answer.
293 */
294 static void require_nonzero_diagonal(const Complex& diagonal, int wannier, int kpoint) {
296 throw WannierDiagnosticError(
298 "SpreadFunctional: the overlap of Wannier function " + std::to_string(wannier) +
299 " with itself across a neighbouring k-point is zero at k-point " +
300 std::to_string(kpoint) +
301 ", so its Berry phase -- and with it the centre and the spread -- is "
302 "undefined. "
303 "The gauge has collapsed: either two Wannier functions have become the same "
304 "state, or the k-mesh is too coarse to follow them.");
305 }
306 }
307
308 void require_shape(const BlochOverlaps& overlaps) const {
309 if (overlaps.number_of_kpoints() != stencil_.number_of_kpoints() ||
310 overlaps.number_of_directions() != stencil_.number_of_directions()) {
311 throw std::invalid_argument(
312 "SpreadFunctional: the overlaps cover " +
313 std::to_string(overlaps.number_of_kpoints()) + " k-points and " +
314 std::to_string(overlaps.number_of_directions()) + " directions, the stencil " +
315 std::to_string(stencil_.number_of_kpoints()) + " and " +
316 std::to_string(stencil_.number_of_directions()));
317 }
318 for (int kpoint = 0; kpoint < overlaps.number_of_kpoints(); kpoint++) {
319 for (int direction = 0; direction < overlaps.number_of_directions(); direction++) {
320 const DenseMatrix<Complex>& block = overlaps.at(kpoint, direction);
321 if (block.rows() != number_of_wannier_functions_ ||
322 block.columns() != number_of_wannier_functions_) {
323 throw std::invalid_argument(
324 "SpreadFunctional: the overlap block at k-point " + std::to_string(kpoint) +
325 " is " + std::to_string(block.rows()) + " x " +
326 std::to_string(block.columns()) + ", not the square " +
327 std::to_string(number_of_wannier_functions_) +
328 "-state block a gauge of that many Wannier functions gives");
329 }
330 }
331 }
332 }
333
334 const KPointStencil& stencil_;
335 int number_of_wannier_functions_;
336 std::shared_ptr<const PhaseBranch> phase_branch_;
337};
338
339} // namespace q3
const DenseMatrix< Complex > & at(int kpoint, int direction) const
Real frobenius_norm_squared() const
The sum of every element's squared modulus.
bool has_b_vectors() const
const Vector3D & b_vector(int direction) const
The b-vector of a direction, in whatever units the shells were solved in.
int number_of_directions() const
int number_of_kpoints() const
Real weight(int direction) const
int number_of_wannier_functions() const
static Real square_norm(const std::vector< DenseMatrix< Complex > > &gradients)
sum_k ||G^(k)||_F^2, the square norm the descent step is measured in.
std::vector< DenseMatrix< Complex > > gradient(const BlochOverlaps &rotated_overlaps, const SpreadTerms &terms) const
SpreadFunctional(const KPointStencil &stencil, int number_of_wannier_functions, std::shared_ptr< const PhaseBranch > phase_branch=std::make_shared< const PrincipalPhaseBranch >())
SpreadTerms evaluate(const BlochOverlaps &rotated_overlaps) const
rotated_overlaps: N x N at every (k, b), between the Wannier-gauge states at k and k+b.
constexpr double wannier_diagonal_overlap_threshold
double Real
std::complex< double > Complex
@ gauge_collapsed
A diagonal overlap vanished, so a Berry phase – a centre – is undefined.
Real off_diagonal_bohr_squared
Real gauge_invariant_bohr_squared
std::vector< Vector3D > centers_bohr
One centre per Wannier function, in bohr, in the same Cartesian frame as b.
std::vector< Real > spreads_bohr_squared
Real dot(const Vector3D &other) const
Definition vector_3d.hpp:59