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