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