votca 2026-dev
Loading...
Searching...
No Matches
ewaldreciprocalspacesum.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 <cmath>
23#include <vector>
24
25// Local VOTCA includes
27
28namespace votca {
29namespace xtp {
30
31namespace {
32constexpr double kPi = 3.14159265358979323846;
33} // namespace
34
36 const EwaldRegistry& registry,
37 double alpha, double k_max)
38 : box_(box),
39 volume_(box.col(0).dot(box.col(1).cross(box.col(2)))),
40 registry_(registry),
41 alpha_(alpha),
42 k_max_(k_max) {
44}
45
47 // BUG FIX (this session): this used to evaluate the discrete k-lattice
48 // sum (4*pi/V) * sum_{k!=0} exp(-k^2/4*alpha^2)/k^2 * k k^T over this
49 // class's own k-vector set, on the reasoning that doing so reproduces
50 // exactly the self-term this class's own sum numerically produces.
51 // That reasoning is wrong, for a reason that has nothing to do with
52 // k_max: evaluated at r = 0, that sum is the erf-screened field a site
53 // feels from itself AND from every one of its own periodic images. Only
54 // the n = 0 piece is the spurious self-interaction that wants removing;
55 // the n != 0 image terms are real physical interactions that must be
56 // kept. Subtracting the whole lattice sum therefore removes genuine
57 // physics along with the artifact.
58 //
59 // The quantity actually wanted is the r -> 0 limit of the erf-screened
60 // dipole field, i.e. the standard analytic Ewald self-term below, which
61 // is what legacy applies too (EwdInteractor::FU12_ERF_At_By's own
62 // R1 < 1e-2 branch: 4/3 * a3 * rSqrtPi * U1). An earlier version of
63 // this comment claimed, from a trace of legacy's reciprocal-space code,
64 // that legacy applies no self-correction at all -- that was simply
65 // wrong: legacy applies it in real space, as a separate "atomic ERF
66 // self-interaction correction" pass (PolarBackground's own step (5)),
67 // which a reciprocal-space-only trace never reaches.
68 //
69 // The two differ badly at realistic box sizes, and NOT by a small
70 // amount that tightening k_max would fix. The lattice sum approaches
71 // the analytic value only when the k-lattice resolves the Gaussian
72 // exp(-k^2/4*alpha^2); with spacing 2*pi/L against width 2*alpha that
73 // needs L*alpha/pi >> 1. For a measured case (L = 66.1 bohr, alpha =
74 // 0.1058 bohr^-1) there are only ~2.2 k-points per Gaussian width and
75 // the lattice sum comes out 1.63% low -- which matched, to five
76 // digits, a directly measured 1.63% deficit in this term against
77 // legacy across 5000 sites, with the small residual anisotropy of the
78 // discrete sum showing up as a cos(angle) of 0.999993 rather than 1.
79 //
80 // Isotropic by construction, so no anisotropy artifact remains.
81 const double self_term =
82 (4.0 / 3.0) * alpha_ * alpha_ * alpha_ / std::sqrt(kPi);
83 return self_term * Eigen::Matrix3d::Identity();
84}
85
86std::vector<EwaldReciprocalSpaceSum::KVector>
88 // Reciprocal lattice vectors: columns of 2*pi*(box^-1)^T, the standard
89 // dual basis (b_i . a_j = 2*pi*delta_ij).
90 const Eigen::Matrix3d recip = 2.0 * kPi * box_.inverse().transpose();
91 const Eigen::Vector3d b1 = recip.col(0);
92 const Eigen::Vector3d b2 = recip.col(1);
93 const Eigen::Vector3d b3 = recip.col(2);
94
95 // Generous per-axis bound: enough reciprocal-lattice steps along each
96 // primitive direction to reach k_max even along the shortest axis.
97 const Index n1 = Index(std::ceil(k_max_ / b1.norm())) + 1;
98 const Index n2 = Index(std::ceil(k_max_ / b2.norm())) + 1;
99 const Index n3 = Index(std::ceil(k_max_ / b3.norm())) + 1;
100
101 std::vector<KVector> kvecs;
102 for (Index i1 = -n1; i1 <= n1; ++i1) {
103 for (Index i2 = -n2; i2 <= n2; ++i2) {
104 for (Index i3 = -n3; i3 <= n3; ++i3) {
105 if (i1 == 0 && i2 == 0 && i3 == 0) {
106 continue; // k=0 term is handled separately (uniform background
107 // / net-charge correction), not part of this sum.
108 }
109 Eigen::Vector3d k = double(i1) * b1 + double(i2) * b2 + double(i3) * b3;
110 double k2 = k.squaredNorm();
111 if (k2 <= k_max_ * k_max_) {
112 kvecs.push_back({k, k2});
113 }
114 }
115 }
116 }
117 return kvecs;
118}
119
120std::vector<std::complex<double>>
122 EwaldChargeState source_state, const ProgressCallback& progress) const {
123 std::vector<std::complex<double>> S(kvectors_.size(),
124 std::complex<double>(0.0, 0.0));
125
126 // Flatten every source site into contiguous arrays first. Two reasons:
127 // the k-vector loop below is parallelized over k rather than over
128 // sites, so it needs random access to the site data rather than a
129 // nested registry walk; and streaming three packed arrays is far
130 // friendlier to cache than chasing segment objects through a std::map
131 // for each of ~1e4 k-vectors.
132 std::vector<double> q_flat;
133 std::vector<Eigen::Vector3d> mu_flat;
134 std::vector<Eigen::Vector3d> pos_flat;
135 for (Index source_id : registry_.AllIds()) {
136 if (!registry_.Has(source_id, source_state)) {
137 continue;
138 }
139 const PolarSegment& segment = registry_.Get(source_id, source_state);
140 for (const PolarSite& site : segment) {
141 q_flat.push_back(site.getCharge());
142 mu_flat.push_back(site.getStaticDipole() + site.getInducedDipole());
143 pos_flat.push_back(site.getPos());
144 }
145 }
146 const Index n_sites = Index(q_flat.size());
147 const Index n_k = Index(kvectors_.size());
148
149 // Parallelized over K-VECTORS, not over sites. The natural reading --
150 // sites outer, k inner -- makes S[idx] a reduction target shared by
151 // every thread, needing either atomics or per-thread copies of the
152 // whole S array. Inverting the loops gives each thread sole ownership
153 // of the S entries it writes, so no reduction, no atomics, and the
154 // sum over sites for a given k happens in a fixed order regardless of
155 // thread count -- the result is bitwise identical however many
156 // threads run it.
157 //
158 // The k-loop is chunked so the progress callback can still be invoked
159 // between chunks, from the serial region: calling it from inside the
160 // parallel loop would need the callback itself to be thread-safe,
161 // which is not part of its contract.
162 const Index chunk = std::max<Index>(1, n_k / 20);
163 for (Index k_begin = 0; k_begin < n_k; k_begin += chunk) {
164 const Index k_end = std::min(n_k, k_begin + chunk);
165#pragma omp parallel for schedule(static)
166 for (Index idx = k_begin; idx < k_end; ++idx) {
167 const Eigen::Vector3d& k = kvectors_[std::size_t(idx)].k;
168 std::complex<double> acc(0.0, 0.0);
169 for (Index n = 0; n < n_sites; ++n) {
170 const double kr = k.dot(pos_flat[std::size_t(n)]);
171 // exp(-i*kr) with |exp| == 1: std::polar avoids the redundant
172 // std::exp(0) that std::exp(std::complex) would evaluate.
173 const std::complex<double> phase = std::polar(1.0, -kr);
174 const double k_dot_mu = k.dot(mu_flat[std::size_t(n)]);
175 acc += std::complex<double>(q_flat[std::size_t(n)], -k_dot_mu) * phase;
176 }
177 S[std::size_t(idx)] = acc;
178 }
179 if (progress) {
180 progress(std::size_t(k_end), std::size_t(n_k));
181 }
182 }
183 return S;
184}
185
187 const std::vector<std::pair<const PolarSite*, Eigen::Vector3d>>& foreground,
188 const std::vector<const PolarSite*>& background_exclusions,
189 EwaldChargeState source_state) const {
190 // See this method's own declaration for the formula, for why the two
191 // structure factors are accumulated separately, and for why the
192 // exclusion list is a separate argument rather than being read off the
193 // foreground.
194
195 // Foreground moments, taken from the sites themselves and so in the
196 // job's own charge state, at the positions they actually occupy. The
197 // positions are supplied rather than read off the sites because a
198 // foreground segment sits at one particular periodic image and the
199 // phase factor must use that image's position.
200 std::vector<double> q_fg;
201 std::vector<Eigen::Vector3d> mu_fg;
202 std::vector<Eigen::Vector3d> pos_fg;
203 q_fg.reserve(foreground.size());
204 mu_fg.reserve(foreground.size());
205 pos_fg.reserve(foreground.size());
206 for (const auto& entry : foreground) {
207 const PolarSite& site = *entry.first;
208 q_fg.push_back(site.getCharge());
209 mu_fg.push_back(site.getStaticDipole());
210 pos_fg.push_back(entry.second);
211 }
212
213 // Background sites: every registered site EXCEPT the ones the caller
214 // named. Identity is by address, so a listed site is held out exactly
215 // once.
216 const std::vector<const PolarSite*>& fg_sites = background_exclusions;
217 std::vector<double> q_bg;
218 std::vector<Eigen::Vector3d> mu_bg;
219 std::vector<Eigen::Vector3d> pos_bg;
220 for (Index source_id : registry_.AllIds()) {
221 if (!registry_.Has(source_id, source_state)) {
222 continue;
223 }
224 const PolarSegment& segment = registry_.Get(source_id, source_state);
225 for (const PolarSite& site : segment) {
226 bool is_foreground = false;
227 for (const PolarSite* fg : fg_sites) {
228 if (fg == &site) {
229 is_foreground = true;
230 break;
231 }
232 }
233 if (is_foreground) {
234 continue;
235 }
236 q_bg.push_back(site.getCharge());
237 mu_bg.push_back(site.getStaticDipole());
238 pos_bg.push_back(site.getPos());
239 }
240 }
241
242 const Index n_fg = Index(q_fg.size());
243 const Index n_bg = Index(q_bg.size());
244 const Index n_k = Index(kvectors_.size());
245 const double prefactor = 4.0 * kPi / volume_;
246
247 // Parallel over k-vectors, as TotalStructureFactors is and for the
248 // same reason: each thread owns its own k and the sum over sites for a
249 // given k happens in a fixed order, so the result does not depend on
250 // thread count.
251 double energy = 0.0;
252#pragma omp parallel for schedule(static) reduction(+ : energy)
253 for (Index idx = 0; idx < n_k; ++idx) {
254 const Eigen::Vector3d& k = kvectors_[std::size_t(idx)].k;
255 const double k2 = kvectors_[std::size_t(idx)].k2;
256
257 std::complex<double> s_fg(0.0, 0.0);
258 for (Index n = 0; n < n_fg; ++n) {
259 const double kr = k.dot(pos_fg[std::size_t(n)]);
260 const std::complex<double> phase = std::polar(1.0, -kr);
261 const double k_dot_mu = k.dot(mu_fg[std::size_t(n)]);
262 s_fg += std::complex<double>(q_fg[std::size_t(n)], -k_dot_mu) * phase;
263 }
264
265 std::complex<double> s_bg(0.0, 0.0);
266 for (Index n = 0; n < n_bg; ++n) {
267 const double kr = k.dot(pos_bg[std::size_t(n)]);
268 const std::complex<double> phase = std::polar(1.0, -kr);
269 const double k_dot_mu = k.dot(mu_bg[std::size_t(n)]);
270 s_bg += std::complex<double>(q_bg[std::size_t(n)], -k_dot_mu) * phase;
271 }
272
273 const double weight = std::exp(-k2 / (4.0 * alpha_ * alpha_)) / k2;
274 energy += prefactor * weight * (std::conj(s_fg) * s_bg).real();
275 }
276 return energy;
277}
278
280 const std::vector<std::pair<const PolarSite*, Eigen::Vector3d>>& foreground,
281 const std::vector<const PolarSite*>& background_exclusions,
282 EwaldChargeState source_state) const {
283 // See this method's own declaration. Structurally identical to
284 // CalcStaticEnergyBetween above, with one difference: the background
285 // contributes its INDUCED dipoles and no charge, rather than its
286 // permanent moments.
287
288 // Foreground: permanent moments, at the positions actually occupied.
289 std::vector<double> q_fg;
290 std::vector<Eigen::Vector3d> mu_fg;
291 std::vector<Eigen::Vector3d> pos_fg;
292 q_fg.reserve(foreground.size());
293 mu_fg.reserve(foreground.size());
294 pos_fg.reserve(foreground.size());
295 for (const auto& entry : foreground) {
296 const PolarSite& site = *entry.first;
297 q_fg.push_back(site.getCharge());
298 mu_fg.push_back(site.getStaticDipole());
299 pos_fg.push_back(entry.second);
300 }
301
302 // Background: induced dipoles only. No charge term -- an induced
303 // dipole carries none, and the background's permanent charges are
304 // already accounted for by CalcStaticEnergyBetween.
305 std::vector<Eigen::Vector3d> mu_bg;
306 std::vector<Eigen::Vector3d> pos_bg;
307 for (Index source_id : registry_.AllIds()) {
308 if (!registry_.Has(source_id, source_state)) {
309 continue;
310 }
311 const PolarSegment& segment = registry_.Get(source_id, source_state);
312 for (const PolarSite& site : segment) {
313 bool is_excluded = false;
314 for (const PolarSite* skip : background_exclusions) {
315 if (skip == &site) {
316 is_excluded = true;
317 break;
318 }
319 }
320 if (is_excluded) {
321 continue;
322 }
323 mu_bg.push_back(site.getInducedDipole());
324 pos_bg.push_back(site.getPos());
325 }
326 }
327
328 const Index n_fg = Index(q_fg.size());
329 const Index n_bg = Index(mu_bg.size());
330 const Index n_k = Index(kvectors_.size());
331 const double prefactor = 4.0 * kPi / volume_;
332
333 double energy = 0.0;
334#pragma omp parallel for schedule(static) reduction(+ : energy)
335 for (Index idx = 0; idx < n_k; ++idx) {
336 const Eigen::Vector3d& k = kvectors_[std::size_t(idx)].k;
337 const double k2 = kvectors_[std::size_t(idx)].k2;
338
339 std::complex<double> s_fg(0.0, 0.0);
340 for (Index n = 0; n < n_fg; ++n) {
341 const double kr = k.dot(pos_fg[std::size_t(n)]);
342 const std::complex<double> phase = std::polar(1.0, -kr);
343 const double k_dot_mu = k.dot(mu_fg[std::size_t(n)]);
344 s_fg += std::complex<double>(q_fg[std::size_t(n)], -k_dot_mu) * phase;
345 }
346
347 std::complex<double> s_bg(0.0, 0.0);
348 for (Index n = 0; n < n_bg; ++n) {
349 const double kr = k.dot(pos_bg[std::size_t(n)]);
350 const std::complex<double> phase = std::polar(1.0, -kr);
351 const double k_dot_mu = k.dot(mu_bg[std::size_t(n)]);
352 s_bg += std::complex<double>(0.0, -k_dot_mu) * phase;
353 }
354
355 const double weight = std::exp(-k2 / (4.0 * alpha_ * alpha_)) / k2;
356 energy += prefactor * weight * (std::conj(s_fg) * s_bg).real();
357 }
358 return energy;
359}
360
361template <enum Estatic CE>
363 EwaldChargeState source_state) const {
364 std::vector<PolarSite*> targets = {&target};
365 AddFieldAtMany<CE>(targets, source_state);
366}
367
368template <enum Estatic CE>
370 const std::vector<PolarSite*>& targets, EwaldChargeState source_state,
371 const ProgressCallback& progress) const {
372 const std::vector<std::complex<double>> S =
373 TotalStructureFactors(source_state, progress);
374 const double prefactor = 4.0 * kPi / volume_;
375
376 // Parallel over targets. The structure factors S were reduced over
377 // every site above and are read-only here; each field is written to its
378 // own slot and added to the sites serially afterwards, in input order,
379 // so a site listed twice is not updated by two threads at once and the
380 // result matches the serial one. The progress callback is deliberately
381 // not invoked from inside this loop -- it is called during
382 // TotalStructureFactors, which stays serial.
383 const Index n_targets = Index(targets.size());
384 std::vector<Eigen::Vector3d> fields(targets.size());
385#pragma omp parallel for schedule(static)
386 for (Index t_i = 0; t_i < n_targets; ++t_i) {
387 const PolarSite& target = *targets[std::size_t(t_i)];
388
389 const Eigen::Vector3d r = target.getPos();
390 Eigen::Vector3d field = Eigen::Vector3d::Zero();
391
392 for (std::size_t idx = 0; idx < kvectors_.size(); ++idx) {
393 const KVector& kv = kvectors_[idx];
394 double weight = std::exp(-kv.k2 / (4.0 * alpha_ * alpha_)) / kv.k2;
395
396 // E(r) = (4*pi/V) * sum_{k!=0} (k/k^2) * exp(-k^2/4a^2) *
397 // Im[ S(k) * exp(i*k.r) ]
398 // See class documentation for the derivation.
399 const std::complex<double> phase = std::polar(1.0, kv.k.dot(r));
400 double im_part = (S[idx] * phase).imag();
401 field += prefactor * weight * im_part * kv.k;
402 }
403 fields[std::size_t(t_i)] = field;
404 }
405
406 for (std::size_t t_i = 0; t_i < targets.size(); ++t_i) {
407 if (CE == Estatic::noE_V) {
408 targets[t_i]->V_noE() += fields[t_i];
409 } else {
410 targets[t_i]->V() += fields[t_i];
411 }
412 }
413}
414
416 const std::vector<Eigen::Vector3d>& points, EwaldChargeState source_state,
417 const ProgressCallback& progress) const {
418 const std::vector<std::complex<double>> S =
419 TotalStructureFactors(source_state, progress);
420 const double prefactor = 4.0 * kPi / volume_;
421 const Index n_points = Index(points.size());
422 const std::size_t n_k = kvectors_.size();
423 Eigen::VectorXd phi = Eigen::VectorXd::Zero(n_points);
424
425 // Everything that depends on k alone, folded once. The Gaussian weight
426 // in particular is an exp() that does not vary over the points, so
427 // leaving it in the inner loop costs one transcendental per (point, k)
428 // pair -- on a DFT integration grid that is by far the most expensive
429 // thing in this routine, and none of it does any work.
430 //
431 // Kept as separate real and imaginary parts rather than std::complex so
432 // the inner loop is two multiplies against a cos/sin pair, with no
433 // complex multiply and no temporary.
434 std::vector<double> c_re(n_k);
435 std::vector<double> c_im(n_k);
436 const double inv_four_alpha2 = 1.0 / (4.0 * alpha_ * alpha_);
437 for (std::size_t idx = 0; idx < n_k; ++idx) {
438 const double weight = prefactor *
439 std::exp(-kvectors_[idx].k2 * inv_four_alpha2) /
440 kvectors_[idx].k2;
441 c_re[idx] = weight * S[idx].real();
442 c_im[idx] = weight * S[idx].imag();
443 }
444
445 // Parallel over points rather than over k, the opposite of
446 // TotalStructureFactors above: S is read-only here and each iteration
447 // owns its own accumulator, and a DFT grid has far more points than
448 // this class has k-vectors.
449#pragma omp parallel for schedule(static)
450 for (Index p = 0; p < n_points; ++p) {
451 const Eigen::Vector3d& r = points[std::size_t(p)];
452 double acc = 0.0;
453 for (std::size_t idx = 0; idx < n_k; ++idx) {
454 // Re[(c_re + i c_im) e^{i theta}] = c_re cos(theta) - c_im sin(theta).
455 // Written out rather than through std::polar and a complex multiply,
456 // which compute the same two trig calls plus four multiplies.
457 const double theta = kvectors_[idx].k.dot(r);
458 acc += c_re[idx] * std::cos(theta) - c_im[idx] * std::sin(theta);
459 }
460 phi[p] = acc;
461 }
462 return phi;
463}
464
469
471 const std::vector<PolarSite*>&, EwaldChargeState,
474 const std::vector<PolarSite*>&, EwaldChargeState,
476
477} // namespace xtp
478} // namespace votca
EwaldReciprocalSpaceSum(const Eigen::Matrix3d &box, const EwaldRegistry &registry, double alpha, double k_max)
double CalcInducedSourceEnergyBetween(const std::vector< std::pair< const PolarSite *, Eigen::Vector3d > > &foreground, const std::vector< const PolarSite * > &background_exclusions, EwaldChargeState source_state) const
double CalcStaticEnergyBetween(const std::vector< std::pair< const PolarSite *, Eigen::Vector3d > > &foreground, const std::vector< const PolarSite * > &background_exclusions, EwaldChargeState source_state) const
std::function< void(std::size_t, std::size_t)> ProgressCallback
std::vector< std::complex< double > > TotalStructureFactors(EwaldChargeState source_state, const ProgressCallback &progress) const
Eigen::VectorXd PotentialAtMany(const std::vector< Eigen::Vector3d > &points, EwaldChargeState source_state, const ProgressCallback &progress=ProgressCallback()) const
void AddFieldAtMany(const std::vector< PolarSite * > &targets, EwaldChargeState source_state, const ProgressCallback &progress=ProgressCallback()) const
void AddFieldAt(PolarSite &target, EwaldChargeState source_state) const
std::vector< KVector > GenerateKVectors() const
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
Eigen::Vector3d getStaticDipole() const final
Definition polarsite.cc:58
const Eigen::Vector3d & getPos() const
Definition staticsite.h:80
double getCharge() const
Definition staticsite.h:110
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