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