ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
RegularDecomposition.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
23
24#include "cell_system/Cell.hpp"
27
28#include "communication.hpp"
30#include "errorhandling.hpp"
31#include "system/System.hpp"
32
33#include <utils/Vector.hpp>
34#include <utils/index.hpp>
37
38#include <boost/container/flat_set.hpp>
39#include <boost/mpi/collectives/all_reduce.hpp>
40#include <boost/mpi/communicator.hpp>
41#include <boost/mpi/request.hpp>
42#include <boost/range/numeric.hpp>
43
44#include <Kokkos_Core.hpp>
45
46#include <algorithm>
47#include <array>
48#include <cassert>
49#include <cmath>
50#include <cstddef>
51#include <exception>
52#include <functional>
53#include <initializer_list>
54#include <iterator>
55#include <map>
56#include <mutex>
57#include <numeric>
58#include <set>
59#include <stdexcept>
60#include <string>
61#include <utility>
62#include <vector>
63
64int RegularDecomposition::position_to_cell_index(
65 Utils::Vector3d const &pos) const {
67
68 for (auto i = 0u; i < 3u; i++) {
69 cpos[i] = static_cast<int>(std::floor(pos[i] * inv_cell_size[i])) + 1 -
70 cell_offset[i];
71
72 /* particles outside our box. Still take them if
73 nonperiodic boundary. We also accept the particle if we are at
74 the box boundary, and the particle is within the box. In this case
75 the particle belongs here and could otherwise potentially be dismissed
76 due to rounding errors. */
77 if (cpos[i] < 1) {
78 if ((!m_box.periodic(i) or (pos[i] >= m_box.length()[i])) and
79 m_local_box.boundary()[2u * i])
80 cpos[i] = 1;
81 else
82 return -1;
83 } else if (cpos[i] > cell_grid[i]) {
84 if ((!m_box.periodic(i) or (pos[i] < m_box.length()[i])) and
85 m_local_box.boundary()[2u * i + 1u])
86 cpos[i] = cell_grid[i];
87 else
88 return -1;
89 }
90 }
91
93}
94
95void RegularDecomposition::move_if_local(
96 ParticleList &src, ParticleList &rest,
97 std::vector<ParticleChange> &modified_cells) {
98 for (auto &part : src) {
99 auto target_cell = position_to_cell(part.pos());
100
101 if (target_cell) {
102 target_cell->particles().insert(std::move(part));
103 modified_cells.emplace_back(ModifiedList{target_cell->particles()});
104 } else {
105 rest.insert(std::move(part));
106 }
107 }
108
109 src.clear();
110}
111
112void RegularDecomposition::move_left_or_right(ParticleList &src,
115 int dir) const {
116 auto const is_open_boundary_left = m_local_box.boundary()[2 * dir] != 0;
117 auto const is_open_boundary_right = m_local_box.boundary()[2 * dir + 1] != 0;
120 auto const my_left = m_local_box.my_left()[dir];
121 auto const my_right = m_local_box.my_right()[dir];
122 for (auto it = src.begin(); it != src.end();) {
123 auto const pos = it->pos()[dir];
124 if (m_box.get_mi_coord(pos, my_right, dir) >= 0. and can_move_right) {
125 right.insert(std::move(*it));
126 it = src.erase(it);
127 } else if (m_box.get_mi_coord(pos, my_left, dir) < 0. and can_move_left) {
128 left.insert(std::move(*it));
129 it = src.erase(it);
130 } else {
131 ++it;
132 }
133 }
134}
135
136void RegularDecomposition::exchange_neighbors(
137 ParticleList &pl, std::vector<ParticleChange> &modified_cells) {
138 auto const node_neighbors = Utils::Mpi::cart_neighbors<3>(m_comm);
140
141 for (int dir = 0; dir < 3; dir++) {
142 /* Single node direction, no action needed. */
143 if (Utils::Mpi::cart_get<3>(m_comm).dims[dir] == 1) {
144 continue;
145 /* In this (common) case left and right neighbors are
146 the same, and we need only one communication */
147 }
148 if (Utils::Mpi::cart_get<3>(m_comm).dims[dir] == 2) {
149 move_left_or_right(pl, send_buf_l, send_buf_l, dir);
150
152 node_neighbors[2 * dir], 0, recv_buf_l);
153
154 send_buf_l.clear();
155 } else {
156 using boost::mpi::request;
158
159 move_left_or_right(pl, send_buf_l, send_buf_r, dir);
160
161 auto req_l = isendrecv(m_comm, node_neighbors[2 * dir], 0, send_buf_l,
162 node_neighbors[2 * dir], 0, recv_buf_l);
163 auto req_r = isendrecv(m_comm, node_neighbors[2 * dir + 1], 0, send_buf_r,
164 node_neighbors[2 * dir + 1], 0, recv_buf_r);
165
166 std::array<request, 4> reqs{{req_l[0], req_l[1], req_r[0], req_r[1]}};
167 boost::mpi::wait_all(reqs.begin(), reqs.end());
168
169 send_buf_l.clear();
170 send_buf_r.clear();
171 }
172
173 move_if_local(recv_buf_l, pl, modified_cells);
174 move_if_local(recv_buf_r, pl, modified_cells);
175 }
176}
177
178/**
179 * @brief Fold coordinates to box and reset the old position.
180 */
181static void fold_and_reset(Particle &p, BoxGeometry const &box_geo) {
182 box_geo.fold_position(p.pos(), p.image_box());
183
185}
186
188 std::vector<ParticleChange> &diff) {
190
191 auto const cells_span = local_cells();
192 auto const n_cells = cells_span.size();
193
194 /* Remove a misplaced particle from its cell and hand it to its target
195 * cell (or the displaced list when it left the local domain), recording
196 * the changes. Shared by the serial and the two-phase parallel sweep. */
197 auto const apply_move = [&](ParticleList &parts, Particle &&p,
198 Cell *target_cell) {
199 diff.emplace_back(ModifiedList{parts});
200 /* Particle is not local */
201 if (target_cell == nullptr) {
202 diff.emplace_back(RemovedParticle{p.id()});
203 displaced_parts.insert(std::move(p));
204 }
205 /* Particle belongs on this node but is in the wrong cell. */
206 else {
207 target_cell->particles().insert(std::move(p));
208 diff.emplace_back(ModifiedList{target_cell->particles()});
209 }
210 };
211
212 if (Kokkos::DefaultHostExecutionSpace().concurrency() == 1) {
213 /* Single-threaded rank: one-pass sweep without the bookkeeping overhead
214 * of the two-phase version below. */
215 for (auto *const c : cells_span) {
216 for (auto it = c->particles().begin(); it != c->particles().end();) {
218
219 auto *const target_cell = particle_to_cell(*it);
220
221 /* Particle is in place */
222 if (target_cell == c) {
223 std::advance(it, 1);
224 continue;
225 }
226
227 auto p = std::move(*it);
228 it = c->particles().erase(it);
229 apply_move(c->particles(), std::move(p), target_cell);
230 }
231 }
232 } else {
233 using exec_space = Kokkos::DefaultHostExecutionSpace;
234 /* Phase 1 (parallel): fold every particle position and classify it
235 * against its target cell. Only particle-local state and this cell's own
236 * move list are written, so cells can be swept concurrently.
237 * fold_and_reset() throws on image-box overflow; exceptions must not
238 * escape the parallel region, so the first error is captured and
239 * rethrown afterwards. */
240 struct Move {
241 unsigned index;
242 Cell *target;
243 };
244 std::vector<std::vector<Move>> moves(n_cells);
245 std::mutex fold_error_mutex;
246 std::string fold_error_msg;
247 Kokkos::RangePolicy<exec_space> policy(std::size_t{0}, n_cells);
248 Kokkos::parallel_for(
249 "RegularDecomposition::resort::classify", policy,
250 [&](std::size_t const ci) {
251 auto *cell = cells_span[ci];
252 unsigned index = 0u;
253 for (auto &p : cell->particles()) {
254 try {
256 } catch (std::exception const &err) {
257 std::lock_guard<std::mutex> guard{fold_error_mutex};
258 if (fold_error_msg.empty()) {
259 fold_error_msg = err.what();
260 }
261 return;
262 }
263 if (auto *const target = particle_to_cell(p); target != cell) {
264 moves[ci].emplace_back(index, target);
265 }
266 ++index;
267 }
268 });
269 Kokkos::fence();
270 if (not fold_error_msg.empty()) {
271 throw std::runtime_error(fold_error_msg);
272 }
273
274 /* Phase 2 (serial): apply the moves. This replays the serial sweep
275 * exactly: ParticleList::erase() swaps the last element into the erased
276 * slot, so a slot-to-original-index map is maintained to look up the
277 * phase-1 classification of swapped-in elements. Cells without moves
278 * (the vast majority) are skipped entirely. */
279 std::vector<int> slot;
280 std::vector<Cell *> target_of;
281 for (std::size_t ci = 0; ci < n_cells; ++ci) {
282 if (moves[ci].empty()) {
283 continue;
284 }
285 auto *const c = cells_span[ci];
286 auto &parts = c->particles();
287 auto const n = static_cast<int>(parts.size());
288 target_of.assign(n, c); // target == own cell: particle is in place
289 for (auto const &move : moves[ci]) {
290 target_of[move.index] = move.target;
291 }
292 slot.resize(n);
293 std::iota(slot.begin(), slot.end(), 0);
294 int i = 0;
295 int end = n;
296 while (i < end) {
297 auto *const target_cell = target_of[slot[i]];
298
299 /* Particle is in place */
300 if (target_cell == c) {
301 ++i;
302 continue;
303 }
304
305 auto p = std::move(*(parts.begin() + i));
306 parts.erase(parts.begin() + i); // swaps the last element into slot i
307 slot[i] = slot[--end];
308 apply_move(parts, std::move(p), target_cell);
309 }
310 }
311 }
312
313 if (global) {
314 auto const grid = Utils::Mpi::cart_get<3>(m_comm).dims;
315 /* Worst case we need grid - 1 rounds per direction.
316 * This correctly implies that if there is only one node,
317 * no action should be taken. */
318 int rounds_left = grid[0] + grid[1] + grid[2] - 3;
319 for (; rounds_left > 0; rounds_left--) {
320 exchange_neighbors(displaced_parts, diff);
321
322 auto left_over = boost::mpi::all_reduce(m_comm, displaced_parts.size(),
323 std::plus<std::size_t>());
324
325 if (left_over == 0) {
326 break;
327 }
328 }
329 } else {
330 exchange_neighbors(displaced_parts, diff);
331 }
332
333 if (not displaced_parts.empty()) {
334 auto sort_cell = local_cells()[0];
335
336 for (auto &part : displaced_parts) {
337 runtimeErrorMsg() << "Particle " << part.id() << " moved more "
338 << "than one local box length in one timestep";
339 sort_cell->particles().insert(std::move(part));
340
341 diff.emplace_back(ModifiedList{sort_cell->particles()});
342 }
343 }
344}
345
346void RegularDecomposition::mark_cells() {
347 m_local_cells.clear();
348 m_ghost_cells.clear();
349
350 int cnt_c = 0;
351 for (int o = 0; o < ghost_cell_grid[2]; o++)
352 for (int n = 0; n < ghost_cell_grid[1]; n++)
353 for (int m = 0; m < ghost_cell_grid[0]; m++) {
354 if ((m > 0 && m < ghost_cell_grid[0] - 1 && n > 0 &&
355 n < ghost_cell_grid[1] - 1 && o > 0 && o < ghost_cell_grid[2] - 1))
356 m_local_cells.push_back(&cells.at(cnt_c++));
357 else
358 m_ghost_cells.push_back(&cells.at(cnt_c++));
359 }
360}
361
363 auto dir_max_range = [this](unsigned int i) {
364 return std::min(0.5 * m_box.length()[i], m_local_box.length()[i]);
365 };
366
367 return {dir_max_range(0u), dir_max_range(1u), dir_max_range(2u)};
368}
369
371int RegularDecomposition::calc_processor_min_num_cells() const {
372 /* the minimal number of cells can be lower if there are at least two nodes
373 serving a direction,
374 since this also ensures that the cell size is at most half the box
375 length. However, if there is only one processor for a direction, there
376 have to be at least two cells for this direction. */
377 return boost::accumulate(Utils::Mpi::cart_get<3>(m_comm).dims, 1,
378 [](int n_cells, int grid) {
379 return (grid == 1) ? 2 * n_cells : n_cells;
380 });
381}
382
383void RegularDecomposition::create_cell_grid(double range) {
384 auto const cart_info = Utils::Mpi::cart_get<3>(m_comm);
385
386 int n_local_cells;
387 auto cell_range = Utils::Vector3d::broadcast(range);
388 auto const min_num_cells = calc_processor_min_num_cells();
389
390 if (range <= 0.) {
391 /* this is the non-interacting case */
392 auto const cells_per_dir =
393 static_cast<int>(std::ceil(std::cbrt(min_num_cells)));
394
397 } else {
398 /* Calculate initial cell grid */
399 auto const &local_box_l = m_local_box.length();
400 auto const volume = Utils::product(local_box_l);
401 auto const scale = std::cbrt(RegularDecomposition::max_num_cells / volume);
402
403 for (auto i = 0u; i < 3u; i++) {
404 /* this is at least 1 */
405 cell_grid[i] = static_cast<int>(std::ceil(local_box_l[i] * scale));
406 cell_range[i] = local_box_l[i] / static_cast<double>(cell_grid[i]);
407
408 if (cell_range[i] < range) {
409 /* ok, too many cells for this direction, set to minimum */
410 cell_grid[i] = static_cast<int>(std::floor(local_box_l[i] / range));
411 if (cell_grid[i] < 1) {
413 << "interaction range " << range << " in direction " << i
414 << " is larger than the local box size " << local_box_l[i];
415 cell_grid[i] = 1;
416 }
417 cell_range[i] = local_box_l[i] / static_cast<double>(cell_grid[i]);
418 }
419 }
420
421 /* It may be necessary to asymmetrically assign the scaling to the
422 coordinates, which the above approach will not do.
423 For a symmetric box, it gives a symmetric result. Here we correct that.
424 */
425 for (;;) {
427
428 /* done */
429 if (n_local_cells <= RegularDecomposition::max_num_cells)
430 break;
431
432 /* find coordinate with the smallest cell range */
433 auto min_ind = 0u;
434 auto min_size = cell_range[0];
435
436 for (auto i = 1u; i < 3u; ++i) {
437 if (cell_grid[i] > 1 and cell_range[i] < min_size) {
438 min_ind = i;
439 min_size = cell_range[i];
440 }
441 }
442
445 }
446
447 /* sanity check */
449 runtimeErrorMsg() << "number of cells " << n_local_cells
450 << " is smaller than minimum " << min_num_cells
451 << ": either interaction range is too large for "
452 << "the current skin (range=" << range << ", "
453 << "half_local_box_l=[" << local_box_l / 2. << "]) "
454 << "or min_num_cells too large";
455 }
456 }
457
458 if (n_local_cells > RegularDecomposition::max_num_cells) {
459 runtimeErrorMsg() << "no suitable cell grid found";
460 }
461
462 auto const node_pos = cart_info.coords;
463
464 /* now set all dependent variables */
465 int new_cells = 1;
466 for (auto i = 0u; i < 3u; i++) {
467 ghost_cell_grid[i] = cell_grid[i] + 2;
469 cell_size[i] = m_local_box.length()[i] / static_cast<double>(cell_grid[i]);
470 inv_cell_size[i] = 1.0 / cell_size[i];
471 cell_offset[i] = node_pos[i] * cell_grid[i];
472 }
473
474 /* allocate cell array and cell pointer arrays */
475 cells.clear();
476 cells.resize(static_cast<unsigned int>(new_cells));
479}
480
481template <class K, class Comparator> auto make_flat_set(Comparator &&comp) {
482 return boost::container::flat_set<K, std::remove_reference_t<Comparator>>(
483 std::forward<Comparator>(comp));
484}
485
486void RegularDecomposition::init_cell_interactions() {
487
488 // Note: the global index for physical cells is 0-based.
489 // I.e., a global index of -1 refers to a ghost cell.
490 auto const halo = Utils::Vector3i{1, 1, 1}; // number of ghost layers
491 auto const cart_info = Utils::Mpi::cart_get<3>(m_comm);
492 // 3D index of the MPI rank in the Cartesian grid of MPI ranks
493 auto const &node_pos = cart_info.coords;
494 // size of the Cartesian grid of MPI ranks
495 auto const &node_grid = ::communicator.node_grid;
497 // MD cell index of lower halo layer on this MPI rank
498 auto const global_size = hadamard_product(node_grid, cell_grid);
499
500 // is a cell at the system boundary in the given coord
502 return (cell_idx[coord] == 0 or cell_idx[coord] == global_size[coord] - 1);
503 };
504
505 // For the fully connected feature (cells that don't share at least a corner)
506 // only apply if one cell is a ghost cell (i.e. connections across the
507 // periodic boundary.
509 Utils::Vector3i b) {
511 auto const [fc_normal, fc_dir] = *fully_connected_boundary();
512 auto const involves_ghost_cell =
514 b[fc_normal] == -1 or b[fc_normal] == global_size[fc_normal]);
516 // check if cells do not share at least a corner
517 return std::abs((a - b)[fc_dir]) > 1;
518 }
519 }
520 return false;
521 };
522
523 /* Translate a node local index (relative to the origin of the local grid)
524 * to a global index. */
525 auto global_index =
528 };
529
530 /* Linear index in the global cell grid. */
533
535 };
536
537 /* Translate a global index into a local one */
538 auto local_index =
541 };
542
543 // sanity checks
545 auto const [fc_normal, fc_dir] = *fully_connected_boundary();
546 if (fc_normal == fc_dir) {
547 throw std::domain_error("fully_connected_boundary normal and connection "
548 "coordinates need to differ.");
549 }
550 if (node_grid[fc_dir] != 1) {
551 throw std::runtime_error(
552 "The MPI nodegrid must be 1 in the fully connected direction.");
553 }
555 throw std::runtime_error(
556 "The fully connected boundary requires periodicity in the "
557 "boundary normal direction.");
558 }
559 }
560
561 /* We only consider local cells (e.g. not halo cells), which
562 * span the range [(1,1,1), cell_grid) in local coordinates. */
563 auto const start = global_index(Utils::Vector3i{1, 1, 1});
564 auto const end = start + cell_grid;
565
566 bool one_mpi_rank = m_comm.size() == 1;
567
568 /* loop all local cells */
569 for (int o = start[2]; o < end[2]; o++)
570 for (int n = start[1]; n < end[1]; n++)
571 for (int m = start[0]; m < end[0]; m++) {
572 /* next-nearest neighbors in every direction */
573 Utils::Vector3i lower_index = {m - 1, n - 1, o - 1};
574 Utils::Vector3i upper_index = {m + 1, n + 1, o + 1};
575
576 /* In the fully connected case, we consider all cells
577 * in the direction as neighbors, not only the nearest ones.
578 // */
581
582 // Fully connected is only needed at the box surface
583 if (at_boundary(fc_boundary, {m, n, o})) {
586 }
587 }
588
589 /* In non-periodic directions, the halo needs not
590 * be considered. */
591 for (auto i = 0u; i < 3u; i++) {
592 if (not m_box.periodic(i)) {
593 lower_index[i] = std::max(0, lower_index[i]);
594 upper_index[i] = std::min(global_size[i] - 1, upper_index[i]);
595 }
596 }
597
598 /* Unique set of neighbors, cells are compared by their linear
599 * index in the global cell grid. */
600 auto neighbors = make_flat_set<Utils::Vector3i>(
601 [&](Utils::Vector3i const &a, Utils::Vector3i const &b) {
603 });
604
605 /* Number of images of each neighbor cell in the neighborhood; it can
606 * be larger than one if the global cell grid has less than three
607 * cells in a periodic direction. */
608 std::map<int, int> n_images;
609
610 /* Collect neighbors */
611 for (int p = lower_index[2]; p <= upper_index[2]; p++)
612 for (int q = lower_index[1]; q <= upper_index[1]; q++)
613 for (int r = lower_index[0]; r <= upper_index[0]; r++) {
615 // Avoid fully connecting the boundary layer and the
616 // next INNER layer
617 if (fcb_is_inner_connection({m, n, o}, {r, q, p}))
618 continue;
619 }
620 neighbors.insert(Utils::Vector3i{r, q, p});
622 }
623
624 /* Red-black partition by global index. */
625 auto const ind1 = folded_linear_index({m, n, o});
626
627 std::vector<Cell *> red_neighbors;
628 std::vector<Cell *> black_neighbors;
629
630 /* If we are running on a single MPI rank, it is not necessary to use
631 * ghost cells. Instead of adding a ghost cell as neighbor,
632 * we directly connect to the corresponding
633 * physical cell across the periodic boundary */
634 for (auto &neighbor : neighbors) {
635 auto const offset = neighbor - Utils::Vector3i{m, n, o};
636 if (one_mpi_rank) {
637 for (auto coord : {0u, 1u, 2u}) {
638 if (neighbor[coord] == -1) {
640 } else if (neighbor[coord] == cell_grid[coord]) {
642 }
643 }
644 }
645 auto const ind2 = folded_linear_index(neighbor);
646 /* Exclude cell itself */
647 if (ind1 == ind2)
648 continue;
649
650 auto cell = &cells.at(
652
653 /* Divide red and black neighbors. If the neighbor is reached by a
654 * unique offset, the pair belongs to the cell from which the offset
655 * is positive in lexicographic order (z most significant), so that
656 * the pairs across periodic boundaries are spread evenly over the
657 * cells. Otherwise (several images, or a fully connected boundary)
658 * use the folded indices; both cells of a pair see the same number
659 * of images, so they always apply the same rule. */
660 auto const offset_is_positive =
661 (offset[2] > 0) or (offset[2] == 0 and offset[1] > 0) or
662 (offset[2] == 0 and offset[1] == 0 and offset[0] > 0);
663 auto const is_red =
666 : ind2 > ind1;
667 if (is_red) {
668 red_neighbors.push_back(cell);
669 } else {
670 black_neighbors.push_back(cell);
671 }
672 }
673
674 // Assign neighbors to the cell
677 }
678}
679
680GhostComm::HaloPlan RegularDecomposition::make_halo_plan() {
685
686 HaloPlan plan;
687 plan.comm = m_comm;
688
689 // Match the legacy communicator: on a single MPI rank there are no ghost
690 // cells to fill. The cell neighbourships are set up (see
691 // init_cell_interactions) so that cells across periodic boundaries are
692 // connected directly, so the plan stays empty.
693 if (m_comm.size() == 1)
694 return plan;
695
696 auto const cart_info = Utils::Mpi::cart_get<3>(m_comm);
697 auto const &node_pos = cart_info.coords;
698 auto const &node_grid = ::communicator.node_grid;
699 // Total number of MD cells along each axis across all ranks.
700 auto const global_size = hadamard_product(node_grid, cell_grid);
701 // Global (0-based) cell index of this rank's first *local* cell, i.e. of
702 // ghost-grid coordinate (1,1,1).
704
705 // Cartesian rank owning the cell at (folded) global cell coordinate.
706 // The Cartesian communicator is always fully periodic (see
707 // Communicator::init_comm_cart), so this is well-defined for every offset.
708 auto const owner_of = [&](Utils::Vector3i const &global_cell) {
709 auto const owner_coords =
711 return Utils::Mpi::cart_rank<3>(m_comm, owner_coords);
712 };
713
714 // Deterministic ordering key shared by both ranks of a peer pair: the linear
715 // index of the *real* cell (in the global cell grid) that a ghost mirrors.
716 auto const global_key = [&](Utils::Vector3i const &global_cell) {
717 auto const folded = (global_cell + global_size) % global_size;
719 };
720
721 // Pointer to the particle list of the cell at ghost-grid coordinate c.
722 auto const list_at = [this](Utils::Vector3i const &c) -> ParticleList * {
723 return &cells
724 .at(static_cast<std::size_t>(
726 .particles();
727 };
728
729 // Per-peer accumulators. A "recv" pair maps one of our ghost cells to the
730 // global index of the real cell (on the peer) it mirrors. A "send" pair maps
731 // one of our real cells to its own global index (the peer will receive it
732 // into a ghost). Sorting both lists by their key makes recv[k] line up with
733 // peer.send[k] without exchanging any index arrays.
734 struct PeerBucket {
735 std::vector<std::pair<int, ParticleList *>> recv; // (key, our ghost)
736 std::vector<std::pair<int, ParticleList *>> send; // (key, our real cell)
737 };
738 std::map<int, PeerBucket> peers;
739 std::vector<std::pair<int, LocalComm>> local; // (key, self-ghost copy)
740
741 auto const this_rank = m_comm.rank();
742 auto const one = Utils::Vector3i{1, 1, 1};
743
744 // Enumerate every ghost cell exactly once (any ghost-grid coordinate with a
745 // component in the halo, i.e. == 0 or == cell_grid+1). This guarantees each
746 // ghost is a recv/dst target exactly once, regardless of how small the node
747 // grid is (dims of 1 or 2 collapse several stencil directions onto the same
748 // peer, so a local-cell x offset enumeration would double-count).
749 //
750 // For each ghost we record a *matched pair*:
751 // * recv: this ghost, keyed by the global index of the real cell (on the
752 // peer) it mirrors -- so it lines up with the peer's send of that cell.
753 // * send: our own boundary real cell that the peer mirrors as its ghost in
754 // the opposite direction, keyed by that real cell's own global index --
755 // so it lines up with the peer's recv. The recv<->send pairing is a
756 // bijection, which keeps send.size() == recv.size() per peer.
757 for (int gz = 0; gz < ghost_cell_grid[2]; ++gz) {
758 for (int gy = 0; gy < ghost_cell_grid[1]; ++gy) {
759 for (int gx = 0; gx < ghost_cell_grid[0]; ++gx) {
760 Utils::Vector3i const nc{gx, gy, gz};
761 // Direction sign of the halo crossing (0 if interior in a dim).
763 bool is_ghost = false;
764 for (auto d = 0u; d < 3u; ++d) {
765 if (nc[d] == 0) {
766 side[d] = -1;
767 is_ghost = true;
768 } else if (nc[d] == cell_grid[d] + 1) {
769 side[d] = +1;
770 is_ghost = true;
771 }
772 }
773 if (not is_ghost)
774 continue; // interior (local) cell
775
776 // Global cell mirrored by this ghost and its owning peer.
777 auto const ghost_global = global_origin + (nc - one);
778 auto const peer = owner_of(ghost_global);
779 auto const recv_key = global_key(ghost_global);
780
781 if (peer == this_rank) {
782 // Periodic self-ghost (a node-grid dim equals 1): copy the matching
783 // local cell straight into the ghost (replaces GHOST_LOCL).
784 auto const src_coord = ((ghost_global + global_size) % global_size) -
786 local.emplace_back(recv_key,
787 LocalComm{list_at(src_coord), list_at(nc), {}});
788 continue;
789 }
790
791 peers[peer].recv.emplace_back(recv_key, list_at(nc));
792
793 // Dual send cell: our boundary real cell mirrored by the peer's ghost
794 // in the opposite direction. Snap crossing dims to the near boundary,
795 // keep tangential dims aligned with the ghost.
796 auto mc = nc;
797 for (auto d = 0u; d < 3u; ++d) {
798 if (side[d] == -1)
799 mc[d] = 1;
800 else if (side[d] == +1)
801 mc[d] = cell_grid[d];
802 }
803 auto const send_global = global_origin + (mc - one);
804 peers[peer].send.emplace_back(global_key(send_global), list_at(mc));
805 }
806 }
807 }
808
809 // Emit one NeighborComm per peer, with send/recv sorted by their shared key.
810 auto const by_key = [](auto const &a, auto const &b) {
811 return a.first < b.first;
812 };
813 for (auto &[peer, bucket] : peers) {
814 std::ranges::sort(bucket.recv, by_key);
815 std::ranges::sort(bucket.send, by_key);
816 NeighborComm nc;
817 nc.peer = peer;
818 nc.recv.reserve(bucket.recv.size());
819 for (auto const &[key, cell] : bucket.recv)
820 nc.recv.push_back(cell);
821 nc.send.reserve(bucket.send.size());
822 for (auto const &[key, cell] : bucket.send)
823 nc.send.push_back(SendRegion{cell, {}});
824 plan.neighbors.push_back(std::move(nc));
825 }
826
827 // Sort the self-copies deterministically too (not required, but keeps the
828 // plan reproducible run to run).
829 std::ranges::sort(
830 local, [](auto const &a, auto const &b) { return a.first < b.first; });
831 plan.local.reserve(local.size());
832 for (auto &[key, lc] : local)
833 plan.local.push_back(lc);
834
835 return plan;
836}
837
839 boost::mpi::communicator comm, double range, BoxGeometry const &box_geo,
840 LocalBox const &local_geo,
841 std::optional<std::pair<int, int>> fully_connected)
842 : m_comm(std::move(comm)), m_box(box_geo), m_local_box(local_geo),
843 m_fully_connected_boundary(std::move(fully_connected)) {
844
845 /* set up new regular decomposition cell structure */
846 create_cell_grid(range);
847
848 /* setup cell neighbors */
849 init_cell_interactions();
850
851 /* mark local and ghost cells */
852 mark_cells();
853
854 /* build the topology-agnostic direct-neighbor halo plan */
855 m_halo_plan = make_halo_plan();
856
857 /* Classify local cells as interior or boundary.
858 *
859 * Rule (a): any neighbor that is a ghost cell -> boundary [base rule].
860 * Rule (b): any neighbor relation that crosses a periodic box boundary
861 * must also make the cell boundary, even when both cells are local.
862 * This happens on a single MPI rank (node_grid[i]==1, periodic[i]):
863 * init_cell_interactions() wires the first and last local layer along
864 * axis i directly without going through ghost cells, so the base rule
865 * misses those periodic wrap-around neighbours.
866 *
867 * Implementation: precise pair-predicate that fires iff the two cells
868 * sit in the "first layer ↔ last layer" pair along a wrap axis.
869 * Ghost-grid coordinate of a cell: unpack the column-major linear index
870 * idx = a + G[0]*(b + G[1]*c) (G = ghost_cell_grid).
871 * First local layer along axis i: ghost coord == 1.
872 * Last local layer along axis i: ghost coord == cell_grid[i].
873 */
874 auto const &node_grid = ::communicator.node_grid;
875 auto const idx_of = [this](Cell const *c) {
876 return static_cast<int>(c - cells.data());
877 };
878 auto const ghost_coord_of = [this](int idx) -> Utils::Vector3i {
879 int const a = idx % ghost_cell_grid[0];
880 int const bc = idx / ghost_cell_grid[0];
881 int const b = bc % ghost_cell_grid[1];
882 int const c = bc / ghost_cell_grid[1];
883 return {a, b, c};
884 };
885 // Which axes wrap locally (the local domain spans the whole box along a
886 // periodic axis)? Precomputed by value so the predicate lambda does not
887 // capture node_grid (AppleClang rejects that non-odr-use capture with
888 // -Werror,-Wunused-lambda-capture).
889 std::array<bool, 3> wrap_axis;
890 for (int i = 0; i < 3; ++i)
891 wrap_axis[i] = (node_grid[i] == 1) && m_box.periodic(i);
892 // Build the predicate only when there is at least one wrap axis; otherwise
893 // pass nullptr (no overhead in the inner loop of mark_boundary_cells).
894 bool const has_wrap_axis = wrap_axis[0] || wrap_axis[1] || wrap_axis[2];
895 std::function<bool(Cell const *, Cell const *)> wrap_pred;
896 if (has_wrap_axis) {
898 Cell const *a_cell, Cell const *b_cell) -> bool {
899 auto const a_coord = ghost_coord_of(idx_of(a_cell));
900 auto const b_coord = ghost_coord_of(idx_of(b_cell));
901 for (int i = 0; i < 3; ++i) {
902 if (wrap_axis[i]) {
903 bool const a_first = (a_coord[i] == 1);
904 bool const a_last = (a_coord[i] == cell_grid[i]);
905 bool const b_first = (b_coord[i] == 1);
906 bool const b_last = (b_coord[i] == cell_grid[i]);
907 if ((a_first && b_last) || (a_last && b_first))
908 return true;
909 }
910 }
911 return false;
912 };
913 }
915
916 /* Degenerate case: node_grid[i]==1, periodic[i], cell_grid[i]==1.
917 *
918 * When cell_grid[i]==1 on a wrap axis, the single local cell layer along
919 * that axis has itself as the only periodic neighbour (both ends fold to
920 * the same global index). init_cell_interactions() therefore excludes
921 * the self-pair (ind1==ind2 guard at line ~552), leaving neighbors().all()
922 * empty along that axis. The wrap_predicate above is never called for
923 * that cell, so mark_boundary_cells() leaves it interior — wrong.
924 *
925 * Correct interpretation: the cell interacts with itself across the
926 * periodic boundary, so it is by definition wrap-adjacent and must be
927 * boundary. The simplest safe fix: if any wrap axis has cell_grid[i]==1,
928 * every local cell is boundary (they all span that axis, so all are
929 * wrap-adjacent).
930 */
931 for (int i = 0; i < 3; ++i) {
932 if (wrap_axis[i] && cell_grid[i] == 1) {
933 for (Cell *c : local_cells())
934 c->m_is_boundary = true;
935 break; // one degenerate axis is enough to force all-boundary
936 }
937 }
938
939 /* Plan-membership pass (source 2): mark every local cell that the plan
940 * exports as a send source. The geometric rules above (source 1) miss
941 * plan shapes such as Lees-Edwards fully-connected boundaries and ELC
942 * periodicity-change paths, where boundary cells are determined by the
943 * plan topology rather than ghost-cell adjacency. Both sources are
944 * complementary and must both be applied. */
946
947#ifdef ESPRESSO_ADDITIONAL_CHECKS
950 "RegularDecomposition"));
951 // NOTE: validate_halo_plan_symmetry is NOT called here.
952 // During checkpoint loading, decompositions are transiently rebuilt while
953 // maximal_cutoff is rank-divergent (ranks may have different cell grids for a
954 // brief window before the next consistent rebuild). The transient plan is
955 // never used — it is immediately replaced — so the asymmetry is harmless.
956 // A construction-time collective all_to_all inside a ctor is also dangerous:
957 // if one rank aborts the others block forever in the collective.
958 // Symmetry is instead validated at FIRST USE of the plan in
959 // halo_exchange_start (see GhostComm::halo_exchange_start in
960 // HaloExchange.cpp).
961#endif
962}
static int coord(std::string const &s)
auto make_flat_set(Comparator &&comp)
static void fold_and_reset(Particle &p, BoxGeometry const &box_geo)
Fold coordinates to box and reset the old position.
Vector implementation and trait types for boost qvm interoperability.
Utils::Vector3d const & length() const
Box length.
constexpr bool periodic(unsigned coord) const
Check periodicity in direction.
T get_mi_coord(T a, T b, unsigned coord) const noexcept
Get the minimum-image distance between two coordinates.
void fold_position(Utils::Vector3d &pos, Utils::Vector3i &image_box) const
Fold coordinates to primary simulation box in-place.
Definition Cell.hpp:96
auto const & my_right() const
Right (top, back) corner of this nodes local box.
Definition LocalBox.hpp:47
auto const & boundary() const
Boundary information for the local box.
Definition LocalBox.hpp:59
auto const & my_left() const
Left (bottom, front) corner of this nodes local box.
Definition LocalBox.hpp:45
auto const & length() const
Dimensions of the box a single node is responsible for.
Definition LocalBox.hpp:49
iterator begin()
Definition Bag.hpp:82
void clear()
Remove all elements form container.
Definition Bag.hpp:134
T & insert(T const &v)
Insert an element into the container.
Definition Bag.hpp:147
iterator erase(iterator it)
Remove element from the list.
Definition Bag.hpp:165
iterator end()
Definition Bag.hpp:83
DEVICE_QUALIFIER constexpr size_type size() const noexcept
Definition Array.hpp:167
static DEVICE_QUALIFIER constexpr Vector< T, N > broadcast(typename Base::value_type const &value) noexcept
Create a vector that has all entries set to the same value.
Definition Vector.hpp:138
cudaStream_t stream[1]
CUDA streams for parallel computing on CPU and GPU.
Communicator communicator
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)
void mark_boundary_cells(std::span< Cell *const > local_cells, std::span< Cell *const > ghost_cells, std::function< bool(Cell const *, Cell const *)> wrap_predicate=nullptr)
Classify each local cell as interior or boundary.
void mark_plan_cells_boundary(HaloPlan const &plan, std::span< Cell *const > local_cells)
Mark plan-exported local cells as boundary (source 2, see mark_boundary_cells).
bool report_violations(std::vector< std::string > const &violations, char const *context)
Print violations to stderr and return whether the list was empty.
std::vector< std::string > validate_halo_plan(HaloPlan const &plan, std::span< Cell *const > local_cells, std::span< Cell *const > ghost_cells)
Validate a HaloPlan for correctness.
std::array< mpi::request, 2 > isendrecv(mpi::communicator const &comm, int dest, int stag, const T &sval, int src, int rtag, T &rval)
Definition sendrecv.hpp:73
mpi::status sendrecv(mpi::communicator const &comm, int dest, int stag, const T &sval, int src, int rtag, T &rval)
Definition sendrecv.hpp:66
T product(Vector< T, N > const &v)
Definition Vector.hpp:373
DEVICE_QUALIFIER int get_linear_index(int a, int b, int c, Vector3i const &adim)
Definition index.hpp:36
auto hadamard_division(Vector< T, N > const &a, Vector< U, N > const &b)
Definition Vector.hpp:395
auto hadamard_product(Vector< T, N > const &a, Vector< U, N > const &b)
Definition Vector.hpp:378
STL namespace.
Utils::Vector3i node_grid
Struct holding all information for one particle.
Definition Particle.hpp:436
constexpr auto const & pos() const
Definition Particle.hpp:476
constexpr auto & pos_at_last_verlet_update()
Definition Particle.hpp:487
constexpr auto const & image_box() const
Definition Particle.hpp:489
Utils::Vector3i ghost_cell_grid
linked cell grid with ghost frame.
Utils::Vector3d max_cutoff() const override
Utils::Vector3d inv_cell_size
inverse cell_size.
std::vector< Cell * > m_ghost_cells
BoxGeometry const & m_box
Cell * particle_to_cell(Particle const &p) override
void resort(bool global, std::vector< ParticleChange > &diff) override
std::vector< Cell > cells
RegularDecomposition(boost::mpi::communicator comm, double range, BoxGeometry const &box_geo, LocalBox const &local_geo, std::optional< std::pair< int, int > > fully_connected)
Utils::Vector3d cell_size
Cell size.
std::span< Cell *const > local_cells() const override
GhostComm::HaloPlan m_halo_plan
Topology-agnostic direct-neighbor halo plan (see make_halo_plan).
std::vector< Cell * > m_local_cells
Utils::Vector3d max_range() const override
std::span< Cell *const > ghost_cells() const override
Utils::Vector3i cell_grid
Grid dimensions per node.
Utils::Vector3i cell_offset
Offset in global grid.
boost::mpi::communicator m_comm