votca 2026-dev
Loading...
Searching...
No Matches
ewaldbackground.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#ifndef VOTCA_XTP_EWALDBACKGROUND_H
21#define VOTCA_XTP_EWALDBACKGROUND_H
22
23// Standard includes
24#include <algorithm>
25#include <chrono>
26#include <cmath>
27#include <fstream>
28#include <limits>
29#include <stdexcept>
30
31// Third party includes
32#include <Eigen/Eigenvalues>
33#include <Eigen/IterativeLinearSolvers>
34#include <boost/format.hpp>
35
36// Local VOTCA includes
49
91
92namespace votca {
93namespace xtp {
94
95class EwaldBackground final : public QMCalculator {
96 public:
97 std::string Identify() const { return "ewaldbackground"; }
98 bool WriteToStateFile() const { return false; }
99
100 protected:
101 void ParseOptions(const tools::Property& user_options);
102 bool Evaluate(Topology& top);
103
104 private:
105 std::string mapping_file_;
106 std::string checkpoint_file_;
107
108 double alpha_ = 0.5; // bohr^-1 (internal). User-facing XML value
109 // for coulombmethod.alpha is nm^-1, converted
110 // in ParseOptions; only used as-is if the
111 // user explicitly sets it, otherwise
112 // overwritten in Evaluate() with a value
113 // derived from the box (see there, already
114 // in bohr^-1 at that point).
115 bool alpha_explicit_ = false;
116 double k_max_ = 3.0; // bohr^-1 (internal); same nm^-1-in-XML
117 // pattern as alpha_ -- derived in Evaluate()
118 // from the (possibly also derived) alpha_,
119 // unless explicitly set.
120 bool k_max_explicit_ = false;
121 double thole_a_ = 0.39;
122 double r_min_ = 18.897259886; // bohr (internal) = 1.0 nm, the actual
123 // default applied in ParseOptions. User-
124 // facing XML value for realspace.r_min is
125 // nm, converted in ParseOptions; this member
126 // initializer only matters if ParseOptions
127 // somehow never runs.
128 double field_tol_ = 1e-8;
129 // Dimensionless: the real-space distance cutoff is
130 // screening_factor_/alpha. See EwaldRealSpaceSum's own constructor for
131 // what it controls and what raising or lowering it costs. Unitless, so
132 // unlike r_min/alpha/k_max it needs no nm<->bohr conversion.
133 double screening_factor_ = 6.0;
135
137 double pcg_tolerance_ = 1e-8;
138 bool induce_ = true; // matches legacy's own polarmethod.induce; when
139 // Debug/experimental, no legacy counterpart -- see
140 // EwaldBlockJacobiPreconditioner's own class documentation for what
141 // this is for and why it exists, and its own usage-pattern note (the
142 // real reason this needed its own if/else branch below rather than a
143 // single runtime-switchable cg instance: the preconditioner type is a
144 // compile-time Eigen::ConjugateGradient template parameter). Not yet
145 // validated against a real system this size -- only its own mechanics
146 // (compiles, runs, solves correctly) have been confirmed, with a
147 // synthetic test operator, not this real one.
149 // Debug/experimental. Switches the whole solve from PCG (either
150 // preconditioner) to plain weighted-Jacobi iteration (JOR) -- see
151 // SolveWithJOR's own documentation for the algorithm and its direct
152 // correspondence to legacy PolarBackground's own SOR. Exists
153 // specifically because a real run this session found PCG's own
154 // operator to be genuinely indefinite (p.A.p < 0, a direct algebraic
155 // certificate, not an inference -- confirmed via
156 // SolveWithIndefinitenessCheck), which CG's own convergence theory
157 // cannot handle regardless of preconditioner; JOR makes no positive-
158 // definiteness assumption anywhere in its own derivation.
159 bool use_jor_ = false;
160 // Relaxation factor for use_jor_'s own iteration. Matches legacy
161 // PolarBackground's own default for this specific calculator
162 // (_polar_wSOR_N = 0.35, confirmed directly against legacy's own
163 // source -- NOT the more commonly cited generic 0.25 default declared
164 // on APolarSite::Induce's own signature, which this calculator
165 // overrides).
166 double jor_omega_ = 0.35;
167 // Debug/experimental. See SolveWithJOR's own declaration for what
168 // this does and why it exists (literally replicating legacy's own
169 // two-phase unrelaxed-then-relaxed structure, rather than relying on
170 // an unverified assumption to relate this codebase's own JOR
171 // recursion to legacy's).
173};
174
176 mapping_file_ = opt.get("multipoles").as<std::string>();
178 "output.checkpoint", "ewaldbackground.hdf5");
179
180 alpha_explicit_ = opt.exists("coulombmethod.alpha");
181 if (alpha_explicit_) {
182 // User-facing value is nm^-1; alpha_ is stored internally in
183 // bohr^-1. alpha has units of 1/length, so converting the numeric
184 // value goes the same direction as EwaldBackground's own box-derived
185 // default below: bohr^-1 = nm^-1 * bohr2nm (NOT nm2bohr -- that
186 // would be backwards for an inverse-length quantity; see the field
187 // conversion note in ptop_dump.cc for the same kind of
188 // easy-to-invert factor, worked through carefully there).
189 alpha_ = opt.get("coulombmethod.alpha").as<double>() * tools::conv::bohr2nm;
190 }
191 // Default k_max derived from alpha_, not a fixed literal: the
192 // reciprocal-space Gaussian weight exp(-k^2/4*alpha^2) means a k_max
193 // that isn't scaled to alpha can generate a k-vector count many orders
194 // of magnitude larger than what's actually needed for convergence, for
195 // no numerical benefit -- k=6*alpha already gives exp(-9) ~ 1.2e-4, a
196 // reasonable default cutoff. This was a real, costly mistake in an
197 // earlier version of this class (a fixed k_max=15.0 default, entirely
198 // disconnected from alpha_, caused a real run on a 1000-segment system
199 // to hang for an extremely long time computing near-zero-weight
200 // k-vectors) -- worth being explicit about here so it isn't
201 // reintroduced by, e.g., reverting this line without reading why.
202 // alpha_ itself, in turn, is resolved in Evaluate() rather than here
203 // when not explicit (see there) since a good default genuinely depends
204 // on the box, which isn't available yet at this point -- so k_max_'s
205 // own default is also finalized there, once the (possibly derived)
206 // alpha_ is known.
207 k_max_explicit_ = opt.exists("coulombmethod.k_max");
208 if (k_max_explicit_) {
209 // Same nm^-1 (user-facing) -> bohr^-1 (internal) conversion as
210 // alpha_ above.
211 k_max_ = opt.get("coulombmethod.k_max").as<double>() * tools::conv::bohr2nm;
212 }
213 std::string shape_str = opt.ifExistsReturnElseReturnDefault<std::string>(
214 "coulombmethod.shape", "cube");
215 if (shape_str == "cube" || shape_str == "sphere") {
217 } else if (shape_str == "slab") {
219 } else {
220 throw std::runtime_error(
221 "EwaldBackground: coulombmethod.shape must be 'cube', 'sphere', or "
222 "'slab'");
223 }
224
225 thole_a_ = opt.ifExistsReturnElseReturnDefault<double>("polarmethod.thole_a",
226 thole_a_);
227 // realspace.r_min is user-facing nm; r_min_ is stored internally in
228 // bohr. r_min is a plain length (not an inverse length like alpha_/
229 // k_max_ above), so this conversion goes the more familiar direction:
230 // bohr = nm * nm2bohr. 1.0 nm is the default here (~18.9 bohr,
231 // replacing the old bare-bohr literal default of 20.0).
232 double r_min_nm =
233 opt.ifExistsReturnElseReturnDefault<double>("realspace.r_min", 1.0);
234 r_min_ = r_min_nm * tools::conv::nm2bohr;
236 "realspace.field_tol", field_tol_);
238 "realspace.screening_factor", screening_factor_);
239 if (screening_factor_ <= 0.0) {
240 throw std::runtime_error(
241 "EwaldBackground: realspace.screening_factor must be positive");
242 }
243
244 max_iter_ = opt.ifExistsReturnElseReturnDefault<Index>("polarmethod.max_iter",
245 max_iter_);
247 "polarmethod.tolerance", pcg_tolerance_);
248 induce_ =
249 opt.ifExistsReturnElseReturnDefault<bool>("polarmethod.induce", induce_);
250 // Debug-only, undocumented on purpose (no legacy counterpart to give
251 // it a natural home in the schema) -- see this member's own
252 // declaration for what it's for.
253 // Debug-only, undocumented on purpose, same pattern as the shape one
254 // immediately above.
255 // Debug-only, undocumented on purpose, same pattern as the two above.
256 // Debug-only, undocumented on purpose, same pattern as the three above.
257 // Debug-only, undocumented on purpose, same pattern as the four above.
258 // Debug/experimental, undocumented on purpose -- see
259 // use_block_jacobi_preconditioner_'s own declaration.
261 "polarmethod.use_block_jacobi_preconditioner",
263 // Debug/experimental, see use_jor_'s own declaration.
264 use_jor_ = opt.ifExistsReturnElseReturnDefault<bool>("polarmethod.use_jor",
265 use_jor_);
267 "polarmethod.jor_omega", jor_omega_);
269 "polarmethod.debug_match_legacy_first_step", match_legacy_first_step_);
270}
271
273 Logger log;
275 log.setCommonPreface("\nEWD");
276 // maverick=true means "single job running alone" in VOTCA's own
277 // terminology (job-parallel calculators set this false so multiple
278 // threads' log output can be buffered and interleaved cleanly) --
279 // this calculator is a single xtp_run job, not job-parallel, and
280 // critically, false here would silently buffer every message below
281 // until a final std::cout<<log flush, which would defeat the entire
282 // point of adding real-time progress output at all.
283 log.setMultithreading(true);
284
285 auto t_start = std::chrono::steady_clock::now();
286 auto elapsed_s = [&](std::chrono::steady_clock::time_point since) {
287 return std::chrono::duration<double>(std::chrono::steady_clock::now() -
288 since)
289 .count();
290 };
291
292 XTP_LOG(Log::info, log) << TimeStamp()
293 << " Starting Ewald background calculation"
294 << std::flush;
295
296 PolarMapper polmap(log);
298
299 EwaldRegistry registry;
300 std::vector<Index> ids;
301 // offsets[n] is the global vector index where segment ids[n]'s own
302 // block starts (in units of a single scalar, 3 per site); mirrors
303 // EwaldPeriodicDipoleOperator's own offsets_ table exactly, since b and
304 // x here must use the same layout that operator expects.
305 std::vector<Index> offsets;
306 ids.reserve(top.Segments().size());
307 offsets.reserve(top.Segments().size() + 1);
308 offsets.push_back(0);
309 for (const Segment& seg : top.Segments()) {
310 PolarSegment mol = polmap.map(seg, SegId(seg.getId(), std::string("n")));
311 registry.Register(seg.getId(), EwaldChargeState::Neutral, mol);
312 ids.push_back(seg.getId());
313 offsets.push_back(offsets.back() + 3 * mol.size());
314 }
315 const Index total_size = offsets.back();
316
317 XTP_LOG(Log::info, log) << TimeStamp() << " Mapped " << ids.size()
318 << " segments, " << (total_size / 3)
319 << " polarizable sites total (" << elapsed_s(t_start)
320 << "s)" << std::flush;
321
322 const Eigen::Matrix3d& box = top.getBox();
323 const double volume = box.col(0).dot(box.col(1).cross(box.col(2)));
324
325 // alpha and k_max are derived TOGETHER, from the cost of the two sums
326 // they divide the work between. alpha is free -- it cancels out of the
327 // total, which is what the alpha-independence tests pin -- so the only
328 // thing left to choose it by is how much work each half does.
329 //
330 // Both cutoffs are fixed multiples of alpha, which is what makes the
331 // accuracy alpha-independent in the first place:
332 //
333 // real space EwaldRealSpaceSum truncates at screening_factor/alpha,
334 // leaving a tail of order erfc(screening_factor).
335 // reciprocal the Gaussian weight is exp(-k^2/4 alpha^2), so a cutoff
336 // at s_k*alpha leaves exp(-s_k^2/4).
337 //
338 // so with N sites in a cell of volume V the work at ONE target is
339 //
340 // N_real = (N/V)(4pi/3)(s_r/alpha)^3 falling as alpha^-3
341 // N_k = (4pi/3)(s_k alpha)^3 V/(8pi^3) rising as alpha^3
342 //
343 // and d/dalpha [A alpha^-3 + B alpha^3] = 0 gives alpha^6 = A/B, i.e.
344 //
345 // alpha = sqrt(2 pi) (N^(1/6) / V^(1/3)) sqrt(s_r/s_k)
346 //
347 // the standard Ewald result, whose N^(1/6) is what makes the method
348 // O(N^(3/2)) rather than O(N^2).
349 //
350 // THE PREVIOUS DEFAULTS WERE alpha = 3/L_min AND k_max = 6*alpha, and
351 // between them they switched the splitting off. With those two, and a
352 // cubic cell, k_max/(2pi/L) = (18/L)(L/2pi) = 9/pi ~ 2.86 NO MATTER HOW
353 // BIG THE CELL IS: the reciprocal half is a fixed ~98 k-vectors for
354 // every system ever run, while the real-space cutoff sits at 6/alpha =
355 // 2L -- twice the box -- so the real half grows linearly with the site
356 // count and carries everything. That is a direct lattice sum with an
357 // erfc in it. Measured per target, on thiophene at experimental
358 // density: 301,691 terms at 1000 segments and 1,508,063 at 5000, of
359 // which 98 were reciprocal in both cases.
360 //
361 // The two defaults were also inconsistent with each other about
362 // accuracy, and expensively so: screening_factor = 6 truncates real
363 // space at erfc(6) = 2e-17 while k_max = 6*alpha truncates reciprocal
364 // space at exp(-9) = 1.2e-4, thirteen orders apart, with the money
365 // spent on the side that needed it less.
366 //
367 // So screening_factor is now the single accuracy knob and k_max
368 // follows it: eps = erfc(s_r), then s_k = 2*sqrt(-ln eps) makes the
369 // reciprocal tail match. At the unchanged default s_r = 6 that is
370 // s_k = 12.39, so nothing about real-space accuracy moves and the
371 // reciprocal side improves by thirteen orders -- while the balanced
372 // alpha makes the whole thing 3x faster on a 216-molecule methane box,
373 // 9x on 1000 thiophenes, 21x on 5000, and 62x on a 400,000-site cell.
374 // Cheaper AND stricter, which is only possible because the starting
375 // point was so badly out of balance.
376 //
377 // Setting <alpha> or <k_max> explicitly still overrides either half.
379 const double s_r = screening_factor_;
380 // erfc underflows to 0 for s_r beyond ~27; the clamp keeps the log
381 // finite there rather than producing an infinite s_k. Well outside
382 // any sane setting -- erfc(9) is already 4e-37 -- but this is a
383 // user-settable option, so it is not left to chance.
384 const double eps = std::max(std::erfc(s_r), 1e-300);
385 const double s_k = 2.0 * std::sqrt(-std::log(eps));
386
387 if (!alpha_explicit_) {
388 const double n_sites = double(total_size / 3);
389 alpha_ = std::sqrt(2.0 * tools::conv::Pi) * std::pow(n_sites, 1.0 / 6.0) /
390 std::cbrt(volume) * std::sqrt(s_r / s_k);
391
392 // NOT clamped against r_min_. An earlier version of this capped
393 // alpha at s_r/r_min_, on the theory that a real-space cutoff
394 // inside r_min_ would overrule it. It does not: r_min_ bounds the
395 // shell search by TRANSLATION magnitude |t| and says only that
396 // convergence may not be declared before that radius, while
397 // real_space_cutoff_ culls by actual PAIR SEPARATION. Accuracy is
398 // erfc(s_r) either way. What a large r_min_ costs is time -- the
399 // search walks shells whose pairs are all culled, contributing
400 // exactly nothing -- so clamping alpha to avoid that would trade a
401 // real cost for an imagined risk, and would do so hardest for the
402 // users who raised r_min_ deliberately. Reported below instead.
403 }
404 if (!k_max_explicit_) {
405 k_max_ = s_k * alpha_;
406 }
407 }
408
409 EwaldRealSpaceSum real_sum(box, registry, alpha_, thole_a_, r_min_,
410 field_tol_, /*shell_width=*/0.945,
411 /*n_max=*/15, screening_factor_);
412 EwaldReciprocalSpaceSum recip_sum(box, registry, alpha_, k_max_);
413 EwaldShapeCorrection shape(volume, registry, shape_);
414
415 XTP_LOG(Log::info, log) << TimeStamp()
416 << " Real/reciprocal-space sums constructed ("
417 << elapsed_s(t_start) << "s)" << std::flush;
418 XTP_LOG(Log::info, log) << TimeStamp() << " Ewald split: alpha=" << alpha_
419 << " bohr^-1 (" << alpha_ * tools::conv::nm2bohr
420 << " nm^-1), k_max=" << k_max_ << " bohr^-1 ("
421 << k_max_ * tools::conv::nm2bohr << " nm^-1, "
422 << recip_sum.NumKVectors()
423 << " k-vectors), real-space cutoff "
425 << " bohr (screening_factor=" << screening_factor_
426 << ")" << std::flush;
427 // The two halves printed side by side, because the whole point of the
428 // derived alpha is that they should come out comparable. A run where
429 // these differ by orders of magnitude is one where alpha was set by
430 // hand, or where the derivation was given a cell it does not suit --
431 // either way it is the number to look at first when the background
432 // solve is slower than expected.
433 {
434 const double n_sites = double(total_size / 3);
435 const double n_real_est = (n_sites / volume) *
436 (4.0 * tools::conv::Pi / 3.0) *
437 std::pow(screening_factor_ / alpha_, 3);
438 XTP_LOG(Log::info, log)
439 << TimeStamp()
440 << " Ewald balance (terms per target, estimated): " << "real "
441 << n_real_est << " vs reciprocal " << recip_sum.NumKVectors()
442 << std::flush;
443
444 // r_min_ past the cutoff means the shell search is required to walk
445 // out to a radius where every pair is already culled: those shells
446 // contribute exactly zero and then let it stop. Harmless, and pure
447 // cost, so say so rather than silently absorbing it.
448 const double cutoff = screening_factor_ / alpha_;
449 if (r_min_ > cutoff) {
450 XTP_LOG(Log::info, log)
451 << TimeStamp() << " NOTE: r_min (" << r_min_
452 << " bohr) lies beyond the real-space cutoff (" << cutoff
453 << " bohr). The shell search will walk shells that contribute "
454 "nothing before it is allowed to stop. Accuracy is unaffected "
455 "-- it is erfc(screening_factor) either way -- but lowering "
456 "r_min to at most the cutoff would save that work."
457 << std::flush;
458 }
459 }
460 XTP_LOG(Log::info, log) << TimeStamp() << " Cell: volume=" << volume
461 << " bohr^3, shape="
462 << (shape_ == EwaldShape::Cube ? "cube" : "slab")
463 << ", r_min=" << r_min_ << " bohr ("
465 << " nm), field_tol=" << field_tol_
466 << ", thole_a=" << thole_a_ << std::flush;
467 XTP_LOG(Log::info, log) << TimeStamp() << " Induction: induce="
468 << (induce_ ? "true" : "false")
469 << ", max_iter=" << max_iter_
470 << ", tolerance=" << pcg_tolerance_
471 << ", solver=" << (use_jor_ ? "JOR" : "PCG")
472 << (use_jor_ ? ""
474 ? " (block-Jacobi)"
475 : " (unpreconditioned)"))
476 << std::flush;
477 if (use_jor_) {
478 XTP_LOG(Log::info, log)
479 << TimeStamp() << " JOR omega=" << jor_omega_ << std::flush;
480 }
482 XTP_LOG(Log::info, log)
483 << TimeStamp() << " debug_match_legacy_first_step is ON" << std::flush;
484 }
485
486 // Permanent field only (every induced dipole is still zero at this
487 // point), computed once -- this becomes b for the PCG solve below,
488 // matching CalcInducedDipolesViaPCG's own b-construction pattern, but
489 // with the opposite sign: b = +V here, not -V (see
490 // EwaldPeriodicDipoleOperator's own class documentation for why).
491 auto t_field = std::chrono::steady_clock::now();
492 std::vector<std::pair<Index, PolarSite*>> targets;
493 targets.reserve(std::size_t(total_size / 3));
494 for (std::size_t n = 0; n < ids.size(); ++n) {
495 PolarSegment& segment = registry.Get(ids[n], EwaldChargeState::Neutral);
496 for (Index s = 0; s < segment.size(); ++s) {
497 PolarSite& site = segment[s];
498 site.setInduced_Dipole(Eigen::Vector3d::Zero());
499 site.Reset();
500 targets.push_back({ids[n], &site});
501 }
502 }
503 for (const auto& entry : targets) {
504 real_sum.AddFieldAt<Estatic::V>(entry.first, *entry.second,
506 }
507 XTP_LOG(Log::info, log) << TimeStamp() << " Real-space permanent field done ("
508 << elapsed_s(t_field) << "s)" << std::flush;
509
510 auto t_recip = std::chrono::steady_clock::now();
511 // EwaldReciprocalSpaceSum no longer takes a segment id (it never
512 // excludes anything -- see its own class documentation), so only the
513 // bare site pointers are needed here.
514 std::vector<PolarSite*> recip_targets;
515 recip_targets.reserve(targets.size());
516 for (const auto& entry : targets) {
517 recip_targets.push_back(entry.second);
518 }
519 recip_sum.AddFieldAtMany<Estatic::V>(
520 recip_targets, EwaldChargeState::Neutral,
521 [&](std::size_t done, std::size_t total) {
522 // Debug, not info: twenty of these per run drown the lines that
523 // matter, and the total is already on the split line above.
524 XTP_LOG(Log::debug, log)
525 << TimeStamp() << " k-space progress: " << done << "/" << total
526 << " k-vectors (" << elapsed_s(t_recip) << "s)" << std::flush;
527 });
528 XTP_LOG(Log::info, log) << TimeStamp()
529 << " Reciprocal-space permanent field done ("
530 << elapsed_s(t_recip) << "s)" << std::flush;
531
532 for (const auto& entry : targets) {
533 shape.AddFieldAt<Estatic::V>(*entry.second, EwaldChargeState::Neutral);
534 }
535 XTP_LOG(Log::info, log) << TimeStamp() << " Permanent field total ("
536 << elapsed_s(t_field) << "s)" << std::flush;
537
538 // Intramolecular static-static compensation: EwaldReciprocalSpaceSum's
539 // own structure factor never excludes anything (see that class's own
540 // documentation), so it unconditionally includes every same-segment
541 // static-static pair's own contribution -- exactly the erf(alpha*r)/r
542 // "other half" of the Ewald split (erf+erfc=1). Legacy explicitly
543 // removes this same leak (see EwdInteractor::FP12_ERF_At_By's own
544 // "Note the (-): This is a compensation term" comment) so that the net
545 // static-static intramolecular contribution comes out to genuinely
546 // zero, not the un-cancelled reciprocal-space leak alone -- see
547 // EwaldRealSpaceInteractor::ApplyErfStaticFieldCorrection's own
548 // documentation for the fuller account of why (a real mechanism in
549 // legacy's own code, missed on an earlier pass through it, not a new
550 // design decision here). Every ordered pair within a segment is
551 // visited, matching legacy's own double loop.
552 //
553 // INCLUDING i == j. "Never excludes anything" means the structure
554 // factor sums over j = i as well, so each site also receives its own
555 // erf-screened field. For a charge that self-field is zero by
556 // symmetry, which is why skipping i == j was harmless as long as
557 // every site was rank 0 -- but a static DIPOLE's own erf field is the
558 // finite Ewald self-term -(4/3)alpha^3/sqrt(pi) * mu, and leaving it
559 // in the permanent field biases every induced dipole in the cell by a
560 // quantity that grows as alpha^3. The same term is already removed
561 // from the INDUCED side, where EwaldPeriodicDipoleOperator subtracts
562 // EwaldReciprocalSpaceSum::SelfFieldMatrix() from its own operator;
563 // this loop is the permanent-field counterpart, and it was missing.
564 // ApplyErfStaticFieldCorrection's own r < kCoincidenceTol branch is
565 // exactly that limit, so the self-pair needs no special case here --
566 // and because the branch is proportional to getStaticDipole(), it
567 // contributes identically zero for rank-0 input, leaving every
568 // charge-only result unchanged.
570 for (Index n = 0; n < Index(ids.size()); ++n) {
571 PolarSegment& segment = registry.Get(ids[n], EwaldChargeState::Neutral);
572 Index n_sites = segment.size();
573 for (Index i = 0; i < n_sites; ++i) {
574 for (Index j = 0; j < n_sites; ++j) {
576 segment[j], segment[i]);
577 }
578 }
579 }
580 XTP_LOG(Log::info, log) << TimeStamp()
581 << " Intramolecular static compensation applied ("
582 << elapsed_s(t_field) << "s)" << std::flush;
583
584 Eigen::VectorXd b(total_size);
585 {
586 std::size_t t = 0;
587 for (std::size_t n = 0; n < ids.size(); ++n) {
588 const PolarSegment& segment =
589 registry.Get(ids[n], EwaldChargeState::Neutral);
590 Index base = offsets[n];
591 for (Index s = 0; s < segment.size(); ++s) {
592 b.segment<3>(base + 3 * s) = targets[t].second->V();
593 targets[t].second->Reset();
594 ++t;
595 }
596 }
597 }
598
599 if (induce_) {
600 XTP_LOG(Log::info, log)
601 << TimeStamp() << " Starting " << (use_jor_ ? "JOR" : "PCG")
602 << " solve (max " << max_iter_ << " iterations, tolerance "
603 << pcg_tolerance_ << ")" << std::flush;
604 auto t_pcg = std::chrono::steady_clock::now();
605
606 EwaldPeriodicDipoleOperator op(registry, real_sum, recip_sum, shape, ids,
608 Eigen::VectorXd x;
609 Index iterations;
610 double residual;
611 bool converged;
612 Index indefinite_at_iteration = -1;
613 double indefinite_curvature = 0.0;
614 double lanczos_min_eigenvalue = std::numeric_limits<double>::quiet_NaN();
615
616 // use_jor_ is an outer, first choice: JOR and PCG are two entirely
617 // different algorithms (see SolveWithJOR's own documentation for
618 // why JOR is the one actually chosen once the operator was directly
619 // confirmed indefinite), not two variants of the same solve -- JOR
620 // has no preconditioner-type branching of its own
621 // (EwaldSitePolarizabilityBlocks is the one and only D^-1 it uses, matching
622 // legacy exactly), so it never enters the PCG-specific branches below at
623 // all.
624 if (use_jor_) {
625 EwaldSitePolarizabilityBlocks site_p(registry, ids);
626 auto result = SolveWithJOR(op, site_p, b, max_iter_, jor_omega_, log,
628 x = result.x;
629 iterations = result.iterations;
630 residual = result.residual;
631 converged = result.converged;
632
633 XTP_LOG(Log::info, log)
634 << TimeStamp() << " JOR finished after " << iterations
635 << " iterations, max_dU=" << result.max_dU
636 << " avg_dU=" << result.avg_dU << " (residual=" << residual
637 << ", informational) (" << elapsed_s(t_pcg) << "s)" << std::flush;
638 if (!converged) {
639 throw std::runtime_error(
640 "EwaldBackground: JOR did not converge within max_iter");
641 }
642 } else {
643
644 // The preconditioner type is a compile-time choice here too (see
645 // SolveWithIndefinitenessCheck's own documentation for why that
646 // function is templated on it) -- these two branches genuinely
647 // instantiate two different specializations, they cannot share one
648 // call. EwaldBlockJacobiPreconditioner builds itself fully in its
649 // own constructor (see its own class documentation); Eigen's own
650 // DiagonalPreconditioner does not -- it needs an explicit compute(op)
651 // call first, unlike the constructor-based pattern
652 // EwaldBlockJacobiPreconditioner itself uses. Getting this backwards
653 // (assuming both work the same way) would be a real, silent
654 // correctness bug -- solve() on an uncompute()'d DiagonalPreconditioner
655 // does not throw, it just returns nonsense -- so this is deliberately
656 // NOT written as a single shared code path that "just" swaps the
657 // preconditioner type.
659 EwaldBlockJacobiPreconditioner precond(registry, ids, alpha_, thole_a_);
660 auto result = SolveWithIndefinitenessCheck(op, precond, b, max_iter_,
661 pcg_tolerance_, log, t_pcg);
662 x = result.x;
663 iterations = result.iterations;
664 residual = result.residual;
665 converged = result.converged;
666 indefinite_at_iteration = result.indefinite_at_iteration;
667 indefinite_curvature = result.indefinite_curvature;
668 lanczos_min_eigenvalue = result.lanczos_min_eigenvalue;
669 } else {
670 Eigen::DiagonalPreconditioner<double> precond;
671 precond.compute(op);
672 auto result = SolveWithIndefinitenessCheck(op, precond, b, max_iter_,
673 pcg_tolerance_, log, t_pcg);
674 x = result.x;
675 iterations = result.iterations;
676 residual = result.residual;
677 converged = result.converged;
678 indefinite_at_iteration = result.indefinite_at_iteration;
679 indefinite_curvature = result.indefinite_curvature;
680 lanczos_min_eigenvalue = result.lanczos_min_eigenvalue;
681 }
682
683 XTP_LOG(Log::info, log)
684 << TimeStamp() << " PCG finished after " << iterations
685 << " iterations, residual " << residual << " (" << elapsed_s(t_pcg)
686 << "s)" << std::flush;
687 {
688 // Phase breakdown of the matvec -- see
689 // EwaldPeriodicDipoleOperator::RawMultiplyTimings' own
690 // declaration. n_calls exceeds the reported iteration count by
691 // one, since baseline_ = RawMultiply(0) is built in the operator's
692 // own constructor. The first call also builds the real-space
693 // neighbour cache, so it is much more expensive than the rest and
694 // inflates the real_space per-call average; read the per-call
695 // figures as an upper bound on steady-state cost.
696 const auto& tm = op.Timings();
697 const double n = double(std::max<Index>(tm.n_calls, 1));
698 XTP_LOG(Log::info, log)
699 << TimeStamp() << " RawMultiply phase breakdown over " << tm.n_calls
700 << " calls (total " << tm.total() << "s):" << std::flush;
701 auto line = [&](const char* nm, double t) {
702 XTP_LOG(Log::info, log)
703 << TimeStamp()
704 << (boost::format(" %1$-12s %2$8.3fs total "
705 "%3$7.3fs/call %4$5.1f%%") %
706 nm % t % (t / n) %
707 (tm.total() > 0 ? 100.0 * t / tm.total() : 0.0))
708 .str()
709 << std::flush;
710 };
711 line("setup", tm.setup);
712 line("real_space", tm.real_space);
713 line("reciprocal", tm.reciprocal);
714 line("shape", tm.shape);
715 line("assemble", tm.assemble);
716 line("intra", tm.intra);
717
718 // Neighbour-list size -- the real cost driver behind the
719 // real_space line above. See EwaldRealSpaceSum::NeighborStats.
720 const auto ns = real_sum.GetNeighborStats();
721 XTP_LOG(Log::info, log)
722 << TimeStamp()
723 << (boost::format(
724 " neighbours: %1$.1f entries/target over %2$d targets "
725 "(%3$d kept, %4$d culled beyond %5$.1f bohr = %6$.1f%%)") %
726 ns.entries_per_target() % ns.targets % ns.entries % ns.culled %
727 real_sum.RealSpaceCutoff() % (100.0 * ns.culled_fraction()))
728 .str()
729 << std::flush;
730 }
731 if (!std::isnan(lanczos_min_eigenvalue)) {
732 XTP_LOG(Log::info, log) << TimeStamp() << " Lanczos min eigenvalue: "
733 << lanczos_min_eigenvalue << std::flush;
734 if (lanczos_min_eigenvalue < 0.0) {
735 XTP_LOG(Log::info, log)
736 << TimeStamp()
737 << " NEGATIVE -- strong evidence the operator is not "
738 "positive-definite (see "
739 "PcgIndefinitenessResult::lanczos_min_eigenvalue for what "
740 "this does and does not guarantee)."
741 << std::flush;
742 }
743 }
744
745 if (indefinite_at_iteration >= 0) {
746 throw std::runtime_error(
747 "EwaldBackground: PCG's own operator was found to be NOT "
748 "positive-definite at iteration " +
749 std::to_string(indefinite_at_iteration) +
750 " (p.A.p = " + std::to_string(indefinite_curvature) +
751 " <= 0) -- this is a direct algebraic certificate, not an "
752 "inference from residual behavior.");
753 }
754
755 if (!converged) {
756 throw std::runtime_error(
757 "EwaldBackground: PCG did not converge (max_iter reached, "
758 "operator was never found indefinite along the way)");
759 }
760 }
761
762 for (std::size_t n = 0; n < ids.size(); ++n) {
763 PolarSegment& segment = registry.Get(ids[n], EwaldChargeState::Neutral);
764 Index base = offsets[n];
765 for (Index s = 0; s < segment.size(); ++s) {
766 segment[s].setInduced_Dipole(x.segment<3>(base + 3 * s));
767 }
768 }
769 } else {
770 // Matches legacy's own polarmethod.induce=0 behaviour: every site's
771 // induced dipole stays at zero (already set before the
772 // permanent-field computation above) -- see induce_'s own class
773 // documentation for why this is a separate code path rather than a
774 // zero-polarizability run through the same PCG solve.
775 XTP_LOG(Log::info, log)
776 << TimeStamp()
777 << " polarmethod.induce is false: skipping the PCG solve, "
778 "induced dipoles left at zero"
779 << std::flush;
780 }
781
782 auto t_cpt = std::chrono::steady_clock::now();
783 // Write the permanent field (b) back into every site's V() before the
784 // checkpoint write, unconditionally regardless of induce_ -- neither
785 // code path above leaves V() holding it otherwise: the PCG path
786 // overwrites V() repeatedly during its own iterations (each
787 // EwaldPeriodicDipoleOperator::multiply() call internally
788 // Reset()s and rewrites every site's V() via RawMultiply), leaving
789 // whatever the *last* iteration's trial field happened to be, not the
790 // permanent field; the induce_=false path leaves V() at the zero
791 // Reset() left it just after b was built above. Restoring b here is
792 // what makes the permanent field actually recoverable from the
793 // checkpoint at all -- without this, hdf5_dump has no way to report
794 // anything but 0.0 for it (see that tool's own former limitation note,
795 // now resolved by this).
796 {
797 for (std::size_t n = 0; n < ids.size(); ++n) {
798 PolarSegment& segment = registry.Get(ids[n], EwaldChargeState::Neutral);
799 Index base = offsets[n];
800 for (Index s = 0; s < segment.size(); ++s) {
801 segment[s].V() = b.segment<3>(base + 3 * s);
802 }
803 }
804 }
805
807 CheckpointWriter w = cpf.getWriter();
808 registry.WriteToCpt(w);
809
810 // The convergence parameters travel with the converged state. A job
811 // that later embeds a foreground in this background must use the same
812 // alpha, k_max and shape -- alpha in particular decides how the
813 // interaction is split between the real- and reciprocal-space sums, so
814 // a different value is not the same physics, and nothing about the
815 // mismatch would be visible at run time. See EwaldParameters.
816 {
817 EwaldParameters params;
818 params.alpha = alpha_;
819 params.k_max = k_max_;
820 params.r_min = r_min_;
821 params.field_tol = field_tol_;
822 params.thole_a = thole_a_;
824 params.shape = shape_;
825 params.box = box;
826 CheckpointWriter wp = w.openChild("ewald_parameters");
827 params.WriteToCpt(wp);
828 }
829
830 XTP_LOG(Log::info, log) << TimeStamp() << " Checkpoint written to "
831 << checkpoint_file_ << " (" << elapsed_s(t_cpt)
832 << "s)" << std::flush;
833 XTP_LOG(Log::info, log) << TimeStamp()
834 << " Ewald background calculation done, total "
835 << elapsed_s(t_start) << "s" << std::flush;
836
837 return true;
838}
839
840} // namespace xtp
841} // namespace votca
842
843#endif // VOTCA_XTP_EWALDBACKGROUND_H
class to manage program options with xml serialization functionality
Definition property.h:55
Property & get(const std::string &key)
get existing property
Definition property.cc:79
bool exists(const std::string &key) const
check whether property exists
Definition property.cc:122
T as() const
return value as type
Definition property.h:283
T ifExistsReturnElseReturnDefault(const std::string &key, T defaultvalue) const
Definition property.h:332
CheckpointWriter getWriter()
CheckpointWriter openChild(const std::string &childName) const
void ParseOptions(const tools::Property &user_options)
bool Evaluate(Topology &top)
std::string Identify() const
Calculator name.
Block-Jacobi preconditioner for EwaldPeriodicDipoleOperator's own PCG solve.
void ApplyErfStaticFieldCorrection(const T &site1, PolarSite &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
NeighborStats GetNeighborStats() const
void AddFieldAt(Index target_segment_id, PolarSite &target, EwaldChargeState source_state, bool include_static=true) const
void AddFieldAtMany(const std::vector< PolarSite * > &targets, EwaldChargeState source_state, const ProgressCallback &progress=ProgressCallback()) const
void WriteToCpt(CheckpointWriter &w) const
const PolarSegment & Get(Index id, EwaldChargeState state) const
void Register(Index id, EwaldChargeState state, PolarSegment segment)
void AddFieldAt(PolarSite &target, EwaldChargeState source_state) const
Logger is used for thread-safe output of messages.
Definition logger.h:164
void setReportLevel(Log::Level ReportLevel)
Definition logger.h:185
void setMultithreading(bool maverick)
Definition logger.h:186
void setCommonPreface(const std::string &preface)
Definition logger.h:198
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
void setInduced_Dipole(const Eigen::Vector3d &induced_dipole)
Definition polarsite.h:93
void LoadMappingFile(const std::string &mapfile)
AtomContainer map(const Segment &seg, const SegId &segid) const
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
Container for segments and box and atoms.
Definition topology.h:41
std::vector< Segment > & Segments()
Definition topology.h:58
const Eigen::Matrix3d & getBox() const
Definition topology.h:64
#define XTP_LOG(level, log)
Definition logger.h:40
const double bohr2nm
Definition constants.h:46
const double Pi
Definition constants.h:36
const double nm2bohr
Definition constants.h:47
Charge transport classes.
Definition ERIs.h:28
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
SegmentMapper< PolarSegment > PolarMapper
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)
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
The Ewald convergence parameters a background was converged with.
void WriteToCpt(CheckpointWriter &w) const