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,
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)) *
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
177 uz2 = Utils::sqr(box_geo.length_inv()[2]);
178 prefuz2 = prefactor * uz2;
179 prefL3_i = prefuz2 * box_geo.length_inv()[2];
180
181 determine_bessel_radii();
182 prepare_polygamma_series();
183}
184
186 double dist) const {
187 auto constexpr c_2pi = 2. * std::numbers::pi;
188 auto const &box_geo = *get_system().box_geo;
189 auto const n_modPsi = static_cast<int>(modPsi.size()) >> 1;
190 auto const rxy2 = d[0] * d[0] + d[1] * d[1];
191 auto const rxy2_d = rxy2 * uz2;
192 auto const z_d = d[2] * box_geo.length_inv()[2];
193 Utils::Vector3d force;
194
195 if (rxy2 <= far_switch_radius_sq) {
196 /* polygamma summation */
197 auto sr = 0.;
198 auto sz = mod_psi_odd(modPsi, 0, z_d);
199 auto r2nm1 = 1.;
200 for (int n = 1; n < n_modPsi; n++) {
201 auto const deriv = static_cast<double>(2 * n);
202 auto const mpe = mod_psi_even(modPsi, n, z_d);
203 auto const mpo = mod_psi_odd(modPsi, n, z_d);
204 auto const r2n = r2nm1 * rxy2_d;
205
206 sz += r2n * mpo;
207 sr += deriv * r2nm1 * mpe;
208
209 if (fabs(deriv * r2nm1 * mpe) < maxPWerror)
210 break;
211
212 r2nm1 = r2n;
213 }
214
215 double Fx = prefL3_i * sr * d[0];
216 double Fy = prefL3_i * sr * d[1];
217 double Fz = prefuz2 * sz;
218
219 /* real space parts */
220
221 double pref, rt, rt2, shift_z;
222
223 pref = 1. / Utils::int_pow<3>(dist);
224 Fx += pref * d[0];
225 Fy += pref * d[1];
226 Fz += pref * d[2];
227
228 shift_z = d[2] + box_geo.length()[2];
229 rt2 = rxy2 + shift_z * shift_z;
230 rt = sqrt(rt2);
231 pref = 1. / (rt2 * rt);
232 Fx += pref * d[0];
233 Fy += pref * d[1];
234 Fz += pref * shift_z;
235
236 shift_z = d[2] - box_geo.length()[2];
237 rt2 = rxy2 + shift_z * shift_z;
238 rt = sqrt(rt2);
239 pref = 1. / (rt2 * rt);
240 Fx += pref * d[0];
241 Fy += pref * d[1];
242 Fz += pref * shift_z;
243
244 force = {Fx, Fy, Fz};
245 } else {
246 /* far range formula */
247 auto const rxy = sqrt(rxy2);
248 auto const rxy_d = rxy * box_geo.length_inv()[2];
249 auto sr = 0., sz = 0.;
250
251 for (int bp = 1; bp < MAXIMAL_B_CUT; bp++) {
252 if (bessel_radii[bp - 1] < rxy)
253 break;
254
255 auto const fq = c_2pi * bp;
256 auto const k0 = gsl_sf_bessel_K0(fq * rxy_d);
257 auto const k1 = gsl_sf_bessel_K1(fq * rxy_d);
258 sr += bp * k1 * cos(fq * z_d);
259 sz += bp * k0 * sin(fq * z_d);
260 }
261 sr *= uz2 * 4. * c_2pi;
262 sz *= uz2 * 4. * c_2pi;
263
264 auto const pref = sr / rxy + 2. * box_geo.length_inv()[2] / rxy2;
265
266 force = {pref * d[0], pref * d[1], sz};
267 }
268
269 return (prefactor * q1q2) * force;
270}
271
272double CoulombMMM1D::pair_energy(double const q1q2, Utils::Vector3d const &d,
273 double const dist) const {
274 if (q1q2 == 0.)
275 return 0.;
276
277 auto constexpr c_2pi = 2. * std::numbers::pi;
278 auto const &box_geo = *get_system().box_geo;
279 auto const n_modPsi = static_cast<int>(modPsi.size()) >> 1;
280 auto const rxy2 = d[0] * d[0] + d[1] * d[1];
281 auto const rxy2_d = rxy2 * uz2;
282 auto const z_d = d[2] * box_geo.length_inv()[2];
283 double energy;
284
285 if (rxy2 <= far_switch_radius_sq) {
286 /* near range formula */
287 energy = -2. * std::numbers::egamma;
288
289 /* polygamma summation */
290 double r2n = 1.;
291 for (int n = 0; n < n_modPsi; n++) {
292 auto const add = mod_psi_even(modPsi, n, z_d) * r2n;
293 energy -= add;
294
295 if (fabs(add) < maxPWerror)
296 break;
297
298 r2n *= rxy2_d;
299 }
300 energy *= box_geo.length_inv()[2];
301
302 /* real space parts */
303
304 double rt, shift_z;
305
306 energy += 1. / dist;
307
308 shift_z = d[2] + box_geo.length()[2];
309 rt = sqrt(rxy2 + shift_z * shift_z);
310 energy += 1. / rt;
311
312 shift_z = d[2] - box_geo.length()[2];
313 rt = sqrt(rxy2 + shift_z * shift_z);
314 energy += 1. / rt;
315 } else {
316 /* far range formula */
317 auto const rxy = sqrt(rxy2);
318 auto const rxy_d = rxy * box_geo.length_inv()[2];
319 /* The first Bessel term will compensate a little bit the
320 log term, so add them close together */
321 energy =
322 -0.25 * log(rxy2_d) + 0.5 * (std::numbers::ln2 - std::numbers::egamma);
323 for (int bp = 1; bp < MAXIMAL_B_CUT; bp++) {
324 if (bessel_radii[bp - 1] < rxy)
325 break;
326
327 auto const fq = c_2pi * bp;
328 energy += gsl_sf_bessel_K0(fq * rxy_d) * cos(fq * z_d);
329 }
330 energy *= 4. * box_geo.length_inv()[2];
331 }
332
333 return prefactor * q1q2 * energy;
334}
335
337 if (is_tuned()) {
338 return;
339 }
340 recalc_boxl_parameters();
341 auto &system = get_system();
342
343 if (far_switch_radius_sq < 0.) {
344 auto const &box_geo = *system.box_geo;
345 auto const maxrad = box_geo.length()[2];
346 auto min_time = std::numeric_limits<double>::infinity();
347 auto min_rad = -1.;
348 auto switch_radius = 0.2 * maxrad;
349 /* determine optimal switching radius. Should be around 0.33 */
350 while (switch_radius < 0.4 * maxrad) {
351 if (switch_radius > bessel_radii.back()) {
352 // this switching radius is large enough for our Bessel series
353 far_switch_radius_sq = Utils::sqr(switch_radius);
354 system.on_coulomb_change();
355
356 /* perform force calculation test */
358
359 if (tune_verbose) {
360 std::printf("r= %f t= %f ms\n", switch_radius, int_time);
361 }
362
363 if (int_time < min_time) {
366 } else if (int_time > 2. * min_time) {
367 // simple heuristic to skip remaining radii when performance drops
368 break;
369 }
370 }
371 switch_radius += 0.025 * maxrad;
372 }
374 far_switch_radius_sq = Utils::sqr(switch_radius);
375 } else if (far_switch_radius_sq <= Utils::sqr(bessel_radii.back())) {
376 // this switching radius is too small for our Bessel series
377 throw std::runtime_error("MMM1D could not find a reasonable Bessel cutoff");
378 }
379
380 m_is_tuned = true;
381 system.on_coulomb_change();
382}
383
384#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.
cudaStream_t stream[1]
CUDA streams for parallel computing on CPU and GPU.
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:63
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:61
Utils::Vector3d pair_force(double q1q2, Utils::Vector3d const &d, double dist) const
Compute the pair force.
Definition mmm1d.cpp:185
double pair_energy(double q1q2, Utils::Vector3d const &d, double dist) const
Compute the pair energy.
Definition mmm1d.cpp:272
bool is_tuned() const
Definition mmm1d.hpp:84
double maxPWerror
Maximal allowed pairwise error for the potential and force.
Definition mmm1d.hpp:56
int tune_timings
Definition mmm1d.hpp:62
void tune()
Definition mmm1d.cpp:336
double benchmark_integration_step(System::System &system, int int_steps)
Benchmark the integration loop.
Definition tuning.cpp:73