ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
mmm1d.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2010-2022 The ESPResSo project
3 * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010
4 * Max-Planck-Institute for Polymer Research, Theory Group
5 *
6 * This file is part of ESPResSo.
7 *
8 * ESPResSo is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * ESPResSo is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22#include <config/config.hpp>
23
24#if defined(ESPRESSO_ELECTROSTATICS) and defined(ESPRESSO_GSL)
25
27
29
30#include "BoxGeometry.hpp"
31#include "LocalBox.hpp"
32#include "Particle.hpp"
34#include "errorhandling.hpp"
35#include "tuning.hpp"
36
37#include <utils/Vector.hpp>
38#include <utils/math/sqr.hpp>
39
40#include <gsl/gsl_sf_bessel.h>
41
42#include <algorithm>
43#include <cmath>
44#include <cstdio>
45#include <limits>
46#include <numbers>
47#include <vector>
48
49static auto far_error(int P, double minrad, Utils::Vector3d const &box_l_inv) {
50 auto const wavenumber = 2. * std::numbers::pi * box_l_inv[2];
51 // this uses an upper bound to all force components and the potential
52 auto const rhores = wavenumber * minrad;
53 auto const pref = 4. * box_l_inv[2] * std::max(1., wavenumber);
54
55 return pref * gsl_sf_bessel_K1(rhores * P) * exp(rhores) / rhores *
56 (P - 1. + 1. / rhores);
57}
58
59static auto determine_minrad(double maxPWerror, int P,
60 Utils::Vector3d const &box_l,
61 Utils::Vector3d const &box_l_inv) {
62 // bisection to search for where the error is maxPWerror
63 auto constexpr min_rad = 0.01;
64 auto const rgranularity = min_rad * box_l[2];
65 auto rmin = rgranularity;
66 auto rmax = std::min(box_l[0], box_l[1]);
67 auto const errmin = far_error(P, rmin, box_l_inv);
68 auto const errmax = far_error(P, rmax, box_l_inv);
69 if (errmin < maxPWerror) {
70 // we can do almost all radii with this P
71 return rmin;
72 }
73 if (errmax > maxPWerror) {
74 // make sure that this switching radius cannot be reached
75 return 2. * std::max(box_l[0], box_l[1]);
76 }
77
78 while (rmax - rmin > rgranularity) {
79 auto const c = 0.5 * (rmin + rmax);
80 auto const errc = far_error(P, c, box_l_inv);
81 if (errc > maxPWerror) {
82 rmin = c;
83 } else {
84 rmax = c;
85 }
86 }
87 return 0.5 * (rmin + rmax);
88}
89
90static double evaluateAsTaylorSeriesAt(std::span<const double> series,
91 double x) {
92 assert(not series.empty());
93 auto const value = std::accumulate(
94 series.rbegin(), series.rend(), 0.,
95 [x](auto const &acc, auto const &coeff) { return acc * x + coeff; });
96 return value;
97}
98
99/** Modified polygamma for even order <tt>2*n, n >= 0</tt> */
100static double mod_psi_even(auto const &modPsi, int n, double x) {
101 return evaluateAsTaylorSeriesAt(modPsi[2 * n], x * x);
102}
103
104/** Modified polygamma for odd order <tt>2*n+1, n>= 0</tt> */
105static double mod_psi_odd(auto const &modPsi, int n, double x) {
106 return x * evaluateAsTaylorSeriesAt(modPsi[2 * n + 1], x * x);
107}
108
109void CoulombMMM1D::determine_bessel_radii() {
110 auto const &box_geo = *get_system().box_geo;
111 auto const &box_l = box_geo.length();
112 auto const &box_l_inv = box_geo.length_inv();
113 for (int i = 0; i < MAXIMAL_B_CUT; ++i) {
114 bessel_radii[i] = determine_minrad(maxPWerror, i + 1, box_l, box_l_inv);
115 }
116}
117
118void CoulombMMM1D::prepare_polygamma_series() {
119 /* polygamma, determine order */
120 double err;
121 auto const rhomax2 = uz2 * far_switch_radius_sq;
122 /* rhomax2 < 1, so rhomax2m2 falls monotonously */
123 int n = 1;
124 auto rhomax2nm2 = 1.0;
125 do {
126 create_mod_psi_up_to(n + 1);
127
128 /* |uz*z| <= 0.5 */
129 err = 2. * static_cast<double>(n) * fabs(mod_psi_even(modPsi, n, 0.5)) *
130 rhomax2nm2;
131 rhomax2nm2 *= rhomax2;
132 n++;
133 } while (err > 0.1 * maxPWerror);
134}
135
136CoulombMMM1D::CoulombMMM1D(double prefactor, double maxPWerror,
137 double switch_rad, int tune_timings,
138 bool tune_verbose)
139 : maxPWerror{maxPWerror}, far_switch_radius{switch_rad},
140 tune_timings{tune_timings}, tune_verbose{tune_verbose}, m_is_tuned{false},
141 far_switch_radius_sq{-1.}, uz2{0.}, prefuz2{0.}, prefL3_i{0.} {
143 if (maxPWerror <= 0.) {
144 throw std::domain_error("Parameter 'maxPWerror' must be > 0");
145 }
146 if (far_switch_radius <= 0. and far_switch_radius != -1.) {
147 throw std::domain_error("Parameter 'far_switch_radius' must be > 0");
148 }
149 if (far_switch_radius > 0.) {
150 far_switch_radius_sq = Utils::sqr(far_switch_radius);
151 }
152 if (tune_timings <= 0) {
153 throw std::domain_error("Parameter 'timings' must be > 0");
154 }
155}
156
157void CoulombMMM1D::sanity_checks_periodicity() const {
158 auto const &box_geo = *get_system().box_geo;
159 if (box_geo.periodic(0) || box_geo.periodic(1) || !box_geo.periodic(2)) {
160 throw std::runtime_error("MMM1D requires periodicity (False, False, True)");
161 }
162}
163
164void CoulombMMM1D::sanity_checks_cell_structure() const {
165 auto const &local_geo = *get_system().local_geo;
166 if (local_geo.cell_structure_type() != CellStructureType::NSQUARE) {
167 throw std::runtime_error("MMM1D requires the N-square cellsystem");
168 }
169}
170
171void CoulombMMM1D::recalc_boxl_parameters() {
172 auto const &box_geo = *get_system().box_geo;
173
174 if (far_switch_radius_sq >= Utils::sqr(box_geo.length()[2])) {
175 far_switch_radius_sq = 0.8 * Utils::sqr(box_geo.length()[2]);
176 far_switch_radius = std::sqrt(far_switch_radius_sq);
177 }
178
179 uz2 = Utils::sqr(box_geo.length_inv()[2]);
180 prefuz2 = prefactor * uz2;
181 prefL3_i = prefuz2 * box_geo.length_inv()[2];
182
183 determine_bessel_radii();
184 prepare_polygamma_series();
185}
186
188 double dist) const {
189 auto constexpr c_2pi = 2. * std::numbers::pi;
190 auto const &box_geo = *get_system().box_geo;
191 auto const n_modPsi = static_cast<int>(modPsi.size()) >> 1;
192 auto const rxy2 = d[0] * d[0] + d[1] * d[1];
193 auto const rxy2_d = rxy2 * uz2;
194 auto const z_d = d[2] * box_geo.length_inv()[2];
195 Utils::Vector3d force;
196
197 if (rxy2 <= far_switch_radius_sq) {
198 /* polygamma summation */
199 auto sr = 0.;
200 auto sz = mod_psi_odd(modPsi, 0, z_d);
201 auto r2nm1 = 1.;
202 for (int n = 1; n < n_modPsi; n++) {
203 auto const deriv = static_cast<double>(2 * n);
204 auto const mpe = mod_psi_even(modPsi, n, z_d);
205 auto const mpo = mod_psi_odd(modPsi, n, z_d);
206 auto const r2n = r2nm1 * rxy2_d;
207
208 sz += r2n * mpo;
209 sr += deriv * r2nm1 * mpe;
210
211 if (fabs(deriv * r2nm1 * mpe) < maxPWerror)
212 break;
213
214 r2nm1 = r2n;
215 }
216
217 double Fx = prefL3_i * sr * d[0];
218 double Fy = prefL3_i * sr * d[1];
219 double Fz = prefuz2 * sz;
220
221 /* real space parts */
222
223 double pref, rt, rt2, shift_z;
224
225 pref = 1. / Utils::int_pow<3>(dist);
226 Fx += pref * d[0];
227 Fy += pref * d[1];
228 Fz += pref * d[2];
229
230 shift_z = d[2] + box_geo.length()[2];
231 rt2 = rxy2 + shift_z * shift_z;
232 rt = sqrt(rt2);
233 pref = 1. / (rt2 * rt);
234 Fx += pref * d[0];
235 Fy += pref * d[1];
236 Fz += pref * shift_z;
237
238 shift_z = d[2] - box_geo.length()[2];
239 rt2 = rxy2 + shift_z * shift_z;
240 rt = sqrt(rt2);
241 pref = 1. / (rt2 * rt);
242 Fx += pref * d[0];
243 Fy += pref * d[1];
244 Fz += pref * shift_z;
245
246 force = {Fx, Fy, Fz};
247 } else {
248 /* far range formula */
249 auto const rxy = sqrt(rxy2);
250 auto const rxy_d = rxy * box_geo.length_inv()[2];
251 auto sr = 0., sz = 0.;
252
253 for (int bp = 1; bp < MAXIMAL_B_CUT; bp++) {
254 if (bessel_radii[bp - 1] < rxy)
255 break;
256
257 auto const fq = c_2pi * bp;
258 auto const k0 = gsl_sf_bessel_K0(fq * rxy_d);
259 auto const k1 = gsl_sf_bessel_K1(fq * rxy_d);
260 sr += bp * k1 * cos(fq * z_d);
261 sz += bp * k0 * sin(fq * z_d);
262 }
263 sr *= uz2 * 4. * c_2pi;
264 sz *= uz2 * 4. * c_2pi;
265
266 auto const pref = sr / rxy + 2. * box_geo.length_inv()[2] / rxy2;
267
268 force = {pref * d[0], pref * d[1], sz};
269 }
270
271 return (prefactor * q1q2) * force;
272}
273
274double CoulombMMM1D::pair_energy(double const q1q2, Utils::Vector3d const &d,
275 double const dist) const {
276 if (q1q2 == 0.)
277 return 0.;
278
279 auto constexpr c_2pi = 2. * std::numbers::pi;
280 auto const &box_geo = *get_system().box_geo;
281 auto const n_modPsi = static_cast<int>(modPsi.size()) >> 1;
282 auto const rxy2 = d[0] * d[0] + d[1] * d[1];
283 auto const rxy2_d = rxy2 * uz2;
284 auto const z_d = d[2] * box_geo.length_inv()[2];
285 double energy;
286
287 if (rxy2 <= far_switch_radius_sq) {
288 /* near range formula */
289 energy = -2. * std::numbers::egamma;
290
291 /* polygamma summation */
292 double r2n = 1.;
293 for (int n = 0; n < n_modPsi; n++) {
294 auto const add = mod_psi_even(modPsi, n, z_d) * r2n;
295 energy -= add;
296
297 if (fabs(add) < maxPWerror)
298 break;
299
300 r2n *= rxy2_d;
301 }
302 energy *= box_geo.length_inv()[2];
303
304 /* real space parts */
305
306 double rt, shift_z;
307
308 energy += 1. / dist;
309
310 shift_z = d[2] + box_geo.length()[2];
311 rt = sqrt(rxy2 + shift_z * shift_z);
312 energy += 1. / rt;
313
314 shift_z = d[2] - box_geo.length()[2];
315 rt = sqrt(rxy2 + shift_z * shift_z);
316 energy += 1. / rt;
317 } else {
318 /* far range formula */
319 auto const rxy = sqrt(rxy2);
320 auto const rxy_d = rxy * box_geo.length_inv()[2];
321 /* The first Bessel term will compensate a little bit the
322 log term, so add them close together */
323 energy =
324 -0.25 * log(rxy2_d) + 0.5 * (std::numbers::ln2 - std::numbers::egamma);
325 for (int bp = 1; bp < MAXIMAL_B_CUT; bp++) {
326 if (bessel_radii[bp - 1] < rxy)
327 break;
328
329 auto const fq = c_2pi * bp;
330 energy += gsl_sf_bessel_K0(fq * rxy_d) * cos(fq * z_d);
331 }
332 energy *= 4. * box_geo.length_inv()[2];
333 }
334
335 return prefactor * q1q2 * energy;
336}
337
339 if (is_tuned()) {
340 return;
341 }
342 recalc_boxl_parameters();
343 auto &system = get_system();
344
345 if (far_switch_radius_sq < 0.) {
346 auto const &box_geo = *system.box_geo;
347 auto const maxrad = box_geo.length()[2];
348 auto min_time = std::numeric_limits<double>::infinity();
349 auto min_rad = -1.;
350 auto switch_radius = 0.2 * maxrad;
351 /* determine optimal switching radius. Should be around 0.33 */
352 while (switch_radius < 0.4 * maxrad) {
353 if (switch_radius > bessel_radii.back()) {
354 // this switching radius is large enough for our Bessel series
355 far_switch_radius_sq = Utils::sqr(switch_radius);
356 system.on_coulomb_change();
357
358 /* perform force calculation test */
359 auto const int_time = benchmark_integration_step(system, tune_timings);
360
361 if (tune_verbose) {
362 std::printf("r= %f t= %f ms\n", switch_radius, int_time);
363 }
364
365 if (int_time < min_time) {
366 min_time = int_time;
367 min_rad = switch_radius;
368 } else if (int_time > 2. * min_time) {
369 // simple heuristic to skip remaining radii when performance drops
370 break;
371 }
372 }
373 switch_radius += 0.025 * maxrad;
374 }
375 switch_radius = min_rad;
376 far_switch_radius = switch_radius;
377 far_switch_radius_sq = Utils::sqr(switch_radius);
378 } else if (far_switch_radius_sq <= Utils::sqr(bessel_radii.back())) {
379 // this switching radius is too small for our Bessel series
380 throw std::runtime_error("MMM1D could not find a reasonable Bessel cutoff");
381 }
382
383 m_is_tuned = true;
384 system.on_coulomb_change();
385}
386
387#endif // defined(ESPRESSO_ELECTROSTATICS) and defined(ESPRESSO_GSL)
@ NSQUARE
Atom decomposition (N-square).
Vector implementation and trait types for boost qvm interoperability.
void set_prefactor(double new_prefactor)
double prefactor
Electrostatics prefactor.
This file contains the errorhandling code for severe errors, like a broken bond or illegal parameter ...
static double evaluateAsTaylorSeriesAt(std::span< const double > series, double x)
Definition mmm1d.cpp:90
static double mod_psi_odd(auto const &modPsi, int n, double x)
Modified polygamma for odd order 2*n+1, n>= 0
Definition mmm1d.cpp:105
static auto determine_minrad(double maxPWerror, int P, Utils::Vector3d const &box_l, Utils::Vector3d const &box_l_inv)
Definition mmm1d.cpp:59
static double mod_psi_even(auto const &modPsi, int n, double x)
Modified polygamma for even order 2*n, n >= 0
Definition mmm1d.cpp:100
static auto far_error(int P, double minrad, Utils::Vector3d const &box_l_inv)
Definition mmm1d.cpp:49
MMM1D algorithm for long-range Coulomb interactions on the CPU.
DEVICE_QUALIFIER constexpr T sqr(T x)
Calculates the SQuaRe of x.
Definition sqr.hpp:28
bool tune_verbose
Definition mmm1d.hpp:64
CoulombMMM1D(double prefactor, double maxPWerror, double switch_rad, int tune_timings, bool tune_verbose)
Definition mmm1d.cpp:136
double far_switch_radius
Far switch radius.
Definition mmm1d.hpp:62
Utils::Vector3d pair_force(double q1q2, Utils::Vector3d const &d, double dist) const
Compute the pair force.
Definition mmm1d.cpp:187
double pair_energy(double q1q2, Utils::Vector3d const &d, double dist) const
Compute the pair energy.
Definition mmm1d.cpp:274
bool is_tuned() const
Definition mmm1d.hpp:85
double maxPWerror
Maximal allowed pairwise error for the potential and force.
Definition mmm1d.hpp:57
int tune_timings
Definition mmm1d.hpp:63
void tune()
Definition mmm1d.cpp:338
double benchmark_integration_step(System::System &system, int int_steps)
Benchmark the integration loop.
Definition tuning.cpp:73