ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
elc.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 P3M
25
27
32
33#include "BoxGeometry.hpp"
34#include "Particle.hpp"
36#include "ParticleRange.hpp"
38#include "communication.hpp"
39#include "errorhandling.hpp"
40#include "system/System.hpp"
41
42#include <utils/Vector.hpp>
43#include <utils/constants.hpp>
44#include <utils/math/sqr.hpp>
45
46#include <boost/mpi/collectives/all_reduce.hpp>
47#include <boost/range/combine.hpp>
48
49#include <algorithm>
50#include <cassert>
51#include <cmath>
52#include <cstddef>
53#include <functional>
54#include <variant>
55#include <vector>
56
57/** \name Product decomposition data organization
58 * For the cell blocks it is assumed that the lower blocks part is in the
59 * lower half. This has to have positive sign, so that has to be first.
60 */
61/**@{*/
62#define POQESP 0
63#define POQECP 1
64#define POQESM 2
65#define POQECM 3
66
67#define PQESSP 0
68#define PQESCP 1
69#define PQECSP 2
70#define PQECCP 3
71#define PQESSM 4
72#define PQESCM 5
73#define PQECSM 6
74#define PQECCM 7
75/**@}*/
76
77/** ELC axes (x and y directions)*/
78enum class PoQ : int { P, Q };
79/** ELC charge sum/assign protocol: real charges, image charges, or both. */
80enum class ChargeProtocol : int { REAL, IMAGE, BOTH };
81
82/** temporary buffers for product decomposition */
83static std::vector<double> partblk;
84/** collected data from the other cells */
85static double gblcblk[8];
86
87/** structure for caching sin and cos values */
88struct SCCache {
89 double s, c;
90};
91
92/** Cached sin/cos values along the x-axis and y-axis */
93/**@{*/
94static std::vector<SCCache> scxcache;
95static std::vector<SCCache> scycache;
96/**@}*/
97
98/**
99 * @brief Calculate cached sin/cos values for one direction.
100 *
101 * @tparam dir Index of the dimension to consider (e.g. 0 for x ...).
102 *
103 * @param particles Particle to calculate values for
104 * @param n_freq Number of frequencies to calculate per particle
105 * @param u Inverse box length
106 * @return Calculated values.
107 */
108template <std::size_t dir>
109static std::vector<SCCache> calc_sc_cache(ParticleRange const &particles,
110 std::size_t n_freq, double u) {
111 auto constexpr c_2pi = 2. * Utils::pi();
112 auto const n_part = particles.size();
113 std::vector<SCCache> ret(n_freq * n_part);
114
115 for (std::size_t freq = 1; freq <= n_freq; freq++) {
116 auto const pref = c_2pi * u * static_cast<double>(freq);
117
118 std::size_t o = (freq - 1) * n_part;
119 for (auto const &p : particles) {
120 auto const arg = pref * p.pos()[dir];
121 ret[o++] = {sin(arg), cos(arg)};
122 }
123 }
124
125 return ret;
126}
127
128static std::pair<std::size_t, std::size_t>
129prepare_sc_cache(ParticleRange const &particles, BoxGeometry const &box_geo,
130 double far_cut) {
131 assert(far_cut >= 0.);
132 auto const n_freq_x =
133 static_cast<std::size_t>(std::ceil(far_cut * box_geo.length()[0]) + 1.);
134 auto const n_freq_y =
135 static_cast<std::size_t>(std::ceil(far_cut * box_geo.length()[1]) + 1.);
136 auto const u_x = box_geo.length_inv()[0];
137 auto const u_y = box_geo.length_inv()[1];
138 scxcache = calc_sc_cache<0>(particles, n_freq_x, u_x);
139 scycache = calc_sc_cache<1>(particles, n_freq_y, u_y);
140 return {n_freq_x, n_freq_y};
141}
142
143/*****************************************************************/
144/* data distribution */
145/*****************************************************************/
146
147static void clear_vec(double *pdc, std::size_t size) {
148 for (std::size_t i = 0; i < size; i++)
149 pdc[i] = 0.;
150}
151
152static void copy_vec(double *pdc_d, double const *pdc_s, std::size_t size) {
153 for (std::size_t i = 0; i < size; i++)
154 pdc_d[i] = pdc_s[i];
155}
156
157static void add_vec(double *pdc_d, double const *pdc_s1, double const *pdc_s2,
158 std::size_t size) {
159 for (std::size_t i = 0; i < size; i++)
160 pdc_d[i] = pdc_s1[i] + pdc_s2[i];
161}
162
163static void addscale_vec(double *pdc_d, double scale, double const *pdc_s1,
164 double const *pdc_s2, std::size_t size) {
165 for (std::size_t i = 0; i < size; i++)
166 pdc_d[i] = scale * pdc_s1[i] + pdc_s2[i];
167}
168
169static void scale_vec(double scale, double *pdc, std::size_t size) {
170 for (std::size_t i = 0; i < size; i++)
171 pdc[i] *= scale;
172}
173
174static double *block(double *p, std::size_t index, std::size_t size) {
175 return &p[index * size];
176}
177
178static void distribute(std::size_t size) {
179 assert(size <= 8);
180 double send_buf[8];
181 copy_vec(send_buf, gblcblk, size);
182 boost::mpi::all_reduce(comm_cart, send_buf, static_cast<int>(size), gblcblk,
183 std::plus<>());
184}
185
186void ElectrostaticLayerCorrection::check_gap(Particle const &p) const {
187 if (p.q() != 0.) {
188 auto const z = p.pos()[2];
189 if (z < 0. or z > elc.box_h) {
190 runtimeErrorMsg() << "Particle " << p.id() << " entered ELC gap "
191 << "region by " << ((z < 0.) ? z : z - elc.box_h);
192 }
193 }
194}
195
196/*****************************************************************/
197/* dipole terms */
198/*****************************************************************/
199
200/** Calculate the dipole force.
201 * See @cite yeh99a.
202 */
203void ElectrostaticLayerCorrection::add_dipole_force(
204 ParticleRange const &particles) const {
205 constexpr std::size_t size = 3;
206 auto const &box_geo = *get_system().box_geo;
207 auto const pref = prefactor * 4. * Utils::pi() / box_geo.volume();
208
209 /* for non-neutral systems, this shift gives the background contribution
210 * (rsp. for this shift, the DM of the background is zero) */
211 auto const shift = box_geo.length_half()[2];
212
213 // collect moments
214
215 gblcblk[0] = 0.; // sum q_i (z_i - L/2)
216 gblcblk[1] = 0.; // sum q_i z_i
217 gblcblk[2] = 0.; // sum q_i
218
219 for (auto const &p : particles) {
220 check_gap(p);
221 auto const q = p.q();
222 auto const z = p.pos()[2];
223
224 gblcblk[0] += q * (z - shift);
225 gblcblk[1] += q * z;
226 gblcblk[2] += q;
227
229 if (z < elc.space_layer) {
230 gblcblk[0] += elc.delta_mid_bot * q * (-z - shift);
231 gblcblk[2] += elc.delta_mid_bot * q;
232 }
233 if (z > (elc.box_h - elc.space_layer)) {
234 gblcblk[0] += elc.delta_mid_top * q * (2. * elc.box_h - z - shift);
235 gblcblk[2] += elc.delta_mid_top * q;
236 }
237 }
238 }
239
240 gblcblk[0] *= pref;
241 gblcblk[1] *= pref / elc.box_h * box_geo.length()[2];
242 gblcblk[2] *= pref;
243
244 distribute(size);
245
246 // Yeh + Berkowitz dipole term @cite yeh99a
247 auto field_tot = gblcblk[0];
248
249 // Constand potential contribution
250 if (elc.const_pot) {
251 auto const field_induced = gblcblk[1];
252 auto const field_applied = elc.pot_diff / elc.box_h;
253 field_tot -= field_applied + field_induced;
254 }
255
256 for (auto &p : particles) {
257 p.force()[2] -= field_tot * p.q();
258
259 if (!elc.neutralize) {
260 // SUBTRACT the forces of the P3M homogeneous neutralizing background
261 p.force()[2] += gblcblk[2] * p.q() * (p.pos()[2] - shift);
262 }
263 }
264}
265
266/** Calculate the dipole energy.
267 * See @cite yeh99a.
268 */
269double ElectrostaticLayerCorrection::dipole_energy(
270 ParticleRange const &particles) const {
271 constexpr std::size_t size = 7;
272 auto const &box_geo = *get_system().box_geo;
273 auto const pref = prefactor * 2. * Utils::pi() / box_geo.volume();
274 auto const lz = box_geo.length()[2];
275 /* for nonneutral systems, this shift gives the background contribution
276 (rsp. for this shift, the DM of the background is zero) */
277 auto const shift = box_geo.length_half()[2];
278
279 // collect moments
280
281 gblcblk[0] = 0.; // sum q_i primary box
282 gblcblk[1] = 0.; // sum q_i boundary layers
283 gblcblk[2] = 0.; // sum q_i (z_i - L/2) primary box
284 gblcblk[3] = 0.; // sum q_i (z_i - L/2) boundary layers
285 gblcblk[4] = 0.; // sum q_i (z_i - L/2)^2 primary box
286 gblcblk[5] = 0.; // sum q_i (z_i - L/2)^2 boundary layers
287 gblcblk[6] = 0.; // sum q_i z_i primary box
288
289 for (auto const &p : particles) {
290 check_gap(p);
291 auto const q = p.q();
292 auto const z = p.pos()[2];
293
294 gblcblk[0] += q;
295 gblcblk[2] += q * (z - shift);
296 gblcblk[4] += q * (Utils::sqr(z - shift));
297 gblcblk[6] += q * z;
298
300 if (z < elc.space_layer) {
301 gblcblk[1] += elc.delta_mid_bot * q;
302 gblcblk[3] += elc.delta_mid_bot * q * (-z - shift);
303 gblcblk[5] += elc.delta_mid_bot * q * (Utils::sqr(-z - shift));
304 }
305 if (z > (elc.box_h - elc.space_layer)) {
306 gblcblk[1] += elc.delta_mid_top * q;
307 gblcblk[3] += elc.delta_mid_top * q * (2. * elc.box_h - z - shift);
308 gblcblk[5] +=
309 elc.delta_mid_top * q * (Utils::sqr(2. * elc.box_h - z - shift));
310 }
311 }
312 }
313
314 distribute(size);
315
316 // Yeh + Berkowitz term @cite yeh99a
317 auto energy = 2. * pref * (Utils::sqr(gblcblk[2]) + gblcblk[2] * gblcblk[3]);
318
319 if (!elc.neutralize) {
320 // SUBTRACT the energy of the P3M homogeneous neutralizing background
321 energy += 2. * pref *
322 (-gblcblk[0] * gblcblk[4] -
323 (.25 - .5 / 3.) * Utils::sqr(gblcblk[0] * lz));
324 }
325
327 if (elc.const_pot) {
328 // zero potential difference contribution
329 energy += pref / elc.box_h * lz * Utils::sqr(gblcblk[6]);
330 // external potential shift contribution
331 energy -= 2. * elc.pot_diff / elc.box_h * gblcblk[6];
332 }
333
334 /* counter the P3M homogeneous background contribution to the
335 boundaries. We never need that, since a homogeneous background
336 spanning the artificial boundary layers is aphysical. */
337 energy +=
338 pref * (-(gblcblk[1] * gblcblk[4] + gblcblk[0] * gblcblk[5]) -
339 (1. - 2. / 3.) * gblcblk[0] * gblcblk[1] * Utils::sqr(lz));
340 }
341
342 return this_node == 0 ? energy : 0.;
343}
344
345/*****************************************************************/
346
347struct ImageSum {
348 double delta;
349 double shift;
350 double lz;
351 double dci; // delta complement inverse
352
353 ImageSum(double delta, double shift, double lz)
354 : delta{delta}, shift{shift}, lz{lz}, dci{1. / (1. - delta)} {}
355
356 /** @brief Image sum from the bottom layer. */
357 double b(double q, double z) const {
358 return q * dci * (z - 2. * delta * lz * dci) - q * dci * shift;
359 }
360
361 /** @brief Image sum from the top layer. */
362 double t(double q, double z) const {
363 return q * dci * (z + 2. * delta * lz * dci) - q * dci * shift;
364 }
365};
366
367double
368ElectrostaticLayerCorrection::z_energy(ParticleRange const &particles) const {
369 constexpr std::size_t size = 4;
370 auto const &box_geo = *get_system().box_geo;
371 auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1];
372 auto const pref = prefactor * 2. * Utils::pi() * xy_area_inv;
373 auto const delta = elc.delta_mid_top * elc.delta_mid_bot;
374 auto const fac_delta_mid_bot = elc.delta_mid_bot / (1. - delta);
375 auto const fac_delta_mid_top = elc.delta_mid_top / (1. - delta);
376 auto const fac_delta = delta / (1. - delta);
377
378 /* for non-neutral systems, this shift gives the background contribution
379 * (rsp. for this shift, the DM of the background is zero) */
380 auto const shift = box_geo.length_half()[2];
381 auto const lz = box_geo.length()[2];
382
384 if (elc.const_pot) {
385 clear_vec(gblcblk, size);
386 for (auto const &p : particles) {
387 auto const z = p.pos()[2];
388 auto const q = p.q();
389 gblcblk[0] += q;
390 gblcblk[1] += q * (z - shift);
391 if (z < elc.space_layer) {
392 gblcblk[2] -= elc.delta_mid_bot * q;
393 gblcblk[3] -= elc.delta_mid_bot * q * (-z - shift);
394 }
395 if (z > (elc.box_h - elc.space_layer)) {
396 gblcblk[2] += elc.delta_mid_top * q;
397 gblcblk[3] += elc.delta_mid_top * q * (2. * elc.box_h - z - shift);
398 }
399 }
400 } else {
401 // metallic boundaries
402 clear_vec(gblcblk, size);
403 auto const h = elc.box_h;
404 ImageSum const image_sum{delta, shift, lz};
405 for (auto const &p : particles) {
406 auto const z = p.pos()[2];
407 auto const q = p.q();
408 gblcblk[0] += q;
409 gblcblk[1] += q * (z - shift);
411 if (z < elc.space_layer) {
412 gblcblk[2] += fac_delta * (elc.delta_mid_bot + 1.) * q;
413 gblcblk[3] +=
414 q * (image_sum.b(elc.delta_mid_bot * delta, -(2. * h + z)) +
415 image_sum.b(delta, -(2. * h - z)));
416 } else {
417 gblcblk[2] += fac_delta_mid_bot * (1. + elc.delta_mid_top) * q;
418 gblcblk[3] += q * (image_sum.b(elc.delta_mid_bot, -z) +
419 image_sum.b(delta, -(2. * h - z)));
420 }
421 if (z > (h - elc.space_layer)) {
422 // note the minus sign here which is required due to |z_i-z_j|
423 gblcblk[2] -= fac_delta * (elc.delta_mid_top + 1.) * q;
424 gblcblk[3] -=
425 q * (image_sum.t(elc.delta_mid_top * delta, 4. * h - z) +
426 image_sum.t(delta, 2. * h + z));
427 } else {
428 // note the minus sign here which is required due to |z_i-z_j|
429 gblcblk[2] -= fac_delta_mid_top * (1. + elc.delta_mid_bot) * q;
430 gblcblk[3] -= q * (image_sum.t(elc.delta_mid_top, 2. * h - z) +
431 image_sum.t(delta, 2. * h + z));
432 }
433 }
434 }
435 }
436 }
437 distribute(size);
438
439 auto const energy = gblcblk[1] * gblcblk[2] - gblcblk[0] * gblcblk[3];
440 return (this_node == 0) ? -pref * energy : 0.;
441}
442
443void ElectrostaticLayerCorrection::add_z_force(
444 ParticleRange const &particles) const {
445 constexpr std::size_t size = 1;
446 auto const &box_geo = *get_system().box_geo;
447 auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1];
448 auto const pref = prefactor * 2. * Utils::pi() * xy_area_inv;
449 auto const delta = elc.delta_mid_top * elc.delta_mid_bot;
450 auto const fac_delta_mid_bot = elc.delta_mid_bot / (1. - delta);
451 auto const fac_delta_mid_top = elc.delta_mid_top / (1. - delta);
452 auto const fac_delta = delta / (1. - delta);
453
455 if (elc.const_pot) {
456 clear_vec(gblcblk, size);
457 /* just counter the 2 pi |z| contribution stemming from P3M */
458 for (auto const &p : particles) {
459 auto const z = p.pos()[2];
460 auto const q = p.q();
461 if (z < elc.space_layer)
462 gblcblk[0] -= elc.delta_mid_bot * q;
463 if (z > (elc.box_h - elc.space_layer))
464 gblcblk[0] += elc.delta_mid_top * q;
465 }
466 } else {
467 clear_vec(gblcblk, size);
468 for (auto const &p : particles) {
469 auto const z = p.pos()[2];
470 auto const q = p.q();
471 if (z < elc.space_layer) {
472 gblcblk[0] += fac_delta * (elc.delta_mid_bot + 1.) * q;
473 } else {
474 gblcblk[0] += fac_delta_mid_bot * (elc.delta_mid_top + 1.) * q;
475 }
476 if (z > (elc.box_h - elc.space_layer)) {
477 // note the minus sign here which is required due to |z_i-z_j|
478 gblcblk[0] -= fac_delta * (elc.delta_mid_top + 1.) * q;
479 } else {
480 // note the minus sign here which is required due to |z_i-z_j|
481 gblcblk[0] -= fac_delta_mid_top * (elc.delta_mid_bot + 1.) * q;
482 }
483 }
484 }
485
486 gblcblk[0] *= pref;
487
488 distribute(size);
489
490 for (auto &p : particles) {
491 p.force()[2] += gblcblk[0] * p.q();
492 }
493 }
494}
495
496/*****************************************************************/
497/* PoQ exp sum */
498/*****************************************************************/
499
500/** \name q=0 or p=0 per frequency code */
501/**@{*/
502template <PoQ axis>
503void setup_PoQ(elc_data const &elc, double prefactor, std::size_t index,
504 double omega, ParticleRange const &particles,
505 BoxGeometry const &box_geo) {
506 assert(index >= 1);
507 constexpr std::size_t size = 4;
508 auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1];
509 auto const pref_di = prefactor * 4. * Utils::pi() * xy_area_inv;
510 auto const pref = -pref_di / expm1(omega * box_geo.length()[2]);
511 double lclimgebot[4], lclimgetop[4], lclimge[4];
512 double fac_delta_mid_bot = 1., fac_delta_mid_top = 1., fac_delta = 1.;
513
514 if (elc.dielectric_contrast_on) {
515 auto const delta = elc.delta_mid_top * elc.delta_mid_bot;
516 auto const fac_elc = 1. / (1. - delta * exp(-omega * 2. * elc.box_h));
517 fac_delta_mid_bot = elc.delta_mid_bot * fac_elc;
518 fac_delta_mid_top = elc.delta_mid_top * fac_elc;
519 fac_delta = fac_delta_mid_bot * elc.delta_mid_top;
520 }
521
522 clear_vec(lclimge, size);
523 clear_vec(gblcblk, size);
524 auto const &sc_cache = (axis == PoQ::P) ? scxcache : scycache;
525
526 std::size_t ic = 0;
527 auto const o = (index - 1) * particles.size();
528 for (auto const &p : particles) {
529 auto const z = p.pos()[2];
530 auto const q = p.q();
531 auto e = exp(omega * z);
532
533 partblk[size * ic + POQESM] = q * sc_cache[o + ic].s / e;
534 partblk[size * ic + POQESP] = q * sc_cache[o + ic].s * e;
535 partblk[size * ic + POQECM] = q * sc_cache[o + ic].c / e;
536 partblk[size * ic + POQECP] = q * sc_cache[o + ic].c * e;
537
538 add_vec(gblcblk, gblcblk, block(partblk.data(), ic, size), size);
539
540 if (elc.dielectric_contrast_on) {
541 if (z < elc.space_layer) { // handle the lower case first
542 // negative sign is okay here as the image is located at -z
543
544 e = exp(-omega * z);
545
546 auto const scale = q * elc.delta_mid_bot;
547
548 lclimgebot[POQESM] = sc_cache[o + ic].s / e;
549 lclimgebot[POQESP] = sc_cache[o + ic].s * e;
550 lclimgebot[POQECM] = sc_cache[o + ic].c / e;
551 lclimgebot[POQECP] = sc_cache[o + ic].c * e;
552
553 addscale_vec(gblcblk, scale, lclimgebot, gblcblk, size);
554
555 e = (exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot +
556 exp(omega * (+z - 2. * elc.box_h))) *
557 fac_delta;
558 } else {
559 e = (exp(-omega * z) +
560 exp(omega * (z - 2. * elc.box_h)) * elc.delta_mid_top) *
561 fac_delta_mid_bot;
562 }
563
564 lclimge[POQESP] += q * sc_cache[o + ic].s * e;
565 lclimge[POQECP] += q * sc_cache[o + ic].c * e;
566
567 if (z > (elc.box_h - elc.space_layer)) { // handle the upper case now
568 e = exp(omega * (2. * elc.box_h - z));
569
570 auto const scale = q * elc.delta_mid_top;
571
572 lclimgetop[POQESM] = sc_cache[o + ic].s / e;
573 lclimgetop[POQESP] = sc_cache[o + ic].s * e;
574 lclimgetop[POQECM] = sc_cache[o + ic].c / e;
575 lclimgetop[POQECP] = sc_cache[o + ic].c * e;
576
577 addscale_vec(gblcblk, scale, lclimgetop, gblcblk, size);
578
579 e = (exp(omega * (+z - 4. * elc.box_h)) * elc.delta_mid_top +
580 exp(omega * (-z - 2. * elc.box_h))) *
581 fac_delta;
582 } else {
583 e = (exp(omega * (+z - 2. * elc.box_h)) +
584 exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot) *
585 fac_delta_mid_top;
586 }
587
588 lclimge[POQESM] += q * sc_cache[o + ic].s * e;
589 lclimge[POQECM] += q * sc_cache[o + ic].c * e;
590 }
591
592 ++ic;
593 }
594
595 scale_vec(pref, gblcblk, size);
596
597 if (elc.dielectric_contrast_on) {
598 scale_vec(pref_di, lclimge, size);
599 add_vec(gblcblk, gblcblk, lclimge, size);
600 }
601}
602
603template <PoQ axis> void add_PoQ_force(ParticleRange const &particles) {
604 constexpr auto i = static_cast<int>(axis);
605 constexpr std::size_t size = 4;
606
607 std::size_t ic = 0;
608 for (auto &p : particles) {
609 auto &force = p.force();
610 force[i] += partblk[size * ic + POQESM] * gblcblk[POQECP] -
611 partblk[size * ic + POQECM] * gblcblk[POQESP] +
612 partblk[size * ic + POQESP] * gblcblk[POQECM] -
613 partblk[size * ic + POQECP] * gblcblk[POQESM];
614 force[2] += partblk[size * ic + POQECM] * gblcblk[POQECP] +
615 partblk[size * ic + POQESM] * gblcblk[POQESP] -
616 partblk[size * ic + POQECP] * gblcblk[POQECM] -
617 partblk[size * ic + POQESP] * gblcblk[POQESM];
618 ++ic;
619 }
620}
621
622static double PoQ_energy(double omega, std::size_t n_part) {
623 constexpr std::size_t size = 4;
624
625 auto energy = 0.;
626 for (std::size_t ic = 0; ic < n_part; ic++) {
627 energy += partblk[size * ic + POQECM] * gblcblk[POQECP] +
628 partblk[size * ic + POQESM] * gblcblk[POQESP] +
629 partblk[size * ic + POQECP] * gblcblk[POQECM] +
630 partblk[size * ic + POQESP] * gblcblk[POQESM];
631 }
632
633 return energy / omega;
634}
635/**@}*/
636
637/*****************************************************************/
638/* PQ particle blocks */
639/*****************************************************************/
640
641/** \name p,q <> 0 per frequency code */
642/**@{*/
643static void setup_PQ(elc_data const &elc, double prefactor, std::size_t index_p,
644 std::size_t index_q, double omega,
645 ParticleRange const &particles,
646 BoxGeometry const &box_geo) {
647 assert(index_p >= 1);
648 assert(index_q >= 1);
649 constexpr std::size_t size = 8;
650 auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1];
651 auto const pref_di = prefactor * 8 * Utils::pi() * xy_area_inv;
652 auto const pref = -pref_di / expm1(omega * box_geo.length()[2]);
653 double lclimgebot[8], lclimgetop[8], lclimge[8];
654 double fac_delta_mid_bot = 1, fac_delta_mid_top = 1, fac_delta = 1;
655 if (elc.dielectric_contrast_on) {
656 auto const delta = elc.delta_mid_top * elc.delta_mid_bot;
657 auto const fac_elc = 1. / (1. - delta * exp(-omega * 2. * elc.box_h));
658 fac_delta_mid_bot = elc.delta_mid_bot * fac_elc;
659 fac_delta_mid_top = elc.delta_mid_top * fac_elc;
660 fac_delta = fac_delta_mid_bot * elc.delta_mid_top;
661 }
662
663 clear_vec(lclimge, size);
664 clear_vec(gblcblk, size);
665
666 std::size_t ic = 0;
667 auto const ox = (index_p - 1) * particles.size();
668 auto const oy = (index_q - 1) * particles.size();
669 for (auto const &p : particles) {
670 auto const z = p.pos()[2];
671 auto const q = p.q();
672 auto e = exp(omega * z);
673
674 partblk[size * ic + PQESSM] =
675 scxcache[ox + ic].s * scycache[oy + ic].s * q / e;
676 partblk[size * ic + PQESCM] =
677 scxcache[ox + ic].s * scycache[oy + ic].c * q / e;
678 partblk[size * ic + PQECSM] =
679 scxcache[ox + ic].c * scycache[oy + ic].s * q / e;
680 partblk[size * ic + PQECCM] =
681 scxcache[ox + ic].c * scycache[oy + ic].c * q / e;
682
683 partblk[size * ic + PQESSP] =
684 scxcache[ox + ic].s * scycache[oy + ic].s * q * e;
685 partblk[size * ic + PQESCP] =
686 scxcache[ox + ic].s * scycache[oy + ic].c * q * e;
687 partblk[size * ic + PQECSP] =
688 scxcache[ox + ic].c * scycache[oy + ic].s * q * e;
689 partblk[size * ic + PQECCP] =
690 scxcache[ox + ic].c * scycache[oy + ic].c * q * e;
691
692 add_vec(gblcblk, gblcblk, block(partblk.data(), ic, size), size);
693
694 if (elc.dielectric_contrast_on) {
695 if (z < elc.space_layer) { // handle the lower case first
696 // change e to take into account the z position of the images
697
698 e = exp(-omega * z);
699 auto const scale = q * elc.delta_mid_bot;
700
701 lclimgebot[PQESSM] = scxcache[ox + ic].s * scycache[oy + ic].s / e;
702 lclimgebot[PQESCM] = scxcache[ox + ic].s * scycache[oy + ic].c / e;
703 lclimgebot[PQECSM] = scxcache[ox + ic].c * scycache[oy + ic].s / e;
704 lclimgebot[PQECCM] = scxcache[ox + ic].c * scycache[oy + ic].c / e;
705
706 lclimgebot[PQESSP] = scxcache[ox + ic].s * scycache[oy + ic].s * e;
707 lclimgebot[PQESCP] = scxcache[ox + ic].s * scycache[oy + ic].c * e;
708 lclimgebot[PQECSP] = scxcache[ox + ic].c * scycache[oy + ic].s * e;
709 lclimgebot[PQECCP] = scxcache[ox + ic].c * scycache[oy + ic].c * e;
710
711 addscale_vec(gblcblk, scale, lclimgebot, gblcblk, size);
712
713 e = (exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot +
714 exp(omega * (+z - 2. * elc.box_h))) *
715 fac_delta * q;
716
717 } else {
718
719 e = (exp(-omega * z) +
720 exp(omega * (z - 2. * elc.box_h)) * elc.delta_mid_top) *
721 fac_delta_mid_bot * q;
722 }
723
724 lclimge[PQESSP] += scxcache[ox + ic].s * scycache[oy + ic].s * e;
725 lclimge[PQESCP] += scxcache[ox + ic].s * scycache[oy + ic].c * e;
726 lclimge[PQECSP] += scxcache[ox + ic].c * scycache[oy + ic].s * e;
727 lclimge[PQECCP] += scxcache[ox + ic].c * scycache[oy + ic].c * e;
728
729 if (z > (elc.box_h - elc.space_layer)) { // handle the upper case now
730
731 e = exp(omega * (2. * elc.box_h - z));
732 auto const scale = q * elc.delta_mid_top;
733
734 lclimgetop[PQESSM] = scxcache[ox + ic].s * scycache[oy + ic].s / e;
735 lclimgetop[PQESCM] = scxcache[ox + ic].s * scycache[oy + ic].c / e;
736 lclimgetop[PQECSM] = scxcache[ox + ic].c * scycache[oy + ic].s / e;
737 lclimgetop[PQECCM] = scxcache[ox + ic].c * scycache[oy + ic].c / e;
738
739 lclimgetop[PQESSP] = scxcache[ox + ic].s * scycache[oy + ic].s * e;
740 lclimgetop[PQESCP] = scxcache[ox + ic].s * scycache[oy + ic].c * e;
741 lclimgetop[PQECSP] = scxcache[ox + ic].c * scycache[oy + ic].s * e;
742 lclimgetop[PQECCP] = scxcache[ox + ic].c * scycache[oy + ic].c * e;
743
744 addscale_vec(gblcblk, scale, lclimgetop, gblcblk, size);
745
746 e = (exp(omega * (+z - 4. * elc.box_h)) * elc.delta_mid_top +
747 exp(omega * (-z - 2. * elc.box_h))) *
748 fac_delta * q;
749
750 } else {
751
752 e = (exp(omega * (+z - 2. * elc.box_h)) +
753 exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot) *
754 fac_delta_mid_top * q;
755 }
756
757 lclimge[PQESSM] += scxcache[ox + ic].s * scycache[oy + ic].s * e;
758 lclimge[PQESCM] += scxcache[ox + ic].s * scycache[oy + ic].c * e;
759 lclimge[PQECSM] += scxcache[ox + ic].c * scycache[oy + ic].s * e;
760 lclimge[PQECCM] += scxcache[ox + ic].c * scycache[oy + ic].c * e;
761 }
762
763 ic++;
764 }
765
766 scale_vec(pref, gblcblk, size);
767 if (elc.dielectric_contrast_on) {
768 scale_vec(pref_di, lclimge, size);
769 add_vec(gblcblk, gblcblk, lclimge, size);
770 }
771}
772
773static void add_PQ_force(std::size_t index_p, std::size_t index_q, double omega,
774 ParticleRange const &particles,
775 BoxGeometry const &box_geo) {
776 auto constexpr c_2pi = 2. * Utils::pi();
777 auto const pref_x =
778 c_2pi * box_geo.length_inv()[0] * static_cast<double>(index_p) / omega;
779 auto const pref_y =
780 c_2pi * box_geo.length_inv()[1] * static_cast<double>(index_q) / omega;
781 constexpr std::size_t size = 8;
782
783 std::size_t ic = 0;
784 for (auto &p : particles) {
785 auto &force = p.force();
786 force[0] += pref_x * (partblk[size * ic + PQESCM] * gblcblk[PQECCP] +
787 partblk[size * ic + PQESSM] * gblcblk[PQECSP] -
788 partblk[size * ic + PQECCM] * gblcblk[PQESCP] -
789 partblk[size * ic + PQECSM] * gblcblk[PQESSP] +
790 partblk[size * ic + PQESCP] * gblcblk[PQECCM] +
791 partblk[size * ic + PQESSP] * gblcblk[PQECSM] -
792 partblk[size * ic + PQECCP] * gblcblk[PQESCM] -
793 partblk[size * ic + PQECSP] * gblcblk[PQESSM]);
794 force[1] += pref_y * (partblk[size * ic + PQECSM] * gblcblk[PQECCP] +
795 partblk[size * ic + PQESSM] * gblcblk[PQESCP] -
796 partblk[size * ic + PQECCM] * gblcblk[PQECSP] -
797 partblk[size * ic + PQESCM] * gblcblk[PQESSP] +
798 partblk[size * ic + PQECSP] * gblcblk[PQECCM] +
799 partblk[size * ic + PQESSP] * gblcblk[PQESCM] -
800 partblk[size * ic + PQECCP] * gblcblk[PQECSM] -
801 partblk[size * ic + PQESCP] * gblcblk[PQESSM]);
802 force[2] += (partblk[size * ic + PQECCM] * gblcblk[PQECCP] +
803 partblk[size * ic + PQECSM] * gblcblk[PQECSP] +
804 partblk[size * ic + PQESCM] * gblcblk[PQESCP] +
805 partblk[size * ic + PQESSM] * gblcblk[PQESSP] -
806 partblk[size * ic + PQECCP] * gblcblk[PQECCM] -
807 partblk[size * ic + PQECSP] * gblcblk[PQECSM] -
808 partblk[size * ic + PQESCP] * gblcblk[PQESCM] -
809 partblk[size * ic + PQESSP] * gblcblk[PQESSM]);
810 ic++;
811 }
812}
813
814static double PQ_energy(double omega, std::size_t n_part) {
815 constexpr std::size_t size = 8;
816
817 auto energy = 0.;
818 for (std::size_t ic = 0; ic < n_part; ic++) {
819 energy += partblk[size * ic + PQECCM] * gblcblk[PQECCP] +
820 partblk[size * ic + PQECSM] * gblcblk[PQECSP] +
821 partblk[size * ic + PQESCM] * gblcblk[PQESCP] +
822 partblk[size * ic + PQESSM] * gblcblk[PQESSP] +
823 partblk[size * ic + PQECCP] * gblcblk[PQECCM] +
824 partblk[size * ic + PQECSP] * gblcblk[PQECSM] +
825 partblk[size * ic + PQESCP] * gblcblk[PQESCM] +
826 partblk[size * ic + PQESSP] * gblcblk[PQESSM];
827 }
828 return energy / omega;
829}
830/**@}*/
831
832void ElectrostaticLayerCorrection::add_force(
833 ParticleRange const &particles) const {
834 auto constexpr c_2pi = 2. * Utils::pi();
835 auto const &box_geo = *get_system().box_geo;
836 auto const n_freqs = prepare_sc_cache(particles, box_geo, elc.far_cut);
837 auto const n_scxcache = std::get<0>(n_freqs);
838 auto const n_scycache = std::get<1>(n_freqs);
839 partblk.resize(particles.size() * 8);
840
841 add_dipole_force(particles);
842 add_z_force(particles);
843
844 /* the second condition is just for the case of numerical accident */
845 for (std::size_t p = 1;
846 box_geo.length_inv()[0] * static_cast<double>(p - 1) < elc.far_cut &&
847 p <= n_scxcache;
848 p++) {
849 auto const omega = c_2pi * box_geo.length_inv()[0] * static_cast<double>(p);
850 setup_PoQ<PoQ::P>(elc, prefactor, p, omega, particles, box_geo);
851 distribute(4);
852 add_PoQ_force<PoQ::P>(particles);
853 }
854
855 for (std::size_t q = 1;
856 box_geo.length_inv()[1] * static_cast<double>(q - 1) < elc.far_cut &&
857 q <= n_scycache;
858 q++) {
859 auto const omega = c_2pi * box_geo.length_inv()[1] * static_cast<double>(q);
860 setup_PoQ<PoQ::Q>(elc, prefactor, q, omega, particles, box_geo);
861 distribute(4);
862 add_PoQ_force<PoQ::Q>(particles);
863 }
864
865 for (std::size_t p = 1;
866 box_geo.length_inv()[0] * static_cast<double>(p - 1) < elc.far_cut &&
867 p <= n_scxcache;
868 p++) {
869 for (std::size_t q = 1;
870 Utils::sqr(box_geo.length_inv()[0] * static_cast<double>(p - 1)) +
871 Utils::sqr(box_geo.length_inv()[1] *
872 static_cast<double>(q - 1)) <
873 elc.far_cut2 &&
874 q <= n_scycache;
875 q++) {
876 auto const omega =
877 c_2pi *
878 sqrt(Utils::sqr(box_geo.length_inv()[0] * static_cast<double>(p)) +
879 Utils::sqr(box_geo.length_inv()[1] * static_cast<double>(q)));
880 setup_PQ(elc, prefactor, p, q, omega, particles, box_geo);
881 distribute(8);
882 add_PQ_force(p, q, omega, particles, box_geo);
883 }
884 }
885}
886
887double ElectrostaticLayerCorrection::calc_energy(
888 ParticleRange const &particles) const {
889 auto constexpr c_2pi = 2. * Utils::pi();
890 auto const &box_geo = *get_system().box_geo;
891 auto energy = dipole_energy(particles) + z_energy(particles);
892 auto const n_freqs = prepare_sc_cache(particles, box_geo, elc.far_cut);
893 auto const n_scxcache = std::get<0>(n_freqs);
894 auto const n_scycache = std::get<1>(n_freqs);
895
896 auto const n_localpart = particles.size();
897 partblk.resize(n_localpart * 8);
898
899 /* the second condition is just for the case of numerical accident */
900 for (std::size_t p = 1;
901 box_geo.length_inv()[0] * static_cast<double>(p - 1) < elc.far_cut &&
902 p <= n_scxcache;
903 p++) {
904 auto const omega = c_2pi * box_geo.length_inv()[0] * static_cast<double>(p);
905 setup_PoQ<PoQ::P>(elc, prefactor, p, omega, particles, box_geo);
906 distribute(4);
907 energy += PoQ_energy(omega, n_localpart);
908 }
909
910 for (std::size_t q = 1;
911 box_geo.length_inv()[1] * static_cast<double>(q - 1) < elc.far_cut &&
912 q <= n_scycache;
913 q++) {
914 auto const omega = c_2pi * box_geo.length_inv()[1] * static_cast<double>(q);
915 setup_PoQ<PoQ::Q>(elc, prefactor, q, omega, particles, box_geo);
916 distribute(4);
917 energy += PoQ_energy(omega, n_localpart);
918 }
919
920 for (std::size_t p = 1;
921 box_geo.length_inv()[0] * static_cast<double>(p - 1) < elc.far_cut &&
922 p <= n_scxcache;
923 p++) {
924 for (std::size_t q = 1;
925 Utils::sqr(box_geo.length_inv()[0] * static_cast<double>(p - 1)) +
926 Utils::sqr(box_geo.length_inv()[1] *
927 static_cast<double>(q - 1)) <
928 elc.far_cut2 &&
929 q <= n_scycache;
930 q++) {
931 auto const omega =
932 c_2pi *
933 sqrt(Utils::sqr(box_geo.length_inv()[0] * static_cast<double>(p)) +
934 Utils::sqr(box_geo.length_inv()[1] * static_cast<double>(q)));
935 setup_PQ(elc, prefactor, p, q, omega, particles, box_geo);
936 distribute(8);
937 energy += PQ_energy(omega, n_localpart);
938 }
939 }
940 /* we count both i<->j and j<->i, so return just half of it */
941 return 0.5 * energy;
942}
943
944double ElectrostaticLayerCorrection::tune_far_cut() const {
945 // Largest reasonable cutoff for far formula
946 auto constexpr maximal_far_cut = 50.;
947 auto const &box_geo = *get_system().box_geo;
948 auto const box_l_x_inv = box_geo.length_inv()[0];
949 auto const box_l_y_inv = box_geo.length_inv()[1];
950 auto const min_inv_boxl = std::min(box_l_x_inv, box_l_y_inv);
951 auto const box_l_z = box_geo.length()[2];
952 // adjust lz according to dielectric layer method
953 auto const lz =
955
956 auto tuned_far_cut = min_inv_boxl;
957 double err;
958 do {
959 auto const pref = 2. * Utils::pi() * tuned_far_cut;
960 auto const sum = pref + 2. * (box_l_x_inv + box_l_y_inv);
961 auto const den = -expm1(-pref * lz);
962 auto const num1 = exp(pref * (elc.box_h - lz));
963 auto const num2 = exp(-pref * (elc.box_h + lz));
964
965 err = 0.5 / den *
966 (num1 * (sum + 1. / (lz - elc.box_h)) / (lz - elc.box_h) +
967 num2 * (sum + 1. / (lz + elc.box_h)) / (lz + elc.box_h));
968
969 tuned_far_cut += min_inv_boxl;
970 } while (err > elc.maxPWerror and tuned_far_cut < maximal_far_cut);
971 if (tuned_far_cut >= maximal_far_cut) {
972 throw std::runtime_error("ELC tuning failed: maxPWerror too small");
973 }
974 return tuned_far_cut - min_inv_boxl;
975}
976
977static auto calc_total_charge(CellStructure const &cell_structure) {
978 auto local_q = 0.;
979 for (auto const &p : cell_structure.local_particles()) {
980 local_q += p.q();
981 }
982 return boost::mpi::all_reduce(comm_cart, local_q, std::plus<>());
983}
984
985void ElectrostaticLayerCorrection::sanity_checks_periodicity() const {
986 auto const &box_geo = *get_system().box_geo;
987 if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) {
988 throw std::runtime_error("ELC: requires periodicity (True, True, True)");
989 }
990}
991
992void ElectrostaticLayerCorrection::sanity_checks_dielectric_contrasts() const {
994 auto const &cell_structure = *get_system().cell_structure;
995 auto const precision_threshold = std::sqrt(ROUND_ERROR_PREC);
996 auto const total_charge = std::abs(calc_total_charge(cell_structure));
997 if (total_charge >= precision_threshold) {
998 if (elc.const_pot) {
999 // Disable this line to make ELC work again with non-neutral systems
1000 // and metallic boundaries
1001 throw std::runtime_error("ELC does not currently support non-neutral "
1002 "systems with a dielectric contrast.");
1003 }
1004 // ELC with non-neutral systems and no fully metallic boundaries
1005 // does not work
1006 throw std::runtime_error("ELC does not work for non-neutral systems and "
1007 "non-metallic dielectric contrast.");
1008 }
1009 }
1010}
1011
1012void ElectrostaticLayerCorrection::adapt_solver() {
1013 std::visit(
1014 [this](auto &solver) {
1015 set_prefactor(solver->prefactor);
1016 solver->p3m.params.epsilon = P3M_EPSILON_METALLIC;
1017 },
1018 base_solver);
1019}
1020
1021void ElectrostaticLayerCorrection::recalc_box_h() {
1022 m_box_geo = get_system().box_geo.get();
1023 auto const box_z = m_box_geo->length()[2];
1024 auto const new_box_h = box_z - elc.gap_size;
1025 if (new_box_h < 0.) {
1026 throw std::runtime_error("ELC gap size (" + std::to_string(elc.gap_size) +
1027 ") larger than box length in z-direction (" +
1028 std::to_string(box_z) + ")");
1029 }
1030 elc.box_h = new_box_h;
1031}
1032
1033void ElectrostaticLayerCorrection::recalc_space_layer() {
1035 auto const p3m_r_cut = std::visit(
1036 [](auto &solver) { return solver->p3m.params.r_cut; }, base_solver);
1037 // recalculate the space layer size:
1038 // 1. set the space_layer to be 1/3 of the gap size, so that box = layer
1039 elc.space_layer = (1. / 3.) * elc.gap_size;
1040 // 2. but make sure we don't overlap with the near-field formula
1041 auto const free_space = elc.gap_size - p3m_r_cut;
1042 // 3. and make sure the space layer is not bigger than half the actual
1043 // simulation box, to avoid overlaps
1044 auto const half_box_h = elc.box_h / 2.;
1045 auto const max_space_layer = std::min(free_space, half_box_h);
1046 if (elc.space_layer > max_space_layer) {
1047 if (max_space_layer <= 0.) {
1048 throw std::runtime_error("P3M real-space cutoff too large for ELC w/ "
1049 "dielectric contrast");
1050 }
1051 elc.space_layer = max_space_layer;
1052 }
1054 }
1055}
1056
1057elc_data::elc_data(double maxPWerror, double gap_size, double far_cut,
1058 bool neutralize, double delta_top, double delta_bot,
1059 bool with_const_pot, double potential_diff)
1060 : maxPWerror{maxPWerror}, gap_size{gap_size}, box_h{-1.}, far_cut{far_cut},
1061 far_cut2{-1.}, far_calculated{far_cut == -1.},
1062 dielectric_contrast_on{delta_top != 0. or delta_bot != 0.},
1063 const_pot{with_const_pot and dielectric_contrast_on},
1064 neutralize{neutralize and !dielectric_contrast_on},
1065 delta_mid_top{std::clamp(delta_top, -1., +1.)},
1066 delta_mid_bot{std::clamp(delta_bot, -1., +1.)},
1067 pot_diff{(with_const_pot) ? potential_diff : 0.},
1068 // initial setup of parameters, may change later when P3M is finally tuned
1069 // set the space_layer to be 1/3 of the gap size, so that box = layer
1070 space_layer{(dielectric_contrast_on) ? gap_size / 3. : 0.},
1071 space_box{gap_size - ((dielectric_contrast_on) ? 2. * space_layer : 0.)} {
1072
1073 auto const delta_range = 1. + std::sqrt(ROUND_ERROR_PREC);
1074 if (far_cut <= 0. and not far_calculated) {
1075 throw std::domain_error("Parameter 'far_cut' must be > 0");
1076 }
1077 if (maxPWerror <= 0.) {
1078 throw std::domain_error("Parameter 'maxPWerror' must be > 0");
1079 }
1080 if (gap_size <= 0.) {
1081 throw std::domain_error("Parameter 'gap_size' must be > 0");
1082 }
1083 if (potential_diff != 0. and not with_const_pot) {
1084 throw std::invalid_argument(
1085 "Parameter 'const_pot' must be True when 'pot_diff' is non-zero");
1086 }
1087 if (delta_top < -delta_range or delta_top > delta_range) {
1088 throw std::domain_error(
1089 "Parameter 'delta_mid_top' must be >= -1 and <= +1");
1090 }
1091 if (delta_bot < -delta_range or delta_bot > delta_range) {
1092 throw std::domain_error(
1093 "Parameter 'delta_mid_bot' must be >= -1 and <= +1");
1094 }
1095 /* Dielectric contrasts: the deltas should be either both -1 or both +1 when
1096 * no constant potential difference is applied. The case of two non-metallic
1097 * parallel boundaries can only be treated with a constant potential. */
1098 if (dielectric_contrast_on and not const_pot and
1099 (std::fabs(1. - delta_mid_top * delta_mid_bot) < ROUND_ERROR_PREC)) {
1100 throw std::domain_error("ELC with two parallel metallic boundaries "
1101 "requires the const_pot option");
1102 }
1103}
1104
1106 elc_data &&parameters, BaseSolver &&solver)
1107 : elc{parameters}, base_solver{solver} {
1108 adapt_solver();
1109}
1110
1111static void p3m_assign_image_charge(elc_data const &elc, CoulombP3M &p3m,
1112 double q, Utils::Vector3d const &pos) {
1113 if (pos[2] < elc.space_layer) {
1114 auto const q_eff = elc.delta_mid_bot * q;
1115 p3m.assign_charge(q_eff, {pos[0], pos[1], -pos[2]});
1116 }
1117 if (pos[2] > (elc.box_h - elc.space_layer)) {
1118 auto const q_eff = elc.delta_mid_top * q;
1119 p3m.assign_charge(q_eff, {pos[0], pos[1], 2. * elc.box_h - pos[2]});
1120 }
1121}
1122
1123template <ChargeProtocol protocol, typename combined_ranges>
1124void charge_assign(elc_data const &elc, CoulombP3M &solver,
1125 combined_ranges const &p_q_pos_range) {
1126 if (protocol == ChargeProtocol::BOTH or protocol == ChargeProtocol::IMAGE) {
1127 solver.p3m.inter_weights.reset(solver.p3m.params.cao);
1128 }
1129 /* prepare local FFT mesh */
1130 for (int i = 0; i < solver.p3m.local_mesh.size; i++)
1131 solver.p3m.rs_mesh[i] = 0.;
1132
1133 for (auto zipped : p_q_pos_range) {
1134 auto const p_q = boost::get<0>(zipped);
1135 auto const &p_pos = boost::get<1>(zipped);
1136 if (p_q != 0.) {
1137 if (protocol == ChargeProtocol::BOTH or
1138 protocol == ChargeProtocol::REAL) {
1139 solver.assign_charge(p_q, p_pos, solver.p3m.inter_weights);
1140 }
1141 if (protocol == ChargeProtocol::BOTH or
1142 protocol == ChargeProtocol::IMAGE) {
1143 p3m_assign_image_charge(elc, solver, p_q, p_pos);
1144 }
1145 }
1146 }
1147}
1148
1149template <ChargeProtocol protocol, typename combined_range>
1150void modify_p3m_sums(elc_data const &elc, CoulombP3M &solver,
1151 combined_range const &p_q_pos_range) {
1152
1153 Utils::Vector3d node_sums{};
1154 for (auto zipped : p_q_pos_range) {
1155 auto const p_q = boost::get<0>(zipped);
1156 auto const &p_pos = boost::get<1>(zipped);
1157 if (p_q != 0.) {
1158 auto const p_z = p_pos[2];
1159
1160 if (protocol == ChargeProtocol::BOTH or
1161 protocol == ChargeProtocol::REAL) {
1162 node_sums[0] += 1.;
1163 node_sums[1] += Utils::sqr(p_q);
1164 node_sums[2] += p_q;
1165 }
1166
1167 if (protocol == ChargeProtocol::BOTH or
1168 protocol == ChargeProtocol::IMAGE) {
1169 if (p_z < elc.space_layer) {
1170 node_sums[0] += 1.;
1171 node_sums[1] += Utils::sqr(elc.delta_mid_bot * p_q);
1172 node_sums[2] += elc.delta_mid_bot * p_q;
1173 }
1174
1175 if (p_z > (elc.box_h - elc.space_layer)) {
1176 node_sums[0] += 1.;
1177 node_sums[1] += Utils::sqr(elc.delta_mid_top * p_q);
1178 node_sums[2] += elc.delta_mid_top * p_q;
1179 }
1180 }
1181 }
1182 }
1183
1184 auto const tot_sums =
1185 boost::mpi::all_reduce(comm_cart, node_sums, std::plus<>());
1186 solver.p3m.sum_qpart = static_cast<int>(tot_sums[0] + 0.1);
1187 solver.p3m.sum_q2 = tot_sums[1];
1188 solver.p3m.square_sum_q = Utils::sqr(tot_sums[2]);
1189}
1190
1192 ParticleRange const &particles) const {
1193 auto const energy = std::visit(
1194 [this, &particles](auto const &solver_ptr) {
1195 auto &solver = *solver_ptr;
1196 auto const &box_geo = *get_system().box_geo;
1197
1198 auto p_q_range = ParticlePropertyRange::charge_range(particles);
1199 auto p_pos_range = ParticlePropertyRange::pos_range(particles);
1200 auto p_q_pos_range = boost::combine(p_q_range, p_pos_range);
1201
1202 // assign the original charges (they may not have been assigned yet)
1203 solver.charge_assign(particles);
1204
1206 return solver.long_range_energy(particles);
1207 }
1208
1209 auto energy = 0.;
1210 energy += 0.5 * solver.long_range_energy(particles);
1211 energy +=
1212 0.5 * elc.dielectric_layers_self_energy(solver, box_geo, particles);
1213
1214 // assign both original and image charges
1215 charge_assign<ChargeProtocol::BOTH>(elc, solver, p_q_pos_range);
1216 modify_p3m_sums<ChargeProtocol::BOTH>(elc, solver, p_q_pos_range);
1217 energy += 0.5 * solver.long_range_energy(particles);
1218
1219 // assign only the image charges now
1220 charge_assign<ChargeProtocol::IMAGE>(elc, solver, p_q_pos_range);
1221 modify_p3m_sums<ChargeProtocol::IMAGE>(elc, solver, p_q_pos_range);
1222 energy -= 0.5 * solver.long_range_energy(particles);
1223
1224 // restore modified sums
1225 modify_p3m_sums<ChargeProtocol::REAL>(elc, solver, p_q_pos_range);
1226
1227 return energy;
1228 },
1229 base_solver);
1230 return energy + calc_energy(particles);
1231}
1232
1234 ParticleRange const &particles) const {
1235 std::visit(
1236 [this, &particles](auto const &solver_ptr) {
1237 auto &solver = *solver_ptr;
1238 auto p_q_range = ParticlePropertyRange::charge_range(particles);
1239 auto p_pos_range = ParticlePropertyRange::pos_range(particles);
1240 auto p_q_pos_range = boost::combine(p_q_range, p_pos_range);
1242 auto const &box_geo = *get_system().box_geo;
1243 modify_p3m_sums<ChargeProtocol::BOTH>(elc, solver, p_q_pos_range);
1244 charge_assign<ChargeProtocol::BOTH>(elc, solver, p_q_pos_range);
1245 elc.dielectric_layers_self_forces(solver, box_geo, particles);
1246 } else {
1247 solver.charge_assign(particles);
1248 }
1249 solver.add_long_range_forces(particles);
1251 modify_p3m_sums<ChargeProtocol::REAL>(elc, solver, p_q_pos_range);
1252 }
1253 },
1254 base_solver);
1255 add_force(particles);
1256}
1257
1258#endif
Vector implementation and trait types for boost qvm interoperability.
float sum
__global__ float * force
__shared__ int pos[MAXDEPTH *THREADS5/WARPSIZE]
float h[3]
float u[3]
Utils::Vector3d const & length() const
Box length.
Utils::Vector3d const & length_inv() const
Inverse box length.
A range of particles.
base_type::size_type size() const
void reset(int cao)
Reset the cache.
auto constexpr P3M_EPSILON_METALLIC
This value indicates metallic boundary conditions.
Definition common.hpp:45
boost::mpi::communicator comm_cart
The communicator.
int this_node
The number of this node.
This file contains the defaults for ESPResSo.
#define ROUND_ERROR_PREC
Precision for capture of round off errors.
Definition config.hpp:66
T image_sum(InputIterator begin, InputIterator end, InputIterator it, bool with_replicas, Utils::Vector3i const &ncut, BoxGeometry const &box_geo, T init, F f)
Sum over all pairs with periodic images.
static void addscale_vec(double *pdc_d, double scale, double const *pdc_s1, double const *pdc_s2, std::size_t size)
Definition elc.cpp:163
#define PQESSM
Definition elc.cpp:71
#define POQESP
Definition elc.cpp:62
static std::pair< std::size_t, std::size_t > prepare_sc_cache(ParticleRange const &particles, BoxGeometry const &box_geo, double far_cut)
Definition elc.cpp:129
#define PQESCP
Definition elc.cpp:68
static void add_PQ_force(std::size_t index_p, std::size_t index_q, double omega, ParticleRange const &particles, BoxGeometry const &box_geo)
Definition elc.cpp:773
static auto calc_total_charge(CellStructure const &cell_structure)
Definition elc.cpp:977
static std::vector< double > partblk
temporary buffers for product decomposition
Definition elc.cpp:83
#define PQESCM
Definition elc.cpp:72
static void clear_vec(double *pdc, std::size_t size)
Definition elc.cpp:147
static double * block(double *p, std::size_t index, std::size_t size)
Definition elc.cpp:174
void setup_PoQ(elc_data const &elc, double prefactor, std::size_t index, double omega, ParticleRange const &particles, BoxGeometry const &box_geo)
Definition elc.cpp:503
static void distribute(std::size_t size)
Definition elc.cpp:178
void modify_p3m_sums(elc_data const &elc, CoulombP3M &solver, combined_range const &p_q_pos_range)
Definition elc.cpp:1150
static double PoQ_energy(double omega, std::size_t n_part)
Definition elc.cpp:622
static std::vector< SCCache > scxcache
Cached sin/cos values along the x-axis and y-axis.
Definition elc.cpp:94
static void p3m_assign_image_charge(elc_data const &elc, CoulombP3M &p3m, double q, Utils::Vector3d const &pos)
Definition elc.cpp:1111
#define PQECCP
Definition elc.cpp:70
static double PQ_energy(double omega, std::size_t n_part)
Definition elc.cpp:814
void charge_assign(elc_data const &elc, CoulombP3M &solver, combined_ranges const &p_q_pos_range)
Definition elc.cpp:1124
#define POQECP
Definition elc.cpp:63
static std::vector< SCCache > scycache
Definition elc.cpp:95
static void setup_PQ(elc_data const &elc, double prefactor, std::size_t index_p, std::size_t index_q, double omega, ParticleRange const &particles, BoxGeometry const &box_geo)
Definition elc.cpp:643
static void copy_vec(double *pdc_d, double const *pdc_s, std::size_t size)
Definition elc.cpp:152
#define PQECCM
Definition elc.cpp:74
#define POQECM
Definition elc.cpp:65
static void add_vec(double *pdc_d, double const *pdc_s1, double const *pdc_s2, std::size_t size)
Definition elc.cpp:157
#define POQESM
Definition elc.cpp:64
static void scale_vec(double scale, double *pdc, std::size_t size)
Definition elc.cpp:169
void add_PoQ_force(ParticleRange const &particles)
Definition elc.cpp:603
ChargeProtocol
ELC charge sum/assign protocol: real charges, image charges, or both.
Definition elc.cpp:80
static std::vector< SCCache > calc_sc_cache(ParticleRange const &particles, std::size_t n_freq, double u)
Calculate cached sin/cos values for one direction.
Definition elc.cpp:109
#define PQESSP
Definition elc.cpp:67
#define PQECSP
Definition elc.cpp:69
static double gblcblk[8]
collected data from the other cells
Definition elc.cpp:85
PoQ
ELC axes (x and y directions)
Definition elc.cpp:78
#define PQECSM
Definition elc.cpp:73
ELC algorithm for long-range Coulomb interactions.
This file contains the errorhandling code for severe errors, like a broken bond or illegal parameter ...
#define runtimeErrorMsg()
Common parts of the MMM family of methods for the electrostatic interaction: MMM1D and ELC.
__constant__ float maxPWerror[1]
ParticleRange particles(Utils::Span< Cell *const > cells)
auto charge_range(ParticleRange const &particles)
auto pos_range(ParticleRange const &particles)
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
Vector< T, N > sqrt(Vector< T, N > const &a)
Definition Vector.hpp:342
P3M algorithm for long-range Coulomb interaction.
P3M electrostatics on GPU.
Describes a cell structure / cell system.
ParticleRange local_particles() const
P3M solver.
Definition p3m.hpp:85
void assign_charge(double q, Utils::Vector3d const &real_pos, p3m_interpolation_cache &inter_weights)
Assign a single charge into the current charge grid.
Definition p3m.cpp:353
p3m_data_struct p3m
P3M parameters.
Definition p3m.hpp:87
BaseSolver base_solver
Electrostatics solver that is adapted.
Definition elc.hpp:179
ElectrostaticLayerCorrection(elc_data &&parameters, BaseSolver &&solver)
Definition elc.cpp:1105
std::variant< std::shared_ptr< CoulombP3MGPU >, std::shared_ptr< CoulombP3M > > BaseSolver
Definition elc.hpp:173
BoxGeometry * m_box_geo
Definition elc.hpp:176
void add_long_range_forces(ParticleRange const &particles) const
Definition elc.cpp:1233
double long_range_energy(ParticleRange const &particles) const
Definition elc.cpp:1191
double b(double q, double z) const
Image sum from the bottom layer.
Definition elc.cpp:357
double dci
Definition elc.cpp:351
double shift
Definition elc.cpp:349
ImageSum(double delta, double shift, double lz)
Definition elc.cpp:353
double t(double q, double z) const
Image sum from the top layer.
Definition elc.cpp:362
double lz
Definition elc.cpp:350
double delta
Definition elc.cpp:348
int size
number of local mesh points.
Definition common.hpp:188
int cao
charge assignment order ([0,7]).
Definition common.hpp:82
Struct holding all information for one particle.
Definition Particle.hpp:393
auto const & q() const
Definition Particle.hpp:506
auto const & pos() const
Definition Particle.hpp:429
auto const & force() const
Definition Particle.hpp:433
auto const & id() const
Definition Particle.hpp:412
structure for caching sin and cos values
Definition elc.cpp:88
double c
Definition elc.cpp:89
double s
Definition elc.cpp:89
Parameters for the ELC method.
Definition elc.hpp:63
double dielectric_layers_self_energy(CoulombP3M const &p3m, BoxGeometry const &box_geo, ParticleRange const &particles) const
self energies of top and bottom layers with their virtual images
Definition elc.hpp:139
double maxPWerror
Maximal allowed pairwise error for the potential and force.
Definition elc.hpp:72
double pot_diff
Constant potential difference.
Definition elc.hpp:111
double box_h
Up to where particles can be found.
Definition elc.hpp:78
bool dielectric_contrast_on
Flag whether there is any dielectric contrast in the system.
Definition elc.hpp:94
elc_data(double maxPWerror, double gap_size, double far_cut, bool neutralize, double delta_top, double delta_bot, bool const_pot, double pot_diff)
Definition elc.cpp:1057
double space_box
The space that is finally left.
Definition elc.hpp:116
bool neutralize
Flag whether the box is neutralized by a homogeneous background.
Definition elc.hpp:104
double far_cut
Cutoff of the exponential sum.
Definition elc.hpp:84
double space_layer
Layer around the dielectric contrast in which we trick around.
Definition elc.hpp:114
bool far_calculated
Flag whether far_cut was set by the user, or calculated by ESPResSo.
Definition elc.hpp:91
double gap_size
Size of the empty gap.
Definition elc.hpp:76
double delta_mid_bot
dielectric contrast in the lower part of the simulation cell.
Definition elc.hpp:109
void dielectric_layers_self_forces(CoulombP3M const &p3m, BoxGeometry const &box_geo, ParticleRange const &particles) const
forces of particles in border layers with themselves
Definition elc.hpp:154
bool const_pot
Flag whether a constant potential difference is applied.
Definition elc.hpp:96
double far_cut2
Squared value of far_cut.
Definition elc.hpp:86
double delta_mid_top
dielectric contrast in the upper part of the simulation cell.
Definition elc.hpp:107
P3MParameters params
P3MLocalMesh local_mesh
local mesh.
Definition p3m.hpp:63
double sum_q2
Sum of square of charges (only on head node).
Definition p3m.hpp:72
fft_vector< double > rs_mesh
real space mesh (local) for CA/FFT.
Definition p3m.hpp:65
double square_sum_q
square of sum of charges (only on head node).
Definition p3m.hpp:74
int sum_qpart
number of charged particles (only on head node).
Definition p3m.hpp:70
p3m_interpolation_cache inter_weights
Definition p3m.hpp:76