Loading...
Searching...
No Matches
local_thomas_fermi.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * LocalThomasFermiPreconditioner — Thomas-Fermi screening from the LOCAL density.
5 *
6 * Kerker screens every G with one number for the whole cell. That is the right
7 * average answer for a homogeneous system and the wrong answer wherever the
8 * density is not homogeneous: a slab screens like a metal inside the slab and
9 * not at all in the vacuum above it, and a single q0 cannot describe both.
10 * Local Thomas-Fermi replaces the constant q0² with a screening wavevector
11 * built from the density at each point,
12 *
13 * k_TF²(r) = 4 k_F(r) / π, k_F(r) = (3 π² ρ(r))^(1/3)
14 *
15 * (Hartree atomic units: k_TF² = 4π e² N(E_F) with the free-electron density
16 * of states N(E_F) = k_F/π², so the e² and the π² cancel to 4 k_F/π. The
17 * expression is in bohr⁻² for ρ in bohr⁻³, with no further unit factor.)
18 *
19 * The preconditioned residual is then the solution of
20 *
21 * [ G² + k_TF²(r) ] x = G² F
22 *
23 * where G² is diagonal in reciprocal space and k_TF²(r) is diagonal in real
24 * space, so the operator is diagonal in neither and has to be inverted rather
25 * than multiplied. Setting k_TF²(r) to a constant collapses this to
26 * x = G²F/(G²+q0²), which is Kerker exactly -- the identity the unit tests pin.
27 *
28 * Method reference: Raczkowski, Canning & Wang, "Thomas-Fermi charge mixing for
29 * obtaining self-consistency in density functional calculations", PRB 64,
30 * 121101(R) (2001). The conjugate-gradient solve, its diagonal inner
31 * preconditioner, the vacuum floor and the initial guess below are this
32 * implementation's own choices and are not taken from any other code.
33 */
34
35#include <algorithm>
36#include <cmath>
37#include <functional>
38#include <string>
39#include <vector>
40
43
44namespace q3 {
45
46/// Residual preconditioner solving [G² + k_TF²(r)] x = G² F by conjugate gradients.
48 public:
49 /**
50 * @param g_squared |G|² per G-vector, in bohr⁻².
51 * @param q0_squared floor for k_TF², in bohr⁻² (see below).
52 * @param first_nonzero_g_index index of the first G ≠ 0 component.
53 * @param to_reciprocal_space real-space → G-space density transform.
54 * @param to_real_space G-space → real-space density transform.
55 * @param reduce_within_pool sums values over the ranks sharing this
56 * pool's real-space grid; empty when one
57 * rank holds all of it.
58 *
59 * The floor is the existing `mixing.q0` key rather than a second knob. It
60 * matters because ρ → 0 gives k_TF → 0, which is correct physics -- there is
61 * nothing to screen in vacuum -- but leaves the operator singular in exactly
62 * the cells (slabs, molecules in a box) this preconditioner exists to help.
63 * Flooring at q0² recovers Kerker's behaviour in the vacuum region and the
64 * local screening everywhere else.
65 */
67 std::vector<Real> g_squared,
68 Real q0_squared,
69 int first_nonzero_g_index,
70 std::function<void(const std::vector<Real>&, std::vector<Complex>&)> to_reciprocal_space,
71 std::function<void(const std::vector<Complex>&, std::vector<Real>&)> to_real_space,
72 std::function<void(std::vector<Real>&)> reduce_within_pool = {})
73 : g_squared_(std::move(g_squared))
74 , q0_squared_(q0_squared)
75 , first_nonzero_g_index_(first_nonzero_g_index)
76 , to_reciprocal_space_(std::move(to_reciprocal_space))
77 , to_real_space_(std::move(to_real_space))
78 , reduce_within_pool_(std::move(reduce_within_pool)) {
79 /**
80 * Until the first update() the local screening is unknown, so the
81 * operator falls back to the uniform q0² -- i.e. to Kerker. That keeps
82 * apply() well defined if it is ever called before a density exists.
83 */
84 mean_screening_ = q0_squared_;
85 }
86
87 [[nodiscard]] std::string name() const override {
88 return "local-tf";
89 }
90
91 /**
92 * Recompute k_TF²(r) and its cell average from the spin-summed input density.
93 *
94 * The density is this rank's part of the real-space grid. Where ranks share
95 * a pool that is only some of the planes, and the average taken over them
96 * alone differed from rank to rank. The average sets the diagonal of the
97 * conjugate-gradient solve, which then took different steps on different
98 * ranks around the same collective transforms and hung. Sum and point
99 * count are therefore completed within the pool before they are divided.
100 */
101 void update(const std::vector<Real>& density_real) override {
102 screening_real_.resize(density_real.size());
103 Real sum = 0.0;
104 for (std::size_t index = 0; index < density_real.size(); ++index) {
105 /**
106 * FFT round-off leaves small negative values in low-density regions.
107 * A negative density would give a negative k_TF² and an indefinite
108 * operator, so clamp before the cube root rather than after.
109 */
110 Real density = std::max(density_real[index], 0.0);
111 Real fermi_wavevector = std::cbrt(3.0 * M_PI * M_PI * density);
112 Real screening = 4.0 * fermi_wavevector / M_PI;
113 screening_real_[index] = std::max(screening, q0_squared_);
114 sum += screening_real_[index];
115 }
116 std::vector<Real> sum_and_count = {sum, static_cast<Real>(density_real.size())};
117 if (reduce_within_pool_) {
118 reduce_within_pool_(sum_and_count);
119 }
120 mean_screening_ =
121 sum_and_count[1] > 0.0 ? sum_and_count[0] / sum_and_count[1] : q0_squared_;
122 has_screening_ = true;
123 }
124
125 void apply(std::vector<Complex>& residual_g) const override {
126 /**
127 * Asked of a flag rather than of screening_real_: a rank that holds no
128 * real-space planes has an empty screening after update() like any
129 * other, and taking the shortcut there would skip the collective
130 * transforms every other rank runs below.
131 */
132 if (!has_screening_) {
133 /**
134 * No density supplied yet: fall back to the uniform-screening
135 * answer G²F/(G²+q0²), which is what the operator reduces to anyway
136 * -- Kerker exactly.
137 */
138 for (std::size_t g_index = first_index(); g_index < residual_g.size(); ++g_index) {
139 residual_g[g_index] *=
140 g_squared_[g_index] / (g_squared_[g_index] + mean_screening_);
141 }
142 return;
143 }
144
145 std::size_t count = residual_g.size();
146 std::vector<Complex> right_hand_side(count, Complex(0.0, 0.0));
147 for (std::size_t g_index = first_index(); g_index < count; ++g_index) {
148 right_hand_side[g_index] = g_squared_[g_index] * residual_g[g_index];
149 }
150
151 /**
152 * Initial guess: the exact solution of the uniform-screening problem,
153 * which is free and is already correct wherever k_TF² is close to its
154 * cell average -- the error it leaves is only the *variation* of the
155 * screening. This is deliberately not warm-started from the previous
156 * SCF step: the residual shrinks by orders of magnitude as the SCF
157 * converges, so a stale iterate has the wrong scale and is a worse
158 * starting point than this one.
159 */
160 std::vector<Complex> solution(count, Complex(0.0, 0.0));
161 apply_diagonal_inverse(right_hand_side, solution);
162
163 std::vector<Complex> operator_times_solution(count, Complex(0.0, 0.0));
164 apply_operator(solution, operator_times_solution);
165
166 std::vector<Complex> gradient(count, Complex(0.0, 0.0));
167 for (std::size_t g_index = first_index(); g_index < count; ++g_index) {
168 gradient[g_index] = right_hand_side[g_index] - operator_times_solution[g_index];
169 }
170
171 Real right_hand_side_norm = std::sqrt(dot_product(right_hand_side, right_hand_side));
172 if (right_hand_side_norm <= 0.0) {
173 for (std::size_t g_index = first_index(); g_index < count; ++g_index) {
174 residual_g[g_index] = solution[g_index];
175 }
176 return;
177 }
178 Real tolerance = relative_tolerance_ * right_hand_side_norm;
179
180 std::vector<Complex> preconditioned_gradient(count, Complex(0.0, 0.0));
181 apply_diagonal_inverse(gradient, preconditioned_gradient);
182 std::vector<Complex> search_direction = preconditioned_gradient;
183 Real gradient_dot = dot_product(gradient, preconditioned_gradient);
184
185 std::vector<Complex> operator_times_direction(count, Complex(0.0, 0.0));
186 int iteration = 0;
187 for (; iteration < maximum_iterations_; ++iteration) {
188 if (std::sqrt(dot_product(gradient, gradient)) <= tolerance)
189 break;
190
191 apply_operator(search_direction, operator_times_direction);
192 Real curvature = dot_product(search_direction, operator_times_direction);
193 if (curvature <= 0.0)
194 break; // Not expected: the operator is positive definite on G ≠ 0.
195
196 Real step = gradient_dot / curvature;
197 for (std::size_t g_index = first_index(); g_index < count; ++g_index) {
198 solution[g_index] += step * search_direction[g_index];
199 gradient[g_index] -= step * operator_times_direction[g_index];
200 }
201
202 apply_diagonal_inverse(gradient, preconditioned_gradient);
203 Real gradient_dot_new = dot_product(gradient, preconditioned_gradient);
204 Real direction_weight = gradient_dot_new / gradient_dot;
205 gradient_dot = gradient_dot_new;
206 for (std::size_t g_index = first_index(); g_index < count; ++g_index) {
207 search_direction[g_index] =
208 preconditioned_gradient[g_index] + direction_weight * search_direction[g_index];
209 }
210 }
211
212 if (iteration == maximum_iterations_ &&
213 std::sqrt(dot_product(gradient, gradient)) > tolerance) {
214 /**
215 * A truncated solve is not wrong, but it is no longer local-TF: it
216 * sits somewhere between Kerker and the identity, and the SCF would
217 * only show it as unexplained slow convergence. Say so.
218 */
219 log::info(" local-TF preconditioner: conjugate gradients hit the {}-iteration cap "
220 "(relative residual {:.3e}); the preconditioner is only partly applied",
221 maximum_iterations_,
222 std::sqrt(dot_product(gradient, gradient)) / right_hand_side_norm);
223 }
224
225 /**
226 * Copy back the G != 0 components only. G = 0 is left exactly as the
227 * caller passed it: it is fixed by charge conservation, and the solve
228 * never represented it.
229 */
230 for (std::size_t g_index = first_index(); g_index < count; ++g_index) {
231 residual_g[g_index] = solution[g_index];
232 }
233 }
234
235 /// Cell-averaged k_TF², in bohr⁻². Exposed for tests.
236 [[nodiscard]] Real mean_screening() const {
237 return mean_screening_;
238 }
239
240 private:
241 [[nodiscard]] std::size_t first_index() const {
242 return static_cast<std::size_t>(first_nonzero_g_index_);
243 }
244
245 /**
246 * Real inner product over the G ≠ 0 components.
247 *
248 * On a gamma-only half-sphere every retained component stands for the pair
249 * (G, −G) and so carries the same weight 2. A uniform factor cancels in
250 * every conjugate-gradient ratio, so it is left out rather than carried:
251 * G = 0, the one component whose weight differs, is excluded from the solve
252 * altogether.
253 */
254 [[nodiscard]] Real dot_product(const std::vector<Complex>& left,
255 const std::vector<Complex>& right) const {
256 Real result = 0.0;
257 for (std::size_t g_index = first_index(); g_index < left.size(); ++g_index) {
258 result += std::real(std::conj(left[g_index]) * right[g_index]);
259 }
260 return result;
261 }
262
263 /// x ← [G² + ⟨k_TF²⟩]⁻¹ v, the operator's diagonal part; exact when k_TF² is uniform.
264 void apply_diagonal_inverse(const std::vector<Complex>& input,
265 std::vector<Complex>& output) const {
266 output.assign(input.size(), Complex(0.0, 0.0));
267 for (std::size_t g_index = first_index(); g_index < input.size(); ++g_index) {
268 output[g_index] = input[g_index] / (g_squared_[g_index] + mean_screening_);
269 }
270 }
271
272 /**
273 * y ← [G² + k_TF²(r)] x, one FFT pair.
274 *
275 * The real-space multiply repopulates G = 0 even when x has none, so the
276 * result is projected back onto G ≠ 0. That projection is what keeps the
277 * operator symmetric positive definite on the subspace the solve runs in,
278 * and it is why charge is conserved: the G = 0 component of the residual is
279 * never touched.
280 */
281 void apply_operator(const std::vector<Complex>& input, std::vector<Complex>& output) const {
282 std::vector<Real> input_real;
283 to_real_space_(input, input_real);
284 std::size_t points = std::min(input_real.size(), screening_real_.size());
285 for (std::size_t index = 0; index < points; ++index) {
286 input_real[index] *= screening_real_[index];
287 }
288 to_reciprocal_space_(input_real, output);
289
290 output.resize(input.size());
291 for (std::size_t g_index = 0; g_index < first_index(); ++g_index) {
292 output[g_index] = Complex(0.0, 0.0);
293 }
294 for (std::size_t g_index = first_index(); g_index < input.size(); ++g_index) {
295 output[g_index] += g_squared_[g_index] * input[g_index];
296 }
297 }
298
299 std::vector<Real> g_squared_;
300 Real q0_squared_{1.0};
301 int first_nonzero_g_index_{0};
302 std::function<void(const std::vector<Real>&, std::vector<Complex>&)> to_reciprocal_space_;
303 std::function<void(const std::vector<Complex>&, std::vector<Real>&)> to_real_space_;
304 std::function<void(std::vector<Real>&)> reduce_within_pool_;
305 std::vector<Real> screening_real_;
306 Real mean_screening_{1.0};
307 bool has_screening_{false};
308
309 /// Cap and exit tolerance for the conjugate-gradient solve.
310 static constexpr int maximum_iterations_ = 50;
311 static constexpr Real relative_tolerance_ = 1.0e-8;
312};
313
314} // namespace q3
Residual preconditioner solving [G² + k_TF²(r)] x = G² F by conjugate gradients.
LocalThomasFermiPreconditioner(std::vector< Real > g_squared, Real q0_squared, int first_nonzero_g_index, std::function< void(const std::vector< Real > &, std::vector< Complex > &)> to_reciprocal_space, std::function< void(const std::vector< Complex > &, std::vector< Real > &)> to_real_space, std::function< void(std::vector< Real > &)> reduce_within_pool={})
void apply(std::vector< Complex > &residual_g) const override
std::string name() const override
void update(const std::vector< Real > &density_real) override
Real mean_screening() const
Cell-averaged k_TF², in bohr⁻². Exposed for tests.
Abstract base class for all SCF residual preconditioners.
Definition init.hpp:26
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
double Real
std::complex< double > Complex