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 behind 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 auto const &system = get_system();
293 auto const &box_geo = *system.box_geo;
294 auto const &box_l = box_geo.length();
295 auto const particles = system.cell_structure->local_particles();
296 auto [local_particles, all_posmom, reqs, offset] =
297 gather_particle_data(box_geo, particles);
298
299 /* Number of image boxes considered */
300 auto const ncut = get_n_cut(box_geo, n_replicas);
301 auto const with_replicas = (ncut.norm2() > 0);
302
303 /* Range of particles we calculate the ia for on this node */
304 auto const local_posmom_begin = all_posmom.begin() + offset;
305 auto const local_posmom_end =
306 local_posmom_begin + static_cast<long>(local_particles.size());
307
308 /* Output iterator for the force */
309 auto p = local_particles.begin();
310
311 /* IA with local particles */
312 for (auto it = local_posmom_begin; it != local_posmom_end; ++it, ++p) {
313 /* IA with own images */
314 auto fi = image_sum(
315 it, std::next(it), it, with_replicas, ncut, box_geo, ParticleForce{},
316 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
317 return pair_force(rn, it->m, mj);
318 });
319
320 /* IA with other local particles */
321 auto q = std::next(p);
322 for (auto jt = std::next(it); jt != local_posmom_end; ++jt, ++q) {
323 auto const d = (with_replicas) ? (it->pos - jt->pos)
324 : box_geo.get_mi_vector(it->pos, jt->pos);
325
326 ParticleForce fij{};
327 ParticleForce fji{};
328 for_each_image(ncut, [&](int nx, int ny, int nz) {
329 auto const rn =
330 d + Utils::Vector3d{nx * box_l[0], ny * box_l[1], nz * box_l[2]};
331 auto const pf = pair_force(rn, it->m, jt->m);
332 fij += pf;
333 fji.f -= pf.f;
334 /* Conservation of angular momentum mandates that
335 * 0 = t_i + r_ij x F_ij + t_j */
336 fji.torque += vector_product(pf.f, rn) - pf.torque;
337 });
338
339 fi += fij;
340 (*q)->force() += prefactor * fji.f;
341 (*q)->torque() += prefactor * fji.torque;
342 }
343
344 (*p)->force() += prefactor * fi.f;
345 (*p)->torque() += prefactor * fi.torque;
346 }
347
348 /* Wait for the rest of the data to arrive */
349 boost::mpi::wait_all(reqs.begin(), reqs.end());
350
351 /* Output iterator for the force */
352 p = local_particles.begin();
353
354 /* Interaction with all the other particles */
355 for (auto it = local_posmom_begin; it != local_posmom_end; ++it, ++p) {
356 // red particles
357 auto fi =
358 image_sum(all_posmom.begin(), local_posmom_begin, it, with_replicas,
359 ncut, box_geo, ParticleForce{},
360 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
361 return pair_force(rn, it->m, mj);
362 });
363
364 // black particles
365 fi += image_sum(local_posmom_end, all_posmom.end(), it, with_replicas, ncut,
366 box_geo, ParticleForce{},
367 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
368 return pair_force(rn, it->m, mj);
369 });
370
371 (*p)->force() += prefactor * fi.f;
372 (*p)->torque() += prefactor * fi.torque;
373 }
374#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
376#endif
377}
378
379/**
380 * @brief Calculate the interaction potential.
381 *
382 * This employs a parallel N-square loop over all particle pairs.
383 */
385 auto const &system = get_system();
386 auto const &box_geo = *system.box_geo;
387 auto const particles = system.cell_structure->local_particles();
388 auto [local_particles, all_posmom, reqs, offset] =
389 gather_particle_data(box_geo, particles);
390
391 /* Number of image boxes considered */
392 auto const ncut = get_n_cut(box_geo, n_replicas);
393 auto const with_replicas = (ncut.norm2() > 0);
394
395 /* Wait for the rest of the data to arrive */
396 boost::mpi::wait_all(reqs.begin(), reqs.end());
397
398 /* Range of particles we calculate the ia for on this node */
399 auto const local_posmom_begin = all_posmom.begin() + offset;
400 auto const local_posmom_end =
401 local_posmom_begin + static_cast<long>(local_particles.size());
402
403 auto u = 0.;
404 for (auto it = local_posmom_begin; it != local_posmom_end; ++it) {
405 u = image_sum(it, all_posmom.end(), it, with_replicas, ncut, box_geo, u,
406 [it](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
407 return pair_potential(rn, it->m, mj);
408 });
409 }
410
411 return prefactor * u;
412}
413
414/**
415 * @brief Calculate total dipole field of each particle.
416 *
417 * This employs a parallel N-square loop over all particles.
418 * Logically this is equivalent to the potential calculation
419 * in @ref DipolarDirectSum::long_range_energy, which calculates
420 * a naive N-square sum. The difference is summation range,
421 * and the kernel calculates the dipole field rather than the energy.
422 */
423#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
425 auto const &system = get_system();
426 auto const &box_geo = *system.box_geo;
427 auto const particles = system.cell_structure->local_particles();
428 /* collect particle data */
429 auto [local_particles, all_posmom, reqs, offset] =
430 gather_particle_data(box_geo, particles);
431
432 auto const ncut = get_n_cut(box_geo, n_replicas);
433 auto const with_replicas = (ncut.norm2() > 0);
434
435 boost::mpi::wait_all(reqs.begin(), reqs.end());
436
437 auto const local_posmom_begin = all_posmom.begin() + offset;
438 auto const local_posmom_end =
439 local_posmom_begin + static_cast<long>(local_particles.size());
440
441 Utils::Vector3d u_init = {0., 0., 0.};
442 auto p = local_particles.begin();
443 for (auto pi = local_posmom_begin; pi != local_posmom_end; ++pi, ++p) {
444 auto const u = image_sum(
445 all_posmom.begin(), all_posmom.end(), pi, with_replicas, ncut, box_geo,
446 u_init, [](Utils::Vector3d const &rn, Utils::Vector3d const &mj) {
447 return dipole_field(rn, mj);
448 });
449 (*p)->dip_fld() = prefactor * u;
450 }
451}
452#endif
453
454DipolarDirectSum::DipolarDirectSum(double prefactor, int n_replicas) {
456 this->n_replicas = n_replicas;
457 if (n_replicas < 0) {
458 throw std::domain_error("Parameter 'n_replicas' must be >= 0");
459 }
460}
461
462#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.
DipolarDirectSum(double prefactor, int n_replicas)
void add_long_range_forces() const
Calculate and add the interaction forces/torques to the particles.
double long_range_energy() const
Calculate the interaction potential.
void dipole_field_at_part() const
Calculate total dipole field of each particle.
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