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