Loading...
Searching...
No Matches
spread_minimizer.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * SpreadMinimizer — rotate the gauge at every k-point until the Wannier
5 * functions are as localised as this mesh allows.
6 *
7 * The subspace is already chosen when this runs; what is left is the
8 * orientation inside it, one N x N unitary per k-point. Omega_I cannot move,
9 * so what is descended is Omega_D + Omega_OD, and the descent is the
10 * Marzari-Vanderbilt steepest descent: rotate by exp(-step G), where G is the
11 * analytic gradient and the step is whatever length actually lowers Omega.
12 *
13 * The step is found by backtracking rather than fixed. A fixed step is what the
14 * literature does and it is fine when a person is watching the spread history;
15 * for a driver that must not be watched, a step that overshoots looks exactly
16 * like a system that will not localise. Backtracking makes "Omega never
17 * increases" an invariant of this class rather than a hope, and turns the two
18 * failure modes into different, nameable outcomes: a gradient that reaches the
19 * tolerance is converged, a step that collapses without lowering Omega is a
20 * stationary point, and neither is silently reported as the other.
21 *
22 * What is descended is the functional the caller hands in, branch and all:
23 * which centres guide its phases is the caller's decision, made once, because
24 * a functional whose branch choice moved with the gauge would be a different
25 * functional at every iteration, and neither a line search nor a conjugate
26 * direction means anything against a moving target.
27 *
28 * The rotation is applied to the ORIGINAL overlaps every iteration, not
29 * composed onto the previous iteration's rotated ones. Composing accumulates a
30 * unitary drift that shows up first as an Omega_I that moves -- the one
31 * quantity guaranteed not to -- which is how the error is caught, and by then
32 * the centres are already wrong. Recomputing costs one N x N product per (k,b)
33 * per iteration, which is nothing beside the eigenproblems.
34 */
35
36#include <algorithm>
37#include <cmath>
38#include <complex>
39#include <cstddef>
40#include <limits>
41#include <optional>
42#include <stdexcept>
43#include <string>
44#include <utility>
45#include <vector>
46
61
62namespace q3 {
63
65 public:
66 /// Settings that describe no minimisation are refused here rather than mid-run.
68 : stencil_(stencil), settings_(settings) {
69 if (settings.maximum_iterations < 1) {
70 throw std::invalid_argument(
71 "SpreadMinimizer: maximum_iterations must be at least 1, got " +
72 std::to_string(settings.maximum_iterations));
73 }
74 if (settings.maximum_step_reductions < 0) {
75 throw std::invalid_argument(
76 "SpreadMinimizer: maximum_step_reductions must not be negative, got " +
77 std::to_string(settings.maximum_step_reductions));
78 }
79 if (settings.restart_interval < 1) {
80 throw std::invalid_argument(
81 "SpreadMinimizer: restart_interval must be at least 1, got " +
82 std::to_string(settings.restart_interval));
83 }
84 if (settings.relative_gradient_tolerance < 0.0) {
85 throw std::invalid_argument(
86 "SpreadMinimizer: relative_gradient_tolerance must not be negative, got " +
87 std::to_string(settings.relative_gradient_tolerance));
88 }
89 if (!(settings.step_reduction_factor > 0.0 && settings.step_reduction_factor < 1.0)) {
90 throw std::invalid_argument(
91 "SpreadMinimizer: step_reduction_factor must lie in (0, 1), got " +
92 std::to_string(settings.step_reduction_factor));
93 }
94 if (settings.step_growth_factor < 1.0) {
95 throw std::invalid_argument(
96 "SpreadMinimizer: step_growth_factor must be at least 1, got " +
97 std::to_string(settings.step_growth_factor));
98 }
99 }
100
101 /**
102 * `overlaps` are N x N at every (k, b), between the states the gauge has
103 * already selected -- the composite V(k)U(k) applied on both sides.
104 * `functional` is the spread to descend, over the same stencil.
105 */
106 [[nodiscard]] SpreadMinimizationResult run(const BlochOverlaps& overlaps,
107 const SpreadFunctional& functional) const {
108 GaugeState state = starting_gauge(overlaps, functional);
109 const Real initial_minimizable = state.terms.minimizable_bohr_squared();
110
111 /**
112 * The natural scale of the problem: the gradient carries a factor of
113 * four and a sum over the b-weights, so this makes the first trial
114 * rotation an angle of order one radian's worth of the actual descent
115 * direction rather than an arbitrary number of them.
116 */
117 Real step = 1.0 / (4.0 * stencil_.total_weight());
118
120 PolakRibiereDirection conjugate;
121 Real gradient_floor = 0.0;
122 for (int iteration = 1; iteration <= settings_.maximum_iterations; iteration++) {
123 const std::vector<DenseMatrix<Complex>> gradients =
124 functional.gradient(state.rotated, state.terms);
125 const Real gradient_norm = norm_per_kpoint(gradients);
126 if (iteration == 1) {
127 gradient_floor = settings_.relative_gradient_tolerance *
128 std::max(gradient_norm, std::numeric_limits<Real>::min());
129 }
130 if (gradient_norm < gradient_floor) {
131 return concluded(Ending::Stationary,
132 std::move(state),
133 iteration,
134 gradient_norm,
135 gradient_floor,
136 initial_minimizable);
137 }
138
139 const std::vector<DenseMatrix<Complex>>& direction =
140 conjugate.next(gradients, iteration % settings_.restart_interval == 0);
141 std::optional<GaugeState> lowered =
142 line_search(overlaps, functional, state, direction, step);
143 if (!lowered) {
144 /**
145 * The gauge is the one the gradient above was taken at, and
146 * that gradient did not pass the floor or the run would have
147 * ended there -- so this ending is never stationary.
148 */
149 return concluded(Ending::StepCollapsed,
150 std::move(state),
151 iteration,
152 gradient_norm,
153 gradient_floor,
154 initial_minimizable);
155 }
156 state = std::move(*lowered);
157 window.record(state.terms.minimizable_bohr_squared());
158 step *= settings_.step_growth_factor;
159
160 if (window.has_converged()) {
161 /**
162 * Omega stopped moving. Whether that is a minimum is settled
163 * by the gradient and not by the fact that it stopped: a line
164 * search whose steps have gone tiny produces a spread that
165 * changes by nothing, which satisfies any relative-change test
166 * while the gauge is nowhere near stationary. Copper stops here
167 * at a real minimum; a rattled four-atom supercell stops here
168 * with a gradient four orders of magnitude too large, and the
169 * two must not get the same word.
170 *
171 * The gradient is taken again, at the gauge being returned:
172 * the one above belongs to the gauge before this iteration's
173 * step.
174 */
175 const Real settled_gradient_norm =
176 norm_per_kpoint(functional.gradient(state.rotated, state.terms));
177 return concluded(Ending::SpreadSettled,
178 std::move(state),
179 iteration,
180 settled_gradient_norm,
181 gradient_floor,
182 initial_minimizable);
183 }
184 }
185
186 const Real gradient_norm = norm_per_kpoint(functional.gradient(state.rotated, state.terms));
187 return concluded(Ending::BudgetExhausted,
188 std::move(state),
189 settings_.maximum_iterations,
190 gradient_norm,
191 gradient_floor,
192 initial_minimizable);
193 }
194
195 private:
196 /// A gauge at every k-point, the overlaps rotated into it, and the spread there.
197 struct GaugeState {
198 std::vector<DenseMatrix<Complex>> gauges;
199 BlochOverlaps rotated;
200 SpreadTerms terms;
201 };
202
203 /// The four ways a run ends; only the first has found a stationary gauge for certain.
204 enum class Ending { Stationary, StepCollapsed, SpreadSettled, BudgetExhausted };
205
206 /// The identity at every k-point: the gauge the subspace selection handed over.
207 [[nodiscard]] GaugeState starting_gauge(const BlochOverlaps& overlaps,
208 const SpreadFunctional& functional) const {
209 std::vector<DenseMatrix<Complex>> gauges(
210 static_cast<std::size_t>(stencil_.number_of_kpoints()),
211 DenseMatrix<Complex>::identity(functional.number_of_wannier_functions()));
212 BlochOverlaps rotated = overlaps.transformed_by(gauges, gauges, stencil_);
213 SpreadTerms terms = functional.evaluate(rotated);
214 return GaugeState{std::move(gauges), std::move(rotated), std::move(terms)};
215 }
216
217 /**
218 * Backtracking along `direction` from `state`: the first gauge that
219 * lowers Omega, or nothing when every trial step down to the smallest
220 * raised it. `step` is shortened in place by each failure and stays
221 * shortened, which is what makes the next iteration start from a length
222 * that last worked. "Omega never increases" is an invariant of this
223 * class because nothing else is ever accepted.
224 */
225 [[nodiscard]] std::optional<GaugeState>
226 line_search(const BlochOverlaps& overlaps,
227 const SpreadFunctional& functional,
228 const GaugeState& state,
229 const std::vector<DenseMatrix<Complex>>& direction,
230 Real& step) const {
231 for (int reduction = 0; reduction <= settings_.maximum_step_reductions; reduction++) {
232 std::vector<DenseMatrix<Complex>> gauges = advanced(state.gauges, direction, step);
233 BlochOverlaps rotated = overlaps.transformed_by(gauges, gauges, stencil_);
234 SpreadTerms terms = functional.evaluate(rotated);
235 if (terms.minimizable_bohr_squared() < state.terms.minimizable_bohr_squared()) {
236 return GaugeState{std::move(gauges), std::move(rotated), std::move(terms)};
237 }
238 step *= settings_.step_reduction_factor;
239 }
240 return std::nullopt;
241 }
242
243 /// U(k) -> U(k) exp(step D(k)), one k-point at a time.
244 [[nodiscard]] static std::vector<DenseMatrix<Complex>>
245 advanced(const std::vector<DenseMatrix<Complex>>& gauges,
246 const std::vector<DenseMatrix<Complex>>& directions,
247 Real step) {
248 std::vector<DenseMatrix<Complex>> result;
249 result.reserve(gauges.size());
250 for (std::size_t kpoint = 0; kpoint < gauges.size(); kpoint++) {
251 result.push_back(
253 gauges[kpoint],
255 exponential_of_anti_hermitian(directions[kpoint].scaled(step))));
256 }
257 return result;
258 }
259
260 /// Root-mean-square gradient per k-point: the size every ending is judged and reported by.
261 [[nodiscard]] static Real norm_per_kpoint(const std::vector<DenseMatrix<Complex>>& gradients) {
262 return std::sqrt(SpreadFunctional::square_norm(gradients) /
263 static_cast<Real>(gradients.size()));
264 }
265
266 /**
267 * The result of a run that ended `ending`, said in the log in the words
268 * that ending has earned. A gauge that reached stationarity is reported
269 * quietly; one that stopped short is warned about with the numbers that
270 * show it, and one cut off by the budget says so -- it is returned all
271 * the same, because every accepted step lowered Omega, so the gauge in
272 * hand is never worse than the projection gauge it started from, and
273 * throwing it away would leave the caller with no model at all. What
274 * matters is that it is not called converged, and it is not.
275 */
276 [[nodiscard]] SpreadMinimizationResult concluded(Ending ending,
277 GaugeState state,
278 int iterations,
279 Real gradient_norm,
280 Real gradient_floor,
281 Real initial_minimizable) const {
282 SpreadMinimizationResult result;
283 result.iterations = iterations;
284 result.initial_minimizable_bohr_squared = initial_minimizable;
285 result.gradient_norm = gradient_norm;
286 result.converged = ending != Ending::BudgetExhausted;
287 result.stationary = ending == Ending::Stationary ||
288 (ending == Ending::SpreadSettled && gradient_norm < gradient_floor);
289 report(ending, result, state.terms, gradient_floor);
290 result.gauges = std::move(state.gauges);
291 result.terms = std::move(state.terms);
292 return result;
293 }
294
295 void report(Ending ending,
296 const SpreadMinimizationResult& result,
297 const SpreadTerms& terms,
298 Real gradient_floor) const {
299 if (ending == Ending::Stationary) {
300 log::info("localization: stationary after {} iterations, Omega = {:.9f} bohr^2",
301 result.iterations,
302 terms.total_bohr_squared());
303 return;
304 }
305 if (ending == Ending::BudgetExhausted) {
306 log::warn(
307 "localization ran out of iterations after {}: Omega = {:.9f} bohr^2 (Omega_I "
308 "{:.9f}, Omega_D {:.9f}, Omega_OD {:.9f}), gradient {:.3e}. The gauge returned "
309 "is the best one found and is never worse than the projection gauge it started "
310 "from, but the minimisation was cut off rather than finished.",
311 settings_.maximum_iterations,
312 terms.total_bohr_squared(),
313 terms.gauge_invariant_bohr_squared,
314 terms.diagonal_bohr_squared,
315 terms.off_diagonal_bohr_squared,
316 result.gradient_norm);
317 return;
318 }
319 if (result.stationary) {
320 log::info("localization: converged in {} iterations, Omega = {:.9f} bohr^2 (Omega_I "
321 "{:.9f}, Omega_D {:.9f}, Omega_OD {:.9f})",
322 result.iterations,
323 terms.total_bohr_squared(),
324 terms.gauge_invariant_bohr_squared,
325 terms.diagonal_bohr_squared,
326 terms.off_diagonal_bohr_squared);
327 return;
328 }
329 log::warn("localization stopped short after {} iterations: Omega = {:.9f} bohr^2 (Omega_I "
330 "{:.9f}, Omega_D {:.9f}, Omega_OD {:.9f}), down from {:.9f} minimisable at the "
331 "projection gauge, but the gradient is {:.3e} against the {:.3e} that would "
332 "make it stationary. {}, so this is as far as the minimisation goes; the gauge "
333 "is better than the projection gauge it started from and is not maximally "
334 "localised. Treat the model accordingly.",
335 result.iterations,
336 terms.total_bohr_squared(),
337 terms.gauge_invariant_bohr_squared,
338 terms.diagonal_bohr_squared,
339 terms.off_diagonal_bohr_squared,
340 result.initial_minimizable_bohr_squared,
341 result.gradient_norm,
342 gradient_floor,
343 ending == Ending::StepCollapsed
344 ? "No further step lowered Omega"
345 : "Omega stopped moving while steps were still being accepted");
346 }
347
348 const KPointStencil& stencil_;
349 SpreadMinimizationSettings settings_;
350};
351
352} // namespace q3
void record(Real value)
The objective at the end of one more step.
int number_of_kpoints() const
Real total_weight() 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
SpreadMinimizer(const KPointStencil &stencil, SpreadMinimizationSettings settings)
Settings that describe no minimisation are refused here rather than mid-run.
SpreadMinimizationResult run(const BlochOverlaps &overlaps, const SpreadFunctional &functional) const
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
DenseMatrix< Complex > exponential_of_anti_hermitian(const DenseMatrix< Complex > &generator)
double Real
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.
int maximum_step_reductions
Halvings of the trial step before a stationary point is declared.
int convergence_window
Iterations the relative change must stay below the tolerance for.
Real step_reduction_factor
What a trial step is multiplied by each time it fails to lower Omega.
Real convergence_tolerance
Relative change in Omega_D + Omega_OD that counts as converged.