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 {
66 Utils::Vector3i cpos;
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,
113 ParticleList &left,
114 ParticleList &right,
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;
118 auto const can_move_left = m_box.periodic(dir) or not is_open_boundary_left;
119 auto const can_move_right = m_box.periodic(dir) or not is_open_boundary_right;
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);
139 static ParticleList send_buf_l, send_buf_r, recv_buf_l, recv_buf_r;
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
151 Utils::Mpi::sendrecv(m_comm, node_neighbors[2 * dir], 0, send_buf_l,
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) {
189 ParticleList displaced_parts;
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();) {
217 fold_and_reset(*it, m_box);
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
395 cell_grid = Utils::Vector3i::broadcast(cells_per_dir);
396 n_local_cells = Utils::product(cell_grid);
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 (;;) {
426 n_local_cells = Utils::product(cell_grid);
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
443 cell_grid[min_ind]--;
444 cell_range[min_ind] = m_local_box.length()[min_ind] / cell_grid[min_ind];
445 }
446
447 /* sanity check */
448 if (n_local_cells < min_num_cells) {
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;
468 new_cells *= ghost_cell_grid[i];
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));
477 m_local_cells.resize(n_local_cells);
478 m_ghost_cells.resize(new_cells - n_local_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;
496 auto const global_halo_offset = hadamard_product(node_pos, cell_grid) - halo;
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
501 auto const at_boundary = [&global_size](int coord, Utils::Vector3i cell_idx) {
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.
508 auto const fcb_is_inner_connection = [&global_size, this](Utils::Vector3i a,
509 Utils::Vector3i b) {
511 auto const [fc_normal, fc_dir] = *fully_connected_boundary();
512 auto const involves_ghost_cell =
513 (a[fc_normal] == -1 or a[fc_normal] == global_size[fc_normal] or
514 b[fc_normal] == -1 or b[fc_normal] == global_size[fc_normal]);
515 if (not involves_ghost_cell) {
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 =
526 [&](Utils::Vector3i const &local_index) -> Utils::Vector3i {
527 return (global_halo_offset + local_index);
528 };
529
530 /* Linear index in the global cell grid. */
531 auto folded_linear_index = [&](Utils::Vector3i const &global_index) {
532 auto const folded_index = (global_index + global_size) % global_size;
533
534 return get_linear_index(folded_index, global_size);
535 };
536
537 /* Translate a global index into a local one */
538 auto local_index =
539 [&](Utils::Vector3i const &global_index) -> Utils::Vector3i {
540 return (global_index - global_halo_offset);
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 }
554 if (not m_box.periodic(fc_normal)) {
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 // */
580 auto const [fc_boundary, fc_direction] = *fully_connected_boundary();
581
582 // Fully connected is only needed at the box surface
583 if (at_boundary(fc_boundary, {m, n, o})) {
584 lower_index[fc_direction] = -1;
585 upper_index[fc_direction] = global_size[fc_direction];
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) {
602 return folded_linear_index(a) < folded_linear_index(b);
603 });
604
605 /* Collect neighbors */
606 for (int p = lower_index[2]; p <= upper_index[2]; p++)
607 for (int q = lower_index[1]; q <= upper_index[1]; q++)
608 for (int r = lower_index[0]; r <= upper_index[0]; r++) {
610 // Avoid fully connecting the boundary layer and the
611 // next INNER layer
612 if (fcb_is_inner_connection({m, n, o}, {r, q, p}))
613 continue;
614 }
615 neighbors.insert(Utils::Vector3i{r, q, p});
616 }
617
618 /* Red-black partition by global index. */
619 auto const ind1 = folded_linear_index({m, n, o});
620
621 std::vector<Cell *> red_neighbors;
622 std::vector<Cell *> black_neighbors;
623
624 /* If we are running on a single MPI rank, it is not necessary to use
625 * ghost cells. Instead of adding a ghost cell as neighbor,
626 * we directly connect to the corresponding
627 * physical cell across the periodic boundary */
628 for (auto &neighbor : neighbors) {
629 if (one_mpi_rank) {
630 for (auto coord : {0u, 1u, 2u}) {
631 if (neighbor[coord] == -1) {
632 neighbor[coord] += cell_grid[coord];
633 } else if (neighbor[coord] == cell_grid[coord]) {
634 neighbor[coord] -= cell_grid[coord];
635 }
636 }
637 }
638 auto const ind2 = folded_linear_index(neighbor);
639 /* Exclude cell itself */
640 if (ind1 == ind2)
641 continue;
642
643 auto cell = &cells.at(
644 get_linear_index(local_index(neighbor), ghost_cell_grid));
645
646 // Divide red and black neighbors
647 if (ind2 > ind1) {
648 red_neighbors.push_back(cell);
649 } else {
650 black_neighbors.push_back(cell);
651 }
652 }
653
654 // Assign neighbors to the cell
655 cells[get_linear_index(local_index({m, n, o}), ghost_cell_grid)]
656 .m_neighbors = Neighbors<Cell *>(red_neighbors, black_neighbors);
657 }
658}
659
660GhostComm::HaloPlan RegularDecomposition::make_halo_plan() {
665
666 HaloPlan plan;
667 plan.comm = m_comm;
668
669 // Match the legacy communicator: on a single MPI rank there are no ghost
670 // cells to fill. The cell neighbourships are set up (see
671 // init_cell_interactions) so that cells across periodic boundaries are
672 // connected directly, so the plan stays empty.
673 if (m_comm.size() == 1)
674 return plan;
675
676 auto const cart_info = Utils::Mpi::cart_get<3>(m_comm);
677 auto const &node_pos = cart_info.coords;
678 auto const &node_grid = ::communicator.node_grid;
679 // Total number of MD cells along each axis across all ranks.
680 auto const global_size = hadamard_product(node_grid, cell_grid);
681 // Global (0-based) cell index of this rank's first *local* cell, i.e. of
682 // ghost-grid coordinate (1,1,1).
683 auto const global_origin = hadamard_product(node_pos, cell_grid);
684
685 // Cartesian rank owning the cell at (folded) global cell coordinate.
686 // The Cartesian communicator is always fully periodic (see
687 // Communicator::init_comm_cart), so this is well-defined for every offset.
688 auto const owner_of = [&](Utils::Vector3i const &global_cell) {
689 auto const owner_coords =
690 hadamard_division((global_cell + global_size) % global_size, cell_grid);
691 return Utils::Mpi::cart_rank<3>(m_comm, owner_coords);
692 };
693
694 // Deterministic ordering key shared by both ranks of a peer pair: the linear
695 // index of the *real* cell (in the global cell grid) that a ghost mirrors.
696 auto const global_key = [&](Utils::Vector3i const &global_cell) {
697 auto const folded = (global_cell + global_size) % global_size;
698 return Utils::get_linear_index(folded, global_size);
699 };
700
701 // Pointer to the particle list of the cell at ghost-grid coordinate c.
702 auto const list_at = [this](Utils::Vector3i const &c) -> ParticleList * {
703 return &cells
704 .at(static_cast<std::size_t>(
706 .particles();
707 };
708
709 // Per-peer accumulators. A "recv" pair maps one of our ghost cells to the
710 // global index of the real cell (on the peer) it mirrors. A "send" pair maps
711 // one of our real cells to its own global index (the peer will receive it
712 // into a ghost). Sorting both lists by their key makes recv[k] line up with
713 // peer.send[k] without exchanging any index arrays.
714 struct PeerBucket {
715 std::vector<std::pair<int, ParticleList *>> recv; // (key, our ghost)
716 std::vector<std::pair<int, ParticleList *>> send; // (key, our real cell)
717 };
718 std::map<int, PeerBucket> peers;
719 std::vector<std::pair<int, LocalComm>> local; // (key, self-ghost copy)
720
721 auto const this_rank = m_comm.rank();
722 auto const one = Utils::Vector3i{1, 1, 1};
723
724 // Enumerate every ghost cell exactly once (any ghost-grid coordinate with a
725 // component in the halo, i.e. == 0 or == cell_grid+1). This guarantees each
726 // ghost is a recv/dst target exactly once, regardless of how small the node
727 // grid is (dims of 1 or 2 collapse several stencil directions onto the same
728 // peer, so a local-cell x offset enumeration would double-count).
729 //
730 // For each ghost we record a *matched pair*:
731 // * recv: this ghost, keyed by the global index of the real cell (on the
732 // peer) it mirrors -- so it lines up with the peer's send of that cell.
733 // * send: our own boundary real cell that the peer mirrors as its ghost in
734 // the opposite direction, keyed by that real cell's own global index --
735 // so it lines up with the peer's recv. The recv<->send pairing is a
736 // bijection, which keeps send.size() == recv.size() per peer.
737 for (int gz = 0; gz < ghost_cell_grid[2]; ++gz) {
738 for (int gy = 0; gy < ghost_cell_grid[1]; ++gy) {
739 for (int gx = 0; gx < ghost_cell_grid[0]; ++gx) {
740 Utils::Vector3i const nc{gx, gy, gz};
741 // Direction sign of the halo crossing (0 if interior in a dim).
742 Utils::Vector3i side{};
743 bool is_ghost = false;
744 for (auto d = 0u; d < 3u; ++d) {
745 if (nc[d] == 0) {
746 side[d] = -1;
747 is_ghost = true;
748 } else if (nc[d] == cell_grid[d] + 1) {
749 side[d] = +1;
750 is_ghost = true;
751 }
752 }
753 if (not is_ghost)
754 continue; // interior (local) cell
755
756 // Global cell mirrored by this ghost and its owning peer.
757 auto const ghost_global = global_origin + (nc - one);
758 auto const peer = owner_of(ghost_global);
759 auto const recv_key = global_key(ghost_global);
760
761 if (peer == this_rank) {
762 // Periodic self-ghost (a node-grid dim equals 1): copy the matching
763 // local cell straight into the ghost (replaces GHOST_LOCL).
764 auto const src_coord = ((ghost_global + global_size) % global_size) -
765 global_origin + one;
766 local.emplace_back(recv_key,
767 LocalComm{list_at(src_coord), list_at(nc), {}});
768 continue;
769 }
770
771 peers[peer].recv.emplace_back(recv_key, list_at(nc));
772
773 // Dual send cell: our boundary real cell mirrored by the peer's ghost
774 // in the opposite direction. Snap crossing dims to the near boundary,
775 // keep tangential dims aligned with the ghost.
776 auto mc = nc;
777 for (auto d = 0u; d < 3u; ++d) {
778 if (side[d] == -1)
779 mc[d] = 1;
780 else if (side[d] == +1)
781 mc[d] = cell_grid[d];
782 }
783 auto const send_global = global_origin + (mc - one);
784 peers[peer].send.emplace_back(global_key(send_global), list_at(mc));
785 }
786 }
787 }
788
789 // Emit one NeighborComm per peer, with send/recv sorted by their shared key.
790 auto const by_key = [](auto const &a, auto const &b) {
791 return a.first < b.first;
792 };
793 for (auto &[peer, bucket] : peers) {
794 std::ranges::sort(bucket.recv, by_key);
795 std::ranges::sort(bucket.send, by_key);
796 NeighborComm nc;
797 nc.peer = peer;
798 nc.recv.reserve(bucket.recv.size());
799 for (auto const &[key, cell] : bucket.recv)
800 nc.recv.push_back(cell);
801 nc.send.reserve(bucket.send.size());
802 for (auto const &[key, cell] : bucket.send)
803 nc.send.push_back(SendRegion{cell, {}});
804 plan.neighbors.push_back(std::move(nc));
805 }
806
807 // Sort the self-copies deterministically too (not required, but keeps the
808 // plan reproducible run to run).
809 std::ranges::sort(
810 local, [](auto const &a, auto const &b) { return a.first < b.first; });
811 plan.local.reserve(local.size());
812 for (auto &[key, lc] : local)
813 plan.local.push_back(lc);
814
815 return plan;
816}
817
819 boost::mpi::communicator comm, double range, BoxGeometry const &box_geo,
820 LocalBox const &local_geo,
821 std::optional<std::pair<int, int>> fully_connected)
822 : m_comm(std::move(comm)), m_box(box_geo), m_local_box(local_geo),
823 m_fully_connected_boundary(std::move(fully_connected)) {
824
825 /* set up new regular decomposition cell structure */
826 create_cell_grid(range);
827
828 /* setup cell neighbors */
829 init_cell_interactions();
830
831 /* mark local and ghost cells */
832 mark_cells();
833
834 /* build the topology-agnostic direct-neighbor halo plan */
835 m_halo_plan = make_halo_plan();
836
837 /* Classify local cells as interior or boundary.
838 *
839 * Rule (a): any neighbor that is a ghost cell -> boundary [base rule].
840 * Rule (b): any neighbor relation that crosses a periodic box boundary
841 * must also make the cell boundary, even when both cells are local.
842 * This happens on a single MPI rank (node_grid[i]==1, periodic[i]):
843 * init_cell_interactions() wires the first and last local layer along
844 * axis i directly without going through ghost cells, so the base rule
845 * misses those periodic wrap-around neighbours.
846 *
847 * Implementation: precise pair-predicate that fires iff the two cells
848 * sit in the "first layer ↔ last layer" pair along a wrap axis.
849 * Ghost-grid coordinate of a cell: unpack the column-major linear index
850 * idx = a + G[0]*(b + G[1]*c) (G = ghost_cell_grid).
851 * First local layer along axis i: ghost coord == 1.
852 * Last local layer along axis i: ghost coord == cell_grid[i].
853 */
854 auto const &node_grid = ::communicator.node_grid;
855 auto const idx_of = [this](Cell const *c) {
856 return static_cast<int>(c - cells.data());
857 };
858 auto const ghost_coord_of = [this](int idx) -> Utils::Vector3i {
859 int const a = idx % ghost_cell_grid[0];
860 int const bc = idx / ghost_cell_grid[0];
861 int const b = bc % ghost_cell_grid[1];
862 int const c = bc / ghost_cell_grid[1];
863 return {a, b, c};
864 };
865 // Which axes wrap locally (the local domain spans the whole box along a
866 // periodic axis)? Precomputed by value so the predicate lambda does not
867 // capture node_grid (AppleClang rejects that non-odr-use capture with
868 // -Werror,-Wunused-lambda-capture).
869 std::array<bool, 3> wrap_axis;
870 for (int i = 0; i < 3; ++i)
871 wrap_axis[i] = (node_grid[i] == 1) && m_box.periodic(i);
872 // Build the predicate only when there is at least one wrap axis; otherwise
873 // pass nullptr (no overhead in the inner loop of mark_boundary_cells).
874 bool const has_wrap_axis = wrap_axis[0] || wrap_axis[1] || wrap_axis[2];
875 std::function<bool(Cell const *, Cell const *)> wrap_pred;
876 if (has_wrap_axis) {
877 wrap_pred = [this, wrap_axis, idx_of, ghost_coord_of](
878 Cell const *a_cell, Cell const *b_cell) -> bool {
879 auto const a_coord = ghost_coord_of(idx_of(a_cell));
880 auto const b_coord = ghost_coord_of(idx_of(b_cell));
881 for (int i = 0; i < 3; ++i) {
882 if (wrap_axis[i]) {
883 bool const a_first = (a_coord[i] == 1);
884 bool const a_last = (a_coord[i] == cell_grid[i]);
885 bool const b_first = (b_coord[i] == 1);
886 bool const b_last = (b_coord[i] == cell_grid[i]);
887 if ((a_first && b_last) || (a_last && b_first))
888 return true;
889 }
890 }
891 return false;
892 };
893 }
895
896 /* Degenerate case: node_grid[i]==1, periodic[i], cell_grid[i]==1.
897 *
898 * When cell_grid[i]==1 on a wrap axis, the single local cell layer along
899 * that axis has itself as the only periodic neighbour (both ends fold to
900 * the same global index). init_cell_interactions() therefore excludes
901 * the self-pair (ind1==ind2 guard at line ~552), leaving neighbors().all()
902 * empty along that axis. The wrap_predicate above is never called for
903 * that cell, so mark_boundary_cells() leaves it interior — wrong.
904 *
905 * Correct interpretation: the cell interacts with itself across the
906 * periodic boundary, so it is by definition wrap-adjacent and must be
907 * boundary. The simplest safe fix: if any wrap axis has cell_grid[i]==1,
908 * every local cell is boundary (they all span that axis, so all are
909 * wrap-adjacent).
910 */
911 for (int i = 0; i < 3; ++i) {
912 if (wrap_axis[i] && cell_grid[i] == 1) {
913 for (Cell *c : local_cells())
914 c->m_is_boundary = true;
915 break; // one degenerate axis is enough to force all-boundary
916 }
917 }
918
919 /* Plan-membership pass (source 2): mark every local cell that the plan
920 * exports as a send source. The geometric rules above (source 1) miss
921 * plan shapes such as Lees-Edwards fully-connected boundaries and ELC
922 * periodicity-change paths, where boundary cells are determined by the
923 * plan topology rather than ghost-cell adjacency. Both sources are
924 * complementary and must both be applied. */
926
927#ifdef ESPRESSO_ADDITIONAL_CHECKS
930 "RegularDecomposition"));
931 // NOTE: validate_halo_plan_symmetry is NOT called here.
932 // During checkpoint loading, decompositions are transiently rebuilt while
933 // maximal_cutoff is rank-divergent (ranks may have different cell grids for a
934 // brief window before the next consistent rebuild). The transient plan is
935 // never used — it is immediately replaced — so the asymmetry is harmless.
936 // A construction-time collective all_to_all inside a ctor is also dangerous:
937 // if one rank aborts the others block forever in the collective.
938 // Symmetry is instead validated at FIRST USE of the plan in
939 // halo_exchange_start (see GhostComm::halo_exchange_start in
940 // HaloExchange.cpp).
941#endif
942}
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
bool empty() const
Is the container empty?
Definition Bag.hpp:96
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
std::size_t size() const
Number of elements in the container.
Definition Bag.hpp:90
void resize(std::size_t new_size)
Resize container.
Definition Bag.hpp:129
DEVICE_QUALIFIER constexpr size_type size() const noexcept
Definition Array.hpp:166
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:134
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:369
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:391
auto hadamard_product(Vector< T, N > const &a, Vector< U, N > const &b)
Definition Vector.hpp:374
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