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