votca 2026-dev
Loading...
Searching...
No Matches
ewaldregion.cc
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// Standard includes
21#include <chrono>
22#include <iomanip>
23#include <sstream>
24#include <stdexcept>
25
26// VOTCA includes
28
29// Local VOTCA includes
31#include "votca/xtp/citations.h"
35#include "votca/xtp/qmregion.h"
37
38namespace votca {
39namespace xtp {
40
41namespace {
42// Use before Initialize() is a caller error, not a missing feature: the
43// background has to be loaded before anything can be asked of it.
44[[noreturn]] void NotInitialized(const std::string& what) {
45 throw std::runtime_error(
46 "EwaldRegion::" + what +
47 " was called before Initialize(). The background must be loaded "
48 "from its checkpoint first.");
49}
50} // namespace
51
54 "checkpoint", "ewaldbackground.hdf5");
55
57 CheckpointReader r = cpf.getReader();
58 registry_.ReadFromCpt(r);
59 CheckpointReader rp = r.openChild("ewald_parameters");
60 params_.ReadFromCpt(rp);
61 loaded_ = true;
62
63 XTP_LOG(Log::info, log_) << TimeStamp() << " Ewald background read from "
64 << checkpoint_file_ << ": " << size()
65 << " segments, alpha=" << params_.alpha
66 << " bohr^-1, k_max=" << params_.k_max
67 << " bohr^-1, box volume="
68 << params_.box.determinant() << " bohr^3"
69 << std::flush;
70}
71
72void EwaldRegion::Evaluate(std::vector<std::unique_ptr<Region>>& regions) {
73 citations::Cite("Poelking:2016");
74 // Nothing polarizes this region, so all this does is record that the
75 // other regions had no effect on it -- the energies are all zero by
76 // construction (see the Interactwith* overrides). It is kept rather
77 // than skipped so the log still shows the region participating.
79}
80
82 // The whole periodic cell, not a share of the job's segments -- see
83 // this class's own documentation, point 3.
84 return Index(registry_.AllIds().size());
85}
86
87double EwaldRegion::charge() const {
88 if (!loaded_) {
89 NotInitialized("charge");
90 }
91 double q = 0.0;
92 for (Index id : registry_.AllIds()) {
94 continue;
95 }
96 for (const PolarSite& site : registry_.Get(id, EwaldChargeState::Neutral)) {
97 q += site.getCharge();
98 }
99 }
100 return q;
101}
102
103double EwaldRegion::Etotal() const {
104 // The background's energy contribution is not implemented, only its
105 // field. Return zero instead of throwing, which would abort the job after
106 // the induction has converged and lose the dipoles. ApplyFieldTo warns on
107 // first use, and AppendResult records the omission in the job output.
108 return 0.0;
109}
110
112 // Only the path is written, not the background itself. The background
113 // is large (thousands of segments) and already lives in its own
114 // checkpoint; copying it into every per-iteration job checkpoint would
115 // multiply that cost for data that cannot change -- this region is
116 // frozen by construction.
117 w(checkpoint_file_, "checkpoint_file");
118}
119
121 // Reloads from the background checkpoint named at write time -- see
122 // WriteToCpt for why the background is not stored inline. If that file
123 // has moved since, this fails loudly here rather than silently
124 // continuing with an empty background.
125 r(checkpoint_file_, "checkpoint_file");
126
128 CheckpointReader br = cpf.getReader();
129 registry_.ReadFromCpt(br);
130 CheckpointReader bp = br.openChild("ewald_parameters");
131 params_.ReadFromCpt(bp);
132 loaded_ = true;
133
134 // The lazily-built sums belong to a particular foreground geometry;
135 // after a reload there is no guarantee it is the same one, so they are
136 // dropped and rebuilt on next use.
137 real_sum_.reset();
138 recip_sum_.reset();
139 shape_.reset();
140 interactor_.reset();
141 foreground_copies_.clear();
142 built_foreground_.clear();
143}
144
146 // Deliberately a no-op rather than a throw: the background has no
147 // job-local geometry worth writing, and a PDB dump should not be able
148 // to abort a run.
149}
150
152 prop.add("E_ewald_background", "not_implemented");
153 prop.add("segments", std::to_string(size()));
154 prop.add("checkpoint", checkpoint_file_);
155}
156
157namespace {
158Eigen::Vector3d Centroid(const PolarSegment& seg) {
159 // Unweighted, matching EwaldRealSpaceSum's own convention (and
160 // legacy's PolarSeg::CalcPos). A mass-weighted centre would place the
161 // foreground copies fractionally off the positions the real-space sum
162 // suppresses, and the erf correction would then remove a copy that was
163 // never dropped.
164 Eigen::Vector3d pos = Eigen::Vector3d::Zero();
165 Index n = 0;
166 for (const PolarSite& site : seg) {
167 pos += site.getPos();
168 ++n;
169 }
170 return (n > 0) ? Eigen::Vector3d(pos / double(n)) : pos;
171}
172} // namespace
173
175 const std::vector<std::pair<Index, Eigen::Vector3d>>& foreground) {
176 // The union is disjoint by construction -- PartitionRegions marks each
177 // segment as it assigns it -- but that guarantee lives far from the
178 // code relying on it, and a repeated id would suppress the same
179 // background copy twice.
180 for (std::size_t i = 0; i < foreground.size(); ++i) {
181 for (std::size_t j = i + 1; j < foreground.size(); ++j) {
182 if (foreground[i].first == foreground[j].first) {
183 std::stringstream message;
184 message << "EwaldRegion::RegisterForeground: segment "
185 << foreground[i].first
186 << " was declared twice. The foreground is the disjoint "
187 "union of the regions that own segments.";
188 throw std::runtime_error(message.str());
189 }
190 }
191 }
192 registered_foreground_ = foreground;
193 // Anything built before the declaration was built from the wrong
194 // foreground.
195 real_sum_.reset();
196 recip_sum_.reset();
197 shape_.reset();
198 interactor_.reset();
199 foreground_copies_.clear();
200 built_foreground_.clear();
201}
202
204 const std::vector<PolarSegment>& foreground) const {
205 // The same 1e-4 bohr EwaldRealSpaceSum uses to recognise a copy
206 // (kForegroundMatchTol, private there, so restated rather than
207 // shared): it absorbs round-off, nothing larger. A foreground's
208 // positions do not move between calls within a job, so any difference
209 // above this is a different segment, not drift.
210 constexpr double kTol = 1e-4;
211 for (const PolarSegment& seg : foreground) {
212 const Eigen::Vector3d centroid = Centroid(seg);
213 bool found = false;
214 for (const auto& entry : built_foreground_) {
215 if (entry.first == seg.getId() &&
216 (centroid - entry.second).norm() <= kTol) {
217 found = true;
218 break;
219 }
220 }
221 if (!found) {
222 std::stringstream message;
223 message << "EwaldRegion: asked about segment " << seg.getId()
224 << ", which is not part of the foreground these sums were "
225 "built for. The suppression list and the real-space "
226 "neighbour cache belong to that foreground, so answering "
227 "would drop the wrong background copies and subtract erf "
228 "corrections for copies that were never dropped -- a wrong "
229 "energy with no symptom. JobTopology declares the whole "
230 "foreground with RegisterForeground before any region is "
231 "evaluated; if this fires, that declaration is missing or "
232 "incomplete.";
233 throw std::runtime_error(message.str());
234 }
235 }
236}
237
238void EwaldRegion::BuildSums(const std::vector<PolarSegment>& fallback) const {
239 std::vector<std::pair<Index, Eigen::Vector3d>> source =
241 if (source.empty()) {
242 for (const PolarSegment& seg : fallback) {
243 source.push_back({seg.getId(), Centroid(seg)});
244 }
245 }
246
247 foreground_copies_.clear();
248 built_foreground_ = source;
249 for (const auto& entry : source) {
250 const Index seg_id = entry.first;
251 if (!registry_.Has(seg_id, EwaldChargeState::Neutral)) {
252 throw std::runtime_error(
253 "EwaldRegion: the foreground contains segment " +
254 std::to_string(seg_id) +
255 ", which is absent from the periodic background. The foreground "
256 "must be carved out of the same system the background was "
257 "converged on.");
258 }
259 // Record the position of the NEUTRAL BACKGROUND COPY this foreground
260 // segment displaces -- not the foreground's own centroid.
261 //
262 // They are not the same point. A charged job maps its central
263 // segment with that charge state's own geometry, so the foreground
264 // is a relaxed cation (or anion) sitting where a neutral molecule
265 // used to be. Its centroid is therefore displaced from the
266 // background copy's by far more than the 1e-4 bohr tolerance
267 // EwaldRealSpaceSum uses to recognise a copy -- that tolerance is
268 // there to absorb round-off, not geometry relaxation.
269 //
270 // Recording the foreground's own centroid therefore meant the
271 // real-space sum found NO match and silently left the neutral copy
272 // in the background, while the reciprocal and shape exclusions
273 // (which match by address) removed it correctly. The copy was
274 // counted in the erfc half and not the erf half: a mismatch that
275 // only appears for a charged foreground, and that breaks
276 // alpha-independence because erfc and erf shift weight with alpha.
277 //
278 // Snapping to the nearest lattice image rather than requiring an
279 // exact hit also makes this robust to any future geometry that
280 // differs between charge states.
281 const PolarSegment& bg_copy =
283 const Eigen::Vector3d bg_centroid = Centroid(bg_copy);
284 const Eigen::Vector3d delta = entry.second - bg_centroid;
285 const Eigen::Vector3d fractional = params_.box.inverse() * delta;
286 const Eigen::Vector3d image =
287 params_.box * fractional.array().round().matrix();
288 const Eigen::Vector3d residual = delta - image;
289
290 // A residual much larger than a molecule means the foreground
291 // segment is not where the background thinks that id lives, which
292 // is a mapping error rather than a relaxation. Loud, because the old
293 // behaviour for exactly this case was to carry on silently.
294 constexpr double kResidualWarn = 5.0; // bohr
295 if (residual.norm() > kResidualWarn) {
297 << TimeStamp() << " WARNING: foreground segment " << seg_id
298 << " sits " << residual.norm()
299 << " bohr from the nearest periodic image of its background "
300 "copy. That is too far to be geometry relaxation, so the "
301 "copy this code is about to suppress may not be the one the "
302 "foreground actually displaces."
303 << std::flush;
304 }
305
306 foreground_copies_.push_back({seg_id, bg_centroid + image});
307 }
308
309 real_sum_ = std::make_unique<EwaldRealSpaceSum>(
310 params_.box, registry_, params_.alpha, params_.thole_a, params_.r_min,
311 params_.field_tol, 0.945, 15, params_.screening_factor,
313 recip_sum_ = std::make_unique<EwaldReciprocalSpaceSum>(
314 params_.box, registry_, params_.alpha, params_.k_max);
315 shape_ = std::make_unique<EwaldShapeCorrection>(params_.box.determinant(),
316 registry_, params_.shape);
317 interactor_ = std::make_unique<EwaldRealSpaceInteractor>(params_.alpha,
318 params_.thole_a);
319}
320
322 const std::vector<Eigen::Vector3d>& points) const {
323 if (!loaded_) {
324 NotInitialized("PotentialAt");
325 }
326 if (registered_foreground_.empty()) {
327 throw std::runtime_error(
328 "EwaldRegion::PotentialAt: no foreground has been declared. "
329 "ApplyFieldTo can fall back on the segments it is handed, but a "
330 "list of points carries no segment identity, so the copies to "
331 "suppress cannot be inferred. JobTopology declares the foreground "
332 "with RegisterForeground before any region is evaluated.");
333 }
334 if (!real_sum_) {
335 BuildSums(std::vector<PolarSegment>());
336 }
337 return PotentialAtPoints(points);
338}
339
341 std::vector<PolarSegment>& foreground) const {
342 if (!loaded_) {
343 NotInitialized("ApplyPotentialTo");
344 }
345 if (!real_sum_) {
346 BuildSums(foreground);
347 }
348 CheckForegroundIsSubset(foreground);
349 std::vector<PolarSite*> sites;
350 std::vector<Eigen::Vector3d> points;
351 for (PolarSegment& seg : foreground) {
352 for (PolarSite& site : seg) {
353 if (site.isChargeResponse()) {
354 sites.push_back(&site);
355 points.push_back(site.getPos());
356 }
357 }
358 }
359 if (points.empty()) {
360 return;
361 }
362 const Eigen::VectorXd phi = PotentialAtPoints(points);
363 for (std::size_t i = 0; i < sites.size(); ++i) {
364 sites[i]->Phi() += phi[Index(i)];
365 }
366}
367
369 const std::vector<Eigen::Vector3d>& points) const {
370 // A foreground segment's id. It decides only whether the
371 // zero-translation self-pair is skipped, and that skip is disabled for
372 // sources that have a foreground copy -- which is what a point that is
373 // not a site of its own wants.
374 const Index probe_segment_id = built_foreground_.front().first;
375
376 using Clock = std::chrono::steady_clock;
377 auto seconds_since = [](Clock::time_point t) {
378 return std::chrono::duration<double>(Clock::now() - t).count();
379 };
380 auto t_start = Clock::now();
381 Eigen::VectorXd phi = real_sum_->PotentialAtMany(probe_segment_id, points,
383 const double t_real = seconds_since(t_start);
384 auto t_recip_start = Clock::now();
385 phi += recip_sum_->PotentialAtMany(points, EwaldChargeState::Neutral);
386 const double t_recip = seconds_since(t_recip_start);
387 auto t_corr_start = Clock::now();
388
389 const Index n_points = Index(points.size());
390
391 // Shape term: depends on the point only through its position relative to
392 // the background's moments, which are computed once.
393 phi += shape_->PotentialAtMany(points, EwaldChargeState::Neutral);
394
395 // Remove the erf-screened half of the neutral foreground copies that the
396 // reciprocal sum necessarily put back -- the same copies, with the same
397 // shift, as step (4) of ApplyFieldTo. Their sites are flattened once.
398 struct ProbeSource {
399 Eigen::Vector3d pos;
400 double q;
401 Eigen::Vector3d mu;
402 };
403 std::vector<ProbeSource> erf_sources;
404 for (const auto& copy : foreground_copies_) {
405 const PolarSegment& bg =
406 registry_.Get(copy.first, EwaldChargeState::Neutral);
407 const Eigen::Vector3d shift = copy.second - Centroid(bg);
408 for (const PolarSite& source : bg) {
409 erf_sources.push_back(
410 {source.getPos() + shift, source.getCharge(),
411 source.getStaticDipole() + source.getInducedDipole()});
412 }
413 }
414#pragma omp parallel for schedule(static)
415 for (Index p = 0; p < n_points; ++p) {
416 const Eigen::Vector3d& point = points[std::size_t(p)];
417 double extra = 0.0;
418 for (const ProbeSource& source : erf_sources) {
419 extra -=
420 interactor_->ErfPotential(source.q, source.mu, point - source.pos);
421 }
422 phi[p] += extra;
423 }
425 << TimeStamp() << " Ewald potential at " << n_points
426 << " points: real space " << std::setprecision(4) << t_real
427 << " s, reciprocal " << t_recip << " s, shape + erf correction "
428 << seconds_since(t_corr_start) << " s (" << foreground_copies_.size()
429 << " foreground copies)" << std::flush;
430 return phi;
431}
432
433double EwaldRegion::ApplyFieldTo(std::vector<PolarSegment>& foreground) const {
434 if (!loaded_) {
435 NotInitialized("ApplyFieldTo");
436 }
437 if (!real_sum_) {
438 BuildSums(foreground);
439 }
440 // Also on the first call: with a registered foreground BuildSums ignores
441 // its argument, so this is what catches a client asking about a segment
442 // nobody declared.
443 CheckForegroundIsSubset(foreground);
444
445 // Flat target list: the reciprocal sum takes them in one batch, which
446 // is what makes its structure factors worth computing once.
447 std::vector<PolarSite*> targets;
448 std::vector<Index> target_segment_ids;
449 for (PolarSegment& seg : foreground) {
450 for (PolarSite& site : seg) {
451 targets.push_back(&site);
452 target_segment_ids.push_back(seg.getId());
453 }
454 }
455
456 // SIGN CONVENTION. The Ewald code and the region framework store V
457 // with OPPOSITE signs, and this is the boundary between them:
458 //
459 // EwaldBackground builds its solver's rhs as b = +V
460 // PolarRegion builds its solver's rhs as b = -(V + V_noE)
461 //
462 // Both are internally consistent; neither is wrong on its own. But the
463 // sums below write in the Ewald convention, and the polar region is
464 // about to negate whatever it finds -- so handing it the field as-is
465 // drives the induction BACKWARDS. Measured on a neutral MM/MM job,
466 // where the foreground should reproduce the background it was carved
467 // from: deviations of 100-700%, flat with distance rather than growing
468 // outward, which is what first exposed this.
469 //
470 // So this region's own contribution is negated before it is handed
471 // over. Only the contribution is touched: whatever other regions have
472 // already accumulated is preserved, which is why the prior values are
473 // recorded rather than assuming V starts at zero.
474 std::vector<Eigen::Vector3d> v_before;
475 v_before.reserve(targets.size());
476 for (const PolarSite* target : targets) {
477 v_before.push_back(target->V());
478 }
479
480 // (1) real space, foreground copies suppressed. Through AddFieldAtMany,
481 // not an omp loop around AddFieldAt: on the first call no target
482 // has a neighbour list yet, AddFieldAt inserts one into a shared
483 // unordered_map on every miss, and concurrent insertion silently
484 // loses entries. The energy loops below then find no list for
485 // those targets and throw -- which is how this surfaced, on a
486 // 28-thread production QMMM job.
487 const Index n_targets = Index(targets.size());
488 {
489 std::vector<std::pair<Index, PolarSite*>> real_targets;
490 real_targets.reserve(targets.size());
491 for (Index i = 0; i < n_targets; ++i) {
492 real_targets.push_back(
493 {target_segment_ids[std::size_t(i)], targets[std::size_t(i)]});
494 }
495 real_sum_->AddFieldAtMany<Estatic::V>(real_targets,
497 }
498
499 // Permanent-permanent (Q-Q) energy with the background, over exactly
500 // the neighbour set the field above used -- same suppression, same
501 // cull. Only the PERMANENT part: PolarRegion accounts for the induced
502 // contribution itself, as E_polar_ext = sum of mu_ind . V, computed
503 // from the very field being delivered here. Adding it again would
504 // double-count.
505 //
506 // Runs after the field loop, not inside it, because the neighbour
507 // cache must exist first and CalcStaticEnergyAt deliberately refuses
508 // to build one.
509 double e_real = 0.0;
510#pragma omp parallel for schedule(dynamic, 16) reduction(+ : e_real)
511 for (Index i = 0; i < n_targets; ++i) {
512 e_real += real_sum_->CalcStaticEnergyAt(*targets[std::size_t(i)],
514 }
515 double energy = e_real;
516
517 // (1b) [foreground permanent] x [background INDUCED], real space.
518 // The corner of the permanent/induced product that nothing else
519 // covers: PolarRegion's E_polar_ext contracts the FOREGROUND's
520 // induced dipoles against the delivered field, which gives
521 // [fg induced] x [bg anything]; e_real above gives
522 // [fg permanent] x [bg permanent]. Without this term a job's
523 // induced energy comes out short -- measurably so, by a factor
524 // near two on a neutral foreground, against legacy's _pu channel.
525 //
526 // Same neighbour set, same cache, same suppression as e_real.
527 double e_real_pu = 0.0;
528#pragma omp parallel for schedule(dynamic, 16) reduction(+ : e_real_pu)
529 for (Index i = 0; i < n_targets; ++i) {
530 e_real_pu += real_sum_->CalcInducedSourceEnergyAt(
531 *targets[std::size_t(i)], EwaldChargeState::Neutral);
532 }
533 energy += e_real_pu;
534
535 // (2) reciprocal space, over the full periodic density
536 recip_sum_->AddFieldAtMany<Estatic::V>(targets, EwaldChargeState::Neutral);
537
538 // (3) shape/surface. Target-independent, so evaluated once.
539 {
540 PolarSite probe(-1, "X", Eigen::Vector3d::Zero());
541 shape_->AddFieldAt<Estatic::V>(probe, EwaldChargeState::Neutral);
542 const Eigen::Vector3d shape_field = probe.V();
543 for (PolarSite* target : targets) {
544 target->V() += shape_field;
545 }
546 }
547
548 // (4) remove the erf-screened field of the neutral foreground copies
549 // that step (2) necessarily put back. See ApplyFieldTo's own
550 // declaration for why these use the background's own multipoles
551 // and dipoles rather than the job's charge state.
552#pragma omp parallel for schedule(dynamic, 16)
553 for (Index i = 0; i < n_targets; ++i) {
554 PolarSite& target = *targets[std::size_t(i)];
555 for (const auto& copy : foreground_copies_) {
556 const PolarSegment& bg =
557 registry_.Get(copy.first, EwaldChargeState::Neutral);
558 const Eigen::Vector3d shift = copy.second - Centroid(bg);
559 for (const PolarSite& source : bg) {
560 interactor_->ApplyErfStaticFieldCorrection<PolarSite, Estatic::V>(
561 source, target, shift);
562 interactor_->ApplyErfInducedFieldCorrection<Estatic::V>(source, target,
563 shift);
564 }
565 }
566 }
567 // (5) reciprocal-space and shape Q-Q energy between the foreground and
568 // the rest of the cell.
569 //
570 // NOTHING is held out of S_bg. Every segment contributes at every
571 // image; what is removed instead is the erf-screened energy of the
572 // COINCIDENT copy of each foreground segment, because that is
573 // exactly what real space suppresses.
574 //
575 // Two earlier versions were wrong in opposite directions and the
576 // answer sits between them. Holding the whole foreground out of
577 // S_bg also deletes its periodic IMAGES -- a lattice of vacancies
578 // rather than one carved-out cavity, worth 1e-3 eV of
579 // alpha-dependence on a charged 18-segment job. Holding out only
580 // the target's own segment fixed that but still deleted each
581 // segment's interaction with its own images, and was needed only
582 // because EwaldRealSpaceSum was skipping that segment at every
583 // translation. With that skip gone, real space suppresses the same
584 // set for every target, so one structure factor is correct.
585 //
586 // recip + shape together are the erf interaction over all images
587 // (the shape term IS the k=0 limit the reciprocal sum omits), so
588 // subtracting a pair's erf energy removes that pair exactly. For a
589 // segment's own coincident copy that subtraction is the r -> 0
590 // branch of CalcErfStaticEnergy -- whose sign had to be fixed
591 // before this change was possible.
592 {
593 double e_recip = 0.0;
594 double e_shape = 0.0;
595 double e_erf = 0.0;
596
597 std::vector<std::pair<const PolarSite*, Eigen::Vector3d>> fg_sites;
598 fg_sites.reserve(targets.size());
599 for (std::size_t n = 0; n < targets.size(); ++n) {
600 fg_sites.push_back({targets[n], targets[n]->getPos()});
601 }
602 const std::vector<const PolarSite*> no_exclusions;
603
604 e_recip = recip_sum_->CalcStaticEnergyBetween(fg_sites, no_exclusions,
606 e_shape = shape_->CalcStaticEnergyBetween(fg_sites, no_exclusions,
608
609 // Every foreground copy, the target's own included. Real space
610 // suppressed exactly these, so exactly these come back out.
611 for (const auto& copy : foreground_copies_) {
612 const PolarSegment& bg_copy =
613 registry_.Get(copy.first, EwaldChargeState::Neutral);
614 const Eigen::Vector3d shift = copy.second - Centroid(bg_copy);
615 for (const PolarSite& source : bg_copy) {
616 for (const auto& entry : fg_sites) {
617 e_erf += interactor_->CalcErfStaticEnergy<PolarSite, PolarSite>(
618 source, *entry.first, shift);
619 }
620 }
621 }
622 energy += e_recip + e_shape - e_erf;
623
624 // (5b), (6b) reciprocal and shape partners of the induced-source
625 // term added at (1b), with exactly the same exclusion structure
626 // as (5) above and for exactly the same reason.
627 double e_recip_pu = recip_sum_->CalcInducedSourceEnergyBetween(
628 fg_sites, no_exclusions, EwaldChargeState::Neutral);
629 double e_shape_pu = shape_->CalcInducedSourceEnergyBetween(
630 fg_sites, no_exclusions, EwaldChargeState::Neutral);
631 double e_erf_pu = 0.0;
632 for (const auto& copy : foreground_copies_) {
633 const PolarSegment& bg_copy =
634 registry_.Get(copy.first, EwaldChargeState::Neutral);
635 const Eigen::Vector3d shift = copy.second - Centroid(bg_copy);
636 for (const PolarSite& source : bg_copy) {
637 for (const auto& entry : fg_sites) {
638 e_erf_pu += interactor_->CalcErfInducedSourceEnergy(
639 source, *entry.first, shift);
640 }
641 }
642 }
643 energy += e_recip_pu + e_shape_pu - e_erf_pu;
644
645 // Term-by-term report, for comparison against the legacy `ewald`
646 // job calculator's terms_o block. The correspondence is now
647 // one-to-one -- real <-> R_pp, recip <-> K_pp, shape <-> J_pp,
648 // erf <-> C_pp -- since this code stopped excluding foreground
649 // copies from S_bg and started subtracting their erf energy the way
650 // legacy does. Measured agreement on an 18-segment job: every term
651 // to legacy's six printed figures, on both a rank-0 and an
652 // artificially dipolar methane. Printed in eV, the unit legacy
653 // reports and the job XML carries.
654 const double h2ev = tools::conv::hrt2ev;
656 << TimeStamp()
657 << " Ewald energy [eV], permanent x permanent: real = " << e_real * h2ev
658 << " recip = " << e_recip * h2ev << " shape = " << e_shape * h2ev
659 << " erf = " << e_erf * h2ev << std::flush;
661 << TimeStamp()
662 << " Ewald energy [eV], fg permanent x bg induced: real = "
663 << e_real_pu * h2ev << " recip = " << e_recip_pu * h2ev
664 << " shape = " << e_shape_pu * h2ev << " erf = " << e_erf_pu * h2ev
665 << std::flush;
667 << TimeStamp() << " Ewald energy [eV], total = " << energy * h2ev
668 << std::flush;
670 << TimeStamp() << " Ewald split: alpha = " << params_.alpha
671 << " 1/bohr (" << params_.alpha * 18.8972612 << " 1/nm)"
672 << ", k_max = " << params_.k_max << " 1/bohr ("
673 << params_.k_max * 18.8972612 << " 1/nm)"
674 << ", r_min = " << params_.r_min
675 << " bohr, V = " << params_.box.determinant()
676 << " bohr^3, thole_a = " << params_.thole_a << std::flush;
678 << TimeStamp() << " Foreground: " << foreground_copies_.size()
679 << " segments, " << targets.size() << " sites, carved from "
680 << registry_.AllIds().size() << " registered" << std::flush;
681
682 // Suppression audit. A shortfall means a foreground copy was left in
683 // the background -- silent otherwise, and exactly the failure this
684 // check exists for. Every copy is suppressed for every target, the
685 // target's own segment included, hence size() and not size() - 1.
687 real_sum_->GetNeighborStats();
688 const Index expected =
689 Index(targets.size()) * Index(foreground_copies_.size());
691 << TimeStamp() << " Foreground copies suppressed: " << stats.foreground
692 << " of " << expected
693 << ((stats.foreground == expected) ? " (ok)" : " <-- MISMATCH")
694 << std::flush;
695 }
696
697 // Convert this region's contribution into the convention the polar
698 // region expects -- see the note where v_before is captured.
699 for (std::size_t i = 0; i < targets.size(); ++i) {
700 const Eigen::Vector3d ewald_contribution = targets[i]->V() - v_before[i];
701 targets[i]->V() = v_before[i] - ewald_contribution;
702 }
703
704 if (!warned_no_energy_) {
705 warned_no_energy_ = true;
707 << TimeStamp()
708 << " NOTE: the background's own internal energy is deliberately "
709 "absent -- it is a constant that cancels in any charge-state "
710 "difference. Everything else is here; see ApplyFieldTo's "
711 "declaration for the full accounting."
712 << std::flush;
713 }
714 return energy;
715}
716
717} // namespace xtp
718} // namespace votca
class to manage program options with xml serialization functionality
Definition property.h:55
Property & add(const std::string &key, const std::string &value)
add a new property to structure
Definition property.cc:108
T ifExistsReturnElseReturnDefault(const std::string &key, T defaultvalue) const
Definition property.h:332
CheckpointReader getReader()
CheckpointReader openChild(const std::string &childName) const
EwaldParameters params_
double Etotal() const override
void Initialize(const tools::Property &prop) override
void CheckForegroundIsSubset(const std::vector< PolarSegment > &foreground) const
Eigen::VectorXd PotentialAt(const std::vector< Eigen::Vector3d > &points) const
void Evaluate(std::vector< std::unique_ptr< Region > > &regions) override
std::vector< std::pair< Index, Eigen::Vector3d > > built_foreground_
void WritePDB(csg::PDBWriter &writer) const override
EwaldRegistry registry_
std::unique_ptr< EwaldShapeCorrection > shape_
double charge() const override
Eigen::VectorXd PotentialAtPoints(const std::vector< Eigen::Vector3d > &points) const
std::unique_ptr< EwaldReciprocalSpaceSum > recip_sum_
std::unique_ptr< EwaldRealSpaceSum > real_sum_
std::vector< std::pair< Index, Eigen::Vector3d > > registered_foreground_
void RegisterForeground(const std::vector< std::pair< Index, Eigen::Vector3d > > &foreground)
std::unique_ptr< EwaldRealSpaceInteractor > interactor_
std::string checkpoint_file_
Index size() const override
void AppendResult(tools::Property &prop) const override
void ApplyPotentialTo(std::vector< PolarSegment > &foreground) const
double ApplyFieldTo(std::vector< PolarSegment > &foreground) const
std::vector< std::pair< Index, Eigen::Vector3d > > foreground_copies_
void BuildSums(const std::vector< PolarSegment > &fallback) const
void ReadFromCpt(CheckpointReader &r) override
void WriteToCpt(CheckpointWriter &w) const override
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
const Eigen::Vector3d & V() const
Definition polarsite.h:68
std::vector< double > ApplyInfluenceOfOtherRegions(std::vector< std::unique_ptr< Region > > &regions)
Definition region.cc:33
Logger & log_
Definition region.h:106
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 hrt2ev
Definition constants.h:53
void Cite(const std::string &key)
marks a reference as used (thread safe; unknown keys are printed as such)
Definition citations.cc:289
Charge transport classes.
Definition ERIs.h:28
ClassicalSegment< PolarSite > PolarSegment
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26