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#ifdef ELECTROSTATICS
25
27
31
32#include "BoxGeometry.hpp"
33#include "LocalBox.hpp"
34#include "Particle.hpp"
36#include "errorhandling.hpp"
37#include "specfunc.hpp"
38#include "tuning.hpp"
39
40#include <utils/Vector.hpp>
41#include <utils/constants.hpp>
42#include <utils/math/sqr.hpp>
43
44#include <algorithm>
45#include <cmath>
46#include <cstdio>
47#include <limits>
48#include <vector>
49
50/* if you define this feature, the Bessel functions are calculated up
51 * to machine precision, otherwise 10^-14, which should be
52 * definitely enough for daily life. */
53#ifndef MMM1D_MACHINE_PREC
54#define K0 LPK0
55#define K1 LPK1
56#endif
57
58static auto far_error(int P, double minrad, Utils::Vector3d const &box_l_inv) {
59 auto const wavenumber = 2. * Utils::pi() * box_l_inv[2];
60 // this uses an upper bound to all force components and the potential
61 auto const rhores = wavenumber * minrad;
62 auto const pref = 4. * box_l_inv[2] * std::max(1., wavenumber);
63
64 return pref * K1(rhores * P) * exp(rhores) / rhores * (P - 1. + 1. / rhores);
65}
66
67static auto determine_minrad(double maxPWerror, int P,
68 Utils::Vector3d const &box_l,
69 Utils::Vector3d const &box_l_inv) {
70 // bisection to search for where the error is maxPWerror
71 auto constexpr min_rad = 0.01;
72 auto const rgranularity = min_rad * box_l[2];
73 auto rmin = rgranularity;
74 auto rmax = std::min(box_l[0], box_l[1]);
75 auto const errmin = far_error(P, rmin, box_l_inv);
76 auto const errmax = far_error(P, rmax, box_l_inv);
77 if (errmin < maxPWerror) {
78 // we can do almost all radii with this P
79 return rmin;
80 }
81 if (errmax > maxPWerror) {
82 // make sure that this switching radius cannot be reached
83 return 2. * std::max(box_l[0], box_l[1]);
84 }
85
86 while (rmax - rmin > rgranularity) {
87 auto const c = 0.5 * (rmin + rmax);
88 auto const errc = far_error(P, c, box_l_inv);
89 if (errc > maxPWerror) {
90 rmin = c;
91 } else {
92 rmax = c;
93 }
94 }
95 return 0.5 * (rmin + rmax);
96}
97
98void CoulombMMM1D::determine_bessel_radii() {
99 auto const &box_geo = *get_system().box_geo;
100 auto const &box_l = box_geo.length();
101 auto const &box_l_inv = box_geo.length_inv();
102 for (int i = 0; i < MAXIMAL_B_CUT; ++i) {
103 bessel_radii[i] = determine_minrad(maxPWerror, i + 1, box_l, box_l_inv);
104 }
105}
106
107void CoulombMMM1D::prepare_polygamma_series() {
108 /* polygamma, determine order */
109 double err;
110 auto const rhomax2 = uz2 * far_switch_radius_sq;
111 /* rhomax2 < 1, so rhomax2m2 falls monotonously */
112 int n = 1;
113 auto rhomax2nm2 = 1.0;
114 do {
116
117 /* |uz*z| <= 0.5 */
118 err = 2. * static_cast<double>(n) * fabs(mod_psi_even(n, 0.5)) * rhomax2nm2;
119 rhomax2nm2 *= rhomax2;
120 n++;
121 } while (err > 0.1 * maxPWerror);
122}
123
124CoulombMMM1D::CoulombMMM1D(double prefactor, double maxPWerror,
125 double switch_rad, int tune_timings,
126 bool tune_verbose)
127 : maxPWerror{maxPWerror}, far_switch_radius{switch_rad},
128 tune_timings{tune_timings}, tune_verbose{tune_verbose}, m_is_tuned{false},
129 far_switch_radius_sq{-1.}, uz2{0.}, prefuz2{0.}, prefL3_i{0.} {
131 if (maxPWerror <= 0.) {
132 throw std::domain_error("Parameter 'maxPWerror' must be > 0");
133 }
134 if (far_switch_radius <= 0. and far_switch_radius != -1.) {
135 throw std::domain_error("Parameter 'far_switch_radius' must be > 0");
136 }
137 if (far_switch_radius > 0.) {
139 }
140 if (tune_timings <= 0) {
141 throw std::domain_error("Parameter 'timings' must be > 0");
142 }
143}
144
145void CoulombMMM1D::sanity_checks_periodicity() const {
146 auto const &box_geo = *get_system().box_geo;
147 if (box_geo.periodic(0) || box_geo.periodic(1) || !box_geo.periodic(2)) {
148 throw std::runtime_error("MMM1D requires periodicity (False, False, True)");
149 }
150}
151
152void CoulombMMM1D::sanity_checks_cell_structure() const {
153 auto const &local_geo = *get_system().local_geo;
154 if (local_geo.cell_structure_type() != CellStructureType::NSQUARE) {
155 throw std::runtime_error("MMM1D requires the N-square cellsystem");
156 }
157}
158
159void CoulombMMM1D::recalc_boxl_parameters() {
160 auto const &box_geo = *get_system().box_geo;
161
162 if (far_switch_radius_sq >= Utils::sqr(box_geo.length()[2]))
163 far_switch_radius_sq = 0.8 * Utils::sqr(box_geo.length()[2]);
164
165 uz2 = Utils::sqr(box_geo.length_inv()[2]);
166 prefuz2 = prefactor * uz2;
167 prefL3_i = prefuz2 * box_geo.length_inv()[2];
168
169 determine_bessel_radii();
170 prepare_polygamma_series();
171}
172
174 double dist) const {
175 auto constexpr c_2pi = 2. * Utils::pi();
176 auto const &box_geo = *get_system().box_geo;
177 auto const n_modPsi = static_cast<int>(modPsi.size()) >> 1;
178 auto const rxy2 = d[0] * d[0] + d[1] * d[1];
179 auto const rxy2_d = rxy2 * uz2;
180 auto const z_d = d[2] * box_geo.length_inv()[2];
182
183 if (rxy2 <= far_switch_radius_sq) {
184 /* polygamma summation */
185 auto sr = 0.;
186 auto sz = mod_psi_odd(0, z_d);
187 auto r2nm1 = 1.;
188 for (int n = 1; n < n_modPsi; n++) {
189 auto const deriv = static_cast<double>(2 * n);
190 auto const mpe = mod_psi_even(n, z_d);
191 auto const mpo = mod_psi_odd(n, z_d);
192 auto const r2n = r2nm1 * rxy2_d;
193
194 sz += r2n * mpo;
195 sr += deriv * r2nm1 * mpe;
196
197 if (fabs(deriv * r2nm1 * mpe) < maxPWerror)
198 break;
199
200 r2nm1 = r2n;
201 }
202
203 double Fx = prefL3_i * sr * d[0];
204 double Fy = prefL3_i * sr * d[1];
205 double Fz = prefuz2 * sz;
206
207 /* real space parts */
208
209 double pref, rt, rt2, shift_z;
210
211 pref = 1. / Utils::int_pow<3>(dist);
212 Fx += pref * d[0];
213 Fy += pref * d[1];
214 Fz += pref * d[2];
215
216 shift_z = d[2] + box_geo.length()[2];
217 rt2 = rxy2 + shift_z * shift_z;
218 rt = sqrt(rt2);
219 pref = 1. / (rt2 * rt);
220 Fx += pref * d[0];
221 Fy += pref * d[1];
222 Fz += pref * shift_z;
223
224 shift_z = d[2] - box_geo.length()[2];
225 rt2 = rxy2 + shift_z * shift_z;
226 rt = sqrt(rt2);
227 pref = 1. / (rt2 * rt);
228 Fx += pref * d[0];
229 Fy += pref * d[1];
230 Fz += pref * shift_z;
231
232 force = {Fx, Fy, Fz};
233 } else {
234 /* far range formula */
235 auto const rxy = sqrt(rxy2);
236 auto const rxy_d = rxy * box_geo.length_inv()[2];
237 auto sr = 0., sz = 0.;
238
239 for (int bp = 1; bp < MAXIMAL_B_CUT; bp++) {
240 if (bessel_radii[bp - 1] < rxy)
241 break;
242
243 auto const fq = c_2pi * bp;
244#ifdef MMM1D_MACHINE_PREC
245 auto const k0 = K0(fq * rxy_d);
246 auto const k1 = K1(fq * rxy_d);
247#else
248 auto const [k0, k1] = LPK01(fq * rxy_d);
249#endif
250 sr += bp * k1 * cos(fq * z_d);
251 sz += bp * k0 * sin(fq * z_d);
252 }
253 sr *= uz2 * 4. * c_2pi;
254 sz *= uz2 * 4. * c_2pi;
255
256 auto const pref = sr / rxy + 2. * box_geo.length_inv()[2] / rxy2;
257
258 force = {pref * d[0], pref * d[1], sz};
259 }
260
261 return (prefactor * q1q2) * force;
262}
263
264double CoulombMMM1D::pair_energy(double const q1q2, Utils::Vector3d const &d,
265 double const dist) const {
266 if (q1q2 == 0.)
267 return 0.;
268
269 auto constexpr c_2pi = 2. * Utils::pi();
270 auto const &box_geo = *get_system().box_geo;
271 auto const n_modPsi = static_cast<int>(modPsi.size()) >> 1;
272 auto const rxy2 = d[0] * d[0] + d[1] * d[1];
273 auto const rxy2_d = rxy2 * uz2;
274 auto const z_d = d[2] * box_geo.length_inv()[2];
275 double energy;
276
277 if (rxy2 <= far_switch_radius_sq) {
278 /* near range formula */
279 energy = -2. * Utils::gamma();
280
281 /* polygamma summation */
282 double r2n = 1.0;
283 for (int n = 0; n < n_modPsi; n++) {
284 auto const add = mod_psi_even(n, z_d) * r2n;
285 energy -= add;
286
287 if (fabs(add) < maxPWerror)
288 break;
289
290 r2n *= rxy2_d;
291 }
292 energy *= box_geo.length_inv()[2];
293
294 /* real space parts */
295
296 double rt, shift_z;
297
298 energy += 1. / dist;
299
300 shift_z = d[2] + box_geo.length()[2];
301 rt = sqrt(rxy2 + shift_z * shift_z);
302 energy += 1. / rt;
303
304 shift_z = d[2] - box_geo.length()[2];
305 rt = sqrt(rxy2 + shift_z * shift_z);
306 energy += 1. / rt;
307 } else {
308 /* far range formula */
309 auto const rxy = sqrt(rxy2);
310 auto const rxy_d = rxy * box_geo.length_inv()[2];
311 /* The first Bessel term will compensate a little bit the
312 log term, so add them close together */
313 energy = -0.25 * log(rxy2_d) + 0.5 * (Utils::ln_2() - Utils::gamma());
314 for (int bp = 1; bp < MAXIMAL_B_CUT; bp++) {
315 if (bessel_radii[bp - 1] < rxy)
316 break;
317
318 auto const fq = c_2pi * bp;
319 energy += K0(fq * rxy_d) * cos(fq * z_d);
320 }
321 energy *= 4. * box_geo.length_inv()[2];
322 }
323
324 return prefactor * q1q2 * energy;
325}
326
328 if (is_tuned()) {
329 return;
330 }
331 recalc_boxl_parameters();
332 auto &system = get_system();
333
334 if (far_switch_radius_sq < 0.) {
335 auto const &box_geo = *system.box_geo;
336 auto const maxrad = box_geo.length()[2];
337 auto min_time = std::numeric_limits<double>::infinity();
338 auto min_rad = -1.;
339 auto switch_radius = 0.2 * maxrad;
340 /* determine optimal switching radius. Should be around 0.33 */
341 while (switch_radius < 0.4 * maxrad) {
342 if (switch_radius > bessel_radii.back()) {
343 // this switching radius is large enough for our Bessel series
344 far_switch_radius_sq = Utils::sqr(switch_radius);
345 system.on_coulomb_change();
346
347 /* perform force calculation test */
348 auto const int_time = benchmark_integration_step(system, tune_timings);
349
350 if (tune_verbose) {
351 std::printf("r= %f t= %f ms\n", switch_radius, int_time);
352 }
353
354 if (int_time < min_time) {
355 min_time = int_time;
356 min_rad = switch_radius;
357 } else if (int_time > 2. * min_time) {
358 // simple heuristic to skip remaining radii when performance drops
359 break;
360 }
361 }
362 switch_radius += 0.025 * maxrad;
363 }
364 switch_radius = min_rad;
365 far_switch_radius_sq = Utils::sqr(switch_radius);
366 } else if (far_switch_radius_sq <= Utils::sqr(bessel_radii.back())) {
367 // this switching radius is too small for our Bessel series
368 throw std::runtime_error("MMM1D could not find a reasonable Bessel cutoff");
369 }
370
371 m_is_tuned = true;
372 system.on_coulomb_change();
373}
374
375#endif // ELECTROSTATICS
@ NSQUARE
Atom decomposition (N-square).
Vector implementation and trait types for boost qvm interoperability.
__global__ float * force
void set_prefactor(double new_prefactor)
double prefactor
Electrostatics prefactor.
This file contains the defaults for ESPResSo.
This file contains the errorhandling code for severe errors, like a broken bond or illegal parameter ...
Common parts of the MMM family of methods for the electrostatic interaction: MMM1D and ELC.
double mod_psi_even(int n, double x)
Modified polygamma for even order 2*n, n >= 0
double mod_psi_odd(int n, double x)
Modified polygamma for odd order 2*n+1, n>= 0
std::vector< std::vector< double > > modPsi
Table of the Taylor expansions of the modified polygamma functions.
void create_mod_psi_up_to(int new_n)
Create both the even and odd polygamma functions up to order 2*new_n
Common parts of the MMM family of methods for the electrostatic interaction: MMM1D and ELC.
static auto determine_minrad(double maxPWerror, int P, Utils::Vector3d const &box_l, Utils::Vector3d const &box_l_inv)
Definition mmm1d.cpp:67
static auto far_error(int P, double minrad, Utils::Vector3d const &box_l_inv)
Definition mmm1d.cpp:58
MMM1D algorithm for long-range Coulomb interactions on the CPU.
__constant__ float far_switch_radius_sq[1]
__constant__ float maxPWerror[1]
DEVICE_QUALIFIER constexpr T pi()
Ratio of diameter and circumference of a circle.
Definition constants.hpp:36
DEVICE_QUALIFIER constexpr T sqr(T x)
Calculates the SQuaRe of x.
Definition sqr.hpp:26
DEVICE_QUALIFIER constexpr T ln_2()
Natural logarithm of 2.
Definition constants.hpp:57
DEVICE_QUALIFIER constexpr T gamma()
Euler-Mascheroni constant.
Definition constants.hpp:50
std::pair< double, double > LPK01(double x)
Modified Bessel functions of second kind, order 0 and 1, low precision.
Definition specfunc.cpp:408
double K1(double x)
Modified Bessel function of second kind, order 1.
Definition specfunc.cpp:262
double K0(double x)
Modified Bessel function of second kind, order 0.
Definition specfunc.cpp:250
This file contains implementations for some special functions which are needed by the MMM family of a...
bool tune_verbose
Definition mmm1d.hpp:58
CoulombMMM1D(double prefactor, double maxPWerror, double switch_rad, int tune_timings, bool tune_verbose)
Definition mmm1d.cpp:124
double far_switch_radius
Far switch radius.
Definition mmm1d.hpp:56
Utils::Vector3d pair_force(double q1q2, Utils::Vector3d const &d, double dist) const
Compute the pair force.
Definition mmm1d.cpp:173
double pair_energy(double q1q2, Utils::Vector3d const &d, double dist) const
Compute the pair energy.
Definition mmm1d.cpp:264
bool is_tuned() const
Definition mmm1d.hpp:79
double maxPWerror
Maximal allowed pairwise error for the potential and force.
Definition mmm1d.hpp:51
int tune_timings
Definition mmm1d.hpp:57
void tune()
Definition mmm1d.cpp:327
double benchmark_integration_step(System::System &system, int int_steps)
Benchmark the integration loop.
Definition tuning.cpp:75