votca 2026-dev
Loading...
Searching...
No Matches
ewaldrealspaceinteractor.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 <cmath>
22
23// Local VOTCA includes
25
26namespace votca {
27namespace xtp {
28
29namespace {
30constexpr double kRSqrtPi = 0.5641895835477563; // 1/sqrt(pi)
31} // namespace
32
34 double r) const {
35 const double rr1 = 1.0 / r;
36 const double rr2 = rr1 * rr1;
37 const double a1 = alpha_;
38 const double a3 = a1 * a1 * a1;
39 const double expTerm = kRSqrtPi * std::exp(-a1 * a1 * r * r);
40
41 BFunctions b;
42 b.B0 = std::erfc(a1 * r) * rr1;
43 b.B1 = rr2 * (b.B0 + 2.0 * a1 * expTerm);
44 b.B2 = rr2 * (3.0 * b.B1 + 4.0 * a3 * expTerm);
45 return b;
46}
47
49 double r) const {
50 const BFunctions erfc_b = ComputeB(r);
51 const double rr1 = 1.0 / r;
52 const double rr3 = rr1 * rr1 * rr1;
53 BFunctions b;
54 b.B0 = rr1 - erfc_b.B0;
55 b.B1 = rr3 - erfc_b.B1;
56 b.B2 = 3.0 * rr3 * rr1 * rr1 - erfc_b.B2;
57 return b;
58}
59
61 double r, const PolarSite& site1, const PolarSite& site2) const {
62 // Reuses the exponential Thole model already implemented in
63 // eeInteractor::FillTholeInteraction, so that the induced-induced
64 // damping used here is identical to the one PolarRegion's own
65 // finite-region induction solve already uses.
66 const double au3 = thole_ * r * r * r * site1.getSqrtInvEigenDamp() *
67 site2.getSqrtInvEigenDamp();
68 TholeFactors t{1.0, 1.0};
69 if (au3 < 40.0) {
70 const double expUa = std::exp(-au3);
71 t.l3 = 1.0 - expUa;
72 t.l5 = 1.0 - (1.0 + au3) * expUa;
73 }
74 return t;
75}
76
77namespace {
78
79// Potential and field generated by a screened point charge q and dipole mu
80// (source), evaluated at a point separated from the source by r_vec
81// (pointing from source to target). Field is what a target site
82// experiences; the target's own energy contribution is computed by the
83// caller from this and the target's own moments.
84struct ScreenedPotentialField {
85 double phi;
86 Eigen::Vector3d field;
87};
88
89ScreenedPotentialField EvaluateSource(
90 double q, const Eigen::Vector3d& mu, const Eigen::Vector3d& r_vec, double r,
91 const EwaldRealSpaceInteractor::BFunctions& b) {
92 ScreenedPotentialField result;
93 const double mu_dot_r = mu.dot(r_vec);
94
95 // phi(r) = q*B0 + (mu . r_vec)*B1
96 result.phi = q * b.B0 + mu_dot_r * b.B1;
97
98 // E(r) = -grad(phi) = q*r_vec*B1 + [(mu.r_vec)*r_vec*B2 - mu*B1]
99 // Note: B2 (from ComputeB's recursion) already carries the factor of
100 // (2l-1)=3 for l=2, so the r_vec*r_vec dyadic term is (mu.r_vec)*B2,
101 // not 3*(mu.r_vec)*B2 -- an earlier version of this file double-counted
102 // that factor of 3 and was caught by the dipole-dipole cross-check in
103 // test_ewaldrealspaceinteractor.cc.
104 result.field = q * b.B1 * r_vec + mu_dot_r * b.B2 * r_vec - b.B1 * mu;
105 (void)r;
106 return result;
107}
108
109} // namespace
110
111template <class T, enum Estatic CE>
113 const T& site1, PolarSite& site2,
114 const Eigen::Vector3d& source_shift) const {
115 const Eigen::Vector3d r_vec =
116 site2.getPos() - (site1.getPos() + source_shift);
117 const double r = r_vec.norm();
118 const BFunctions b = ComputeB(r);
119
120 const double q1 = site1.getCharge();
121 const Eigen::Vector3d mu1 = site1.getStaticDipole();
122 const ScreenedPotentialField src = EvaluateSource(q1, mu1, r_vec, r, b);
123
124 if (CE == Estatic::noE_V) {
125 site2.V_noE() += src.field;
126 } else {
127 site2.V() += src.field;
128 }
129
130 const double q2 = site2.getCharge();
131 const Eigen::Vector3d mu2 = site2.getStaticDipole();
132 // U = q2*phi1(r2) - mu2.E1(r2)
133 return q2 * src.phi - mu2.dot(src.field);
134}
135
136template <class T, enum Estatic CE>
138 const T& site1, PolarSite& site2,
139 const Eigen::Vector3d& source_shift) const {
140 const Eigen::Vector3d r_vec =
141 site2.getPos() - (site1.getPos() + source_shift);
142 const double r = r_vec.norm();
143
144 // Coincident sites. The erf-screened functions are singular at r = 0
145 // in form only: erf(ar)/r tends to 2a/sqrt(pi), and the dipole field
146 // tends to the analytic Ewald self-term below. This case is reached
147 // whenever the correction is applied to a foreground copy that
148 // contains the target itself, which happens for every target -- so it
149 // is the common path, not an edge case. Legacy guards it identically
150 // (EwdInteractor::FP12_ERF_At_By / FU12_ERF_At_By, R1 < 1e-2 branch),
151 // with the same 4/3 * alpha^3 / sqrt(pi) coefficient that
152 // EwaldReciprocalSpaceSum::SelfFieldMatrix returns.
153 //
154 // A charge contributes no field to itself (no direction), so only the
155 // dipole term survives.
156 if (r < kCoincidenceTol) {
157 const double self_coeff = (4.0 / 3.0) * alpha_ * alpha_ * alpha_ /
158 std::sqrt(votca::tools::conv::Pi);
159 // SIGN: this branch must be the r -> 0 limit of src.field below, so
160 // that the "-=" means the same on both sides of kCoincidenceTol.
161 // That limit is NEGATIVE -- EvaluateSource's dipole term is -B1*mu
162 // and B1_erf -> +4/3 alpha^3/sqrt(pi) -- so the field tends to
163 // -self_coeff*mu. The other sign makes the correction jump by
164 // 2*self_coeff*mu across the threshold, which no continuous
165 // function does.
166 const Eigen::Vector3d self_field = -self_coeff * site1.getStaticDipole();
167 if (CE == Estatic::noE_V) {
168 site2.V_noE() -= self_field;
169 } else {
170 site2.V() -= self_field;
171 }
172 return;
173 }
174 const BFunctions b = ComputeErfB(r);
175
176 const double q1 = site1.getCharge();
177 const Eigen::Vector3d mu1 = site1.getStaticDipole();
178 const ScreenedPotentialField src = EvaluateSource(q1, mu1, r_vec, r, b);
179
180 // Subtracted, not added -- see this method's own header documentation
181 // (and legacy EwdInteractor::FP12_ERF_At_By's own explicit "Note the
182 // (-)" comment) for why: this removes the reciprocal-space leak for
183 // this intramolecular pair, it does not add a genuine field
184 // contribution of its own.
185 if (CE == Estatic::noE_V) {
186 site2.V_noE() -= src.field;
187 } else {
188 site2.V() -= src.field;
189 }
190}
191
192template <enum Estatic CE>
194 const PolarSite& site1, PolarSite& site2,
195 const Eigen::Vector3d& source_shift) const {
196 const Eigen::Vector3d r_vec =
197 site2.getPos() - (site1.getPos() + source_shift);
198 const double r = r_vec.norm();
199
200 // Coincident sites. The erf-screened functions are singular at r = 0
201 // in form only: erf(ar)/r tends to 2a/sqrt(pi), and the dipole field
202 // tends to the analytic Ewald self-term below. This case is reached
203 // whenever the correction is applied to a foreground copy that
204 // contains the target itself, which happens for every target -- so it
205 // is the common path, not an edge case. Legacy guards it identically
206 // (EwdInteractor::FP12_ERF_At_By / FU12_ERF_At_By, R1 < 1e-2 branch),
207 // with the same 4/3 * alpha^3 / sqrt(pi) coefficient that
208 // EwaldReciprocalSpaceSum::SelfFieldMatrix returns.
209 //
210 // A charge contributes no field to itself (no direction), so only the
211 // dipole term survives.
212 if (r < kCoincidenceTol) {
213 const double self_coeff = (4.0 / 3.0) * alpha_ * alpha_ * alpha_ /
214 std::sqrt(votca::tools::conv::Pi);
215 // SIGN: negative, as in ApplyErfStaticFieldCorrection's branch.
216 //
217 // This is the one of the four coincidence branches that is live for
218 // a rank-0 system -- the other three contract against a STATIC
219 // dipole, zero when the .mps carry only charges -- and it was the
220 // whole of an alpha^3 drift in the delivered induced field, measured
221 // at alpha^2.9 over alpha = 1...4 1/nm on an 18-segment job and flat
222 // once the background's induced dipoles were switched off.
223 // EwaldPeriodicDipoleOperator::multiply has this sign right (it
224 // forms V + S*mu), which is why the background solve was already
225 // alpha-independent while this path was not.
226 const Eigen::Vector3d self_field = -self_coeff * site1.getInducedDipole();
227 if (CE == Estatic::noE_V) {
228 site2.V_noE() -= self_field;
229 } else {
230 site2.V() -= self_field;
231 }
232 return;
233 }
234 const BFunctions b = ComputeErfB(r);
235
236 // Charge deliberately zero: this removes the field of site1's INDUCED
237 // dipole only. Its permanent multipoles are handled by
238 // ApplyErfStaticFieldCorrection, and passing them here too would
239 // remove them twice.
240 //
241 // No ComputeThole call -- see this method's own header documentation.
242 const ScreenedPotentialField src =
243 EvaluateSource(0.0, site1.getInducedDipole(), r_vec, r, b);
244
245 if (CE == Estatic::noE_V) {
246 site2.V_noE() -= src.field;
247 } else {
248 site2.V() -= src.field;
249 }
250}
251
252template <enum Estatic CE>
254 const PolarSite& site1, PolarSite& site2,
255 const Eigen::Vector3d& source_shift) const {
256 const Eigen::Vector3d r_vec =
257 site2.getPos() - (site1.getPos() + source_shift);
258 const double r = r_vec.norm();
259 const BFunctions b = ComputeB(r);
260 const BFunctions berf = ComputeErfB(r);
261 const TholeFactors t = ComputeThole(r, site1, site2);
262
263 const Eigen::Vector3d mu1 = site1.getInducedDipole();
264 const double mu_dot_r = mu1.dot(r_vec);
265
266 // Thole damping combines as l3*B1 + l5*B2 (legacy's rule). B2 already
267 // carries its (2l-1)=3 recursion factor -- see EvaluateSource.
268 //
269 // WHAT IS DAMPED: the BARE interaction, not the erfc-screened one.
270 // Thole damping is physics (overlap of two smeared densities, so the
271 // real interaction is l*T_bare); the Ewald split is bookkeeping, with
272 // alpha carrying no physical content. So l*T_bare is the object to be
273 // split, and since the reciprocal sum contributes an UNDAMPED B_erf,
274 // this half must supply
275 //
276 // l*B_bare - B_erf == l*B_erfc + (l-1)*B_erf
277 //
278 // l*B_erfc alone sums to l*T_bare + (1-l)*erf(ar)/r, which depends on
279 // alpha -- not a split at all. Written in the second form so l = 1 is
280 // visibly unchanged and no large bare terms cancel at small r.
281 const double c3 = t.l3 * b.B1 + (t.l3 - 1.0) * berf.B1;
282 const double c5 = t.l5 * b.B2 + (t.l5 - 1.0) * berf.B2;
283 const Eigen::Vector3d field = mu_dot_r * c5 * r_vec - c3 * mu1;
284
285 if (CE == Estatic::noE_V) {
286 site2.V_noE() += field;
287 } else {
288 site2.V() += field;
289 }
290
291 const Eigen::Vector3d mu2 = site2.getInducedDipole();
292 return -mu2.dot(field);
293}
294
295template <class S1, class S2>
297 const S1& site1, const S2& site2,
298 const Eigen::Vector3d& source_shift) const {
299 const Eigen::Vector3d r_vec =
300 site2.getPos() - (site1.getPos() + source_shift);
301 const double r = r_vec.norm();
302 const BFunctions b = ComputeB(r);
303
304 const double q1 = site1.getCharge();
305 const Eigen::Vector3d mu1 = site1.getStaticDipole();
306 const ScreenedPotentialField src = EvaluateSource(q1, mu1, r_vec, r, b);
307
308 const double q2 = site2.getCharge();
309 const Eigen::Vector3d mu2 = site2.getStaticDipole();
310 return q2 * src.phi - mu2.dot(src.field);
311}
312
313template <class S1, class S2>
315 const S1& site1, const S2& site2,
316 const Eigen::Vector3d& source_shift) const {
317 const Eigen::Vector3d r_vec =
318 site2.getPos() - (site1.getPos() + source_shift);
319 const double r = r_vec.norm();
320
321 const double q1 = site1.getCharge();
322 const Eigen::Vector3d mu1 = site1.getStaticDipole();
323 const double q2 = site2.getCharge();
324 const Eigen::Vector3d mu2 = site2.getStaticDipole();
325
326 // Coincident sites. As in the field corrections, the erf-screened
327 // functions are singular in form only at r = 0: erf(ar)/r tends to
328 // 2a/sqrt(pi), and the dipole-dipole term to the analytic Ewald
329 // self-value. Reached for every foreground target, since each sits
330 // inside a copy that is being removed.
331 if (r < kCoincidenceTol) {
332 const double sqrt_pi = std::sqrt(votca::tools::conv::Pi);
333 const double phi_self = 2.0 * alpha_ / sqrt_pi;
334 const double dip_self = (4.0 / 3.0) * alpha_ * alpha_ * alpha_ / sqrt_pi;
335 // Charge-charge through the self-potential, dipole-dipole through
336 // the self-field. The cross terms vanish: a charge produces no field
337 // at its own position, and a dipole no potential.
338 //
339 // SIGN: the general branch returns q2*src.phi - mu2.src.field, and
340 // src.field tends to -dip_self*mu1 (see
341 // ApplyErfStaticFieldCorrection's coincidence branch), so the
342 // dipole-dipole piece enters with a PLUS here. Inert while the .mps
343 // files are rank 0, since mu1 and mu2 are then both zero.
344 return q2 * q1 * phi_self + dip_self * mu2.dot(mu1);
345 }
346
347 const BFunctions b = ComputeErfB(r);
348 const ScreenedPotentialField src = EvaluateSource(q1, mu1, r_vec, r, b);
349 return q2 * src.phi - mu2.dot(src.field);
350}
351
353 const PolarSite& site1, const PolarSite& site2,
354 const Eigen::Vector3d& source_shift) const {
355 // See this method's own declaration. Mirrors CalcErfStaticEnergy, with
356 // the source's INDUCED dipole in place of its permanent moments and no
357 // Thole damping.
358 const Eigen::Vector3d r_vec =
359 site2.getPos() - (site1.getPos() + source_shift);
360 const double r = r_vec.norm();
361
362 const Eigen::Vector3d mu1 = site1.getInducedDipole();
363 const double q2 = site2.getCharge();
364 const Eigen::Vector3d mu2 = site2.getStaticDipole();
365
366 if (r < kCoincidenceTol) {
367 const double sqrt_pi = std::sqrt(votca::tools::conv::Pi);
368 const double dip_self = (4.0 / 3.0) * alpha_ * alpha_ * alpha_ / sqrt_pi;
369 // A dipole produces no potential at its own position, so the
370 // charge-dipole cross term drops; only dipole-dipole survives.
371 //
372 // SIGN: plus, matching the r -> 0 limit of the general branch below
373 // (see CalcErfStaticEnergy's own coincidence branch). Inert at rank
374 // 0, since mu2 is the TARGET's static dipole.
375 return dip_self * mu2.dot(mu1);
376 }
377
378 const BFunctions b = ComputeErfB(r);
379 const ScreenedPotentialField src = EvaluateSource(0.0, mu1, r_vec, r, b);
380 return q2 * src.phi - mu2.dot(src.field);
381}
382
384 const PolarSite& site1, const PolarSite& site2) const {
385 const Eigen::Vector3d r_vec = site2.getPos() - site1.getPos();
386 const double r = r_vec.norm();
387 const BFunctions b = ComputeB(r);
388 const BFunctions berf = ComputeErfB(r);
389 const TholeFactors t = ComputeThole(r, site1, site2);
390
391 const Eigen::Vector3d mu1 = site1.getInducedDipole();
392 const Eigen::Vector3d mu2 = site2.getInducedDipole();
393 const double mu1_dot_r = mu1.dot(r_vec);
394
395 // Same damped-bare-minus-erf combination as ApplyInducedField; see the
396 // long note there for why l multiplies the bare interaction and not the
397 // erfc-screened one. This method has no production caller at present
398 // (only tests), and is converted with the others so that a future
399 // caller does not inherit a convention the rest of the class has left
400 // behind.
401 const double c3 = t.l3 * b.B1 + (t.l3 - 1.0) * berf.B1;
402 const double c5 = t.l5 * b.B2 + (t.l5 - 1.0) * berf.B2;
403 const Eigen::Vector3d field1 = mu1_dot_r * c5 * r_vec - c3 * mu1;
404 return -mu2.dot(field1);
405}
406
408 const PolarSite& site1, const PolarSite& site2,
409 const Eigen::Vector3d& source_shift, bool damp) const {
410 // See this method's own declaration for what this term is, why it
411 // needs a home of its own, why it is damped by default, and why a
412 // field-evaluation point asks for damp = false rather than being
413 // handed a zero polarizability.
414 const Eigen::Vector3d r_vec =
415 site2.getPos() - (site1.getPos() + source_shift);
416 const double r = r_vec.norm();
417 const BFunctions b = ComputeB(r);
418 const BFunctions berf = ComputeErfB(r);
419 const TholeFactors t =
420 damp ? ComputeThole(r, site1, site2) : TholeFactors{1.0, 1.0};
421
422 const Eigen::Vector3d mu1 = site1.getInducedDipole();
423 const double mu_dot_r = mu1.dot(r_vec);
424
425 // Potential and field of a screened, damped point dipole. The field
426 // is character-for-character ApplyInducedField's own expression -- see
427 // the long note there for why the damping multiplies the BARE
428 // interaction and the undamped erf piece is subtracted back off. The
429 // potential is its r^-3 partner, phi = (mu . r_vec) * B1, carrying the
430 // same c3 that the field's mu term does. Writing them together here
431 // keeps the two from drifting apart.
432 const double c3 = t.l3 * b.B1 + (t.l3 - 1.0) * berf.B1;
433 const double c5 = t.l5 * b.B2 + (t.l5 - 1.0) * berf.B2;
434 const double phi = c3 * mu_dot_r;
435 const Eigen::Vector3d field = mu_dot_r * c5 * r_vec - c3 * mu1;
436
437 // Target's PERMANENT moments only: its induced dipole is PolarRegion's
438 // business, via the field this code separately delivers.
439 const double q2 = site2.getCharge();
440 const Eigen::Vector3d mu2 = site2.getStaticDipole();
441 return q2 * phi - mu2.dot(field);
442}
443
444// Explicit instantiations for the source types actually used.
445template double
447 const StaticSite&, PolarSite&, const Eigen::Vector3d&) const;
448template double
450 const StaticSite&, PolarSite&, const Eigen::Vector3d&) const;
451template double
453 const PolarSite&, PolarSite&, const Eigen::Vector3d&) const;
454template double
456 const PolarSite&, PolarSite&, const Eigen::Vector3d&) const;
457
460 const Eigen::Vector3d&) const;
463 const Eigen::Vector3d&) const;
466 const Eigen::Vector3d&) const;
469 const Eigen::Vector3d&) const;
470
472 Estatic::V>(const PolarSite&, PolarSite&, const Eigen::Vector3d&) const;
474 Estatic::noE_V>(const PolarSite&, PolarSite&, const Eigen::Vector3d&) const;
475
477 const PolarSite&, PolarSite&, const Eigen::Vector3d&) const;
479 const PolarSite&, PolarSite&, const Eigen::Vector3d&) const;
480
481template double
483 const StaticSite&, const StaticSite&, const Eigen::Vector3d&) const;
484template double
486 const StaticSite&, const PolarSite&, const Eigen::Vector3d&) const;
487template double
489 const PolarSite&, const StaticSite&, const Eigen::Vector3d&) const;
490template double
492 const PolarSite&, const PolarSite&, const Eigen::Vector3d&) const;
493
494template double
496 const StaticSite&, const StaticSite&, const Eigen::Vector3d&) const;
497template double
499 const StaticSite&, const PolarSite&, const Eigen::Vector3d&) const;
500template double
502 const PolarSite&, const StaticSite&, const Eigen::Vector3d&) const;
503template double
505 const PolarSite&, const PolarSite&, const Eigen::Vector3d&) const;
506
507} // namespace xtp
508} // namespace votca
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
double getSqrtInvEigenDamp() const
Definition polarsite.h:61
const Eigen::Vector3d & V_noE() const
Definition polarsite.h:72
Eigen::Vector3d getStaticDipole() const final
Definition polarsite.cc:58
const Eigen::Vector3d & V() const
Definition polarsite.h:68
Eigen::Vector3d getInducedDipole() const final
Definition polarsite.cc:60
Class to represent Atom/Site in electrostatic.
Definition staticsite.h:37
const Eigen::Vector3d & getPos() const
Definition staticsite.h:80
double getCharge() const
Definition staticsite.h:122
const double Pi
Definition constants.h:36
Charge transport classes.
Definition ERIs.h:28
Provides a means for comparing floating point numbers.
Definition basebead.h:33