votca 2026-dev
Loading...
Searching...
No Matches
ewaldperiodicdipoleoperator.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 <array>
23#include <chrono>
24#include <fstream>
25#include <stdexcept>
26
27// Local VOTCA includes
29
30namespace votca {
31namespace xtp {
32
34 EwaldRegistry& registry, const EwaldRealSpaceSum& real_sum,
35 const EwaldReciprocalSpaceSum& recip_sum, const EwaldShapeCorrection& shape,
36 std::vector<Index> ids, double alpha_ewald, double thole_a)
37 : registry_(registry),
38 real_sum_(real_sum),
39 recip_sum_(recip_sum),
40 shape_(shape),
41 ids_(std::move(ids)),
42 intra_interactor_(alpha_ewald, thole_a),
43 self_field_matrix_(recip_sum.SelfFieldMatrix()) {
44 offsets_.reserve(ids_.size() + 1);
45 offsets_.push_back(0);
46 for (Index id : ids_) {
48 throw std::runtime_error(
49 "EwaldPeriodicDipoleOperator: segment id not registered at "
50 "EwaldChargeState::Neutral");
51 }
52 Index n_sites = registry_.Get(id, EwaldChargeState::Neutral).size();
53 offsets_.push_back(offsets_.back() + 3 * n_sites);
54 }
55 size_ = offsets_.back();
56 // Build every target's real-space neighbour list up front. RawMultiply
57 // parallelizes over targets and the cache is written only while a list
58 // is being built, so this pass makes that loop race-free. See
59 // EwaldRealSpaceSum::PrepareNeighborCache.
60 {
61 std::vector<std::pair<Index, PolarSite*>> cache_targets;
62 cache_targets.reserve(std::size_t(size_ / 3));
63 for (std::size_t n = 0; n < ids_.size(); ++n) {
65 for (Index s = 0; s < segment.size(); ++s) {
66 cache_targets.push_back({ids_[n], &segment[s]});
67 }
68 }
69 real_sum_.PrepareNeighborCache(cache_targets, EwaldChargeState::Neutral);
70 }
71
72 // RawMultiply(v) contains a v-independent field from registered segments
73 // not in ids_; it is captured once here and subtracted in multiply() so
74 // that multiply(0) = 0.
75 baseline_ = RawMultiply(Eigen::VectorXd::Zero(size_));
76}
77
78std::pair<Index, Index> EwaldPeriodicDipoleOperator::LocateSite(Index i) const {
79 // upper_bound finds the first offset strictly greater than i; the
80 // segment i belongs to is the one just before that.
81 auto it = std::upper_bound(offsets_.begin(), offsets_.end(), i);
82 Index seg_idx = Index(it - offsets_.begin()) - 1;
83 Index site_idx = (i - offsets_[std::size_t(seg_idx)]) / 3;
84 return {seg_idx, site_idx};
85}
86
88 auto [seg1, site1] = LocateSite(i);
89 auto [seg2, site2] = LocateSite(j);
90 Index xyz1 = (i - offsets_[std::size_t(seg1)]) % 3;
91 Index xyz2 = (j - offsets_[std::size_t(seg2)]) % 3;
92
93 if (seg1 == seg2 && site1 == site2) {
94 const PolarSegment& segment =
95 registry_.Get(ids_[std::size_t(seg1)], EwaldChargeState::Neutral);
96 return segment[site1].getPInv()(xyz1, xyz2);
97 }
98 // Cross-site entries (including distinct sites of one segment) are not
99 // used by the diagonal preconditioner and are not computed.
100 return 0.0;
101}
102
104 const Eigen::VectorXd& v) const {
105 // Phase timers (see RawMultiplyTimings); negligible overhead compared
106 // with the pair evaluations of one matvec.
107 using clock = std::chrono::steady_clock;
108 auto clk = []() { return clock::now(); };
109 auto secs = [](clock::time_point a, clock::time_point b) {
110 return std::chrono::duration<double>(b - a).count();
111 };
112 ++timings_.n_calls;
113 auto t_phase = clk();
114
115 std::vector<std::pair<Index, PolarSite*>> targets;
116 targets.reserve(std::size_t(size_ / 3));
117
118 for (std::size_t n = 0; n < ids_.size(); ++n) {
120 Index base = offsets_[n];
121 for (Index s = 0; s < segment.size(); ++s) {
122 PolarSite& site = segment[s];
123 site.setInduced_Dipole(v.segment<3>(base + 3 * s));
124 site.Reset();
125 targets.push_back({ids_[n], &site});
126 }
127 }
128
129 timings_.setup += secs(t_phase, clk());
130 t_phase = clk();
131
132 // Parallel over targets: each iteration writes only its own target, and
133 // the neighbour cache was fully built in the constructor, so AddFieldAt
134 // does not modify real_sum_.
135 //
136 // include_static = false: the permanent-multipole field is independent
137 // of v, so it is identical here and in baseline_ = RawMultiply(0), and
138 // cancels exactly in multiply().
139 {
140 const Index n_targets = Index(targets.size());
141#pragma omp parallel for schedule(dynamic, 16)
142 for (Index i = 0; i < n_targets; ++i) {
143 real_sum_.AddFieldAt<Estatic::V>(targets[std::size_t(i)].first,
144 *targets[std::size_t(i)].second,
146 }
147 }
148 timings_.real_space += secs(t_phase, clk());
149 t_phase = clk();
150
151 // The reciprocal-space sum excludes nothing, so it needs no segment ids.
152 std::vector<PolarSite*> recip_targets;
153 recip_targets.reserve(targets.size());
154 for (const auto& entry : targets) {
155 recip_targets.push_back(entry.second);
156 }
157 recip_sum_.AddFieldAtMany<Estatic::V>(recip_targets,
159 timings_.reciprocal += secs(t_phase, clk());
160 t_phase = clk();
161
162 // Shape/surface correction, applied every induction iteration as in
163 // legacy (FU12_ShapeField_At_By). EwaldShapeCorrection::TotalDipoleMoment
164 // includes charges, static and induced dipoles; the v-independent part
165 // cancels against baseline_.
166 //
167 // The field -(4*pi/3V)*M is the same at every site, so it is computed
168 // once with a probe site rather than per target (which would be
169 // O(N^2); 0.45 s per matvec for 5000 sites).
170 {
171 PolarSite shape_probe(-1, "X", Eigen::Vector3d::Zero());
172 shape_.AddFieldAt<Estatic::V>(shape_probe, EwaldChargeState::Neutral);
173 const Eigen::Vector3d shape_field = shape_probe.V();
174 for (const auto& entry : targets) {
175 entry.second->V() += shape_field;
176 }
177 }
178
179 timings_.shape += secs(t_phase, clk());
180 t_phase = clk();
181
182 Eigen::VectorXd result(size_);
183 for (std::size_t n = 0; n < ids_.size(); ++n) {
184 const PolarSegment& segment =
186 Index base = offsets_[n];
187 for (Index s = 0; s < segment.size(); ++s) {
188 const PolarSite& site = segment[s];
189 // The system is mu = P*(F_perm + FU(mu)), i.e. (P^-1 - C)*mu = F_perm
190 // with C*mu = FU the induced-dipole field, so the coupling enters
191 // with a minus sign. site.V() contains the spurious reciprocal-space
192 // self-field -M*v_i; adding M*v_i to the field (hence -M*v_i here)
193 // removes it, as legacy's self-interaction correction does.
194 result.segment<3>(base + 3 * s) =
195 site.getPInv() * v.segment<3>(base + 3 * s) - site.V() -
196 self_field_matrix_ * v.segment<3>(base + 3 * s);
197 }
198 }
199 // Intramolecular coupling is part of C and is subtracted as well;
200 // AddIntraSegmentCoupling itself returns the field, not the operator
201 // block.
202 timings_.assemble += secs(t_phase, clk());
203 t_phase = clk();
204
205 Eigen::VectorXd intra = Eigen::VectorXd::Zero(size_);
206 AddIntraSegmentCoupling(v, intra);
207 result -= intra;
208
209 timings_.intra += secs(t_phase, clk());
210 return result;
211}
212
214 const Eigen::VectorXd& v, Eigen::VectorXd& result) const {
215 // Induced-induced intramolecular pairs are Thole damped, as in legacy
216 // FU12_ERFC_At_By (which damps intra- and intermolecular pairs alike).
217 // This differs from the permanent field, where legacy has no real-space
218 // intramolecular term (see ApplyErfStaticFieldCorrection).
219 // Each segment writes only its own slice of result.
220#pragma omp parallel for schedule(dynamic, 4)
221 for (Index nn = 0; nn < Index(ids_.size()); ++nn) {
222 const std::size_t n = std::size_t(nn);
223 const PolarSegment& segment =
225 Index n_sites = segment.size();
226 if (n_sites < 2) {
227 continue;
228 }
229 Index base = offsets_[n];
230 for (Index i = 0; i < n_sites; ++i) {
231 const PolarSite& site_i = segment[i];
232 for (Index j = i + 1; j < n_sites; ++j) {
233 const PolarSite& site_j = segment[j];
234 // r_vec points from site_j (source) to site_i (target), as in
235 // ApplyInducedField (r_vec = site2 - site1, site1 = source).
236 const Eigen::Vector3d r_vec = site_i.getPos() - site_j.getPos();
237 const double r = r_vec.norm();
239 intra_interactor_.ComputeB(r);
241 intra_interactor_.ComputeErfB(r);
243 intra_interactor_.ComputeThole(r, site_j, site_i);
244 // Same l3*B1 / l5*B2 combination as ApplyInducedField (B2
245 // already carries the factor 3). l3/l5 are symmetric in the two
246 // sites, so block is symmetric.
247 //
248 // The pair is excluded from real_sum_ but contained, undamped, in
249 // recip_sum_ (B_erf). The total must be the alpha-independent
250 // l*B_bare, so this term supplies
251 // l*B_bare - B_erf == l*B_erfc + (l-1)*B_erf.
252 // The second form avoids cancellation of large bare terms at
253 // small r. Using only l*B_erfc caused a 0.43% alpha dependence
254 // of the converged dipoles (alpha = 1.5 ... 3.0 1/nm).
255 const double c3 = t.l3 * b.B1 + (t.l3 - 1.0) * berf.B1;
256 const double c5 = t.l5 * b.B2 + (t.l5 - 1.0) * berf.B2;
257 Eigen::Matrix3d block =
258 c5 * (r_vec * r_vec.transpose()) - c3 * Eigen::Matrix3d::Identity();
259 result.segment<3>(base + 3 * i) += block * v.segment<3>(base + 3 * j);
260 result.segment<3>(base + 3 * j) +=
261 block.transpose() * v.segment<3>(base + 3 * i);
262 }
263 }
264 }
265}
266
268 const Eigen::VectorXd& v) const {
269 return RawMultiply(v) - baseline_;
270}
271
272} // namespace xtp
273} // namespace votca
std::pair< Index, Index > LocateSite(Index i) const
void AddIntraSegmentCoupling(const Eigen::VectorXd &v, Eigen::VectorXd &result) const
Eigen::VectorXd RawMultiply(const Eigen::VectorXd &v) const
EwaldPeriodicDipoleOperator(EwaldRegistry &registry, const EwaldRealSpaceSum &real_sum, const EwaldReciprocalSpaceSum &recip_sum, const EwaldShapeCorrection &shape, std::vector< Index > ids, double alpha_ewald, double thole_a)
Eigen::VectorXd multiply(const Eigen::VectorXd &v) const
Class to represent Atom/Site in electrostatic+polarization.
Definition polarsite.h:36
void setInduced_Dipole(const Eigen::Vector3d &induced_dipole)
Definition polarsite.h:122
const Eigen::Matrix3d & getPInv() const
Definition polarsite.h:54
const Eigen::Vector3d & V() const
Definition polarsite.h:68
const Eigen::Vector3d & getPos() const
Definition staticsite.h:80
STL namespace.
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