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 // Parallel: builds every neighbour list (the expensive shell search,
504 // formerly one serial pass over all sites) and applies it. The
505 // operator constructed below finds every list already present.
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 // The shape field is the same at every site, -(4 pi / 3V) M for a cube,
533 // but EwaldShapeCorrection::AddFieldAt sums M over the whole registry
534 // on every call. Once per site made this pass O(N^2): about 110 s of
535 // serial work for 70k sites. Evaluated once on a probe and added, as
536 // EwaldPeriodicDipoleOperator already does.
537 auto t_shape = std::chrono::steady_clock::now();
538 {
539 PolarSite shape_probe(-1, "X", Eigen::Vector3d::Zero());
541 const Eigen::Vector3d shape_field = shape_probe.V();
542 for (const auto& entry : targets) {
543 entry.second->V() += shape_field;
544 }
545 }
546 XTP_LOG(Log::info, log) << TimeStamp() << " Shape field done ("
547 << elapsed_s(t_shape) << "s)" << std::flush;
548 // Each step above reports its own time; this one is the sum since the
549 // real-space pass began.
550 XTP_LOG(Log::info, log) << TimeStamp() << " Permanent field total ("
551 << elapsed_s(t_field) << "s)" << std::flush;
552 auto t_intra = std::chrono::steady_clock::now();
553
554 // Intramolecular static-static compensation: EwaldReciprocalSpaceSum's
555 // own structure factor never excludes anything (see that class's own
556 // documentation), so it unconditionally includes every same-segment
557 // static-static pair's own contribution -- exactly the erf(alpha*r)/r
558 // "other half" of the Ewald split (erf+erfc=1). Legacy explicitly
559 // removes this same leak (see EwdInteractor::FP12_ERF_At_By's own
560 // "Note the (-): This is a compensation term" comment) so that the net
561 // static-static intramolecular contribution comes out to genuinely
562 // zero, not the un-cancelled reciprocal-space leak alone -- see
563 // EwaldRealSpaceInteractor::ApplyErfStaticFieldCorrection's own
564 // documentation for the fuller account of why (a real mechanism in
565 // legacy's own code, missed on an earlier pass through it, not a new
566 // design decision here). Every ordered pair within a segment is
567 // visited, matching legacy's own double loop.
568 //
569 // INCLUDING i == j. "Never excludes anything" means the structure
570 // factor sums over j = i as well, so each site also receives its own
571 // erf-screened field. For a charge that self-field is zero by
572 // symmetry, which is why skipping i == j was harmless as long as
573 // every site was rank 0 -- but a static DIPOLE's own erf field is the
574 // finite Ewald self-term -(4/3)alpha^3/sqrt(pi) * mu, and leaving it
575 // in the permanent field biases every induced dipole in the cell by a
576 // quantity that grows as alpha^3. The same term is already removed
577 // from the INDUCED side, where EwaldPeriodicDipoleOperator subtracts
578 // EwaldReciprocalSpaceSum::SelfFieldMatrix() from its own operator;
579 // this loop is the permanent-field counterpart, and it was missing.
580 // ApplyErfStaticFieldCorrection's own r < kCoincidenceTol branch is
581 // exactly that limit, so the self-pair needs no special case here --
582 // and because the branch is proportional to getStaticDipole(), it
583 // contributes identically zero for rank-0 input, leaving every
584 // charge-only result unchanged.
586 for (Index n = 0; n < Index(ids.size()); ++n) {
587 PolarSegment& segment = registry.Get(ids[n], EwaldChargeState::Neutral);
588 Index n_sites = segment.size();
589 for (Index i = 0; i < n_sites; ++i) {
590 for (Index j = 0; j < n_sites; ++j) {
592 segment[j], segment[i]);
593 }
594 }
595 }
596 XTP_LOG(Log::info, log) << TimeStamp()
597 << " Intramolecular static compensation applied ("
598 << elapsed_s(t_intra) << "s)" << std::flush;
599
600 Eigen::VectorXd b(total_size);
601 {
602 std::size_t t = 0;
603 for (std::size_t n = 0; n < ids.size(); ++n) {
604 const PolarSegment& segment =
605 registry.Get(ids[n], EwaldChargeState::Neutral);
606 Index base = offsets[n];
607 for (Index s = 0; s < segment.size(); ++s) {
608 b.segment<3>(base + 3 * s) = targets[t].second->V();
609 targets[t].second->Reset();
610 ++t;
611 }
612 }
613 }
614
615 if (induce_) {
616 XTP_LOG(Log::info, log)
617 << TimeStamp() << " Starting " << (use_jor_ ? "JOR" : "PCG")
618 << " solve (max " << max_iter_ << " iterations, tolerance "
619 << pcg_tolerance_ << ")" << std::flush;
620 auto t_pcg = std::chrono::steady_clock::now();
621
622 EwaldPeriodicDipoleOperator op(registry, real_sum, recip_sum, shape, ids,
624 Eigen::VectorXd x;
625 Index iterations;
626 double residual;
627 bool converged;
628 Index indefinite_at_iteration = -1;
629 double indefinite_curvature = 0.0;
630 double lanczos_min_eigenvalue = std::numeric_limits<double>::quiet_NaN();
631
632 // use_jor_ is an outer, first choice: JOR and PCG are two entirely
633 // different algorithms (see SolveWithJOR's own documentation for
634 // why JOR is the one actually chosen once the operator was directly
635 // confirmed indefinite), not two variants of the same solve -- JOR
636 // has no preconditioner-type branching of its own
637 // (EwaldSitePolarizabilityBlocks is the one and only D^-1 it uses, matching
638 // legacy exactly), so it never enters the PCG-specific branches below at
639 // all.
640 if (use_jor_) {
641 EwaldSitePolarizabilityBlocks site_p(registry, ids);
642 auto result = SolveWithJOR(op, site_p, b, max_iter_, jor_omega_, log,
644 x = result.x;
645 iterations = result.iterations;
646 residual = result.residual;
647 converged = result.converged;
648
649 XTP_LOG(Log::info, log)
650 << TimeStamp() << " JOR finished after " << iterations
651 << " iterations, max_dU=" << result.max_dU
652 << " avg_dU=" << result.avg_dU << " (residual=" << residual
653 << ", informational) (" << elapsed_s(t_pcg) << "s)" << std::flush;
654 if (!converged) {
655 throw std::runtime_error(
656 "EwaldBackground: JOR did not converge within max_iter");
657 }
658 } else {
659
660 // The preconditioner type is a compile-time choice here too (see
661 // SolveWithIndefinitenessCheck's own documentation for why that
662 // function is templated on it) -- these two branches genuinely
663 // instantiate two different specializations, they cannot share one
664 // call. EwaldBlockJacobiPreconditioner builds itself fully in its
665 // own constructor (see its own class documentation); Eigen's own
666 // DiagonalPreconditioner does not -- it needs an explicit compute(op)
667 // call first, unlike the constructor-based pattern
668 // EwaldBlockJacobiPreconditioner itself uses. Getting this backwards
669 // (assuming both work the same way) would be a real, silent
670 // correctness bug -- solve() on an uncompute()'d DiagonalPreconditioner
671 // does not throw, it just returns nonsense -- so this is deliberately
672 // NOT written as a single shared code path that "just" swaps the
673 // preconditioner type.
675 EwaldBlockJacobiPreconditioner precond(registry, ids, alpha_, thole_a_);
676 auto result = SolveWithIndefinitenessCheck(op, precond, b, max_iter_,
677 pcg_tolerance_, log, t_pcg);
678 x = result.x;
679 iterations = result.iterations;
680 residual = result.residual;
681 converged = result.converged;
682 indefinite_at_iteration = result.indefinite_at_iteration;
683 indefinite_curvature = result.indefinite_curvature;
684 lanczos_min_eigenvalue = result.lanczos_min_eigenvalue;
685 } else {
686 Eigen::DiagonalPreconditioner<double> precond;
687 precond.compute(op);
688 auto result = SolveWithIndefinitenessCheck(op, precond, b, max_iter_,
689 pcg_tolerance_, log, t_pcg);
690 x = result.x;
691 iterations = result.iterations;
692 residual = result.residual;
693 converged = result.converged;
694 indefinite_at_iteration = result.indefinite_at_iteration;
695 indefinite_curvature = result.indefinite_curvature;
696 lanczos_min_eigenvalue = result.lanczos_min_eigenvalue;
697 }
698
699 XTP_LOG(Log::info, log)
700 << TimeStamp() << " PCG finished after " << iterations
701 << " iterations, residual " << residual << " (" << elapsed_s(t_pcg)
702 << "s)" << std::flush;
703 {
704 // Phase breakdown of the matvec -- see
705 // EwaldPeriodicDipoleOperator::RawMultiplyTimings' own
706 // declaration. n_calls exceeds the reported iteration count by
707 // one, since baseline_ = RawMultiply(0) is built in the operator's
708 // own constructor. The first call also builds the real-space
709 // neighbour cache, so it is much more expensive than the rest and
710 // inflates the real_space per-call average; read the per-call
711 // figures as an upper bound on steady-state cost.
712 const auto& tm = op.Timings();
713 const double n = double(std::max<Index>(tm.n_calls, 1));
714 XTP_LOG(Log::info, log)
715 << TimeStamp() << " RawMultiply phase breakdown over " << tm.n_calls
716 << " calls (total " << tm.total() << "s):" << std::flush;
717 auto line = [&](const char* nm, double t) {
718 XTP_LOG(Log::info, log)
719 << TimeStamp()
720 << (boost::format(" %1$-12s %2$8.3fs total "
721 "%3$7.3fs/call %4$5.1f%%") %
722 nm % t % (t / n) %
723 (tm.total() > 0 ? 100.0 * t / tm.total() : 0.0))
724 .str()
725 << std::flush;
726 };
727 line("setup", tm.setup);
728 line("real_space", tm.real_space);
729 line("reciprocal", tm.reciprocal);
730 line("shape", tm.shape);
731 line("assemble", tm.assemble);
732 line("intra", tm.intra);
733
734 // Neighbour-list size -- the real cost driver behind the
735 // real_space line above. See EwaldRealSpaceSum::NeighborStats.
736 const auto ns = real_sum.GetNeighborStats();
737 XTP_LOG(Log::info, log)
738 << TimeStamp()
739 << (boost::format(
740 " neighbours: %1$.1f entries/target over %2$d targets "
741 "(%3$d kept, %4$d culled beyond %5$.1f bohr = %6$.1f%%)") %
742 ns.entries_per_target() % ns.targets % ns.entries % ns.culled %
743 real_sum.RealSpaceCutoff() % (100.0 * ns.culled_fraction()))
744 .str()
745 << std::flush;
746 }
747 if (!std::isnan(lanczos_min_eigenvalue)) {
748 XTP_LOG(Log::info, log) << TimeStamp() << " Lanczos min eigenvalue: "
749 << lanczos_min_eigenvalue << std::flush;
750 if (lanczos_min_eigenvalue < 0.0) {
751 XTP_LOG(Log::info, log)
752 << TimeStamp()
753 << " NEGATIVE -- strong evidence the operator is not "
754 "positive-definite (see "
755 "PcgIndefinitenessResult::lanczos_min_eigenvalue for what "
756 "this does and does not guarantee)."
757 << std::flush;
758 }
759 }
760
761 if (indefinite_at_iteration >= 0) {
762 throw std::runtime_error(
763 "EwaldBackground: PCG's own operator was found to be NOT "
764 "positive-definite at iteration " +
765 std::to_string(indefinite_at_iteration) +
766 " (p.A.p = " + std::to_string(indefinite_curvature) +
767 " <= 0) -- this is a direct algebraic certificate, not an "
768 "inference from residual behavior.");
769 }
770
771 if (!converged) {
772 throw std::runtime_error(
773 "EwaldBackground: PCG did not converge (max_iter reached, "
774 "operator was never found indefinite along the way)");
775 }
776 }
777
778 for (std::size_t n = 0; n < ids.size(); ++n) {
779 PolarSegment& segment = registry.Get(ids[n], EwaldChargeState::Neutral);
780 Index base = offsets[n];
781 for (Index s = 0; s < segment.size(); ++s) {
782 segment[s].setInduced_Dipole(x.segment<3>(base + 3 * s));
783 }
784 }
785 } else {
786 // Matches legacy's own polarmethod.induce=0 behaviour: every site's
787 // induced dipole stays at zero (already set before the
788 // permanent-field computation above) -- see induce_'s own class
789 // documentation for why this is a separate code path rather than a
790 // zero-polarizability run through the same PCG solve.
791 XTP_LOG(Log::info, log)
792 << TimeStamp()
793 << " polarmethod.induce is false: skipping the PCG solve, "
794 "induced dipoles left at zero"
795 << std::flush;
796 }
797
798 auto t_cpt = std::chrono::steady_clock::now();
799 // Write the permanent field (b) back into every site's V() before the
800 // checkpoint write, unconditionally regardless of induce_ -- neither
801 // code path above leaves V() holding it otherwise: the PCG path
802 // overwrites V() repeatedly during its own iterations (each
803 // EwaldPeriodicDipoleOperator::multiply() call internally
804 // Reset()s and rewrites every site's V() via RawMultiply), leaving
805 // whatever the *last* iteration's trial field happened to be, not the
806 // permanent field; the induce_=false path leaves V() at the zero
807 // Reset() left it just after b was built above. Restoring b here is
808 // what makes the permanent field actually recoverable from the
809 // checkpoint at all -- without this, hdf5_dump has no way to report
810 // anything but 0.0 for it (see that tool's own former limitation note,
811 // now resolved by this).
812 {
813 for (std::size_t n = 0; n < ids.size(); ++n) {
814 PolarSegment& segment = registry.Get(ids[n], EwaldChargeState::Neutral);
815 Index base = offsets[n];
816 for (Index s = 0; s < segment.size(); ++s) {
817 segment[s].V() = b.segment<3>(base + 3 * s);
818 }
819 }
820 }
821
823 CheckpointWriter w = cpf.getWriter();
824 registry.WriteToCpt(w);
825
826 // The convergence parameters travel with the converged state. A job
827 // that later embeds a foreground in this background must use the same
828 // alpha, k_max and shape -- alpha in particular decides how the
829 // interaction is split between the real- and reciprocal-space sums, so
830 // a different value is not the same physics, and nothing about the
831 // mismatch would be visible at run time. See EwaldParameters.
832 {
833 EwaldParameters params;
834 params.alpha = alpha_;
835 params.k_max = k_max_;
836 params.r_min = r_min_;
837 params.field_tol = field_tol_;
838 params.thole_a = thole_a_;
840 params.shape = shape_;
841 params.box = box;
842 CheckpointWriter wp = w.openChild("ewald_parameters");
843 params.WriteToCpt(wp);
844 }
845
846 XTP_LOG(Log::info, log) << TimeStamp() << " Checkpoint written to "
847 << checkpoint_file_ << " (" << elapsed_s(t_cpt)
848 << "s)" << std::flush;
849 XTP_LOG(Log::info, log) << TimeStamp()
850 << " Ewald background calculation done, total "
851 << elapsed_s(t_start) << "s" << std::flush;
852
853 return true;
854}
855
856} // namespace xtp
857} // namespace votca
858
859#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 AddFieldAtMany(const std::vector< std::pair< Index, PolarSite * > > &targets, 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
const Eigen::Vector3d & V() const
Definition polarsite.h:68
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