ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
elc.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2010-2022 The ESPResSo project
3 * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010
4 * Max-Planck-Institute for Polymer Research, Theory Group
5 *
6 * This file is part of ESPResSo.
7 *
8 * ESPResSo is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * ESPResSo is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22#include "config/config.hpp"
23
24#ifdef P3M
25
27
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 = 0;
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 = 0;
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:357
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:55
virtual void count_charged_particles_elc(int, double, double)=0
virtual void prepare_fft_mesh(bool reset_weights)=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