ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
particle_node.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 "particle_node.hpp"
23
24#include "BoxGeometry.hpp"
25#include "Particle.hpp"
27#include "cells.hpp"
28#include "communication.hpp"
30#include "system/System.hpp"
31
32#include <utils/Cache.hpp>
33#include <utils/Vector.hpp>
34#include <utils/mpi/gatherv.hpp>
35
36#include <boost/mpi/collectives/all_gather.hpp>
37#include <boost/mpi/collectives/all_reduce.hpp>
38#include <boost/mpi/collectives/gather.hpp>
39#include <boost/mpi/collectives/reduce.hpp>
40#include <boost/mpi/collectives/scatter.hpp>
41
42#include <algorithm>
43#include <cmath>
44#include <functional>
45#include <iterator>
46#include <ranges>
47#include <span>
48#include <stdexcept>
49#include <string>
50#include <unordered_map>
51#include <unordered_set>
52#include <utility>
53#include <vector>
54
55constexpr auto some_tag = 42;
56
57/** @brief Mapping particle ids to MPI ranks. */
58static std::unordered_map<int, int> particle_node;
59
60static auto &get_cell_structure() {
62}
63
64/**
65 * @brief Keep track of the largest particle id.
66 * This book-keeping variable is necessary to make particle insertion run
67 * in constant time. Traversing the @ref particle_node to find the largest
68 * particle id scales with O(N) and traversing the local cells in parallel
69 * followed by a reduction scales with O(N^2).
70 */
71static int max_seen_pid = -1;
72
73static auto rebuild_needed() {
74 auto is_rebuild_needed = ::particle_node.empty();
75 boost::mpi::broadcast(::comm_cart, is_rebuild_needed, 0);
76 return is_rebuild_needed;
77}
78
80 boost::mpi::broadcast(::comm_cart, ::max_seen_pid, 0);
81}
82
83namespace {
84/* Limit cache to 100 MiB */
85std::size_t const max_cache_size = (100ul * 1048576ul) / sizeof(Particle);
87} // namespace
88
90std::size_t fetch_cache_max_size() { return particle_fetch_cache.max_size(); }
91
92static void mpi_send_particle_data_local(int p_id) {
93 auto const p = get_cell_structure().get_local_particle(p_id);
94 auto const found = p and not p->is_ghost();
95 assert(1 == boost::mpi::all_reduce(::comm_cart, static_cast<int>(found),
96 std::plus<>()) &&
97 "Particle not found");
98 if (found) {
99 ::comm_cart.send(0, 42, *p);
100 }
101}
102
104
105const Particle &get_particle_data(int p_id) {
106 auto const pnode = get_particle_node(p_id);
107
108 if (pnode == this_node) {
109 auto const p = get_cell_structure().get_local_particle(p_id);
110 assert(p != nullptr);
111 return *p;
112 }
113
114 /* Query the cache */
115 auto const p_ptr = particle_fetch_cache.get(p_id);
116 if (p_ptr) {
117 return *p_ptr;
118 }
119
120 /* Cache miss, fetch the particle,
121 * put it into the cache and return a pointer into the cache. */
123 Particle result{};
124 ::comm_cart.recv(boost::mpi::any_source, boost::mpi::any_tag, result);
125 return *(particle_fetch_cache.put(p_id, std::move(result)));
126}
127
129 std::vector<int> local_ids;
130 boost::mpi::scatter(comm_cart, local_ids, 0);
131
132 std::vector<Particle> parts(local_ids.size());
133 std::ranges::transform(local_ids, parts.begin(), [](int p_id) {
134 auto const p = get_cell_structure().get_local_particle(p_id);
135 assert(p != nullptr);
136 return *p;
137 });
138
139 Utils::Mpi::gatherv(comm_cart, parts.data(), static_cast<int>(parts.size()),
140 0);
141}
142
144
145/**
146 * @brief Get multiple particles at once.
147 *
148 * *WARNING* Particles are returned in an arbitrary order.
149 *
150 * @param ids The ids of the particles that should be returned.
151 *
152 * @returns The particle list.
153 */
154static std::vector<Particle> mpi_get_particles(std::span<const int> ids) {
156 /* Return value */
157 std::vector<Particle> parts(ids.size());
158
159 /* Group ids per node */
160 static std::vector<std::vector<int>> node_ids(comm_cart.size());
161 for (auto &per_node : node_ids) {
162 per_node.clear();
163 }
164
165 for (auto const &p_id : ids) {
166 auto const p_node = get_particle_node(p_id);
167 node_ids[p_node].emplace_back(p_id);
168 }
169 /* We shouldn't be prefetching particles that are already on the head node */
170 assert(node_ids[this_node].empty());
171
172 /* Distribute ids to the worker nodes */
173 {
174 static std::vector<int> ignore;
175 boost::mpi::scatter(comm_cart, node_ids, ignore, 0);
176 assert(ignore.empty());
177 }
178
179 static std::vector<int> node_sizes(comm_cart.size());
180 // cannot use range-based transform with GCC 13 + ASAN
181 std::transform(node_ids.begin(), node_ids.end(), node_sizes.begin(),
182 std::size<std::vector<int>>);
183
184 Utils::Mpi::gatherv(comm_cart, parts.data(), static_cast<int>(parts.size()),
185 parts.data(), node_sizes.data(), 0);
186
187 return parts;
188}
189
190void prefetch_particle_data(std::span<const int> in_ids) {
191 /* Nothing to do on a single node. */
192 // NOLINTNEXTLINE(clang-analyzer-core.NonNullParamChecker)
193 if (comm_cart.size() == 1)
194 return;
195
196 static std::vector<int> ids;
197 ids.clear();
198 auto out_ids = std::back_inserter(ids);
199
200 /* Don't prefetch particles already on the head node or already cached. */
201 std::ranges::copy_if(in_ids, out_ids, [](int id) {
202 return (get_particle_node(id) != this_node) && particle_fetch_cache.has(id);
203 });
204
205 /* Don't prefetch more particles than fit the cache. */
206 if (ids.size() > particle_fetch_cache.max_size())
207 ids.resize(particle_fetch_cache.max_size());
208
209 /* Fetch the particles... */
210 for (auto &p : mpi_get_particles(ids)) {
211 auto id = p.id();
212 particle_fetch_cache.put(id, std::move(p));
213 }
214}
215
216static void mpi_who_has_local() {
217 static std::vector<int> sendbuf;
218
219 auto local_particles = get_cell_structure().local_particles();
220 auto const n_part = static_cast<int>(local_particles.size());
221 boost::mpi::gather(comm_cart, n_part, 0);
222
223 if (n_part == 0) {
225 return;
226 }
227
228 sendbuf.resize(n_part);
229
230 std::transform(local_particles.begin(), local_particles.end(),
231 sendbuf.begin(), [](Particle const &p) { return p.id(); });
232
233 comm_cart.send(0, some_tag, sendbuf);
235}
236
238
239static void mpi_who_has_head() {
240 auto local_particles = get_cell_structure().local_particles();
241
242 static std::vector<int> n_parts;
243 boost::mpi::gather(comm_cart, static_cast<int>(local_particles.size()),
244 n_parts, 0);
245
246 static std::vector<int> pdata;
247 auto const n_nodes = ::comm_cart.size();
248 max_seen_pid = -1;
249
250 /* then fetch particle locations */
251 for (int pnode = 0; pnode < n_nodes; pnode++) {
252 if (pnode == this_node) {
253 for (auto const &p : local_particles) {
254 particle_node[p.id()] = this_node;
255 max_seen_pid = std::max(max_seen_pid, p.id());
256 }
257 } else if (n_parts[pnode] > 0) {
258 pdata.resize(n_parts[pnode]);
259 comm_cart.recv(pnode, some_tag, pdata);
260 for (int i = 0; i < n_parts[pnode]; i++) {
261 particle_node[pdata[i]] = pnode;
262 max_seen_pid = std::max(max_seen_pid, pdata[i]);
263 }
264 }
265 }
267}
268
269/**
270 * @brief Rebuild the particle index.
271 */
276
277/**
278 * @brief Rebuild the particle index.
279 */
281 if (this_node == 0) {
283 } else {
285 }
286}
287
288int get_particle_node(int p_id) {
289 if (p_id < 0) {
290 throw std::domain_error("Invalid particle id: " + std::to_string(p_id));
291 }
292
293 if (particle_node.empty())
295
296 auto const needle = particle_node.find(p_id);
297
298 // Check if particle has a node, if not, we assume it does not exist.
299 if (needle == particle_node.end()) {
300 throw std::runtime_error("Particle node for id " + std::to_string(p_id) +
301 " not found!");
302 }
303 return needle->second;
304}
305
307 if (p_id < 0) {
308 throw std::domain_error("Invalid particle id: " + std::to_string(p_id));
309 }
310
311 if (rebuild_needed()) {
313 }
314
315 if (this_node != 0) {
316 return -1;
317 }
318
319 auto const needle = particle_node.find(p_id);
320
321 // Check if particle has a node, if not, we assume it does not exist.
322 if (needle == particle_node.end()) {
323 throw std::runtime_error("Particle node for id " + std::to_string(p_id) +
324 " not found!");
325 }
326 return needle->second;
327}
328
330 ::max_seen_pid = -1;
331 particle_node.clear();
332}
333
334/**
335 * @brief Calculate the largest particle id.
336 * Traversing the @ref particle_node to find the largest particle id
337 * scales with O(N). Consider using the cached value in @ref max_seen_pid
338 * if possible. This function is only necessary when the cached value is
339 * invalidated, for example when removing the particle which has the
340 * largest id.
341 */
343 return std::accumulate(
344 particle_node.begin(), particle_node.end(), -1,
345 [](int max, auto const &kv) { return std::max(max, kv.first); });
346}
347
348/**
349 * @brief Create a new particle and attach it to a cell.
350 * @param p_id The identity of the particle to create.
351 * @param pos The particle position.
352 * @return Whether the particle was created on that node.
353 */
354static bool maybe_insert_particle(int p_id, Utils::Vector3d const &pos) {
355 auto const &box_geo = *System::get_system().box_geo;
356 auto folded_pos = pos;
357 auto image_box = Utils::Vector3i{};
358 box_geo.fold_position(folded_pos, image_box);
359
360 Particle new_part;
361 new_part.id() = p_id;
362 new_part.pos() = folded_pos;
363 new_part.image_box() = image_box;
364
365 return get_cell_structure().add_local_particle(std::move(new_part)) !=
366 nullptr;
367}
368
369/**
370 * @brief Move particle to a new position.
371 * @param p_id The identity of the particle to move.
372 * @param pos The new particle position.
373 * @return Whether the particle was moved from that node.
374 */
375static bool maybe_move_particle(int p_id, Utils::Vector3d const &pos) {
376 auto const &system = System::get_system();
377 auto const &box_geo = *system.box_geo;
378 auto p = system.cell_structure->get_local_particle(p_id);
379 if (p == nullptr) {
380 return false;
381 }
382 auto folded_pos = pos;
383 auto image_box = Utils::Vector3i{};
384 box_geo.fold_position(folded_pos, image_box);
385 p->pos() = folded_pos;
386 p->image_box() = image_box;
387 return true;
388}
389
390void remove_particle(int p_id) {
391 if (this_node == 0) {
392 particle_node[p_id] = -1;
393 }
394 get_cell_structure().remove_particle(p_id);
397 if (this_node == 0) {
398 particle_node.erase(p_id);
399 if (p_id == ::max_seen_pid) {
401 // if there is a gap (i.e. there is no particle with id max_seen_pid - 1,
402 // then the cached value is invalidated and has to be recomputed (slow)
403 if (not particle_node.contains(::max_seen_pid) or
406 }
407 }
408 }
410 get_cell_structure().resort_particles(false);
411}
412
413void make_new_particle(int p_id, Utils::Vector3d const &pos) {
414 if (rebuild_needed()) {
416 }
417 auto const has_created = maybe_insert_particle(p_id, pos);
419
420 auto node = -1;
421 auto const node_local = (has_created) ? ::comm_cart.rank() : 0;
422 boost::mpi::reduce(::comm_cart, node_local, node, std::plus<int>{}, 0);
423 if (::this_node == 0) {
424 particle_node[p_id] = node;
425 max_seen_pid = std::max(max_seen_pid, p_id);
426 assert(not has_created or node == 0);
427 }
429}
430
431void set_particle_pos(int p_id, Utils::Vector3d const &pos) {
432 auto const has_moved = maybe_move_particle(p_id, pos);
433 get_cell_structure().set_resort_particles(Cells::RESORT_GLOBAL);
435
436 auto success = false;
437 boost::mpi::reduce(::comm_cart, has_moved, success, std::plus<bool>{}, 0);
438 if (::this_node == 0 and !success) {
439 throw std::runtime_error("Particle node for id " + std::to_string(p_id) +
440 " not found!");
441 }
442}
443
444bool particle_exists(int p_id) {
445 if (particle_node.empty())
447 return particle_node.contains(p_id);
448}
449
450std::vector<int> get_particle_ids() {
451 if (particle_node.empty())
453
454 std::vector<int> pids{};
455 std::ranges::copy(std::views::keys(particle_node), std::back_inserter(pids));
456 std::ranges::sort(pids);
457
458 return pids;
459}
460
461std::vector<int> get_particle_ids_parallel() {
462 if (rebuild_needed()) {
464 }
465 std::vector<int> pids{};
466 std::ranges::copy(std::views::keys(particle_node), std::back_inserter(pids));
467 boost::mpi::broadcast(::comm_cart, pids, 0);
468 return pids;
469}
470
472 if (rebuild_needed()) {
474 }
475
476 return max_seen_pid;
477}
478
480 if (particle_node.empty())
482
483 return static_cast<int>(particle_node.size());
484}
#define REGISTER_CALLBACK(cb)
Register a static callback without return value.
Vector implementation and trait types for boost qvm interoperability.
This file contains everything related to the global cell structure / cell system.
auto call_all(void(*fp)(Args...), ArgRef &&...args) const
Call a callback on all nodes.
void on_particle_change()
Called every time a particle property changes.
std::shared_ptr< CellStructure > cell_structure
std::shared_ptr< BoxGeometry > box_geo
boost::mpi::communicator comm_cart
The communicator.
int this_node
The number of this node.
MpiCallbacks & mpiCallbacks()
Returns a reference to the global callback class instance.
System & get_system()
void gatherv(const boost::mpi::communicator &comm, const T *in_values, int in_size, T *out_values, const int *sizes, const int *displs, int root)
Definition gatherv.hpp:87
Utils::Cache< int, Particle > particle_fetch_cache(max_cache_size)
STL namespace.
Various procedures concerning interactions between particles.
std::vector< int > get_particle_ids_parallel()
static int max_seen_pid
Keep track of the largest particle id.
static bool maybe_move_particle(int p_id, Utils::Vector3d const &pos)
Move particle to a new position.
static void build_particle_node()
Rebuild the particle index.
void make_new_particle(int p_id, Utils::Vector3d const &pos)
Create a new particle and attach it to a cell.
const Particle & get_particle_data(int p_id)
Get particle data.
static std::unordered_map< int, int > particle_node
Mapping particle ids to MPI ranks.
int get_particle_node(int p_id)
Get the MPI rank which owns the a specific particle.
void set_particle_pos(int p_id, Utils::Vector3d const &pos)
Move particle to a new position.
static void mpi_synchronize_max_seen_pid_local()
static int calculate_max_seen_id()
Calculate the largest particle id.
void remove_particle(int p_id)
Remove particle with a given identity.
static void mpi_get_particles_local()
static void mpi_who_has_head()
static std::vector< Particle > mpi_get_particles(std::span< const int > ids)
Get multiple particles at once.
int get_maximal_particle_id()
Get maximal particle id.
int get_particle_node_parallel(int p_id)
constexpr auto some_tag
std::vector< int > get_particle_ids()
Get all particle ids.
std::size_t fetch_cache_max_size()
Return the maximal number of particles that are kept in the fetch cache.
void prefetch_particle_data(std::span< const int > in_ids)
Fetch a range of particle into the fetch cache.
void invalidate_fetch_cache()
Invalidate the fetch cache for get_particle_data.
static auto rebuild_needed()
static auto & get_cell_structure()
static void build_particle_node_parallel()
Rebuild the particle index.
static bool maybe_insert_particle(int p_id, Utils::Vector3d const &pos)
Create a new particle and attach it to a cell.
void clear_particle_node()
Invalidate particle_node.
static void mpi_who_has_local()
static void mpi_send_particle_data_local(int p_id)
int get_n_part()
Get number of particles.
bool particle_exists(int p_id)
Check if particle exists.
Particles creation and deletion.
Struct holding all information for one particle.
Definition Particle.hpp:436