votca 2026-dev
Loading...
Searching...
No Matches
chargeresponsesolver.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
27
28namespace votca {
29namespace xtp {
30
31namespace {
32// symmetric square root and pseudo-inverse square root of a positive
33// semidefinite matrix; eigenvalues below rel_cutoff * largest are null
34template <class Matrix>
35void SqrtAndInvSqrt(const Matrix& A, Matrix& sqrt, Matrix& inv_sqrt,
36 double rel_cutoff = 1e-12) {
37 Eigen::SelfAdjointEigenSolver<Matrix> es(0.5 * (A + A.transpose()));
38 const auto& w = es.eigenvalues();
39 const double cutoff = rel_cutoff * std::max(w.maxCoeff(), 0.0);
40 auto s = w.array().max(0.0).sqrt().matrix().eval();
41 auto si = s.eval();
42 for (Index i = 0; i < Index(w.size()); ++i) {
43 si(i) = (w(i) > cutoff && w(i) > 0.0) ? 1.0 / s(i) : 0.0;
44 }
45 sqrt = es.eigenvectors() * s.asDiagonal() * es.eigenvectors().transpose();
46 inv_sqrt =
47 es.eigenvectors() * si.asDiagonal() * es.eigenvectors().transpose();
48}
49} // namespace
50
51ChargeResponseSolver::ChargeResponseSolver(std::vector<PolarSegment>& segments,
52 const eeInteractor& interactor)
53 : segments_(segments), interactor_(interactor) {
54 for (Index s = 0; s < Index(segments_.size()); ++s) {
55 const PolarSegment& seg = segments_[s];
56 if (seg.hasChargeResponse()) {
57 SegmentBlock block;
58 block.segment = s;
59 block.q0 = nq_;
60 SqrtAndInvSqrt(seg.getChargeResponse().Pi(), block.sqrt, block.inv_sqrt);
61 blocks_.push_back(std::move(block));
62 }
63 for (Index i = 0; i < seg.size(); ++i) {
64 if (seg.hasChargeResponse() != seg[i].isChargeResponse()) {
65 throw std::runtime_error(
66 "ChargeResponseSolver: the sites of segment " + seg.getType() +
67 " are not set up for its charge response (PolarRegion)");
68 }
69 const Index q = seg.hasChargeResponse() ? nq_++ : -1;
70 sites_.push_back({s, i, q});
71 Eigen::Matrix3d sq, isq;
72 SqrtAndInvSqrt(Eigen::Matrix3d(seg[i].getpolarization()), sq, isq);
73 alpha_sqrt_.push_back(sq);
74 alpha_inv_sqrt_.push_back(isq);
75 }
76 }
77 dim_ = nq_ + 3 * Index(sites_.size());
78}
79
81 Index a, Index b) const {
82 const SiteRef& ra = sites_[std::size_t(a)];
83 const SiteRef& rb = sites_[std::size_t(b)];
84 const PolarSite& sa = segments_[ra.segment][ra.site];
85 const PolarSite& sb = segments_[rb.segment][rb.site];
86 Coupling c;
87 if (ra.segment == rb.segment) {
88 if (ra.q >= 0) {
89 c.skip = true; // nothing inside a charge-response segment
90 return c;
91 }
92 // inside a Thole segment: the Thole couplings of its dipoles
93 c.T = interactor_.FillTholeInteraction(sa, sb);
94 return c;
95 }
96 c.T = interactor_.FillDipoleInteraction(sa, sb);
97 if (ra.q >= 0 || rb.q >= 0) {
98 c.charges = true;
99 c.r = sb.getPos() - sa.getPos();
100 const GaussianKernel k(c.r.norm(), eeInteractor::PairWidth(sa, sb));
101 c.B0 = k.B0;
102 c.B1 = k.B1;
103 }
104 return c;
105}
106
107Eigen::VectorXd ChargeResponseSolver::ApplyT(const Eigen::VectorXd& x) const {
108 const Index ns = Index(sites_.size());
109 Eigen::VectorXd result = Eigen::VectorXd::Zero(dim_);
110#pragma omp parallel for schedule(dynamic) reduction(+ : result)
111 for (Index a = 0; a < ns; ++a) {
112 const Index qa_idx = sites_[std::size_t(a)].q;
113 const Index ma = nq_ + 3 * a;
114 for (Index b = a + 1; b < ns; ++b) {
115 const Coupling c = PairCoupling(a, b);
116 if (c.skip) {
117 continue;
118 }
119 const Index qb_idx = sites_[std::size_t(b)].q;
120 const Index mb = nq_ + 3 * b;
121 // grad phi at b of the dipole at a, and vice versa
122 result.segment<3>(mb) += c.T * x.segment<3>(ma);
123 result.segment<3>(ma) += c.T.transpose() * x.segment<3>(mb);
124 if (!c.charges) {
125 continue;
126 }
127 if (qa_idx >= 0) {
128 const double qa = x(qa_idx);
129 result.segment<3>(mb) -= qa * c.B1 * c.r; // grad phi at b
130 result(qa_idx) -= c.B1 * x.segment<3>(mb).dot(c.r);
131 if (qb_idx >= 0) {
132 result(qb_idx) += qa * c.B0;
133 }
134 }
135 if (qb_idx >= 0) {
136 const double qb = x(qb_idx);
137 result.segment<3>(ma) += qb * c.B1 * c.r; // grad phi at a
138 result(qb_idx) += c.B1 * x.segment<3>(ma).dot(c.r);
139 if (qa_idx >= 0) {
140 result(qa_idx) += qb * c.B0;
141 }
142 }
143 }
144 }
145 return result;
146}
147
148Eigen::MatrixXd ChargeResponseSolver::DenseOperator() const {
149 // T as a dense matrix, entry by entry as ApplyT applies it
150 const Index ns = Index(sites_.size());
151 Eigen::MatrixXd T = Eigen::MatrixXd::Zero(dim_, dim_);
152#pragma omp parallel for schedule(dynamic)
153 for (Index a = 0; a < ns; ++a) {
154 const Index qa = sites_[std::size_t(a)].q;
155 const Index ma = nq_ + 3 * a;
156 for (Index b = a + 1; b < ns; ++b) {
157 const Coupling c = PairCoupling(a, b);
158 if (c.skip) {
159 continue;
160 }
161 const Index qb = sites_[std::size_t(b)].q;
162 const Index mb = nq_ + 3 * b;
163 // rows of a and of b are written by this thread only for this pair;
164 // block (ma, mb) and (mb, ma) belong to the pair a < b
165 T.block<3, 3>(mb, ma) = c.T;
166 T.block<3, 3>(ma, mb) = c.T.transpose();
167 if (!c.charges) {
168 continue;
169 }
170 if (qa >= 0) {
171 T.block<3, 1>(mb, qa) = -c.B1 * c.r;
172 T.block<1, 3>(qa, mb) = -c.B1 * c.r.transpose();
173 }
174 if (qb >= 0) {
175 T.block<3, 1>(ma, qb) = c.B1 * c.r;
176 T.block<1, 3>(qb, ma) = c.B1 * c.r.transpose();
177 }
178 if (qa >= 0 && qb >= 0) {
179 T(qa, qb) = c.B0;
180 T(qb, qa) = c.B0;
181 }
182 }
183 }
184 Eigen::MatrixXd A = ApplyMhalf(ApplyMhalf(T).transpose());
185 A = 0.5 * (A + A.transpose()).eval();
186 A.diagonal().array() += 1.0;
187 return A;
188}
189
191 const Eigen::MatrixXd& X) const {
192 Eigen::MatrixXd result(X.rows(), X.cols());
193 for (const SegmentBlock& block : blocks_) {
194 const Index n = Index(block.sqrt.rows());
195 result.middleRows(block.q0, n) = block.sqrt * X.middleRows(block.q0, n);
196 }
197 for (Index a = 0; a < Index(sites_.size()); ++a) {
198 const Index m = nq_ + 3 * a;
199 result.middleRows<3>(m) = alpha_sqrt_[std::size_t(a)] * X.middleRows<3>(m);
200 }
201 return result;
202}
203
204Eigen::VectorXd ChargeResponseSolver::ApplyMhalf(const Eigen::VectorXd& x,
205 bool inverse) const {
206 Eigen::VectorXd result(dim_);
207 for (const SegmentBlock& block : blocks_) {
208 const Index n = Index(block.sqrt.rows());
209 result.segment(block.q0, n) =
210 (inverse ? block.inv_sqrt : block.sqrt) * x.segment(block.q0, n);
211 }
212 for (Index a = 0; a < Index(sites_.size()); ++a) {
213 const Index m = nq_ + 3 * a;
214 result.segment<3>(m) =
215 (inverse ? alpha_inv_sqrt_[a] : alpha_sqrt_[a]) * x.segment<3>(m);
216 }
217 return result;
218}
219
220Eigen::VectorXd ChargeResponseSolver::Apply(const Eigen::VectorXd& y) const {
221 return y + ApplyMhalf(ApplyT(ApplyMhalf(y, false)), false);
222}
223
224Eigen::VectorXd ChargeResponseSolver::Sources() const {
225 Eigen::VectorXd c(dim_);
226 for (Index a = 0; a < Index(sites_.size()); ++a) {
227 const PolarSite& site = segments_[sites_[a].segment][sites_[a].site];
228 if (sites_[a].q >= 0) {
229 c(sites_[a].q) = -(site.Phi() + site.Phi_noE());
230 }
231 c.segment<3>(nq_ + 3 * a) = -(site.V() + site.V_noE());
232 }
233 return c;
234}
235
236Eigen::VectorXd ChargeResponseSolver::ReadFromSites() const {
237 Eigen::VectorXd x(dim_);
238 for (Index a = 0; a < Index(sites_.size()); ++a) {
239 const PolarSite& site = segments_[sites_[a].segment][sites_[a].site];
240 if (sites_[a].q >= 0) {
241 x(sites_[a].q) = site.Induced_Charge();
242 }
243 x.segment<3>(nq_ + 3 * a) = site.Induced_Dipole();
244 }
245 return x;
246}
247
248void ChargeResponseSolver::WriteToSites(const Eigen::VectorXd& x) {
249 for (Index a = 0; a < Index(sites_.size()); ++a) {
250 PolarSite& site = segments_[sites_[a].segment][sites_[a].site];
251 if (sites_[a].q >= 0) {
252 site.setInduced_Charge(x(sites_[a].q));
253 }
254 site.setInduced_Dipole(x.segment<3>(nq_ + 3 * a));
255 }
256}
257
259 Index max_iter,
260 bool use_previous) {
261 const Eigen::VectorXd rhs = ApplyMhalf(Sources(), false);
262 Info info;
263 const double rhs_norm = rhs.norm();
264 if (rhs_norm == 0.0) {
265 WriteToSites(Eigen::VectorXd::Zero(dim_));
266 info.converged = true;
267 return info;
268 }
269 Eigen::VectorXd y =
270 use_previous ? ApplyMhalf(ReadFromSites(), true) : Eigen::VectorXd(rhs);
271 // conjugate gradients on the symmetric positive definite operator
272 Eigen::VectorXd r = rhs - Apply(y);
273 Eigen::VectorXd p = r;
274 double rr = r.squaredNorm();
275 for (info.iterations = 0; info.iterations < max_iter; ++info.iterations) {
276 info.error = std::sqrt(rr) / rhs_norm;
277 if (info.error < tolerance) {
278 info.converged = true;
279 break;
280 }
281 const Eigen::VectorXd Ap = Apply(p);
282 const double pAp = p.dot(Ap);
283 if (!(pAp > 0.0)) {
284 throw std::runtime_error(
285 "ChargeResponseSolver: the operator 1 + M^1/2 T M^1/2 is not "
286 "positive definite (polarization catastrophe); check the widths "
287 "and polarizabilities");
288 }
289 const double alpha = rr / pAp;
290 y += alpha * p;
291 r -= alpha * Ap;
292 const double rr_new = r.squaredNorm();
293 p = r + (rr_new / rr) * p;
294 rr = rr_new;
295 }
296 if (!info.converged) {
297 info.error = std::sqrt(rr) / rhs_norm;
298 info.converged = info.error < tolerance;
299 }
300 WriteToSites(ApplyMhalf(y, false));
301 return info;
302}
303
305 double e = 0.0;
306 for (const SegmentBlock& block : blocks_) {
307 const PolarSegment& seg = segments_[block.segment];
308 Eigen::VectorXd dq(seg.size());
309 for (Index i = 0; i < seg.size(); ++i) {
310 dq(i) = seg[i].Induced_Charge();
311 }
312 e += 0.5 * (block.inv_sqrt * dq).squaredNorm();
313 }
314 return e;
315}
316
317} // namespace xtp
318} // namespace votca
const std::string & getType() const
Coupling PairCoupling(Index a, Index b) const
std::vector< PolarSegment > & segments_
Eigen::MatrixXd ApplyMhalf(const Eigen::MatrixXd &X) const
M^1/2 X for a matrix of unknown-space columns.
std::vector< SegmentBlock > blocks_
std::vector< Eigen::Matrix3d > alpha_sqrt_
void WriteToSites(const Eigen::VectorXd &x)
std::vector< Eigen::Matrix3d > alpha_inv_sqrt_
Eigen::VectorXd Apply(const Eigen::VectorXd &y) const
ChargeResponseSolver(std::vector< PolarSegment > &segments, const eeInteractor &interactor)
Eigen::VectorXd ApplyT(const Eigen::VectorXd &x) const
Info Solve(double tolerance, Index max_iter, bool use_previous)
const Eigen::MatrixXd & Pi() const
const ChargeResponse & getChargeResponse() 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::Vector3d & Induced_Dipole() const
Definition polarsite.h:121
const Eigen::Vector3d & V_noE() const
Definition polarsite.h:72
double Phi_noE() const
Definition polarsite.h:80
const Eigen::Vector3d & V() const
Definition polarsite.h:68
double Induced_Charge() const
Definition polarsite.h:114
double Phi() const
Definition polarsite.h:78
void setInduced_Charge(double dq)
Definition polarsite.h:115
const Eigen::Vector3d & getPos() const
Definition staticsite.h:80
Mediates interaction between polar and static sites.
static double PairWidth(const PolarSite &site1, const PolarSite &site2)
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
Interaction of two normalized Gaussian charge distributions exp(-r^2/s_i^2) and exp(-r^2/s_j^2): erf(...