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