Loading...
Searching...
No Matches
occupation_mixer.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * HubbardOccupationMixer — damping and Anderson acceleration for the DFT+U
5 * occupation matrix.
6 *
7 * Q3 rebuilds n from the output states every iteration and feeds it straight
8 * to V_U. Undamped, that is a feedback loop the density mixer has to fight,
9 * and it is why FeO with U on Fe 3d ran 100 iterations without converging.
10 * Damping fixes that but is not free: a damped n lags the fresh one, the SCF
11 * stops while the lag is threshold-sized, and E_U reads the lagging matrix --
12 * a *first* order energy error, because E_U is not stationary in n the way the
13 * total energy is in the density.
14 *
15 * Both of this class's decisions follow from that tension:
16 *
17 * - damping switches on by *evidence* (non-monotone progress), so a system
18 * that never needed it takes the fresh matrix bit-for-bit and pays nothing;
19 * - the Anderson step switches off *before* the fixed point, so the final
20 * approach runs on the plain damped path the recorded references were
21 * measured on.
22 *
23 * Kept apart from the SCF iterator so it can be exercised on synthetic
24 * matrices: everything here is a pure function of the sequence of matrices it
25 * is handed, with no plane waves, k-points or Hamiltonian in sight.
26 */
27
28#include <algorithm>
29#include <complex>
30#include <limits>
31#include <vector>
32
36
37namespace q3 {
38
39/// Per-manifold, per-spin occupation matrices, each flattened row-major.
40using HubbardOccupationMatrices = std::vector<std::vector<std::vector<Complex>>>;
41
43 public:
44 /**
45 * Blend `fresh` into the retained state and return what should drive V_U.
46 *
47 * `mixing_beta` is the damping factor once damping is active;
48 * `convergence_threshold` gates both SCF exit (via `residual()`) and the
49 * point at which the Anderson step stands down.
50 */
52 blend(const HubbardOccupationMatrices& fresh, Real mixing_beta, Real convergence_threshold) {
53 if (!matches_layout(damped_, fresh)) {
54 /**
55 * First call, or a manifold layout change mid-run. Both caches are
56 * indexed the same way, so both are dropped rather than indexed
57 * into while mismatched.
58 */
59 damped_ = fresh;
60 residuals_.clear();
61 residual_ = std::numeric_limits<Real>::max();
62 return damped_;
63 }
64
65 /**
66 * How far n still is from its own fixed point, measured before
67 * anything is blended: ||n_fresh - n_damped||^2, zero exactly when the
68 * freshly accumulated matrix already agrees with the one driving V_U.
69 * Same squared-magnitude convention `density_residual_` uses.
70 */
71 Real mismatch = 0.0;
72 for_each_element(fresh, [&](size_t manifold, size_t spin, size_t element) {
73 mismatch +=
74 std::norm(fresh[manifold][spin][element] - damped_[manifold][spin][element]);
75 });
76
77 /**
78 * What earns the damping is non-monotone progress: an occupation
79 * matrix that moves *further* from its fixed point than it was the
80 * iteration before. Monotone runs record zero such events ever; FeO,
81 * which does not converge in 100 iterations undamped, trips the
82 * counter almost immediately. Counted cumulatively rather than
83 * consecutively, because an oscillation alternates rather than growing
84 * every step.
85 */
86 if (!damping_active_) {
87 /**
88 * ">=", not ">": while damping is off the mixer passes the fresh
89 * matrix straight through, so a clean period-2 oscillation
90 * produces a *constant* mismatch rather than a growing one. Firing
91 * only on strict growth would leave exactly the sustained
92 * oscillation this exists to break undetected forever. Anything
93 * that fails to improve counts.
94 */
95 if (mismatch >= previous_mismatch_)
96 ++nonmonotone_iterations_;
97 if (nonmonotone_iterations_ >= constants::hubbard_damping_trigger_iterations)
98 damping_active_ = true;
99 }
100 previous_mismatch_ = mismatch;
101 residual_ = mismatch;
102
103 Real damping = damping_active_ ? mixing_beta : 1.0;
104
105 /**
106 * Anderson accelerates the hard middle of the SCF and then steps
107 * aside. Its extrapolation changes the path n takes, and a path change
108 * moves where the run stops by the size of the convergence threshold --
109 * which is the scale cross-basis energies are compared at. Restricting
110 * it to iterations where n is still further out than that threshold
111 * keeps the acceleration where the cost is and leaves the approach to
112 * the fixed point on the plain damped path.
113 */
114 bool use_anderson = damping_active_ && matches_layout(residuals_, fresh) &&
115 mismatch > convergence_threshold;
116
117 Real anderson_theta = 0.0;
118 if (use_anderson) {
119 /**
120 * Two-point secant: <r, r - r_prev> / |r - r_prev|^2, the one-term
121 * quasi-Newton step. Clamped, because the secant is exact only
122 * where the residual map is locally linear and can otherwise ask
123 * for an arbitrarily long step; at the clamp the update is still a
124 * damped step in the descent direction rather than a leap.
125 */
126 Real numerator = 0.0;
127 Real denominator = 0.0;
128 for_each_element(fresh, [&](size_t manifold, size_t spin, size_t element) {
130 fresh[manifold][spin][element] - damped_[manifold][spin][element];
131 Complex residual_change = residual - residuals_[manifold][spin][element];
132 numerator += (std::conj(residual) * residual_change).real();
133 denominator += std::norm(residual_change);
134 });
135 anderson_theta = denominator > constants::hubbard_anderson_secant_minimum_denominator
136 ? numerator / denominator
137 : 0.0;
138 anderson_theta =
140 std::min(constants::hubbard_anderson_theta_limit, anderson_theta));
141 }
142
143 // Stash this iteration's residual before the update overwrites `damped_`.
144 residuals_.resize(fresh.size());
145 for (size_t manifold = 0; manifold < fresh.size(); ++manifold) {
146 residuals_[manifold].resize(fresh[manifold].size());
147 for (size_t spin = 0; spin < fresh[manifold].size(); ++spin) {
148 std::vector<Complex> residual(fresh[manifold][spin].size());
149 for (size_t element = 0; element < residual.size(); ++element)
150 residual[element] =
151 fresh[manifold][spin][element] - damped_[manifold][spin][element];
152 for (size_t element = 0; element < residual.size(); ++element) {
153 Complex step = damping * residual[element];
154 if (use_anderson)
155 step *= (1.0 - anderson_theta);
156 damped_[manifold][spin][element] += step;
157 }
158 residuals_[manifold][spin] = std::move(residual);
159 }
160 }
161 return damped_;
162 }
163
164 /// ||n_fresh - n_damped||^2 from the last blend; huge until one has run.
165 [[nodiscard]] Real residual() const {
166 return residual_;
167 }
168
169 /// Whether non-monotone progress has switched damping on for this run.
170 [[nodiscard]] bool damping_active() const {
171 return damping_active_;
172 }
173
174 /// The retained damped state, for callers that need it after `blend`.
175 [[nodiscard]] const HubbardOccupationMatrices& damped() const {
176 return damped_;
177 }
178
179 private:
180 /**
181 * Whether a cache is indexed exactly like `fresh`, all three levels deep.
182 * The outer size alone is not enough: a layout change can leave the
183 * manifold count matching while an inner row is shorter, and both caches
184 * are indexed element by element.
185 */
186 [[nodiscard]] static bool matches_layout(const HubbardOccupationMatrices& cache,
187 const HubbardOccupationMatrices& fresh) {
188 if (cache.size() != fresh.size())
189 return false;
190 for (size_t manifold = 0; manifold < fresh.size(); ++manifold) {
191 if (cache[manifold].size() != fresh[manifold].size())
192 return false;
193 for (size_t spin = 0; spin < fresh[manifold].size(); ++spin)
194 if (cache[manifold][spin].size() != fresh[manifold][spin].size())
195 return false;
196 }
197 return true;
198 }
199
200 template <typename Visitor>
201 static void for_each_element(const HubbardOccupationMatrices& matrices, Visitor&& visit) {
202 for (size_t manifold = 0; manifold < matrices.size(); ++manifold)
203 for (size_t spin = 0; spin < matrices[manifold].size(); ++spin)
204 for (size_t element = 0; element < matrices[manifold][spin].size(); ++element)
205 visit(manifold, spin, element);
206 }
207
209 HubbardOccupationMatrices residuals_;
210 Real previous_mismatch_{std::numeric_limits<Real>::max()};
211 Real residual_{std::numeric_limits<Real>::max()};
212 int nonmonotone_iterations_ = 0;
213 bool damping_active_ = false;
214};
215
216} // namespace q3
const HubbardOccupationMatrices & damped() const
The retained damped state, for callers that need it after blend.
Real residual() const
||n_fresh - n_damped||^2 from the last blend; huge until one has run.
const HubbardOccupationMatrices & blend(const HubbardOccupationMatrices &fresh, Real mixing_beta, Real convergence_threshold)
bool damping_active() const
Whether non-monotone progress has switched damping on for this run.
constexpr Real hubbard_anderson_secant_minimum_denominator
constexpr int hubbard_damping_trigger_iterations
Definition constants.hpp:56
constexpr Real hubbard_anderson_theta_limit
Definition constants.hpp:97
double Real
std::complex< double > Complex
std::vector< std::vector< std::vector< Complex > > > HubbardOccupationMatrices
Per-manifold, per-spin occupation matrices, each flattened row-major.