votca 2026-dev
Loading...
Searching...
No Matches
ewaldrealspacesum.h
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20#pragma once
21#ifndef VOTCA_XTP_EWALDREALSPACESUM_H
22#define VOTCA_XTP_EWALDREALSPACESUM_H
23
24// Standard includes
25#include <map>
26#include <string>
27#include <tuple>
28#include <unordered_map>
29#include <utility>
30#include <vector>
31
32// Local VOTCA includes
33#include "eigen.h"
35#include "ewaldregistry.h"
36
91
92namespace votca {
93namespace xtp {
94
96 public:
97 // box: columns are the lattice vectors a, b, c (matching
98 // Topology::getBox()'s own convention).
99 // registry: source of every segment's multipole/induced-dipole state.
100 // Stored by reference; the registry must outlive this object, and
101 // FieldAt() reads whatever is currently in it (e.g. updated induced
102 // dipoles from a prior SCF iteration).
103 // alpha, thole_a: passed through to the underlying
104 // EwaldRealSpaceInteractor.
105 // r_min: minimum shell radius (bohr) before convergence may be declared,
106 // regardless of how small any individual shell's contribution is.
107 // field_tol: convergence threshold (atomic units) on a shell's own
108 // contribution to the field magnitude.
109 // shell_width: radial bin width (bohr) used to group periodic images
110 // into shells.
111 // n_max: safety cap on the lattice-translation search box (translations
112 // with |na|,|nb|,|nc| <= n_max are considered candidates). If
113 // convergence is not reached within this box, FieldAt() throws rather
114 // than silently returning an unconverged result.
115 // screening_factor: sets the real-space distance cutoff to
116 // screening_factor / alpha. A pair separated by more than that
117 // contributes at most ~erfc(screening_factor) of the local field, so
118 // the default 6.0 truncates at ~2e-17 per pair -- far below any
119 // realistic field_tol, while discarding the large majority of
120 // (source segment, translation) pairs a periodic sum would otherwise
121 // evaluate (measured: 78.7% on a 1000-segment box, a 4.5x saving in
122 // real-space cost with the converged dipoles unchanged in every
123 // printed digit). Raise it to tighten the truncation at
124 // proportionally greater cost -- the number of surviving pairs grows
125 // as the cube -- or lower it to trade accuracy for speed
126 // deliberately. Values below ~4 start to matter at the 1e-11 level
127 // and should be checked against a reference before use.
128 //
129 // screening_factor is deliberately the LAST parameter rather than
130 // sitting next to the other accuracy controls it belongs with: adding
131 // it mid-signature silently reinterpreted the positional shell_width
132 // and n_max arguments of existing callers, which still compiled and
133 // then culled every pair. Keep new parameters at the end.
134 //
135 // foreground: (segment id, segment centroid position) pairs that are
136 // handled EXPLICITLY by a polar region rather than by this periodic
137 // sum, and so must be removed from it -- the "carving out" an MM/MM
138 // or QM/MM job performs. Each entry suppresses exactly the one
139 // periodic copy of that segment sitting at the given position; every
140 // OTHER lattice image of the same segment remains part of the
141 // background and is still summed.
142 //
143 // Matched by position rather than by "is this the minimum image",
144 // deliberately. Two foreground segments can be separated by up to
145 // twice the region cutoff, so for a target near the edge of the
146 // foreground the minimum image of another foreground segment can be
147 // a DIFFERENT copy than the one the polar region actually holds --
148 // in which case a minimum-image rule would exclude the wrong one and
149 // silently double-count. Legacy sidesteps the same trap by keying
150 // its own ForegroundTable on explicit (id, na, nb, nc) rather than
151 // on nearest-image. Positions come from JobTopology, which has
152 // already centred them, so an exact-coordinate match is meaningful;
153 // kForegroundMatchTol only absorbs round-off, and is orders of
154 // magnitude below any real inter-segment separation.
155 //
156 // Empty by default, which is the plain periodic background.
158 const Eigen::Matrix3d& box, const EwaldRegistry& registry, double alpha,
159 double thole_a, double r_min, double field_tol,
160 double shell_width = 0.945, Index n_max = 15,
161 double screening_factor = 6.0,
162 const std::vector<std::pair<Index, Eigen::Vector3d>>& foreground = {});
163
164 // Accumulates the total intermolecular real-space field into target's
165 // own V()/V_noE() accumulators (via EwaldRealSpaceInteractor, matching
166 // its convention), generated by every segment in the registry at charge
167 // state source_state, excluding target_segment_id itself (see class
168 // documentation on scope). target's position (target.getPos()) is used
169 // as the field evaluation point; target itself need not already be
170 // registered in registry_. Throws std::runtime_error if convergence is
171 // not reached within n_max shells.
172 //
173 // include_static selects whether each source's PERMANENT multipole
174 // field is accumulated alongside its induced-dipole field. It exists
175 // for the iterative solver: the permanent contribution does not depend
176 // on the induced dipoles, so EwaldPeriodicDipoleOperator's own
177 // RawMultiply recomputes an identical constant on every iteration,
178 // which its multiply() = RawMultiply(v) - baseline_ then cancels
179 // exactly. Passing false there drops that work with no effect on the
180 // result (both RawMultiply(v) and baseline_ shift by the same
181 // constant). Measured at ~53% of this class's own per-pair real-space
182 // cost, i.e. the single largest avoidable cost in the solve.
183 //
184 // Defaults to true, so the permanent-field pass that builds the
185 // solver's own right-hand side (which genuinely needs it) is
186 // unaffected.
187 template <enum Estatic CE>
188 void AddFieldAt(Index target_segment_id, PolarSite& target,
189 EwaldChargeState source_state,
190 bool include_static = true) const;
191
192 // The erfc-screened PERMANENT-multipole interaction energy between one
193 // target site and every background source this class would sum a field
194 // from -- the same neighbour set, with the same foreground copies
195 // suppressed and the same distance cull applied, so the energy and the
196 // field are guaranteed to describe the same system.
197 //
198 // Permanent multipoles on BOTH sides. Neither side's induced dipoles
199 // enter: the foreground's are PolarRegion's business (E_polar_ext),
200 // and the background's belong to CalcInducedSourceEnergyAt below.
201 //
202 // Requires the neighbour cache to exist, i.e. AddFieldAt or
203 // PrepareNeighborCache must have run for this target first. It does
204 // not build the cache itself, because doing so would make an energy
205 // query silently expensive and, worse, order-dependent.
206 // No target_segment_id parameter, unlike AddFieldAt: the neighbour
207 // list is keyed on the target site itself, and the exclusion of the
208 // target's own segment is already baked into the cached list, so the
209 // id would be dead weight here.
210 double CalcStaticEnergyAt(const PolarSite& target,
211 EwaldChargeState source_state) const;
212
213 // The erfc-screened, Thole-damped energy between the BACKGROUND's
214 // induced dipoles (sources) and one foreground target's PERMANENT
215 // moments -- over exactly the neighbour set CalcStaticEnergyAt uses,
216 // for the same reason: the energy and the field must describe the
217 // same system.
218 //
219 // This is the [fg permanent] x [bg induced] corner of the
220 // permanent/induced product. See
221 // EwaldRealSpaceInteractor::CalcInducedSourceEnergy for why it has no
222 // other home, and why it is damped where the permanent energies are
223 // not.
224 //
225 // Same cache requirement as CalcStaticEnergyAt, and for the same
226 // reason.
227 // POTENTIAL at arbitrary points, batched. Total of both channels, to
228 // match EwaldReciprocalSpaceSum::PotentialAtMany and the field
229 // AddFieldAt delivers: permanent moments through CalcStaticEnergy,
230 // background induced dipoles through CalcInducedSourceEnergy.
231 //
232 // NO NEIGHBOUR CACHE, deliberately. AddFieldAt keys its cache on the
233 // target's ADDRESS and writes an entry on first use, which is right
234 // for a fixed set of sites queried repeatedly and wrong for a DFT
235 // grid: one entry per point would run to gigabytes, and reusing one
236 // probe object across positions would silently hand every later point
237 // the first one's neighbour list. Points here are just coordinates, so
238 // nothing is keyed and nothing is kept.
239 //
240 // Its traversal therefore duplicates AddFieldAt's distance cull,
241 // foreground suppression and zero-translation self-skip rather than
242 // sharing them -- those rules are interleaved with a field-based
243 // convergence check there, which has no meaning for a potential.
244 // Duplicated rules drift, so this is held to AddFieldAt's own answer
245 // by unit_probe_potential_reproduces_the_static_energy and the
246 // EwaldRegion case that compares against a direct lattice sum. The
247 // shell-convergence early exit is dropped in favour of the geometric
248 // cutoff alone, which visits a superset of what the shell search
249 // reaches.
250 //
251 // target_segment_id decides only whether the zero-translation
252 // self-pair is skipped, and that skip is disabled for any source with
253 // a foreground copy. Pass a foreground segment's id: a point that is
254 // not a site of its own wants no self-skip.
255 Eigen::VectorXd PotentialAtMany(Index target_segment_id,
256 const std::vector<Eigen::Vector3d>& points,
257 EwaldChargeState source_state) const;
258
259 double CalcInducedSourceEnergyAt(const PolarSite& target,
260 EwaldChargeState source_state) const;
261
262 // Builds the neighbour cache for every target in one serial pass,
263 // WITHOUT applying any field (each target's own V()/V_noE() are
264 // restored before returning). Exists so callers can parallelize over
265 // targets afterwards: neighbor_cache_ and the statistics counters are
266 // mutable and written only while a target's list is being built, so
267 // once every list exists AddFieldAt is read-only with respect to this
268 // object and safe to call concurrently for distinct targets. Calling
269 // this is optional -- AddFieldAt still builds its own list on demand
270 // -- but a caller that skips it must not run AddFieldAt in parallel.
272 const std::vector<std::pair<Index, PolarSite*>>& targets,
273 EwaldChargeState source_state) const;
274
275 // Neighbour-list statistics over every target whose list has been
276 // built so far. entries is the total number of (source segment,
277 // translation) pairs kept; culled is how many were rejected by the
278 // distance cutoff. Both are zero until the first AddFieldAt call.
284 double entries_per_target() const {
285 return targets > 0 ? double(entries) / double(targets) : 0.0;
286 }
287 double culled_fraction() const {
288 const Index seen = entries + culled;
289 return seen > 0 ? double(culled) / double(seen) : 0.0;
290 }
291 };
296 double RealSpaceCutoff() const { return real_space_cutoff_; }
297
298 private:
299 // One periodic image translation vector, tagged with its distance from
300 // the origin so shells can be built by sorting once.
301 struct Translation {
302 Eigen::Vector3d t;
303 double r;
304 };
305 std::vector<Translation> GenerateSortedTranslations() const;
306
307 // std::pair has no default std::hash specialization; EwaldChargeState
308 // (an enum class) does, via std::underlying_type, since C++14, so this
309 // only needs to combine the two.
310 struct PairHash {
311 std::size_t operator()(
312 const std::pair<const PolarSite*, EwaldChargeState>& key) const {
313 return std::hash<const PolarSite*>()(key.first) ^
314 (std::hash<EwaldChargeState>()(key.second) << 1);
315 }
316 };
317
318 // Real-space screening cutoff: the separation beyond which a pair's
319 // erfc(alpha*r)-screened contribution is negligible, set to
320 // screening_factor / alpha (see the constructor). This is a cost
321 // cutoff only: it never extends the sum, and r_min_/field_tol_ still
322 // govern how far the shell search goes.
324 // Tolerance for deciding that a shifted source segment coincides with a
325 // foreground segment. Round-off only -- see the constructor.
326 static constexpr double kForegroundMatchTol = 1e-4;
327 // Tolerance for recognising the ZERO lattice translation, used to skip
328 // a target's own segment at its own position (intramolecular, and
329 // containing the r = 0 self-pair) while keeping its other images. The
330 // quantity tested is built from an exactly-zero baseline shift plus an
331 // exactly-zero lattice vector, so this only has to absorb round-off;
332 // the next-smallest translation is a full lattice vector away.
333 static constexpr double kSelfTranslationTol = 1e-8;
334 // Foreground copies to suppress, grouped by segment id. Most segments
335 // have no entry at all; those that do usually have exactly one.
336 std::map<Index, std::vector<Eigen::Vector3d>> foreground_;
337 // Neighbour-list statistics, accumulated as the cache is built. The
338 // cached list is the real cost driver of the whole solve: every entry
339 // is one (source segment, periodic translation) pair, re-evaluated
340 // against every one of the target's own sites on every iteration.
341 // Reported once per run so the effect of alpha, r_min and the
342 // distance cull on that cost is visible directly, rather than being
343 // inferred from wall-clock.
347 // How many (segment, image) pairs were suppressed as foreground.
349 // Largest site-to-centroid distance over every registered segment,
350 // used as the margin when the cutoff (a per-site-pair quantity) is
351 // applied at segment granularity.
353
354 Eigen::Matrix3d box_;
357 double r_min_;
361
362 // Generated once at construction (depends only on the box, not on the
363 // target position), reused by every FieldAt() call.
364 std::vector<Translation> translations_;
365
366 // Every (source_id, translation_idx) pair that genuinely contributed to
367 // a given target's own converged sum -- i.e. the real, geometry-
368 // determined neighbor list, analogous to legacy PolarBackground's own
369 // RThread::PolarNbs(). Built once per distinct (target site, source
370 // charge state) pair (keyed by the site's own address plus
371 // source_state -- the address alone is NOT enough: this class's
372 // registered segments could in principle be queried at a different
373 // EwaldChargeState for the same physical target site, and geometric
374 // neighbors depend on which segments are actually registered at that
375 // state via Has(), not on the target's own identity alone), then
376 // reused on every subsequent call for that same (target, state) pair.
377 // The motivating caller (EwaldPeriodicDipoleOperator, via its own
378 // targets_ vector built once and reused every RawMultiply call) always
379 // uses EwaldChargeState::Neutral, so this distinction is not
380 // exercised by anything in this codebase today, but the key is chosen
381 // to be genuinely correct rather than correct only for the one
382 // access pattern that happens to exist right now.
383 // The first tuple element is a direct pointer to the source segment,
384 // not its id: resolving an id through registry_ costs a Has() plus a
385 // Get(), i.e. two std::map lookups, and this list is walked once per
386 // (target site, neighbour) pair -- ~3e7 times per solver iteration on
387 // a 1000-segment system, measured at ~59ns per entry against ~3ns for
388 // a stored pointer. Legacy's own PolarNb stores the neighbour segment
389 // by pointer for the same reason. Safe because the registry outlives
390 // this object (see the class documentation) and its segments are
391 // stored in a std::map, whose elements keep stable addresses; the same
392 // stability argument this cache already relies on for target site
393 // addresses being usable as keys.
394 mutable std::unordered_map<
395 std::pair<const PolarSite*, EwaldChargeState>,
396 std::vector<std::tuple<const PolarSegment*, Index, Eigen::Vector3d>>,
397 PairHash>
399};
400
401} // namespace xtp
402} // namespace votca
403
404#endif // VOTCA_XTP_EWALDREALSPACESUM_H
NeighborStats GetNeighborStats() const
static constexpr double kForegroundMatchTol
Eigen::VectorXd PotentialAtMany(Index target_segment_id, const std::vector< Eigen::Vector3d > &points, EwaldChargeState source_state) const
double CalcInducedSourceEnergyAt(const PolarSite &target, EwaldChargeState source_state) 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 AddFieldAt(Index target_segment_id, PolarSite &target, EwaldChargeState source_state, bool include_static=true) const
std::unordered_map< std::pair< const PolarSite *, EwaldChargeState >, std::vector< std::tuple< const PolarSegment *, Index, Eigen::Vector3d > >, PairHash > neighbor_cache_
std::vector< Translation > translations_
void PrepareNeighborCache(const std::vector< std::pair< Index, PolarSite * > > &targets, EwaldChargeState source_state) const
EwaldRealSpaceInteractor interactor_
static constexpr double kSelfTranslationTol
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={})
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
std::size_t operator()(const std::pair< const PolarSite *, EwaldChargeState > &key) const