PeriDEM 0.3.0
PeriDEM -- Peridynamics-based high-fidelity model for granular media
Loading...
Searching...
No Matches
particleMpi.cpp
Go to the documentation of this file.
1/*
2 * -------------------------------------------
3 * Copyright (c) 2021 - 2026 Prashant K. Jha
4 * -------------------------------------------
5 * PeriDEM https://github.com/prashjha/PeriDEM
6 *
7 * Distributed under the Boost Software License, Version 1.0. (See accompanying
8 * file LICENSE)
9 */
10
11#include "particleMpi.h"
12
13#include "baseParticle.h"
14#include "data/modelData.h"
15#include "inp/input.h"
16#include "util/io.h"
17#include "util/parallelUtil.h"
18#include "util/point.h"
19
20#include <algorithm>
21#include <chrono>
22#include <cmath>
23#include <format>
24#include <limits>
25#include <stdexcept>
26#include <vector>
27
30 return true;
31 if (p.isWall())
32 return true;
34}
35
37 std::string s = "auto";
38 if (data.d_input_p && data.d_input_p->d_modelDeck_p)
39 s = data.d_input_p->d_modelDeck_p->d_mpiStrategy;
40 if (s.empty())
41 s = "auto";
42 if (s == "auto") {
43 if (data.d_input_p && data.d_input_p->isMultiParticle())
44 return "particle";
45 return "dof";
46 }
47 return s;
48}
49
50namespace {
51
52void factor2d(int nproc, double Lx, double Ly, int &nx, int &ny) {
53 // nx*ny = nproc; prefer subdomain aspect close to domain aspect.
54 nx = 1;
55 ny = nproc;
56 double best = std::numeric_limits<double>::max();
57 for (int i = 1; i <= nproc; ++i) {
58 if (nproc % i != 0)
59 continue;
60 const int j = nproc / i;
61 const double ax = Lx / static_cast<double>(i);
62 const double ay = Ly / static_cast<double>(j);
63 const double score = std::abs(ax - ay);
64 if (score < best) {
65 best = score;
66 nx = i;
67 ny = j;
68 }
69 }
70}
71
74 std::vector<double> &buf) {
75 const size_t n = p.getNumNodes();
76 const size_t base = buf.size();
77 buf.resize(base + 9 * n);
78 for (size_t i = 0; i < n; ++i) {
79 const size_t g = p.getNodeId(i);
80 const auto &x = data.d_x[g];
81 const auto &u = data.d_u[g];
82 const auto &v = data.d_v[g];
83 const size_t o = base + 9 * i;
84 buf[o + 0] = x.d_x;
85 buf[o + 1] = x.d_y;
86 buf[o + 2] = x.d_z;
87 buf[o + 3] = u.d_x;
88 buf[o + 4] = u.d_y;
89 buf[o + 5] = u.d_z;
90 buf[o + 6] = v.d_x;
91 buf[o + 7] = v.d_y;
92 buf[o + 8] = v.d_z;
93 }
94}
95
97 const std::vector<double> &buf, size_t offset) {
98 const size_t n = p.getNumNodes();
99 for (size_t i = 0; i < n; ++i) {
100 const size_t g = p.getNodeId(i);
101 const size_t o = offset + 9 * i;
102 data.d_x[g] = util::Point(buf[o + 0], buf[o + 1], buf[o + 2]);
103 data.d_u[g] = util::Point(buf[o + 3], buf[o + 4], buf[o + 5]);
104 data.d_v[g] = util::Point(buf[o + 6], buf[o + 7], buf[o + 8]);
105 data.d_vMag[g] = data.d_v[g].length();
106 }
107 return offset + 9 * n;
108}
109
111 size_t interval = data.d_contNeighUpdateInterval;
112 if (interval == 0 && data.d_particleDeck_p)
113 interval = data.d_particleDeck_p->d_pNeighDeck.d_neighUpdateInterval;
114 if (interval == 0)
115 interval = 1;
116 return interval;
117}
118
120 MPI_Comm comm = util::parallel::mpiComm();
121 const int size = util::parallel::mpiSize();
122 const int rank = util::parallel::mpiRank();
123 const auto &grains = data.d_particlesListTypeParticle;
124 const size_t n_grains = grains.size();
125
126 std::vector<double> local_c(4 * n_grains, 0.);
127 for (size_t g = 0; g < n_grains; ++g) {
128 const auto c = grains[g]->getXCenter();
129 local_c[4 * g + 0] = c.d_x;
130 local_c[4 * g + 1] = c.d_y;
131 local_c[4 * g + 2] = c.d_z;
132 local_c[4 * g + 3] = grains[g]->d_geom_p->boundingRadius();
133 }
134 std::vector<double> all_c(4 * n_grains * static_cast<size_t>(size), 0.);
135 MPI_Allgather(local_c.data(), static_cast<int>(local_c.size()), MPI_DOUBLE,
136 all_c.data(), static_cast<int>(local_c.size()), MPI_DOUBLE,
137 comm);
138
139 std::vector<util::Point> centers(n_grains);
140 std::vector<double> radii(n_grains);
141 for (size_t g = 0; g < n_grains; ++g) {
142 const int own = grains[g]->d_mpiOwner;
143 const size_t base = static_cast<size_t>(own) * 4 * n_grains + 4 * g;
144 centers[g] = util::Point(all_c[base + 0], all_c[base + 1], all_c[base + 2]);
145 radii[g] = all_c[base + 3];
146 }
147
148 data.d_mpiIncludeInContactCloud.assign(data.d_particlesListTypeAll.size(), 0);
149 for (auto *p : data.d_particlesListTypeAll) {
150 if (p->isWall() || particle::isLocallyOwned(*p))
151 data.d_mpiIncludeInContactCloud[p->getId()] = 1;
152 }
153
154 // Contact search radius already carries sFactor skin when interval > 1;
155 // add a small extra buffer for MPI plan lifetime between rebuilds.
156 const double extra =
157 std::max(0.25 * data.d_maxContactR, std::max(data.d_hMax, 1.0e-16));
158 const double search_r =
159 (data.d_contNeighSearchRadius > 1.0e-16)
160 ? data.d_contNeighSearchRadius
161 : data.d_maxContactR;
162 const double cutoff = search_r + extra;
163
164 data.d_mpiGhostNeedFrom.assign(static_cast<size_t>(size), {});
165 size_t n_ghost = 0;
166 auto add_ghost = [&](size_t g) {
167 auto *pj = grains[g];
169 return;
170 const int own = pj->d_mpiOwner;
171 if (own < 0 || own == rank)
172 return;
173 auto &vec = data.d_mpiGhostNeedFrom[static_cast<size_t>(own)];
174 const int gid = static_cast<int>(g);
175 if (std::find(vec.begin(), vec.end(), gid) != vec.end())
176 return;
177 vec.push_back(gid);
178 data.d_mpiIncludeInContactCloud[pj->getId()] = 1;
179 ++n_ghost;
180 };
181
182 for (size_t g = 0; g < n_grains; ++g) {
183 bool near = false;
184 for (size_t i = 0; i < n_grains && !near; ++i) {
185 if (!particle::isLocallyOwned(*grains[i]))
186 continue;
187 const double lim = radii[i] + radii[g] + cutoff;
188 if (centers[i].dist(centers[g]) < lim)
189 near = true;
190 }
191 if (near)
192 add_ghost(g);
193 }
194
195 // Wall contact is assembled on ranks that own wall nodes (rank 0 for
196 // particle-MPI). Ghost any grain near a wall so plate/cup searches see them.
197 for (auto *w : data.d_particlesListTypeWall) {
198 if (!w)
199 continue;
200 const auto wc = w->getXCenter();
201 const double wr = w->d_geom_p->boundingRadius();
202 for (size_t g = 0; g < n_grains; ++g) {
203 const double lim = wr + radii[g] + cutoff;
204 if (wc.dist(centers[g]) < lim)
205 add_ghost(g);
206 }
207 }
208
209 std::vector<int> req_sendcounts(static_cast<size_t>(size), 0);
210 std::vector<int> req_recvcounts(static_cast<size_t>(size), 0);
211 for (int r = 0; r < size; ++r)
212 req_sendcounts[static_cast<size_t>(r)] =
213 static_cast<int>(data.d_mpiGhostNeedFrom[static_cast<size_t>(r)].size());
214 MPI_Alltoall(req_sendcounts.data(), 1, MPI_INT, req_recvcounts.data(), 1,
215 MPI_INT, comm);
216
217 std::vector<int> req_sdispls(static_cast<size_t>(size), 0);
218 std::vector<int> req_rdispls(static_cast<size_t>(size), 0);
219 int req_send_total = 0;
220 int req_recv_total = 0;
221 for (int r = 0; r < size; ++r) {
222 req_sdispls[static_cast<size_t>(r)] = req_send_total;
223 req_rdispls[static_cast<size_t>(r)] = req_recv_total;
224 req_send_total += req_sendcounts[static_cast<size_t>(r)];
225 req_recv_total += req_recvcounts[static_cast<size_t>(r)];
226 }
227
228 std::vector<int> req_sendbuf(static_cast<size_t>(req_send_total));
229 for (int r = 0; r < size; ++r) {
230 const auto &v = data.d_mpiGhostNeedFrom[static_cast<size_t>(r)];
231 std::copy(v.begin(), v.end(),
232 req_sendbuf.begin() + req_sdispls[static_cast<size_t>(r)]);
233 }
234 std::vector<int> req_recvbuf(static_cast<size_t>(req_recv_total));
235 MPI_Alltoallv(req_sendbuf.data(), req_sendcounts.data(), req_sdispls.data(),
236 MPI_INT, req_recvbuf.data(), req_recvcounts.data(),
237 req_rdispls.data(), MPI_INT, comm);
238
239 data.d_mpiGhostServeTo.assign(static_cast<size_t>(size), {});
240 for (int r = 0; r < size; ++r) {
241 const int off = req_rdispls[static_cast<size_t>(r)];
242 const int nreq = req_recvcounts[static_cast<size_t>(r)];
243 auto &dst = data.d_mpiGhostServeTo[static_cast<size_t>(r)];
244 dst.resize(static_cast<size_t>(nreq));
245 for (int k = 0; k < nreq; ++k) {
246 const int g = req_recvbuf[static_cast<size_t>(off + k)];
247 if (g < 0 || static_cast<size_t>(g) >= n_grains)
248 throw std::runtime_error("rebuildGhostPlan: bad grain id");
249 if (grains[static_cast<size_t>(g)]->d_mpiOwner != rank)
250 throw std::runtime_error(
251 "rebuildGhostPlan: requested grain not owned here");
252 dst[static_cast<size_t>(k)] = g;
253 }
254 }
255
256 data.d_mpiGhostPlanValid = true;
257 data.d_mpiGhostStepsSinceRebuild = 0;
258 data.setKeyData("mpi_ghost_grain_count", static_cast<double>(n_ghost));
259}
260
262 MPI_Comm comm = util::parallel::mpiComm();
263 const int size = util::parallel::mpiSize();
264 const auto &grains = data.d_particlesListTypeParticle;
265
266 std::vector<std::vector<double>> kin_send(static_cast<size_t>(size));
267 for (int r = 0; r < size; ++r) {
268 for (int g : data.d_mpiGhostServeTo[static_cast<size_t>(r)])
269 packGrainKinematics(data, *grains[static_cast<size_t>(g)],
270 kin_send[static_cast<size_t>(r)]);
271 }
272
273 std::vector<int> kin_sendcounts(static_cast<size_t>(size), 0);
274 std::vector<int> kin_recvcounts(static_cast<size_t>(size), 0);
275 for (int r = 0; r < size; ++r)
276 kin_sendcounts[static_cast<size_t>(r)] =
277 static_cast<int>(kin_send[static_cast<size_t>(r)].size());
278 MPI_Alltoall(kin_sendcounts.data(), 1, MPI_INT, kin_recvcounts.data(), 1,
279 MPI_INT, comm);
280
281 std::vector<int> kin_sdispls(static_cast<size_t>(size), 0);
282 std::vector<int> kin_rdispls(static_cast<size_t>(size), 0);
283 int kin_send_total = 0;
284 int kin_recv_total = 0;
285 for (int r = 0; r < size; ++r) {
286 kin_sdispls[static_cast<size_t>(r)] = kin_send_total;
287 kin_rdispls[static_cast<size_t>(r)] = kin_recv_total;
288 kin_send_total += kin_sendcounts[static_cast<size_t>(r)];
289 kin_recv_total += kin_recvcounts[static_cast<size_t>(r)];
290 }
291
292 std::vector<double> kin_sendbuf(static_cast<size_t>(kin_send_total));
293 for (int r = 0; r < size; ++r) {
294 const auto &v = kin_send[static_cast<size_t>(r)];
295 std::copy(v.begin(), v.end(),
296 kin_sendbuf.begin() + kin_sdispls[static_cast<size_t>(r)]);
297 }
298 std::vector<double> kin_recvbuf(static_cast<size_t>(kin_recv_total));
299 MPI_Alltoallv(kin_sendbuf.data(), kin_sendcounts.data(), kin_sdispls.data(),
300 MPI_DOUBLE, kin_recvbuf.data(), kin_recvcounts.data(),
301 kin_rdispls.data(), MPI_DOUBLE, comm);
302
303 for (int r = 0; r < size; ++r) {
304 size_t off = static_cast<size_t>(kin_rdispls[static_cast<size_t>(r)]);
305 for (int g : data.d_mpiGhostNeedFrom[static_cast<size_t>(r)])
306 off = unpackGrainKinematics(data, *grains[static_cast<size_t>(g)],
307 kin_recvbuf, off);
308 }
309}
310
311} // namespace
312
314 const int size = util::parallel::mpiSize();
315 const int rank = util::parallel::mpiRank();
316 const auto &grains = data.d_particlesListTypeParticle;
317 const std::string strategy = resolvedMpiStrategy(data);
318
319 for (auto *p : data.d_particlesListTypeAll) {
320 if (p->isWall())
321 p->d_mpiOwner = -1;
322 }
323
324 // Particle-MPI only: spatial brick over grain centers.
325 // DOF-MPI assigns nodes in pd::setupDofPartition; do not also brick grains.
326 const bool use_particle_partition =
327 strategy == "particle" && size > 1 && !grains.empty();
328
329 if (!use_particle_partition) {
330 for (auto *p : grains)
331 p->d_mpiOwner = 0;
332 if (util::parallel::isMpiEnabled() && rank == 0)
333 util::io::print(std::format(
334 "MPI strategy={}: grain ownership inactive (all grains → rank 0)\n",
335 strategy));
336 } else {
337 double xmin = std::numeric_limits<double>::max();
338 double xmax = -std::numeric_limits<double>::max();
339 double ymin = std::numeric_limits<double>::max();
340 double ymax = -std::numeric_limits<double>::max();
341 for (auto *p : grains) {
342 const auto &c = p->getXCenter();
343 xmin = std::min(xmin, c.d_x);
344 xmax = std::max(xmax, c.d_x);
345 ymin = std::min(ymin, c.d_y);
346 ymax = std::max(ymax, c.d_y);
347 }
348 const double Lx = std::max(xmax - xmin, 1.0e-16);
349 const double Ly = std::max(ymax - ymin, 1.0e-16);
350 int nx = 1, ny = size;
351 factor2d(size, Lx, Ly, nx, ny);
352
353 const double eps = 1.0e-14 * std::max(Lx, Ly);
354 for (auto *p : grains) {
355 const auto &c = p->getXCenter();
356 int ix = static_cast<int>((c.d_x - xmin) / Lx * nx);
357 int iy = static_cast<int>((c.d_y - ymin) / Ly * ny);
358 if (ix < 0)
359 ix = 0;
360 if (iy < 0)
361 iy = 0;
362 if (ix >= nx)
363 ix = nx - 1;
364 if (iy >= ny)
365 iy = ny - 1;
366 // nudge points on the max edge into the last cell
367 (void)eps;
368 p->d_mpiOwner = iy * nx + ix;
369 }
370
371 if (util::parallel::isMpiEnabled() && rank == 0)
372 util::io::print(std::format(
373 "MPI spatial owners: {} ranks as {}x{} brick over [{:.3g},{:.3g}] x "
374 "[{:.3g},{:.3g}]\n",
375 size, nx, ny, xmin, xmax, ymin, ymax));
376 }
377
378 data.d_mpiIncludeInContactCloud.assign(data.d_particlesListTypeAll.size(), 1);
379 data.d_mpiGhostPlanValid = false;
380 data.d_mpiGhostStepsSinceRebuild = 0;
381 data.d_mpiGhostNeedFrom.clear();
382 data.d_mpiGhostServeTo.clear();
383
385 size_t n_local = 0;
386 for (auto *p : grains)
387 if (isLocallyOwned(*p))
388 ++n_local;
389 util::io::print(std::format(
390 "MPI particle owners: rank {}/{} owns {}/{} grains (walls replicated)\n",
391 rank, size, n_local, grains.size()));
392 }
393}
394
397 data.d_mpiIncludeInContactCloud.assign(data.d_particlesListTypeAll.size(),
398 1);
399 return;
400 }
401
402 // Particle-MPI only. DOF-MPI owns nodes, not whole grains; grain packing
403 // from the "grain owner" rank would overwrite remote-owned nodal u/v with
404 // stale zeros. Nodal sync lives in pd::exchangeGhostDisplacement.
405 if (data.d_pdDofMpi || resolvedMpiStrategy(data) == "dof") {
406 data.d_mpiIncludeInContactCloud.assign(data.d_particlesListTypeAll.size(),
407 1);
408 return;
409 }
410
411 using clock = std::chrono::steady_clock;
412 const auto t0 = clock::now();
413
414 const size_t interval = ghostRebuildInterval(data);
415 int local_need =
416 (!data.d_mpiGhostPlanValid ||
417 data.d_mpiGhostStepsSinceRebuild >= interval)
418 ? 1
419 : 0;
420 // All ranks must take the same branch: rebuild uses Allgather, exchange uses
421 // Alltoall — disagreeing on rebuild deadlocks (seen past ~50% with contact).
422 int global_need = local_need;
423 MPI_Allreduce(MPI_IN_PLACE, &global_need, 1, MPI_INT, MPI_MAX,
425 const bool need_rebuild = global_need != 0;
426
427 auto t_rebuild0 = t0;
428 auto t_rebuild1 = t0;
429 if (need_rebuild) {
430 t_rebuild0 = clock::now();
432 t_rebuild1 = clock::now();
433 }
434
435 const auto t_ex0 = clock::now();
437 const auto t1 = clock::now();
438
439 ++data.d_mpiGhostStepsSinceRebuild;
440
441 data.appendKeyData("mpi_ghost_select_time",
442 util::methods::timeDiff(t_rebuild0, t_rebuild1));
443 data.appendKeyData("mpi_halo_exchange_time",
444 util::methods::timeDiff(t_ex0, t1));
445 data.appendKeyData("mpi_exchange_time", util::methods::timeDiff(t0, t1));
446 data.setKeyData("mpi_ghost_rebuild", need_rebuild ? 1.0 : 0.0);
447}
A class to store model data.
Definition modelData.h:50
A class to store particle geometry, nodal discretization, and methods.
size_t getNumNodes() const
Get the number of nodes.
size_t getNodeId(size_t i_loc) const
Get global id of node given the local id of node in this object.
int d_mpiOwner
MPI owner rank for this grain (-1 = wall / all ranks). Used by particle-parallel (T09); ignored when ...
bool isWall() const
Is this particle a wall?
void factor2d(int nproc, double Lx, double Ly, int &nx, int &ny)
void packGrainKinematics(const data::ModelData &data, const particle::BaseParticle &p, std::vector< double > &buf)
void rebuildGhostPlan(data::ModelData &data)
size_t ghostRebuildInterval(const data::ModelData &data)
size_t unpackGrainKinematics(data::ModelData &data, particle::BaseParticle &p, const std::vector< double > &buf, size_t offset)
void exchangeCachedKinematics(data::ModelData &data)
Definition contact.h:20
std::string resolvedMpiStrategy(const data::ModelData &data)
Resolved MPI strategy: none|particle|dof (auto expanded).
void assignMpiOwners(data::ModelData &data)
Assign grain owners by spatial 2D brick decomposition of centers. Walls get owner -1 (all ranks)....
void exchangeGhostKinematics(data::ModelData &data)
Distance-limited ghosts + kinematics exchange. Rebuilds the ghost plan on a Verlet-skin cadence (tied...
bool isLocallyOwned(const BaseParticle &p)
True if this rank updates / assembles forces for the particle. Walls are replicated on every rank....
void print(const T &msg, int nt=print_default_tab, int printMpiRank=print_default_mpi_rank)
Prints formatted information.
Definition io.h:128
float timeDiff(std::chrono::steady_clock::time_point begin, std::chrono::steady_clock::time_point end, std::string unit="microseconds")
Returns difference between two times.
Definition vecMethods.h:304
bool isMpiEnabled()
Function to check if MPI is enabled.
int mpiSize()
Get size (number) of processors.
int mpiRank()
get rank (id) of this processor
MPI_Comm mpiComm()
Get MPI comm.
A structure to represent 3d vectors.
Definition point.h:30