votca 2026-dev
Loading...
Searching...
No Matches
ewaldsolvers.h
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20#pragma once
21#ifndef VOTCA_XTP_EWALDSOLVERS_H
22#define VOTCA_XTP_EWALDSOLVERS_H
23
24// Standard includes
25#include <chrono>
26#include <cmath>
27#include <limits>
28
29// Third party includes
30#include <Eigen/Eigenvalues>
31#include <boost/format.hpp>
32
33// Local VOTCA includes
35#include "votca/xtp/eigen.h"
38#include "votca/xtp/logger.h"
39
56
57namespace votca {
58namespace xtp {
59
60// Preconditioned CG written out explicitly (rather than via
61// Eigen::ConjugateGradient) so that the curvature p.dot(A*p) of every
62// search direction can be inspected. For an SPD operator it is positive
63// for every nonzero p; a value <= 0 proves the operator is not positive
64// definite, and the solve stops with a diagnostic. This distinguishes a
65// truly indefinite operator from an SPD but poorly preconditioned one,
66// which cannot be told apart from the residual history alone.
67// Templated on the preconditioner, which is a compile-time choice.
69 Eigen::VectorXd x;
71 double residual = 0.0;
72 bool converged = false;
73 // -1 if no non-positive curvature was found; otherwise the 1-based
74 // iteration at which p.dot(A*p) <= 0 first occurred. A positive result
75 // is a certificate of indefiniteness, but the check can miss it if b
76 // has no component along the negative-eigenvalue direction (e.g. the
77 // 3x3 matrix with eigenvalues -1/3/5 and b orthogonal to the eigenvector
78 // of -1, on which CG converges in 2 iterations).
81 // Smallest eigenvalue of the m x m Lanczos tridiagonal matrix built from
82 // the CG coefficients alpha_k/beta_k (m = iterations run). By the
83 // CG-Lanczos correspondence these Ritz values approximate the extreme
84 // eigenvalues of the preconditioned operator over the whole Krylov
85 // subspace, which is more robust than the per-iteration curvature test,
86 // though still b-dependent (it fails on the same orthogonal-b example).
87 // NaN if fewer than 2 iterations ran.
88 double lanczos_min_eigenvalue = std::numeric_limits<double>::quiet_NaN();
89};
90
91template <typename Preconditioner>
93 const EwaldPeriodicDipoleOperator& op, Preconditioner& precond,
94 const Eigen::VectorXd& b, Index max_iter, double tol, Logger& log,
95 std::chrono::steady_clock::time_point t_start) {
96 auto elapsed_s = [&]() {
97 return std::chrono::duration<double>(std::chrono::steady_clock::now() -
98 t_start)
99 .count();
100 };
101
103 Eigen::VectorXd x = Eigen::VectorXd::Zero(b.size());
104 Eigen::VectorXd residual_vec = b - op * x;
105 const double rhs_norm2 = b.squaredNorm();
106 const double threshold =
107 std::max(tol * tol * rhs_norm2, std::numeric_limits<double>::min());
108 double residual_norm2 = residual_vec.squaredNorm();
109 // rhs = 0 (e.g. a background with all permanent moments zero) is solved
110 // exactly by x = 0: residual_norm2 = 0 is below the threshold DBL_MIN,
111 // so the solve returns with zero iterations. The relative residual
112 // would be 0/0 and is reported as 0 rather than NaN, which would
113 // otherwise make a correct solve look failed in the log. The loop
114 // below never runs in that case, so it needs no such guard.
115 double tol_error =
116 (rhs_norm2 > 0.0) ? std::sqrt(residual_norm2 / rhs_norm2) : 0.0;
117 bool converged = (residual_norm2 < threshold);
118 // CG coefficients, kept for the Lanczos estimate (lanczos_min_eigenvalue).
119 std::vector<double> alphas;
120 std::vector<double> betas;
121
122 if (!converged) {
123 Eigen::VectorXd p = precond.solve(residual_vec);
124 Eigen::VectorXd z(b.size()), tmp(b.size());
125 double abs_new = residual_vec.dot(p);
126 Index i = 0;
127 while (i < max_iter) {
128 tmp.noalias() = op * p;
129 const double curvature = p.dot(tmp);
130 XTP_LOG(Log::info, log) << TimeStamp() << " PCG iter " << (i + 1)
131 << ": curvature p.A.p=" << curvature << " ("
132 << elapsed_s() << "s)" << std::flush;
133 if (curvature <= 0.0) {
134 result.indefinite_at_iteration = i + 1;
135 result.indefinite_curvature = curvature;
136 XTP_LOG(Log::info, log)
137 << TimeStamp() << " PCG iter " << (i + 1)
138 << ": p.A.p = " << curvature
139 << " <= 0 -- the operator is NOT positive-definite. Stopping "
140 "CG."
141 << std::flush;
142 break;
143 }
144 const double alpha = abs_new / curvature;
145 alphas.push_back(alpha);
146 x += alpha * p;
147 residual_vec -= alpha * tmp;
148
149 residual_norm2 = residual_vec.squaredNorm();
150 tol_error = std::sqrt(residual_norm2 / rhs_norm2);
151 if (residual_norm2 < threshold) {
152 converged = true;
153 ++i;
154 break;
155 }
156
157 z = precond.solve(residual_vec);
158 const double abs_old = abs_new;
159 abs_new = residual_vec.dot(z);
160 const double beta = abs_new / abs_old;
161 betas.push_back(beta);
162 p = z + beta * p;
163 ++i;
164 }
165 result.iterations = i;
166 } else {
167 result.iterations = 0;
168 }
169
170 // Lanczos tridiagonal matrix from the CG coefficients:
171 // T_00 = 1/alpha_0, T_ii = 1/alpha_i + beta_{i-1}/alpha_{i-1},
172 // T_{i,i-1} = sqrt(beta_{i-1})/alpha_{i-1}.
173 // Requires at least 2 alphas; a 1x1 matrix is not a useful estimate.
174 if (alphas.size() >= 2) {
175 const std::size_t m = alphas.size();
176 Eigen::MatrixXd T = Eigen::MatrixXd::Zero(m, m);
177 T(0, 0) = 1.0 / alphas[0];
178 for (std::size_t i = 1; i < m; ++i) {
179 T(i, i) = 1.0 / alphas[i] + betas[i - 1] / alphas[i - 1];
180 const double off = std::sqrt(betas[i - 1]) / alphas[i - 1];
181 T(i, i - 1) = off;
182 T(i - 1, i) = off;
183 }
184 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(T);
185 result.lanczos_min_eigenvalue = es.eigenvalues()(0);
186 }
187
188 result.x = x;
189 result.residual = tol_error;
190 result.converged = converged;
191 return result;
192}
193
194// Per-site polarizability blocks (P, not P^-1), used for the plain
195// Jacobi-Over-Relaxation (JOR) iteration below. These per-site blocks
196// (not the larger intramolecular-Thole-coupled blocks of
197// EwaldBlockJacobiPreconditioner) reproduce legacy APolarSite::Induce,
198// mu_new = (1-wSOR)*mu_old + wSOR*(-P)*(F_perm+F_induced),
199// which is a weighted-Jacobi step with diagonal block D = P^-1 (the
200// diagonal of EwaldPeriodicDipoleOperator is built from getPInv()), so
201// D^-1 = P.
202//
203// Despite its name, legacy PolarBackground's "SOR" is Jacobi-style, not
204// Gauss-Seidel: all fields are computed from the previous iterate (step
205// III.B) before any dipole is updated (step III.C).
207 public:
209 const std::vector<Index>& ids) {
210 for (Index id : ids) {
211 const PolarSegment& segment = registry.Get(id, EwaldChargeState::Neutral);
212 for (const PolarSite& site : segment) {
213 // Only P^-1 is stored on PolarSite; invert the 3x3 block to
214 // recover P.
215 blocks_.push_back(site.getPInv().inverse());
216 }
217 }
218 }
219
220 Index size() const { return 3 * Index(blocks_.size()); }
221
222 // Exposes the per-site P block directly.
223 const Eigen::Matrix3d& GetBlock(Index n) const { return blocks_[n]; }
224
225 // Returns P * v (= D^-1 * v) per 3-DOF site block. Site ordering
226 // matches the targets_ layout of EwaldPeriodicDipoleOperator (same
227 // registry, ids and site order), so site n occupies [3n, 3n+3) in both.
228 Eigen::VectorXd Apply(const Eigen::VectorXd& v) const {
229 Eigen::VectorXd result(size());
230 for (std::size_t n = 0; n < blocks_.size(); ++n) {
231 result.segment<3>(3 * Index(n)) = blocks_[n] * v.segment<3>(3 * Index(n));
232 }
233 return result;
234 }
235
236 private:
237 std::vector<Eigen::Matrix3d> blocks_;
238};
239
240struct JorResult {
241 Eigen::VectorXd x;
243 double residual = 0.0;
244 double max_dU = 0.0;
245 double avg_dU = 0.0;
246 bool converged = false;
247};
248
249// Mirrors legacy APolarSite::HistdU(): per-site relative change of the
250// induced dipole between successive iterates, with a small-magnitude
251// fallback to avoid dividing by a near-zero dipole (e.g. on the first
252// iteration from x = 0). Legacy's threshold is 1e-10 e*nm; it is
253// converted to bohr since dipoles here are in atomic units. The
254// convergence tolerance epstol (SolveWithJOR) is a ratio and needs no
255// conversion.
256inline double SiteRelativeDipoleChange(const Eigen::Vector3d& U0,
257 const Eigen::Vector3d& U1) {
258 const double small = 1e-10 * tools::conv::nm2bohr;
259 const double abs_U0 = U0.norm();
260 const double abs_U1 = U1.norm();
261 const double abs_dU = (U1 - U0).norm();
262 const double abs_U = (abs_U1 > abs_U0 && small > abs_U0) ? abs_U1 : abs_U0;
263 if (small > abs_U) {
264 return abs_U;
265 }
266 return abs_dU / abs_U;
267}
268
269// Plain weighted-Jacobi iteration for A*x=b (A = EwaldPeriodicDipoleOperator),
270// in the structure of legacy PolarBackground's SOR (see
271// EwaldSitePolarizabilityBlocks). For A = D + R:
272// x_new = x_old + omega * D^-1 * (b - A*x_old)
273// Unlike CG this does not require A to be positive definite; it
274// converges if the spectral radius of (I - omega*D^-1*A) is below 1,
275// and it does converge on the small indefinite test case where CG fails.
276//
277// Convergence criterion as in legacy (step III.D): the per-site relative
278// dipole change (SiteRelativeDipoleChange) must be <= epstol = 1e-3 for
279// every site, or the average must be < 0.1*epstol. The global relative
280// residual ||b-Ax||/||b|| is logged for information only.
281//
282// match_legacy_first_step = true uses omega = 1 for the first iteration
283// and the configured omega afterwards, replicating legacy's unrelaxed
284// InduceDirect() followed by wSOR-relaxed Induce() calls. The iterates
285// are then directly comparable to legacy's (up to the bohr/nm dipole
286// conversion).
288 const EwaldSitePolarizabilityBlocks& site_p,
289 const Eigen::VectorXd& b, Index max_iter,
290 double omega, Logger& log,
291 std::chrono::steady_clock::time_point t_start,
292 bool match_legacy_first_step) {
293 auto elapsed_s = [&]() {
294 return std::chrono::duration<double>(std::chrono::steady_clock::now() -
295 t_start)
296 .count();
297 };
298
299 const double kEpsTol = 1e-3; // legacy's hardcoded value
300
301 JorResult result;
302 Eigen::VectorXd x = Eigen::VectorXd::Zero(b.size());
303 const double rhs_norm = b.norm();
304 const Index n_sites = b.size() / 3;
305
306 Index i = 0;
307 for (; i < max_iter; ++i) {
308 Eigen::VectorXd residual_vec = b - op * x;
309 const double residual_norm = residual_vec.norm();
310 const Eigen::VectorXd x_old = x;
311 const double this_step_omega =
312 (i == 0 && match_legacy_first_step) ? 1.0 : omega;
313 x = x_old + this_step_omega * site_p.Apply(residual_vec);
314
315 double max_dU = -1.0;
316 double avg_dU = 0.0;
317 for (Index n = 0; n < n_sites; ++n) {
318 const double dU = SiteRelativeDipoleChange(x_old.segment<3>(3 * n),
319 x.segment<3>(3 * n));
320 avg_dU += dU;
321 if (dU > max_dU) {
322 max_dU = dU;
323 }
324 }
325 avg_dU /= double(n_sites);
326
327 XTP_LOG(Log::info, log)
328 << TimeStamp() << " JOR iter " << (i + 1) << ": max_dU=" << max_dU
329 << " avg_dU=" << avg_dU << " (residual=" << residual_norm / rhs_norm
330 << ", informational only) (" << elapsed_s() << "s)" << std::flush;
331
332 result.residual = residual_norm / rhs_norm;
333 result.max_dU = max_dU;
334 result.avg_dU = avg_dU;
335
336 bool converged = (max_dU <= kEpsTol);
337 if (avg_dU < kEpsTol * 0.1) {
338 converged = true;
339 }
340 if (converged) {
341 result.converged = true;
342 ++i;
343 break;
344 }
345 }
346 result.iterations = i;
347 result.x = x;
348 return result;
349}
350
351} // namespace xtp
352} // namespace votca
353
354#endif // VOTCA_XTP_EWALDSOLVERS_H
const PolarSegment & Get(Index id, EwaldChargeState state) const
const Eigen::Matrix3d & GetBlock(Index n) const
Eigen::VectorXd Apply(const Eigen::VectorXd &v) const
std::vector< Eigen::Matrix3d > blocks_
EwaldSitePolarizabilityBlocks(const EwaldRegistry &registry, const std::vector< Index > &ids)
Logger is used for thread-safe output of messages.
Definition logger.h:164
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
#define XTP_LOG(level, log)
Definition logger.h:40
const double nm2bohr
Definition constants.h:47
JorResult SolveWithJOR(const EwaldPeriodicDipoleOperator &op, const EwaldSitePolarizabilityBlocks &site_p, const Eigen::VectorXd &b, Index max_iter, double omega, Logger &log, std::chrono::steady_clock::time_point t_start, bool match_legacy_first_step)
ClassicalSegment< PolarSite > PolarSegment
PcgIndefinitenessResult SolveWithIndefinitenessCheck(const EwaldPeriodicDipoleOperator &op, Preconditioner &precond, const Eigen::VectorXd &b, Index max_iter, double tol, Logger &log, std::chrono::steady_clock::time_point t_start)
double SiteRelativeDipoleChange(const Eigen::Vector3d &U0, const Eigen::Vector3d &U1)
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
Eigen::VectorXd x