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