votca 2026-dev
Loading...
Searching...
No Matches
ewaldrealspacesum.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 <algorithm>
22#include <exception>
23#include <fstream>
24#include <iostream>
25#include <sstream>
26#include <stdexcept>
27#include <unordered_map>
28#include <vector>
29
30// Local VOTCA includes
32
33namespace votca {
34namespace xtp {
35
36namespace {
37// BUG FIX (this session): PolarSegment::getPos() (via AtomContainer<T>::
38// calcPos()) is a MASS-weighted center of mass. Legacy's own equivalent
39// (PolarSeg::CalcPos()) is a plain, UNWEIGHTED arithmetic mean of site
40// positions instead -- confirmed by direct comparison of the two
41// implementations. Since this position feeds directly into the
42// minimum-image PBC wrap below (raw_offset = target - source's own
43// representative position), even a small difference between the two
44// definitions can flip which periodic image is chosen as the true
45// minimum for a given pair -- a discrete, not continuous, effect,
46// consistent with the scattered (not uniformly scaled) per-site
47// mismatch pattern that motivated this fix, rather than a smooth
48// site-by-site drift.
49//
50// Deliberately NOT fixed by changing calcPos() itself: that's a shared
51// AtomContainer<T> method used well beyond this file (md2qmengine,
52// segmentmapper), where a genuine physical mass-weighted center of mass
53// may be exactly what's wanted. This local helper instead gives THIS
54// file its own, legacy-matching definition, without touching shared
55// code any other caller depends on.
56Eigen::Vector3d UnweightedCentroid(const PolarSegment& seg) {
57 Eigen::Vector3d pos = Eigen::Vector3d::Zero();
58 Index n = 0;
59 for (const PolarSite& site : seg) {
60 pos += site.getPos();
61 ++n;
62 }
63 if (n > 0) {
64 pos /= double(n);
65 }
66 return pos;
67}
68} // namespace
69
71 const Eigen::Matrix3d& box, const EwaldRegistry& registry, double alpha,
72 double thole_a, double r_min, double field_tol, double shell_width,
73 Index n_max, double screening_factor,
74 const std::vector<std::pair<Index, Eigen::Vector3d>>& foreground)
75 : real_space_cutoff_(screening_factor / alpha),
76 segment_radius_(0.0),
77 box_(box),
78 registry_(registry),
79 interactor_(alpha, thole_a),
80 r_min_(r_min),
81 field_tol_(field_tol),
82 shell_width_(shell_width),
83 n_max_(n_max) {
85
86 for (const auto& entry : foreground) {
87 foreground_[entry.first].push_back(entry.second);
88 }
89
90 // Largest site-to-centroid distance anywhere in the registry -- the
91 // margin the segment-granular distance cull needs (see AddFieldAt).
92 // Computed once here rather than per call: the registry's geometry is
93 // fixed for this object's lifetime, the same assumption the neighbor
94 // cache already relies on.
95 for (Index id : registry_.AllIds()) {
96 for (EwaldChargeState state :
99 if (!registry_.Has(id, state)) {
100 continue;
101 }
102 const PolarSegment& segment = registry_.Get(id, state);
103 const Eigen::Vector3d centroid = UnweightedCentroid(segment);
104 for (const PolarSite& site : segment) {
106 std::max(segment_radius_, (site.getPos() - centroid).norm());
107 }
108 }
109 }
110}
111
112std::vector<EwaldRealSpaceSum::Translation>
114 std::vector<Translation> translations;
115 const Eigen::Vector3d a = box_.col(0);
116 const Eigen::Vector3d b = box_.col(1);
117 const Eigen::Vector3d c = box_.col(2);
118
119 for (Index na = -n_max_; na <= n_max_; ++na) {
120 for (Index nb = -n_max_; nb <= n_max_; ++nb) {
121 for (Index nc = -n_max_; nc <= n_max_; ++nc) {
122 Eigen::Vector3d t = double(na) * a + double(nb) * b + double(nc) * c;
123 translations.push_back({t, t.norm()});
124 }
125 }
126 }
127
128 std::sort(
129 translations.begin(), translations.end(),
130 [](const Translation& x, const Translation& y) { return x.r < y.r; });
131 return translations;
132}
133
134template <enum Estatic CE>
136 PolarSite& target,
137 bool include_static) const {
138 for (const auto& entry : list) {
139 // Stored by pointer rather than by id -- see neighbor_cache_'s own
140 // declaration for why. The Has()/Get() guard the id-based version
141 // needed is gone with it: the cache is keyed on source_state, so
142 // every entry in THIS list was registered at THIS state when the
143 // list was built, and segment addresses are stable for the
144 // registry's lifetime.
145 const PolarSegment& source_segment = *std::get<0>(entry);
146 const Index translation_idx = std::get<1>(entry);
147 const Eigen::Vector3d& baseline_shift = std::get<2>(entry);
148 const Eigen::Vector3d t = baseline_shift + translations_[translation_idx].t;
149 for (const PolarSite& source_site : source_segment) {
150 // Shift passed through rather than applied to a copy of the
151 // source site -- see ApplyStaticField's own source_shift note.
152 if (include_static) {
153 interactor_.ApplyStaticField<PolarSite, CE>(source_site, target, t);
154 }
155 interactor_.ApplyInducedField<CE>(source_site, target, t);
156 }
157 }
158}
159
161 const std::pair<const PolarSite*, EwaldChargeState>& key, NeighborList list,
162 const SearchCounts& counts) const {
163 culled_entries_ += counts.culled;
166 cached_entries_ += Index(list.size());
167 neighbor_cache_.emplace(key, std::move(list));
168}
169
170template <enum Estatic CE>
171void EwaldRealSpaceSum::AddFieldAt(Index target_segment_id, PolarSite& target,
172 EwaldChargeState source_state,
173 bool include_static) const {
174 const std::pair<const PolarSite*, EwaldChargeState> cache_key(&target,
175 source_state);
176 auto cached = neighbor_cache_.find(cache_key);
177 if (cached == neighbor_cache_.end()) {
178 // First call for this target: run the convergence search on a
179 // scratch copy, store the list, then fall through to the same path
180 // every later call takes. Applying the list to target adds exactly
181 // the contributions the search added to the copy, in the same order
182 // and starting from the same V, so the field is bit-identical to
183 // accumulating it during the search.
184 SearchCounts counts;
185 PolarSite scratch = target;
187 target_segment_id, scratch, source_state, include_static, counts);
188 InsertNeighborList(cache_key, std::move(list), counts);
189 cached = neighbor_cache_.find(cache_key);
190 }
191 // Fast path: the real, geometry-determined neighbor set for this
192 // target (see this class's own documentation for why reusing it across
193 // calls for the same target is safe). Just apply every cached (source,
194 // translation) pair directly -- no shell-by-shell search, no scan of
195 // registry_.AllIds() members that turned out not to matter.
196 ApplyNeighborList<CE>(cached->second, target, include_static);
197}
198
199template <enum Estatic CE>
201 Index target_segment_id, PolarSite& target, EwaldChargeState source_state,
202 bool include_static, SearchCounts& counts) const {
203 // The full convergence search. Records every (source, translation)
204 // pair actually visited; the field it accumulates into target (a
205 // scratch copy, see the declaration) is only there for the
206 // shell-convergence test. Nothing in this object is written.
207 NeighborList visited_pairs;
208
209 // See the cull inside the shell loop below for what this is and why.
210 const double cutoff_with_margin = real_space_cutoff_ + 2.0 * segment_radius_;
211
212 Index shell_start = 0;
213 double shell_edge = 0.0;
214 bool converged = false;
215
216 while (shell_start < Index(translations_.size())) {
217 // Advance shell_edge by shell_width_ until it covers at least the next
218 // not-yet-processed translation, then collect every translation up to
219 // that edge into this shell. Since translations_ is sorted by
220 // distance, this always yields a contiguous, radially-ordered shell.
221 shell_edge =
222 translations_[shell_start].r +
223 (shell_edge > translations_[shell_start].r ? 0.0 : shell_width_);
224 Index shell_end = shell_start;
225 while (shell_end < Index(translations_.size()) &&
226 translations_[shell_end].r <= shell_edge) {
227 ++shell_end;
228 }
229
230 // Field is written into target.V()/V_noE() by ApplyStaticField/
231 // ApplyInducedField as a side effect; recover this shell's own
232 // contribution by differencing target's accumulator before and after
233 // processing the whole shell.
234 const Eigen::Vector3d before_shell =
235 (CE == Estatic::noE_V) ? target.V_noE() : target.V();
236
237 for (Index source_id : registry_.AllIds()) {
238 // No per-segment skip: a target's own segment is treated like any
239 // other. Its COINCIDENT copy is dropped by the foreground
240 // suppression below; its other lattice images are ordinary
241 // background molecules and stay. Legacy agrees -- SetupMidground
242 // excludes by (segment id, na, nb, nc), not by segment id.
243 if (!registry_.Has(source_id, source_state)) {
244 continue;
245 }
246 const PolarSegment& source_segment =
247 registry_.Get(source_id, source_state);
248
249 // Per-source baseline wrap (computed once per source_id, not per
250 // shell-translation index, since it depends only on the target
251 // and source_segment's own representative positions, not on
252 // which small shell-translation t is currently being applied).
253 //
254 // BUG FIX (this session, found via a real production comparison
255 // against legacy): translations_ is sorted and shell-converged
256 // by |t| alone (the raw lattice-translation magnitude), but the
257 // quantity that actually determines a pair's true separation is
258 // |raw_offset + t|, not |t| by itself. A translation with a
259 // LARGE |t| can still be the one that gives the SMALLEST actual
260 // distance, if it happens to cancel a large raw target-source
261 // offset -- and such a translation is exactly what the
262 // shell-convergence criterion (stop once shell radius >= r_min_)
263 // can miss entirely, since it never even considers translations
264 // whose OWN magnitude exceeds r_min_, regardless of what they'd
265 // produce once combined with the raw offset. Confirmed on a real
266 // pair: legacy's own minimum-image search (round-based,
267 // independent of any shell-magnitude cutoff) found r=13.6 bohr
268 // via t=(0,-1,-1)*box (|t|~97 bohr, comfortably past r_min_ in
269 // this run's own ~75.6 bohr setting) for a pair this class's own
270 // unfixed search reported no closer than r=55.4 bohr on, because
271 // it never got there.
272 //
273 // Fixed by mirroring legacy's own two-step structure exactly:
274 // wrap the raw (large, unbounded) target-source separation into
275 // its true minimum image FIRST (fractional-coordinate rounding,
276 // correct for any box_ shape, not just orthorhombic), THEN run
277 // the existing small, bounded shell search of translations_ on
278 // top of that already-small baseline -- exactly as legacy's own
279 // dr12_pbc + L structure does. The shell search only ever needs
280 // to explore the neighborhood right around the true minimum
281 // image, not the raw, unbounded separation, so its own
282 // shell_radius >= r_min_ stopping criterion is now sound (it was
283 // never wrong in isolation -- it was being applied to the wrong
284 // starting point).
285 const Eigen::Vector3d source_centroid =
286 UnweightedCentroid(source_segment);
287 const Eigen::Vector3d raw_offset = target.getPos() - source_centroid;
288 const Eigen::Vector3d frac = box_.inverse() * raw_offset;
289 const Eigen::Vector3d wrapped_frac = frac - frac.array().round().matrix();
290 const Eigen::Vector3d min_image_offset = box_ * wrapped_frac;
291 const Eigen::Vector3d baseline_shift = raw_offset - min_image_offset;
292
293 for (Index idx = shell_start; idx < shell_end; ++idx) {
294 // Distance cull. The erfc(alpha*r) screening means a pair's
295 // contribution falls off as fast as erfc does: at alpha*r = 6 a
296 // single pair contributes ~1e-20 of the total field, and the
297 // whole remaining tail sums to far below field_tol_. Without
298 // this test every registered segment is evaluated at every
299 // accepted translation, however far away it is -- measured on a
300 // real 1000-segment system as 14985 (segment, translation)
301 // entries per target, of which only ~22% lie inside the cutoff.
302 // That factor of ~4.6, not any per-pair cost, was the whole
303 // remaining real-space gap against legacy (whose own
304 // PolarNbs list is distance-built, so it never had the excess).
305 //
306 // r_min_ does NOT already do this: it bounds the shell search by
307 // TRANSLATION magnitude |t|, which is unrelated to how far a
308 // given source segment is once that translation is applied.
309 //
310 // The true separation for this (segment, translation) pair is
311 // |min_image_offset - translations_[idx].t| -- the same quantity
312 // the interactor will form, since it evaluates
313 // target - (source + t) with t = baseline_shift +
314 // translations_[idx].t and baseline_shift = raw_offset -
315 // min_image_offset. Compared at segment granularity, so
316 // segment_radius_ (the largest site-to-centroid distance in the
317 // registry) is added twice as a margin: no individual site pair
318 // can then be closer than the cutoff while its segment pair is
319 // culled.
320 const double pair_distance =
321 (min_image_offset - translations_[idx].t).norm();
322 if (pair_distance > cutoff_with_margin) {
323 ++counts.culled;
324 continue;
325 }
326 const Eigen::Vector3d t = baseline_shift + translations_[idx].t;
327
328 // Foreground suppression. This one periodic copy of this segment
329 // is handled explicitly elsewhere (a polar region), so it must
330 // not also appear in the periodic background -- see the
331 // constructor's own foreground documentation. Only the copy
332 // sitting at the recorded position is dropped; the segment's
333 // other lattice images stay in the sum.
334 if (!foreground_.empty()) {
335 auto fg = foreground_.find(source_id);
336 if (fg != foreground_.end()) {
337 const Eigen::Vector3d shifted_centroid = source_centroid + t;
338 bool suppressed = false;
339 for (const Eigen::Vector3d& fg_pos : fg->second) {
340 if ((shifted_centroid - fg_pos).norm() < kForegroundMatchTol) {
341 suppressed = true;
342 break;
343 }
344 }
345 if (suppressed) {
346 ++counts.foreground;
347 continue;
348 }
349 }
350 }
351
352 // The target's own segment at ZERO translation is the segment
353 // itself: intramolecular pairs plus the r = 0 self-pair, never
354 // part of this sum (AddIntraSegmentCoupling owns the induced
355 // side, the permanent side has its own compensation pass).
356 //
357 // Only where the segment has no recorded foreground copy. Where
358 // it does, the suppression above already decides which copy is
359 // carved out, and that need not be the t = 0 one: a foreground
360 // segment sitting at a nonzero image has its t = 0 copy as a
361 // genuine neighbour. Omitting this guard made the field infinite
362 // in every background solve.
363 const bool source_has_foreground =
364 !foreground_.empty() && foreground_.count(source_id) > 0;
365 if (!source_has_foreground && source_id == target_segment_id &&
366 t.squaredNorm() < kSelfTranslationTol * kSelfTranslationTol) {
367 continue;
368 }
369
370 visited_pairs.emplace_back(&source_segment, idx, baseline_shift);
371 for (const PolarSite& source_site : source_segment) {
372 if (include_static) {
373 interactor_.ApplyStaticField<PolarSite, CE>(source_site, target, t);
374 }
375 interactor_.ApplyInducedField<CE>(source_site, target, t);
376 }
377 }
378 }
379
380 const Eigen::Vector3d after_shell =
381 (CE == Estatic::noE_V) ? target.V_noE() : target.V();
382 const Eigen::Vector3d shell_field = after_shell - before_shell;
383
384 const double shell_radius =
385 translations_[shell_end > shell_start ? shell_end - 1 : shell_start].r;
386 if (shell_radius >= r_min_ && shell_field.norm() < field_tol_) {
387 converged = true;
388 break;
389 }
390
391 shell_start = shell_end;
392 }
393
394 if (!converged) {
395 std::stringstream message;
396 message << "EwaldRealSpaceSum: real-space sum for segment "
397 << target_segment_id
398 << " did not converge within the n_max lattice-translation "
399 "search box; increase n_max or field_tol.";
400 throw std::runtime_error(message.str());
401 }
402
403 return visited_pairs;
404}
405
407 const PolarSite& target, EwaldChargeState source_state) const {
408 // See this method's own declaration for what is and is not included.
409 const std::pair<const PolarSite*, EwaldChargeState> cache_key(&target,
410 source_state);
411 auto cached = neighbor_cache_.find(cache_key);
412 if (cached == neighbor_cache_.end()) {
413 throw std::runtime_error(
414 "EwaldRealSpaceSum::CalcStaticEnergyAt: no neighbour list for this "
415 "target. AddFieldAt or PrepareNeighborCache must run first -- this "
416 "method deliberately does not build one, so that an energy query "
417 "cannot silently become the expensive shell search.");
418 }
419
420 double energy = 0.0;
421 for (const auto& entry : cached->second) {
422 const PolarSegment& source_segment = *std::get<0>(entry);
423 const Index translation_idx = std::get<1>(entry);
424 const Eigen::Vector3d& baseline_shift = std::get<2>(entry);
425 const Eigen::Vector3d t = baseline_shift + translations_[translation_idx].t;
426 for (const PolarSite& source_site : source_segment) {
427 energy += interactor_.CalcStaticEnergy<PolarSite, PolarSite>(source_site,
428 target, t);
429 }
430 }
431 return energy;
432}
433
435 Index target_segment_id, const std::vector<Eigen::Vector3d>& points,
436 EwaldChargeState source_state) const {
437 // Sources flattened once: AllIds() returns by value and the centroids
438 // are fixed, so recomputing either per point would cost more than the
439 // sum itself on a grid.
440 // The sites are flattened too: for a unit probe only position, charge and
441 // total (static + induced) dipole matter, and a compact array of those is
442 // far cheaper to stream per grid point than the PolarSite objects.
443 struct ProbeSource {
444 Eigen::Vector3d pos;
445 double q;
446 Eigen::Vector3d mu;
447 };
448 std::vector<std::vector<ProbeSource>> segments;
449 std::vector<Index> segment_ids;
450 std::vector<Eigen::Vector3d> centroids;
451 for (Index source_id : registry_.AllIds()) {
452 if (!registry_.Has(source_id, source_state)) {
453 continue;
454 }
455 const PolarSegment& segment = registry_.Get(source_id, source_state);
456 std::vector<ProbeSource> sites;
457 sites.reserve(segment.size());
458 for (const PolarSite& site : segment) {
459 sites.push_back({site.getPos(), site.getCharge(),
460 site.getStaticDipole() + site.getInducedDipole()});
461 }
462 segments.push_back(std::move(sites));
463 segment_ids.push_back(source_id);
464 centroids.push_back(UnweightedCentroid(segment));
465 }
466
467 const double cutoff_with_margin = real_space_cutoff_ + 2.0 * segment_radius_;
468 const Index n_points = Index(points.size());
469 const Index n_sources = Index(segments.size());
470 Eigen::VectorXd phi = Eigen::VectorXd::Zero(n_points);
471
472 // Hoisted out of both loops below: the box is fixed for this object's
473 // lifetime, and inverting it per (point, source) is n_points * n_sources
474 // 3x3 inversions for one matrix.
475 const Eigen::Matrix3d box_inv = box_.inverse();
476
477#pragma omp parallel for schedule(dynamic, 8)
478 for (Index p = 0; p < n_points; ++p) {
479 const Eigen::Vector3d& point = points[std::size_t(p)];
480
481 double acc = 0.0;
482 for (Index s_i = 0; s_i < n_sources; ++s_i) {
483 const std::vector<ProbeSource>& source_segment =
484 segments[std::size_t(s_i)];
485 const Index source_id = segment_ids[std::size_t(s_i)];
486 const Eigen::Vector3d& source_centroid = centroids[std::size_t(s_i)];
487
488 // Minimum image first, shell search on top of it -- see AddFieldAt's
489 // own account of why the raw separation cannot be handed to a
490 // search bounded by |t|.
491 const Eigen::Vector3d raw_offset = point - source_centroid;
492 const Eigen::Vector3d frac = box_inv * raw_offset;
493 const Eigen::Vector3d wrapped_frac = frac - frac.array().round().matrix();
494 const Eigen::Vector3d min_image_offset = box_ * wrapped_frac;
495 const Eigen::Vector3d baseline_shift = raw_offset - min_image_offset;
496
497 const auto fg = foreground_.find(source_id);
498 const bool source_has_foreground = fg != foreground_.end();
499
500 // STOP, don't skip. translations_ is sorted by |t| ascending, and
501 // |min_image_offset - t| >= |t| - |min_image_offset|, so once |t|
502 // passes this limit every remaining translation is culled as well.
503 // The bound is exact, not a heuristic: nothing inside the cutoff can
504 // sit beyond it.
505 //
506 // This matters far more here than in the site-based paths above,
507 // which never walk the whole list -- their shell-convergence search
508 // hands them a [shell_start, shell_end) window. Without the break
509 // this loop runs (2*n_max+1)^3 translations, 29791 at the default
510 // n_max of 15, for EVERY (point, source) pair. On a DFT integration
511 // grid against a 1000-segment registry that is of order 1e12
512 // distance evaluations and hours of wall time; with it the surviving
513 // range is a couple of lattice shells.
514 const double t_limit = cutoff_with_margin + min_image_offset.norm();
515
516 for (std::size_t idx = 0; idx < translations_.size(); ++idx) {
517 if (translations_[idx].r > t_limit) {
518 break;
519 }
520 const double pair_distance =
521 (min_image_offset - translations_[idx].t).norm();
522 if (pair_distance > cutoff_with_margin) {
523 continue;
524 }
525 const Eigen::Vector3d t = baseline_shift + translations_[idx].t;
526
527 // Foreground suppression: this one periodic copy is handled
528 // explicitly elsewhere, so it must not also appear here.
529 if (source_has_foreground) {
530 const Eigen::Vector3d shifted_centroid = source_centroid + t;
531 bool suppressed = false;
532 for (const Eigen::Vector3d& fg_pos : fg->second) {
533 if ((shifted_centroid - fg_pos).norm() < kForegroundMatchTol) {
534 suppressed = true;
535 break;
536 }
537 }
538 if (suppressed) {
539 continue;
540 }
541 }
542
543 if (!source_has_foreground && source_id == target_segment_id &&
544 t.squaredNorm() < kSelfTranslationTol * kSelfTranslationTol) {
545 continue;
546 }
547
548 // Unit-charge probe at the point: what CalcStaticEnergy plus the
549 // UNDAMPED CalcInducedSourceEnergy return for it. Undamped because
550 // the target is a point in space, not a point-polarizable site, so
551 // there is no overlap for Thole to correct -- and the rest of the
552 // package already treats [induced dipole] x [QM density] undamped
553 // (AOMultipole and DFTEngine::ExternalRepulsion apply no Thole).
554 const Eigen::Vector3d probe_offset = point - t;
555 for (const ProbeSource& source_site : source_segment) {
556 acc += interactor_.ScreenedPotential(source_site.q, source_site.mu,
557 probe_offset - source_site.pos);
558 }
559 }
560 }
561 phi[p] = acc;
562 }
563 return phi;
564}
565
567 const PolarSite& target, EwaldChargeState source_state) const {
568 // Deliberately a near-copy of CalcStaticEnergyAt above rather than a
569 // shared template over the interactor call: the two differ only in
570 // which moments they contract, but they are validated separately and
571 // against different legacy channels (_pp and half of _pu), and a
572 // shared body would let a change to one silently move the other.
573 const std::pair<const PolarSite*, EwaldChargeState> cache_key(&target,
574 source_state);
575 auto cached = neighbor_cache_.find(cache_key);
576 if (cached == neighbor_cache_.end()) {
577 throw std::runtime_error(
578 "EwaldRealSpaceSum::CalcInducedSourceEnergyAt: no neighbour list for "
579 "this target. AddFieldAt or PrepareNeighborCache must run first -- "
580 "this method deliberately does not build one, so that an energy "
581 "query cannot silently become the expensive shell search.");
582 }
583
584 double energy = 0.0;
585 for (const auto& entry : cached->second) {
586 const PolarSegment& source_segment = *std::get<0>(entry);
587 const Index translation_idx = std::get<1>(entry);
588 const Eigen::Vector3d& baseline_shift = std::get<2>(entry);
589 const Eigen::Vector3d t = baseline_shift + translations_[translation_idx].t;
590 for (const PolarSite& source_site : source_segment) {
591 energy += interactor_.CalcInducedSourceEnergy(source_site, target, t);
592 }
593 }
594 return energy;
595}
596
598 const std::vector<std::pair<Index, PolarSite*>>& targets,
599 EwaldChargeState source_state, bool converge_static_field) const {
600 // The list AddFieldAt<V>(..., converge_static_field) would build.
601 PrepareNeighborCacheImpl<Estatic::V>(targets, source_state,
602 converge_static_field);
603}
604
605template <enum Estatic CE>
607 const std::vector<std::pair<Index, PolarSite*>>& targets,
608 EwaldChargeState source_state, bool include_static) const {
609 // See PrepareNeighborCache's own declaration for what this is for. The search
610 // is the one AddFieldAt runs on a miss (SearchNeighbors), so the two
611 // paths cannot drift apart.
612 //
613 // Serial selection: skip targets that already have a list, and
614 // repeated entries, so each missing list is searched exactly once.
615 std::vector<std::size_t> todo;
616 {
617 std::unordered_map<std::pair<const PolarSite*, EwaldChargeState>, bool,
618 PairHash>
619 seen;
620 for (std::size_t i = 0; i < targets.size(); ++i) {
621 const std::pair<const PolarSite*, EwaldChargeState> key(targets[i].second,
622 source_state);
623 if (neighbor_cache_.count(key) > 0 || !seen.emplace(key, true).second) {
624 continue;
625 }
626 todo.push_back(i);
627 }
628 }
629 if (todo.empty()) {
630 return;
631 }
632
633 // Parallel search. Each iteration reads this object and writes only
634 // its own slots below. An exception cannot leave an omp region, so
635 // the first one is carried out and rethrown afterwards.
636 std::vector<NeighborList> lists(todo.size());
637 std::vector<SearchCounts> counts(todo.size());
638 std::exception_ptr failure = nullptr;
639 const Index n_todo = Index(todo.size());
640#pragma omp parallel for schedule(dynamic, 16)
641 for (Index k = 0; k < n_todo; ++k) {
642 try {
643 const auto& entry = targets[todo[std::size_t(k)]];
644 PolarSite scratch = *entry.second;
645 lists[std::size_t(k)] =
646 SearchNeighbors<CE>(entry.first, scratch, source_state,
647 include_static, counts[std::size_t(k)]);
648 } catch (...) {
649#pragma omp critical(ewald_real_prepare_failure)
650 {
651 if (!failure) {
652 failure = std::current_exception();
653 }
654 }
655 }
656 }
657 if (failure) {
658 std::rethrow_exception(failure);
659 }
660
661 // Serial insertion, in target order.
662 for (std::size_t k = 0; k < todo.size(); ++k) {
663 const std::pair<const PolarSite*, EwaldChargeState> key(
664 targets[todo[k]].second, source_state);
665 InsertNeighborList(key, std::move(lists[k]), counts[k]);
666 }
667}
668
669template <enum Estatic CE>
671 const std::vector<std::pair<Index, PolarSite*>>& targets,
672 EwaldChargeState source_state, bool include_static) const {
673 // Lists first: afterwards every AddFieldAt below takes the read-only
674 // fast path. The list is converged on the same field AddFieldAt's own
675 // on-demand search would have used, so results do not depend on
676 // whether a target was prepared here or found on demand.
677 PrepareNeighborCacheImpl<CE>(targets, source_state, include_static);
678 // A site may appear more than once in targets. Its occurrences are
679 // grouped and applied by one thread, in input order, so no two threads
680 // add to the same site and the sum matches the serial one bit for bit.
681 std::unordered_map<const PolarSite*, std::size_t> group_of;
682 std::vector<std::vector<std::size_t>> groups;
683 group_of.reserve(targets.size());
684 groups.reserve(targets.size());
685 for (std::size_t i = 0; i < targets.size(); ++i) {
686 const auto inserted = group_of.emplace(targets[i].second, groups.size());
687 if (inserted.second) {
688 groups.emplace_back();
689 }
690 groups[inserted.first->second].push_back(i);
691 }
692 const Index n_groups = Index(groups.size());
693#pragma omp parallel for schedule(dynamic, 16)
694 for (Index g = 0; g < n_groups; ++g) {
695 for (const std::size_t i : groups[std::size_t(g)]) {
696 PolarSite& target = *targets[i].second;
697 const auto cached = neighbor_cache_.find({&target, source_state});
698 ApplyNeighborList<CE>(cached->second, target, include_static);
699 }
700 }
701}
702
705 bool) const;
708 bool) const;
710 const std::vector<std::pair<Index, PolarSite*>>&, EwaldChargeState,
711 bool) const;
713 const std::vector<std::pair<Index, PolarSite*>>&, EwaldChargeState,
714 bool) const;
715
716} // namespace xtp
717} // namespace votca
static constexpr double kForegroundMatchTol
Eigen::VectorXd PotentialAtMany(Index target_segment_id, const std::vector< Eigen::Vector3d > &points, EwaldChargeState source_state) const
std::unordered_map< std::pair< const PolarSite *, EwaldChargeState >, NeighborList, PairHash > neighbor_cache_
std::vector< std::tuple< const PolarSegment *, Index, Eigen::Vector3d > > NeighborList
double CalcInducedSourceEnergyAt(const PolarSite &target, EwaldChargeState source_state) const
void AddFieldAtMany(const std::vector< std::pair< Index, PolarSite * > > &targets, EwaldChargeState source_state, bool include_static=true) const
std::map< Index, std::vector< Eigen::Vector3d > > foreground_
double CalcStaticEnergyAt(const PolarSite &target, EwaldChargeState source_state) const
std::vector< Translation > GenerateSortedTranslations() const
const EwaldRegistry & registry_
void PrepareNeighborCache(const std::vector< std::pair< Index, PolarSite * > > &targets, EwaldChargeState source_state, bool converge_static_field=false) const
void AddFieldAt(Index target_segment_id, PolarSite &target, EwaldChargeState source_state, bool include_static=true) const
void PrepareNeighborCacheImpl(const std::vector< std::pair< Index, PolarSite * > > &targets, EwaldChargeState source_state, bool include_static) const
std::vector< Translation > translations_
void InsertNeighborList(const std::pair< const PolarSite *, EwaldChargeState > &key, NeighborList list, const SearchCounts &counts) const
EwaldRealSpaceInteractor interactor_
static constexpr double kSelfTranslationTol
void ApplyNeighborList(const NeighborList &list, PolarSite &target, bool include_static) const
EwaldRealSpaceSum(const Eigen::Matrix3d &box, const EwaldRegistry &registry, double alpha, double thole_a, double r_min, double field_tol, double shell_width=0.945, Index n_max=15, double screening_factor=6.0, const std::vector< std::pair< Index, Eigen::Vector3d > > &foreground={})
NeighborList SearchNeighbors(Index target_segment_id, PolarSite &scratch, EwaldChargeState source_state, bool include_static, SearchCounts &counts) const
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
const Eigen::Vector3d & V_noE() const
Definition polarsite.h:72
const Eigen::Vector3d & V() const
Definition polarsite.h:68
const Eigen::Vector3d & getPos() const
Definition staticsite.h:80
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