votca 2026-dev
Loading...
Searching...
No Matches
ewaldrealspaceinteractor.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_EWALDREALSPACEINTERACTOR_H
22#define VOTCA_XTP_EWALDREALSPACEINTERACTOR_H
23
24// Local VOTCA includes
25#include "classicalsegment.h"
26#include "eeinteractor.h"
27#include "eigen.h"
28
71
72namespace votca {
73namespace xtp {
74
76 public:
77 explicit EwaldRealSpaceInteractor(double alpha, double thole_a = 0.39)
78 : alpha_(alpha), thole_(thole_a) {};
79
80 // Applies the erfc-screened field generated by a permanent multipole
81 // (charge + dipole, taken from site1's static moments) at site2, and
82 // returns the corresponding screened interaction energy. Undamped by
83 // Thole -- site1 is treated as a permanent source regardless of whether
84 // it happens to be a PolarSite.
85 //
86 // CE selects which of site2's field accumulators (V() or V_noE()) is
87 // updated, mirroring eeInteractor::ApplyStaticField.
88 //
89 // source_shift is a periodic-image translation added to site1's own
90 // position, WITHOUT modifying site1. It exists purely to avoid
91 // materializing a shifted copy of the source site in the caller's hot
92 // loop: EwaldRealSpaceSum evaluates every (target site, source site,
93 // translation) triple, and constructing a PolarSite copy per triple
94 // dominated the real-space cost (a PolarSite carries a Vector9d of
95 // multipoles, two 3x3 matrices, several vectors, and a std::string
96 // element name -- so the copy is far from free, and there are ~1e8 of
97 // them per solver iteration on a 1000-segment system). Legacy's own
98 // equivalent takes the shift as an argument for the same reason (see
99 // EwdInteractor::FU12_ERFC_At_By's own vec &s overload). Defaults to
100 // zero, so existing two-argument calls are unaffected.
101 template <class T, enum Estatic CE>
102 double ApplyStaticField(
103 const T& site1, PolarSite& site2,
104 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero()) const;
105
106 // SUBTRACTS the erf(alpha*r)-screened (NOT erfc) field generated by a
107 // permanent multipole at site2, for two sites of the SAME molecule --
108 // matching legacy EwdInteractor::FP12_ERF_At_By exactly (see its own
109 // "Note the (-): This is a compensation term" comment). This exists
110 // because EwaldReciprocalSpaceSum's own structure factor never excludes
111 // anything (see that class's own documentation), so it unconditionally
112 // includes every intramolecular static-static pair's own contribution
113 // -- exactly the erf(alpha*r)/r "other half" of the Ewald split (since
114 // erf+erfc=1, erf(alpha*r)/r is precisely the infinite-k-limit
115 // reciprocal-space contribution for that pair). This method removes
116 // that leak so the NET static-static intramolecular contribution comes
117 // out to genuinely zero -- matching legacy's own convention (and the
118 // standard molecular-mechanics rationale for it: permanent charges are
119 // already implicitly accounted for by a force field's own bonded
120 // terms, so explicit intramolecular Coulomb on top would double-count;
121 // see EwaldPeriodicDipoleOperator's own class documentation for the
122 // fuller account of how this was found -- a real, if initially missed,
123 // mechanism in legacy's own code, not a new design decision here).
124 // Same CE/accumulator convention as ApplyStaticField.
125 //
126 // source_shift (see ApplyStaticField's own note) is last in the
127 // parameter list, not next to the sites it belongs with: a defaulted
128 // parameter inserted mid-signature silently reinterprets existing
129 // positional arguments at call sites that still compile. Keep new
130 // parameters at the end.
131 template <class T, enum Estatic CE>
133 const T& site1, PolarSite& site2,
134 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero()) const;
135
136 // The induced-dipole counterpart of ApplyErfStaticFieldCorrection:
137 // removes the erf-screened (reciprocal-space) field that site1's
138 // INDUCED dipole contributes at site2. Subtracted, like its static
139 // sibling.
140 //
141 // Deliberately NOT Thole-damped. Thole damping is a short-range
142 // real-space construct; the reciprocal-space sum this term removes
143 // has no damping of its own, so damping the removal would not cancel
144 // what was actually added. Legacy agrees -- EwdInteractor::
145 // FU12_ERF_At_By uses the bare C1/C2 functions with no l3/l5 factor
146 // anywhere, unlike its erfc sibling FU12_ERFC_At_By.
147 //
148 // Built on the same EvaluateSource path as the static correction
149 // rather than re-deriving the field expression, so it inherits that
150 // path's already-validated sign convention instead of depending on a
151 // fresh derivation of it.
152 template <enum Estatic CE>
154 const PolarSite& site1, PolarSite& site2,
155 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero()) const;
156
157 // Applies the erfc-screened, Thole-damped field generated by site1's
158 // induced dipole at site2, and returns the corresponding energy.
159 // source_shift: see ApplyStaticField's own note above.
160 template <enum Estatic CE>
161 double ApplyInducedField(
162 const PolarSite& site1, PolarSite& site2,
163 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero()) const;
164
165 // erfc-screened interaction energy between two permanent multipole
166 // sites (charge + dipole only), undamped by Thole.
167 //
168 // source_shift, as elsewhere, is last in the parameter list: a
169 // defaulted parameter inserted mid-signature silently reinterprets
170 // existing positional arguments at call sites that still compile.
171 template <class S1, class S2>
172 double CalcStaticEnergy(
173 const S1& site1, const S2& site2,
174 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero()) const;
175
176 // The erf-screened counterpart of CalcStaticEnergy: the permanent
177 // multipole interaction energy that the RECIPROCAL sum contributes for
178 // this pair. Subtracting it removes a foreground copy's energy from
179 // the periodic total, exactly as ApplyErfStaticFieldCorrection does
180 // for the field.
181 //
182 // Undamped, for the same reason its field sibling is: the
183 // reciprocal-space contribution it removes carries no Thole damping.
184 // Takes the analytic r -> 0 limit for coincident sites, which is the
185 // common case here rather than an edge case -- every foreground target
186 // sits inside a suppressed copy.
187 template <class S1, class S2>
188 double CalcErfStaticEnergy(
189 const S1& site1, const S2& site2,
190 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero()) const;
191
192 // erfc-screened, Thole-damped interaction energy between two induced
193 // dipoles.
194 double CalcInducedEnergy(const PolarSite& site1,
195 const PolarSite& site2) const;
196
197 // erfc-screened, Thole-damped interaction energy between site1's
198 // INDUCED dipole (as source) and site2's PERMANENT moments (as
199 // target) -- the [background induced] x [foreground permanent] term.
200 //
201 // This is the half of legacy's _pu channel that nothing else in this
202 // code path covers. PolarRegion's own E_polar_ext contracts the
203 // FOREGROUND's induced dipoles against the delivered field, which
204 // gives [fg induced] x [bg permanent + bg induced]; the permanent
205 // energies here give [fg permanent] x [bg permanent]. The remaining
206 // corner, [fg permanent] x [bg induced], has no other home, and its
207 // absence was measurable: it accounts for the factor ~1.9 by which
208 // the neutral job's induced energy fell short of legacy's.
209 //
210 // Damped, unlike the permanent-multipole energies, because the source
211 // is an induced dipole and Thole damping is exactly the short-range
212 // correction to induced-dipole interactions. l3 multiplies the r^-3
213 // terms (the potential, and the field's mu term), l5 the r^-5 term,
214 // matching ApplyInducedField's own combination so the two cannot
215 // drift apart.
216 //
217 // NOTE that this makes the total mildly alpha-dependent, since the
218 // reciprocal-space partner carries no damping (a long-range sum has
219 // no short-range correction to apply). Legacy has the identical
220 // structure -- FU12_ERFC_At_By damps, FU12_ERF_At_By does not -- so
221 // this reproduces its convention rather than improving on it. The
222 // decomposition IS exactly alpha-independent once damping is switched
223 // off, which is how the unit test pins it down.
224 //
225 // damp = false is for a target that is a POINT rather than a site: a
226 // field-evaluation point, as EwaldRealSpaceSum::PotentialAtMany uses.
227 // Thole damping corrects the overlap of two POINT-POLARIZABLE sites,
228 // and a coordinate in space is not one of those -- so there is nothing
229 // to correct, and the undamped screened interaction is the whole
230 // answer.
231 //
232 // This is not a free choice, it is what the rest of the package
233 // already does for the same physical interaction. A QM region gets the
234 // classical regions' induced dipoles through AOMultipole (which reads
235 // getDipole(), permanent + induced) and through DFTEngine::
236 // ExternalRepulsion -> eeInteractor::CalcStaticEnergy_site: neither
237 // applies Thole at all. In eeInteractor, Thole lives only in
238 // FillTholeInteraction, which the induction solve and the polar-polar
239 // energies use and the QM path never touches. Damping here would give
240 // a three-region job two different conventions for [induced dipole] x
241 // [QM density] in one Hamiltonian, decided by which code path the
242 // dipole arrived through.
243 //
244 // It cannot be expressed by giving the probe no polarizability.
245 // ComputeThole would then form au3 = 0, read that as COMPLETE overlap,
246 // and damp maximally at every distance (l3 = l5 = 0, c3 = -B1_erf
247 // where undamped wants +B1). A PolarSite cannot be built without a
248 // polarizability in any case -- its constructor assigns one from the
249 // element name -- so the probe unavoidably carries a number that means
250 // nothing, and this flag is how the caller says so.
252 const PolarSite& site1, const PolarSite& site2,
253 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero(),
254 bool damp = true) const;
255
256 // The erf-screened counterpart of CalcInducedSourceEnergy, used to
257 // remove a coincident foreground copy from the reciprocal side, just
258 // as CalcErfStaticEnergy does for the permanent channel.
259 //
260 // UNDAMPED, matching ApplyErfInducedFieldCorrection: the
261 // reciprocal-space contribution this removes carries no Thole damping,
262 // so damping the removal would not cancel what was actually added.
264 const PolarSite& site1, const PolarSite& site2,
265 const Eigen::Vector3d& source_shift = Eigen::Vector3d::Zero()) const;
266
267 // The screened-Coulomb derivative functions B0, B1, B2 for separation r:
268 // B0 = erfc(alpha*r) / r
269 // B_l = [ (2l-1)*B_{l-1} + (2*alpha)^(2l-1) / sqrt(pi) * exp(-alpha^2 r^2)
270 // ] / r^2
271 // B0 supports charge-charge, B1 additionally supports charge-dipole,
272 // B2 additionally supports dipole-dipole. Cross-checked against
273 // EwdInteractor::UpdateAllBls (xtp/ewald/ewaldactor.h) for correctness
274 // -- that file was deleted with the legacy code; the check is in the
275 // git history, not in the tree.
276 // Public (rather than private) so the free helper functions in
277 // ewaldrealspaceinteractor.cc's anonymous namespace can take it by
278 // value/reference as a plain parameter type.
279 struct BFunctions {
280 double B0;
281 double B1;
282 double B2;
283 };
284
285 // Thole damping factors l3, l5 (multiplying the r^-3 and r^-5 field
286 // terms respectively) for a pair of induced-dipole sites, using the
287 // exponential Thole model already implemented in
288 // eeInteractor::FillTholeInteraction. Returns {1.0, 1.0} (no damping)
289 // once the pair is far enough apart that the exponential term is
290 // numerically negligible, matching FillTholeInteraction's own cutoff.
292 double l3;
293 double l5;
294 };
295
296 BFunctions ComputeB(double r) const;
297
298 // The erf(alpha*r)-screened complement to ComputeB's own erfc-screened
299 // B-functions -- same B0/B1/B2 roles, but for erf(alpha*r)/r rather
300 // than erfc(alpha*r)/r. Computed as the bare (undamped, alpha-
301 // independent) Coulomb B-functions minus ComputeB's own result: since
302 // erf(x)+erfc(x)=1, and the B-function recursion is linear in B0, this
303 // decomposition is exact, not an approximation (confirmed both
304 // algebraically and against a finite-difference derivative of
305 // erf(alpha*r)/r directly, matching to the precision the finite
306 // difference itself allows). B0_bare=1/r, B1_bare=1/r^3, B2_bare=3/r^5
307 // are the standard undamped point-multipole tensor coefficients.
308 BFunctions ComputeErfB(double r) const;
309 TholeFactors ComputeThole(double r, const PolarSite& site1,
310 const PolarSite& site2) const;
311
312 private:
313 // Below this separation two sites are treated as coincident by the
314 // erf-screened corrections, which take their analytic r -> 0 limit
315 // there. Matches legacy's own 1e-2 threshold in EwdInteractor.
316 static constexpr double kCoincidenceTol = 1e-2;
317
318 double alpha_;
319 double thole_;
320};
321
322} // namespace xtp
323} // namespace votca
324
325#endif // VOTCA_XTP_EWALDREALSPACEINTERACTOR_H
EwaldRealSpaceInteractor(double alpha, double thole_a=0.39)
double CalcInducedSourceEnergy(const PolarSite &site1, const PolarSite &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero(), bool damp=true) const
double CalcStaticEnergy(const S1 &site1, const S2 &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
TholeFactors ComputeThole(double r, const PolarSite &site1, const PolarSite &site2) const
void ApplyErfInducedFieldCorrection(const PolarSite &site1, PolarSite &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
double CalcInducedEnergy(const PolarSite &site1, const PolarSite &site2) const
double ApplyInducedField(const PolarSite &site1, PolarSite &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
double CalcErfInducedSourceEnergy(const PolarSite &site1, const PolarSite &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
double CalcErfStaticEnergy(const S1 &site1, const S2 &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
double ApplyStaticField(const T &site1, PolarSite &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
void ApplyErfStaticFieldCorrection(const T &site1, PolarSite &site2, const Eigen::Vector3d &source_shift=Eigen::Vector3d::Zero()) const
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
Provides a means for comparing floating point numbers.
Definition basebead.h:33