ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
dipolar_direct_sum.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2010-2022 The ESPResSo project
3 * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010
4 * Max-Planck-Institute for Polymer Research, Theory Group
5 *
6 * This file is part of ESPResSo.
7 *
8 * ESPResSo is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * ESPResSo is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22#include "config/config.hpp"
23
24#ifdef ESPRESSO_DIPOLES
25
27
28#include "BoxGeometry.hpp"
29#include "cells.hpp"
30#include "communication.hpp"
31#include "errorhandling.hpp"
32#include "system/System.hpp"
33
35#include <utils/math/sqr.hpp>
37
38#include <boost/mpi/collectives.hpp>
39#include <boost/mpi/communicator.hpp>
40
41#include <mpi.h>
42
43#include <algorithm>
44#include <cassert>
45#include <cmath>
46#include <iterator>
47#include <ranges>
48#include <stdexcept>
49#include <tuple>
50#include <utility>
51#include <vector>
52
53/**
54 * @brief Pair force of two interacting dipoles.
55 *
56 * @param d Distance vector.
57 * @param m1 Dipole moment of one particle.
58 * @param m2 Dipole moment of the other particle.
59 *
60 * @return Resulting force.
61 */
62static auto pair_force(Utils::Vector3d const &d, Utils::Vector3d const &m1,
63 Utils::Vector3d const &m2) {
64 auto const pe2 = m1 * d;
65 auto const pe3 = m2 * d;
66
67 auto const r2 = d.norm2();
68 auto const r = std::sqrt(r2);
69 auto const r5 = r2 * r2 * r;
70 auto const r7 = r5 * r2;
71
72 auto const a = 3.0 * (m1 * m2) / r5;
73 auto const b = -15.0 * pe2 * pe3 / r7;
74
75 auto const f = (a + b) * d + 3.0 * (pe3 * m1 + pe2 * m2) / r5;
76 auto const r3 = r2 * r;
77 auto const t =
78 -vector_product(m1, m2) / r3 + 3.0 * pe3 * vector_product(m1, d) / r5;
79
80 return ParticleForce{f, t};
81}
82
83/**
84 * @brief Pair potential for two interacting dipoles.
85 *
86 * @param d Distance vector.
87 * @param m1 Dipole moment of one particle.
88 * @param m2 Dipole moment of the other particle.
89 *
90 * @return Interaction energy.
91 */
92static auto pair_potential(Utils::Vector3d const &d, Utils::Vector3d const &m1,
93 Utils::Vector3d const &m2) {
94 auto const r2 = d * d;
95 auto const r = sqrt(r2);
96 auto const r3 = r2 * r;
97 auto const r5 = r3 * r2;
98
99 auto const pe1 = m1 * m2;
100 auto const pe2 = m1 * d;
101 auto const pe3 = m2 * d;
102
103 return pe1 / r3 - 3.0 * pe2 * pe3 / r5;
104}
105
106#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
107/**
108 * @brief Dipole field contribution from a particle with dipole moment @c m1
109 * at a distance @c d.
110 *
111 * @param d Distance vector.
112 * @param m1 Dipole moment of one particle.
113 *
114 * @return Utils::Vector3d containing dipole field components.
115 */
116static auto dipole_field(Utils::Vector3d const &d, Utils::Vector3d const &m1) {
117 auto const r2 = d * d;
118 auto const r = sqrt(r2);
119 auto const r3 = r2 * r;
120 auto const r5 = r3 * r2;
121 auto const pe2 = m1 * d;
122
123 return 3.0 * pe2 * d / r5 - m1 / r3;
124}
125#endif
126
127/**
128 * @brief Call kernel for every 3d index in a sphere around the origin.
129 *
130 * This calls a callable for all index-triples
131 * that are within ball around the origin with
132 * radius |ncut|.
133 *
134 * @tparam F Callable
135 * @param ncut Limits in the three directions,
136 * all non-zero elements have to be
137 * the same number.
138 * @param f will be called for each index triple
139 * within the limits of @p ncut.
140 */
141template <typename F> void for_each_image(Utils::Vector3i const &ncut, F f) {
142 auto const ncut2 = ncut.norm2();
143
144 /* This runs over the index "cube"
145 * [-ncut[0], ncut[0]] x ... x [-ncut[2], ncut[2]]
146 * (inclusive on both sides), and calls f with
147 * all the elements as argument. Counting range
148 * is a range that just enumerates a range.
149 */
151 [&](int nx, int ny, int nz) {
152 if (nx * nx + ny * ny + nz * nz <= ncut2) {
153 f(nx, ny, nz);
154 }
155 },
156 std::views::iota(-ncut[0], ncut[0] + 1),
157 std::views::iota(-ncut[1], ncut[1] + 1),
158 std::views::iota(-ncut[2], ncut[2] + 1));
159}
160
161/**
162 * @brief Position and dipole moment of one particle.
163 */
164struct PosMom {
167
168 template <class Archive> void serialize(Archive &ar, long int) {
169 ar & pos & m;
170 }
171};
172
173/**
174 * @brief Sum over all pairs with periodic images.
175 *
176 * This implements the "primed" pair sum, the sum over all
177 * pairs between one particles and all other particles,
178 * including @p ncut periodic replicas in each direction.
179 * Primed means that in the primary replica the self-interaction
180 * is excluded, but not with the other periodic replicas. E.g.
181 * a particle does not interact with its self, but does with
182 * its periodically shifted versions.
183 *
184 * @param begin Iterator pointing to begin of particle range
185 * @param end Iterator pointing past the end of particle range
186 * @param it Pointer to particle that is considered
187 * @param with_replicas If periodic replicas are to be considered
188 * at all. If false, distances are calculated as Euclidean
189 * distances, and not using minimum image convention.
190 * @param ncut Number of replicas in each direction.
191 * @param box_geo Box geometry.
192 * @param init Initial value of the sum.
193 * @param f Binary operation mapping distance and moment of the
194 * interaction partner to the value to be summed up for this pair.
195 *
196 * @return The total sum.
197 */
198template <class InputIterator, class T, class F>
199T image_sum(InputIterator begin, InputIterator end, InputIterator it,
200 bool with_replicas, Utils::Vector3i const &ncut,
201 BoxGeometry const &box_geo, T init, F f) {
202
203 auto const &box_l = box_geo.length();
204 for (auto jt = begin; jt != end; ++jt) {
205 auto const exclude_primary = (it == jt);
206 auto const primary_distance = (with_replicas)
207 ? (it->pos - jt->pos)
208 : box_geo.get_mi_vector(it->pos, jt->pos);
209
210 for_each_image(ncut, [&](int nx, int ny, int nz) {
211 if (!(exclude_primary && nx == 0 && ny == 0 && nz == 0)) {
212 auto const rn =
213 primary_distance +
214 Utils::Vector3d{nx * box_l[0], ny * box_l[1], nz * box_l[2]};
215 init += f(rn, jt->m);
216 }
217 });
218 }
219
220 return init;
221}
222
223static auto gather_particle_data(BoxGeometry const &box_geo,
224 ParticleRange const &particles) {
225 auto const &comm = ::comm_cart;
226 std::vector<Particle *> local_particles;
227 std::vector<PosMom> local_posmom;
228 std::vector<PosMom> all_posmom;
229 std::vector<boost::mpi::request> reqs;
230
231 local_particles.reserve(particles.size());
232 local_posmom.reserve(particles.size());
233
234 for (auto &p : particles) {
235 if (p.dipm() != 0.0) {
236 local_particles.emplace_back(&p);
237 local_posmom.emplace_back(
238 PosMom{box_geo.folded_position(p.pos()), p.calc_dip()});
239 }
240 }
241
242 auto const local_size = static_cast<int>(local_posmom.size());
243 std::vector<int> all_sizes;
244 boost::mpi::all_gather(comm, local_size, all_sizes);
245
246 auto const offset =
247 std::accumulate(all_sizes.begin(), all_sizes.begin() + comm.rank(), 0);
248 auto const total_size =
249 std::accumulate(all_sizes.begin() + comm.rank(), all_sizes.end(), offset);
250
251 if (comm.size() > 1) {
252 all_posmom.resize(total_size);
253 reqs = Utils::Mpi::iall_gatherv(comm, local_posmom.data(), local_size,
254 all_posmom.data(), all_sizes.data());
255 } else {
256 std::swap(all_posmom, local_posmom);
257 }
258
259 return std::make_tuple(std::move(local_particles), std::move(all_posmom),
260 std::move(reqs), offset);
261}
262
263static auto get_n_cut(BoxGeometry const &box_geo, int n_replicas) {
264 return n_replicas * Utils::Vector3i{static_cast<int>(box_geo.periodic(0)),
265 static_cast<int>(box_geo.periodic(1)),
266 static_cast<int>(box_geo.periodic(2))};
267}
268
269/**
270 * @brief Calculate and add the interaction forces/torques to the particles.
271 *
272 * This employs a parallel N-square loop over all particle pairs.
273 * The computation the partitioned into several steps so that the
274 * communication latency can be hidden behinder some local computation:
275 *
276 * 1. The local particle positions and momenta are packed into
277 * one array.
278 * 2. The asynchronous distribution of the local arrays to all
279 * ranks is started.
280 * 3. The interaction for the local pairs is started, here every
281 * pair is visited only once, and the force is added to both particles.
282 * 4. Wait for the data from the other nodes.
283 * 5. Calculate the interaction with the rest of the particles. Here
284 * every pair is visited twice (not necessarily on the same rank)
285 * so that no reduction of the forces is needed.
286 *
287 * Logically this is equivalent to the potential calculation
288 * in @ref DipolarDirectSum::long_range_energy, which calculates
289 * a naive N-square sum, but has better performance and scaling.
290 */
292 ParticleRange const &particles) const {
293 auto const &box_geo = *get_system().box_geo;
294 auto const &box_l = box_geo.length();
295 auto [local_particles, all_posmom, reqs, offset] =
296 gather_particle_data(box_geo, particles);
297
298 /* Number of image boxes considered */
299 auto const ncut = get_n_cut(box_geo, n_replicas);
300 auto const with_replicas = (ncut.norm2() > 0);
301
302 /* Range of particles we calculate the ia for on this node */
303 auto const local_posmom_begin = all_posmom.begin() + offset;
304 auto const local_posmom_end =
305 local_posmom_begin + static_cast<long>(local_particles.size());
306
307 /* Output iterator for the force */
308 auto p = local_particles.begin();
309
310 /* IA with local particles */
311 for (auto it = local_posmom_begin; it != local_posmom_end; ++it, ++p) {
312 /* IA with own images */
313 auto fi = image_sum(
314 it, std::next(it), it, with_replicas, ncut, box_geo, ParticleForce{},
315 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
316 return pair_force(rn, it->m, mj);
317 });
318
319 /* IA with other local particles */
320 auto q = std::next(p);
321 for (auto jt = std::next(it); jt != local_posmom_end; ++jt, ++q) {
322 auto const d = (with_replicas) ? (it->pos - jt->pos)
323 : box_geo.get_mi_vector(it->pos, jt->pos);
324
325 ParticleForce fij{};
326 ParticleForce fji{};
327 for_each_image(ncut, [&](int nx, int ny, int nz) {
328 auto const rn =
329 d + Utils::Vector3d{nx * box_l[0], ny * box_l[1], nz * box_l[2]};
330 auto const pf = pair_force(rn, it->m, jt->m);
331 fij += pf;
332 fji.f -= pf.f;
333 /* Conservation of angular momentum mandates that
334 * 0 = t_i + r_ij x F_ij + t_j */
335 fji.torque += vector_product(pf.f, rn) - pf.torque;
336 });
337
338 fi += fij;
339 (*q)->force() += prefactor * fji.f;
340 (*q)->torque() += prefactor * fji.torque;
341 }
342
343 (*p)->force() += prefactor * fi.f;
344 (*p)->torque() += prefactor * fi.torque;
345 }
346
347 /* Wait for the rest of the data to arrive */
348 boost::mpi::wait_all(reqs.begin(), reqs.end());
349
350 /* Output iterator for the force */
351 p = local_particles.begin();
352
353 /* Interaction with all the other particles */
354 for (auto it = local_posmom_begin; it != local_posmom_end; ++it, ++p) {
355 // red particles
356 auto fi =
357 image_sum(all_posmom.begin(), local_posmom_begin, it, with_replicas,
358 ncut, box_geo, ParticleForce{},
359 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
360 return pair_force(rn, it->m, mj);
361 });
362
363 // black particles
364 fi += image_sum(local_posmom_end, all_posmom.end(), it, with_replicas, ncut,
365 box_geo, ParticleForce{},
366 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
367 return pair_force(rn, it->m, mj);
368 });
369
370 (*p)->force() += prefactor * fi.f;
371 (*p)->torque() += prefactor * fi.torque;
372 }
373#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
375#endif
376}
377
378/**
379 * @brief Calculate the interaction potential.
380 *
381 * This employs a parallel N-square loop over all particle pairs.
382 */
383double
385 auto const &box_geo = *get_system().box_geo;
386 auto [local_particles, all_posmom, reqs, offset] =
387 gather_particle_data(box_geo, particles);
388
389 /* Number of image boxes considered */
390 auto const ncut = get_n_cut(box_geo, n_replicas);
391 auto const with_replicas = (ncut.norm2() > 0);
392
393 /* Wait for the rest of the data to arrive */
394 boost::mpi::wait_all(reqs.begin(), reqs.end());
395
396 /* Range of particles we calculate the ia for on this node */
397 auto const local_posmom_begin = all_posmom.begin() + offset;
398 auto const local_posmom_end =
399 local_posmom_begin + static_cast<long>(local_particles.size());
400
401 auto u = 0.;
402 for (auto it = local_posmom_begin; it != local_posmom_end; ++it) {
403 u = image_sum(it, all_posmom.end(), it, with_replicas, ncut, box_geo, u,
404 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
405 return pair_potential(rn, it->m, mj);
406 });
407 }
408
409 return prefactor * u;
410}
411
412/**
413 * @brief Calculate total dipole field of each particle.
414 *
415 * This employs a parallel N-square loop over all particles.
416 * Logically this is equivalent to the potential calculation
417 * in @ref DipolarDirectSum::long_range_energy, which calculates
418 * a naive N-square sum. The difference is summation range,
419 * and the kernel calculates the dipole field rather than the energy.
420 */
421#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
423 ParticleRange const &particles) const {
424 auto const &box_geo = *get_system().box_geo;
425 /* collect particle data */
426 auto [local_particles, all_posmom, reqs, offset] =
427 gather_particle_data(box_geo, particles);
428
429 auto const ncut = get_n_cut(box_geo, n_replicas);
430 auto const with_replicas = (ncut.norm2() > 0);
431
432 boost::mpi::wait_all(reqs.begin(), reqs.end());
433
434 auto const local_posmom_begin = all_posmom.begin() + offset;
435 auto const local_posmom_end =
436 local_posmom_begin + static_cast<long>(local_particles.size());
437
438 Utils::Vector3d u_init = {0., 0., 0.};
439 auto p = local_particles.begin();
440 for (auto pi = local_posmom_begin; pi != local_posmom_end; ++pi, ++p) {
441 auto const u = image_sum(
442 all_posmom.begin(), all_posmom.end(), pi, with_replicas, ncut, box_geo,
443 u_init, [](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
444 return dipole_field(rn, mj);
445 });
446 (*p)->dip_fld() = prefactor * u;
447 }
448}
449#endif
450
451DipolarDirectSum::DipolarDirectSum(double prefactor, int n_replicas) {
453 this->n_replicas = n_replicas;
454 if (n_replicas < 0) {
455 throw std::domain_error("Parameter 'n_replicas' must be >= 0");
456 }
457}
458
459#endif // ESPRESSO_DIPOLES
This file contains everything related to the global cell structure / cell system.
auto folded_position(Utils::Vector3d const &pos) const
Calculate coordinates folded to primary simulation box.
Utils::Vector3d const & length() const
Box length.
constexpr bool periodic(unsigned coord) const
Check periodicity in direction.
ESPRESSO_ATTR_ALWAYS_INLINE Utils::Vector< T, 3 > get_mi_vector(const Utils::Vector< T, 3 > &a, const Utils::Vector< T, 3 > &b) const
Get the minimum-image vector between two coordinates.
void set_prefactor(double new_prefactor)
double prefactor
Magnetostatics prefactor.
A range of particles.
base_type::size_type size() const
DEVICE_QUALIFIER constexpr iterator begin() noexcept
Definition Array.hpp:140
constexpr T norm2() const
Definition Vector.hpp:159
boost::mpi::communicator comm_cart
The communicator.
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.
void for_each_image(Utils::Vector3i const &ncut, F f)
Call kernel for every 3d index in a sphere around the origin.
static auto dipole_field(Utils::Vector3d const &d, Utils::Vector3d const &m1)
Dipole field contribution from a particle with dipole moment m1 at a distance d.
static auto pair_potential(Utils::Vector3d const &d, Utils::Vector3d const &m1, Utils::Vector3d const &m2)
Pair potential for two interacting dipoles.
static auto gather_particle_data(BoxGeometry const &box_geo, ParticleRange const &particles)
static auto get_n_cut(BoxGeometry const &box_geo, int n_replicas)
static auto pair_force(Utils::Vector3d const &d, Utils::Vector3d const &m1, Utils::Vector3d const &m2)
Pair force of two interacting dipoles.
__device__ void vector_product(float const *a, float const *b, float *out)
This file contains the errorhandling code for severe errors, like a broken bond or illegal parameter ...
auto iall_gatherv(boost::mpi::communicator const &comm, T const *in_values, int in_size, T *out_values, int const *sizes)
void cartesian_product(const Body &op, const ForwardRange &...rng)
Call op with each element of the cartesian product set of rng.
void dipole_field_at_part(ParticleRange const &particles) const
Calculate total dipole field of each particle.
DipolarDirectSum(double prefactor, int n_replicas)
void add_long_range_forces(ParticleRange const &particles) const
Calculate and add the interaction forces/torques to the particles.
double long_range_energy(ParticleRange const &particles) const
Calculate the interaction potential.
Force information on a particle.
Definition Particle.hpp:345
Utils::Vector3d f
force.
Definition Particle.hpp:371
Position and dipole moment of one particle.
void serialize(Archive &ar, long int)
Utils::Vector3d m
Utils::Vector3d pos