pnm 1.0.3
peclet.pnm — pore-network extraction from SDF geometry (Kokkos)
Loading...
Searching...
No Matches
pore_extraction_mpi.hpp
Go to the documentation of this file.
1
39#ifndef PECLET_PNM_PORE_EXTRACTION_MPI_HPP
40#define PECLET_PNM_PORE_EXTRACTION_MPI_HPP
41
42#include <mpi.h>
43
44#include <algorithm>
45#include <array>
46#include <cstdint>
47#include <Kokkos_Core.hpp>
48#include <Kokkos_UnorderedMap.hpp>
49#include <map>
50#include <stdexcept>
51#include <string>
52#include <type_traits>
53#include <utility>
54#include <vector>
55
56#include "peclet/core/common/types.hpp"
57#include "peclet/core/common/view.hpp"
58#include "peclet/core/decomp/block_decomposer.hpp"
59#include "peclet/core/halo/grid_halo.hpp"
60#include "peclet/core/halo/grid_halo_topology.hpp"
61#include "pore_extraction.hpp"
62
63namespace pnm {
64
65static_assert(std::is_same_v<Index, peclet::core::Index>,
66 "pnm::Index must be core's Index (the halo exchanges carry it)");
67using peclet::core::IVec;
68
69namespace detail_mpi {
70
71inline int allreduceMaxInt(int v, MPI_Comm comm) {
72 int g = 0;
73 MPI_Allreduce(&v, &g, 1, MPI_INT, MPI_MAX, comm);
74 return g;
75}
76
78template <class T>
79inline std::vector<T> allgatherv(const std::vector<T>& local, MPI_Comm comm) {
80 int size = 1;
81 MPI_Comm_size(comm, &size);
82 const int nloc = static_cast<int>(local.size() * sizeof(T));
83 std::vector<int> counts(size, 0), displs(size + 1, 0);
84 MPI_Allgather(&nloc, 1, MPI_INT, counts.data(), 1, MPI_INT, comm);
85 for (int r = 0; r < size; ++r)
86 displs[r + 1] = displs[r] + counts[r];
87 std::vector<T> all(displs[size] / sizeof(T));
88 MPI_Allgatherv(local.data(), nloc, MPI_BYTE, all.data(), counts.data(), displs.data(), MPI_BYTE,
89 comm);
90 return all;
91}
92
94template <class T>
95inline void sortUnique(std::vector<T>& v) {
96 std::sort(v.begin(), v.end());
97 v.erase(std::unique(v.begin(), v.end()), v.end());
98}
99
101template <class Topo>
102inline BlockGeo blockGeoOf(const Topo& topo, std::array<int, 3> gdims, int G) {
103 const auto& idxr = topo.indexer();
104 BlockGeo geo{};
105 geo.ex = (int)idxr.sizeInclGhost()[0];
106 geo.ey = (int)idxr.sizeInclGhost()[1];
107 geo.ez = (int)idxr.sizeInclGhost()[2];
108 geo.ox = (int)idxr.originInclGhost()[0];
109 geo.oy = (int)idxr.originInclGhost()[1];
110 geo.oz = (int)idxr.originInclGhost()[2];
111 geo.nx = (int)idxr.sizeInner()[0];
112 geo.ny = (int)idxr.sizeInner()[1];
113 geo.nz = (int)idxr.sizeInner()[2];
114 geo.gnx = gdims[0];
115 geo.gny = gdims[1];
116 geo.gnz = gdims[2];
117 geo.g = G;
118 return geo;
119}
120
123template <class T>
124inline peclet::core::View<T> stageInner(const Exec& space, const BlockGeo& geo,
125 const std::vector<T>& h, const char* name) {
126 const BlockGeo g = geo;
127 peclet::core::View<T> e(name, g.numExt());
128 Kokkos::View<T*, Mem> i0("pnm::mpi::stage_in", g.numOwned());
129 uploadVec(h, i0);
130 Kokkos::parallel_for(
131 "pnm::mpi::stage", kernels::ownedRange(space, g), KOKKOS_LAMBDA(int ix, int iy, int iz) {
132 e(g.cell(ix + g.g, iy + g.g, iz + g.g)) = i0(g.owned(ix, iy, iz));
133 });
134 space.fence();
135 return e;
136}
137
139template <class T>
140inline std::vector<T> packInner(const Exec& space, const BlockGeo& geo,
141 const peclet::core::View<T>& e) {
142 const BlockGeo g = geo;
143 Kokkos::View<T*, Mem> i0("pnm::mpi::pack", g.numOwned());
144 Kokkos::parallel_for(
145 "pnm::mpi::seg_pack", kernels::ownedRange(space, g), KOKKOS_LAMBDA(int ix, int iy, int iz) {
146 i0(g.owned(ix, iy, iz)) = e(g.cell(ix + g.g, iy + g.g, iz + g.g));
147 });
148 space.fence();
149 return downloadN(i0, g.numOwned());
150}
151
153template <class PairView>
154inline std::vector<std::pair<Index, Index>> localPairs(const PairView& d, std::size_t count) {
155 std::vector<Index> flat = downloadN(d, 2 * count);
156 std::vector<std::pair<Index, Index>> loc(count);
157 for (std::size_t i = 0; i < count; ++i)
158 loc[i] = {flat[2 * i], flat[2 * i + 1]};
159 sortUnique(loc);
160 return loc;
161}
162
169 peclet::core::View<Index> keys, roots;
171};
172inline MergeRemap globalMerge(const std::vector<std::pair<Index, Index>>& loc, MPI_Comm comm) {
173 auto all = allgatherv(loc, comm);
174 std::map<Index, Index> par;
175 auto find = [&par](Index x) {
176 while (true) {
177 auto it = par.find(x);
178 if (it == par.end() || it->second == x)
179 return x;
180 x = it->second;
181 }
182 };
183 for (const auto& pr : all) {
184 const Index ra = find(pr.first), rb = find(pr.second);
185 if (ra == rb)
186 continue;
187 const Index lo = ra < rb ? ra : rb, hi = ra < rb ? rb : ra;
188 par[hi] = lo;
189 par.emplace(lo, lo);
190 }
191 std::vector<Index> rkeys, rvals;
192 for (const auto& kv : par) {
193 const Index root = find(kv.first);
194 if (root != kv.first) {
195 rkeys.push_back(kv.first); // std::map order: ascending keys, as bsearchKey needs
196 rvals.push_back(root);
197 }
198 }
199 MergeRemap m;
200 m.nk = Index(rkeys.size());
201 m.keys = peclet::core::View<Index>("pnm::mpi::rk", rkeys.size());
202 m.roots = peclet::core::View<Index>("pnm::mpi::rv", rvals.size());
203 uploadVec(rkeys, m.keys);
204 uploadVec(rvals, m.roots);
205 return m;
206}
207
209template <class FieldView, class PredFn>
210inline void applyRemap(const Exec& space, const MergeRemap& m, const FieldView& field,
211 std::size_t n, const PredFn& pred) {
212 if (m.nk == 0)
213 return;
214 const auto dk = m.keys;
215 const auto dv = m.roots;
216 const Index nk = m.nk;
217 Kokkos::parallel_for(
218 "pnm::mpi::apply_merge", kernels::R1(space, 0, n), KOKKOS_LAMBDA(std::size_t i) {
219 if (!pred(i))
220 return;
221 const Index k = kernels::bsearchKey(dk, nk, field(i));
222 if (k >= 0)
223 field(i) = dv(k);
224 });
225 space.fence();
226}
227
232template <class Halo, class TargetView>
233inline void resolveToFixpoint(const Exec& space, const BlockGeo& geo, Halo& halo,
234 const TargetView& target, MPI_Comm comm, const char* what) {
235 int rounds = 0;
236 for (;;) {
237 halo.exchange(target);
238 const int pending = kernels::resolveHoldAtGhost(space, geo, target);
239 int globalPending = 0;
240 MPI_Allreduce(&pending, &globalPending, 1, MPI_INT, MPI_SUM, comm);
241 if (!globalPending)
242 break;
243 if (++rounds > 100000) {
244 // collective: the round count and the Allreduce'd stop are identical on every rank
245 throw std::runtime_error(std::string("[pnm::mpi] ") + what + " did not converge");
246 }
247 }
248}
249
253template <class KeyValFn>
254inline std::vector<std::pair<Index, Index>> minByKey(std::size_t n, const KeyValFn& kv) {
255 using Map = Kokkos::UnorderedMap<Index, Index, Exec>;
256 std::size_t cap = 4096;
257 for (;;) {
258 Map map(cap);
259 int failed = 0;
260 Kokkos::parallel_reduce(
261 "pnm::mpi::min_by_key", Kokkos::RangePolicy<Exec>(0, n),
262 KOKKOS_LAMBDA(std::size_t i, int& f) {
263 Index key, val;
264 if (!kv(i, key, val))
265 return;
266 auto r = map.insert(key, val);
267 if (r.failed()) {
268 f += 1;
269 return;
270 }
271 Kokkos::atomic_min(&map.value_at(r.index()), val);
272 },
273 failed);
274 Kokkos::fence();
275 if (failed == 0) {
276 // Harvest valid entries.
277 const std::size_t mcap = map.capacity();
278 Kokkos::View<Index*, Mem> keys("pnm::mpi::keys", map.size()),
279 vals("pnm::mpi::vals", map.size());
280 Kokkos::View<int, Mem> slot("pnm::mpi::slot");
281 Kokkos::deep_copy(slot, 0);
282 Kokkos::parallel_for(
283 "pnm::mpi::harvest", Kokkos::RangePolicy<Exec>(0, mcap), KOKKOS_LAMBDA(std::size_t k) {
284 if (map.valid_at(k)) {
285 const int s = Kokkos::atomic_fetch_add(&slot(), 1);
286 keys(s) = map.key_at(k);
287 vals(s) = map.value_at(k);
288 }
289 });
290 Kokkos::fence();
291 auto hk = downloadN(keys, map.size());
292 auto hv = downloadN(vals, map.size());
293 std::vector<std::pair<Index, Index>> out(hk.size());
294 for (std::size_t i = 0; i < hk.size(); ++i)
295 out[i] = {hk[i], hv[i]};
296 return out;
297 }
298 cap *= 4;
299 }
300}
301
302} // namespace detail_mpi
303
306struct PoreRec {
307 int id;
308 int pgx, pgy, pgz; // peak voxel global coords: the deterministic min-image anchor
310 double press;
311};
312
317 std::vector<Pore> pores;
318 std::vector<int> seg;
319 std::vector<std::pair<int, int>> connections;
320 std::array<int, 3> block_origin{0, 0, 0}; // xyz, this rank's inner block
321 std::array<int, 3> block_size{0, 0, 0};
322};
323
327inline void mpi_block_of(std::array<int, 3> gdims, MPI_Comm comm, std::array<int, 3>& offset,
328 std::array<int, 3>& size) {
329 int rank = 0, nranks = 1;
330 MPI_Comm_rank(comm, &rank);
331 MPI_Comm_size(comm, &nranks);
332 peclet::core::decomp::BlockDecomposer<3> dec(static_cast<std::size_t>(nranks),
333 IVec<3>{gdims[0], gdims[1], gdims[2]});
334 for (int a = 0; a < 3; ++a) {
335 offset[a] = static_cast<int>(dec.origins()[rank][a]);
336 size[a] = static_cast<int>(dec.sizes()[rank][a]);
337 }
338}
339
343inline MpiPoreNetwork extract_pore_network_mpi(const std::vector<float>& sdf_local,
344 std::array<int, 3> gdims,
345 std::array<float, 3> origin,
346 std::array<float, 3> spacing, MPI_Comm comm) {
347 namespace dm = detail_mpi;
348 namespace kn = kernels;
349 using peclet::core::View;
350 using peclet::core::halo::GridHalo;
351 using peclet::core::halo::GridHaloTopology;
352
353 int rank = 0, nranks = 1;
354 MPI_Comm_rank(comm, &rank);
355 MPI_Comm_size(comm, &nranks);
356
357 // ---- decomposition + g=1 halo (full 26-neighbour ghost ring, periodic all axes) ----
358 peclet::core::decomp::BlockDecomposer<3> dec(static_cast<std::size_t>(nranks),
359 IVec<3>{gdims[0], gdims[1], gdims[2]});
360 constexpr int G = 1;
361 GridHaloTopology<3> topo;
362 topo.buildTopology(dec, rank, G, {true, true, true}, comm);
363 GridHalo<float> haloF;
364 haloF.init(topo);
365 GridHalo<Index> haloI;
366 haloI.init(topo);
367 GridHalo<int> haloS;
368 haloS.init(topo);
369
370 const BlockGeo geo = dm::blockGeoOf(topo, gdims, G);
371 const BlockGeo g = geo;
372 MpiPoreNetwork out;
373 out.block_origin = {geo.ox + G, geo.oy + G, geo.oz + G};
374 out.block_size = {geo.nx, geo.ny, geo.nz};
375 const std::size_t nInner = geo.numOwned();
376 const std::size_t nExt = geo.numExt();
377 // A caller-side size error is rank-local, but everything below is collective: agree on it
378 // first so every rank throws together instead of the good ranks hanging in the next exchange.
379 {
380 int bad = sdf_local.size() != nInner ? 1 : 0, anyBad = 0;
381 MPI_Allreduce(&bad, &anyBad, 1, MPI_INT, MPI_MAX, comm);
382 if (anyBad)
383 throw std::runtime_error(bad ? "[pnm::mpi] rank " + std::to_string(rank) +
384 ": sdf_local size " + std::to_string(sdf_local.size()) +
385 " != block " + std::to_string(geo.nx) + "x" +
386 std::to_string(geo.ny) + "x" + std::to_string(geo.nz)
387 : "[pnm::mpi] rank " + std::to_string(rank) +
388 ": sdf_local size mismatch on another rank");
389 }
390
391 Exec space;
392 const auto inner = kn::ownedRange(space, geo);
393 const auto ext = kn::MD3(space, {0, 0, 0}, {geo.ex, geo.ey, geo.ez});
394
395 // ---- SDF onto the extended block + one exchange ----
396 View<float> sdfE = dm::stageInner(space, geo, sdf_local, "pnm::mpi::sdfE");
397 haloF.exchange(sdfE);
398
399 // ---- pore detection (owned peaks under the global (sdf, gid) tie-break) ----
400 {
401 const int max_pores = 1000000;
402 View<Pore> pores("pnm::mpi::pores", max_pores);
403 Kokkos::View<int, Mem> counter("pnm::mpi::pore_count");
404 Kokkos::deep_copy(counter, 0);
405 kn::detectPores(space, geo, sdfE, origin, spacing, pores, counter, max_pores);
406 out.pores = downloadN(pores, std::min<std::size_t>(readScalar(counter), max_pores));
407 }
408
409 // ---- markers + LOCAL union-find CCL over the owned box (single-rank kernels, owned indices) --
410 const float min_sp = std::min(spacing[0], std::min(spacing[1], spacing[2]));
411 const float thr = -1.5f * min_sp;
412
413 View<Index> labelE("pnm::mpi::labelE", nExt); // gid labels, -1 = unlabelled
414 {
415 Kokkos::View<int*, Mem> parent("pnm::mpi::parent", nInner);
416 Kokkos::View<int, Mem> changed("pnm::mpi::changed");
417 kn::initMarkers(space, geo, sdfE, thr, parent);
418 kn::cclFixpoint(space, parent, nInner, changed,
419 [&]() { kn::cclMergeMarkers(space, geo, parent, changed); });
420 // Per-component min gid -> the component label (what the single-rank atomic_min CCL yields
421 // when parents are voxel ids), scattered onto the extended label field.
422 View<Index> mg("pnm::mpi::mg", nInner);
423 Kokkos::deep_copy(mg, Index(0x7fffffffffffffffLL));
424 Kokkos::parallel_for(
425 "pnm::mpi::ccl_mingid", inner, KOKKOS_LAMBDA(int ix, int iy, int iz) {
426 const Index o = g.owned(ix, iy, iz);
427 if (parent(o) != -1)
428 Kokkos::atomic_min(&mg(parent(o)), g.gid(ix + 1, iy + 1, iz + 1));
429 });
430 space.fence();
431 Kokkos::parallel_for(
432 "pnm::mpi::ccl_scatter", ext, KOKKOS_LAMBDA(int lx, int ly, int lz) {
433 const Index e = g.cell(lx, ly, lz);
434 const int ix = lx - 1, iy = ly - 1, iz = lz - 1;
435 if (g.inOwned(ix, iy, iz)) {
436 const int p = parent(g.owned(ix, iy, iz));
437 labelE(e) = (p == -1) ? Index(-1) : mg(p);
438 } else {
439 labelE(e) = -1;
440 }
441 });
442 space.fence();
443 }
444 haloI.exchange(labelE);
445
446 // ---- global merge: boundary equivalence graph, allgathered, union-found on the host ----
447 {
448 const Index maxPairs = Index(nInner) * 4 + 1024;
449 View<Index> bpairs("pnm::mpi::bpairs", std::size_t(2 * maxPairs));
450 Kokkos::View<int, Mem> bcnt("pnm::mpi::bcnt");
451 Kokkos::deep_copy(bcnt, 0);
452 Kokkos::parallel_for(
453 "pnm::mpi::boundary_pairs", inner, KOKKOS_LAMBDA(int ix, int iy, int iz) {
454 // Only owned cells within one cell of a block face can see a ghost.
455 if (!g.onSurface(ix, iy, iz))
456 return;
457 const int lx = ix + 1, ly = iy + 1, lz = iz + 1;
458 const Index my = labelE(g.cell(lx, ly, lz));
459 if (my == -1)
460 return;
461 for (int dz = -1; dz <= 1; ++dz)
462 for (int dy = -1; dy <= 1; ++dy)
463 for (int dx = -1; dx <= 1; ++dx) {
464 if (dx == 0 && dy == 0 && dz == 0)
465 continue;
466 if (g.inOwned(ix + dx, iy + dy, iz + dz))
467 continue; // owned-owned adjacency was the local CCL's job
468 const Index nl = labelE(g.cell(lx + dx, ly + dy, lz + dz));
469 if (nl == -1 || nl == my)
470 continue;
471 const int s = Kokkos::atomic_fetch_add(&bcnt(), 1);
472 if (s < maxPairs) {
473 bpairs(2 * s) = my < nl ? my : nl;
474 bpairs(2 * s + 1) = my < nl ? nl : my;
475 }
476 }
477 });
478 space.fence();
479 const auto loc =
480 dm::localPairs(bpairs, std::size_t(std::min<Index>(readScalar(bcnt), maxPairs)));
481 const dm::MergeRemap merge = dm::globalMerge(loc, comm);
482 dm::applyRemap(
483 space, merge, labelE, nExt, KOKKOS_LAMBDA(std::size_t e) { return labelE(e) != -1; });
484 }
485 haloI.exchange(labelE);
486
487 // ---- flood fill of the shallow solid: Jacobi sweeps, exchange + Allreduce per sweep ----
488 {
489 View<Index> labelN("pnm::mpi::labelN", nExt);
490 Kokkos::View<int, Mem> changed("pnm::mpi::fchanged");
491 kn::floodFixpoint(space, geo, sdfE, labelE, labelN, changed, [&](int c) {
492 const int gc = dm::allreduceMaxInt(c, comm);
493 if (gc)
494 haloI.exchange(labelE);
495 return gc;
496 });
497 }
498
499 // ---- gradient-path pore roots: steepest-neighbour forest + cross-rank root resolution ----
500 // The chase state per cell is either FINALIZED (a negative value ~root once the tree root is
501 // known) or a HOLD position: the gid of a cell inside THIS extended block whose own state we are
502 // waiting on. A cell never stores a gid it cannot reach — the naive "adopt whatever the ghost
503 // points at" strands the chase on a remote mid-chain gid outside the ghost ring (measured: basin
504 // fragmentation on plateau-heavy fields). Roots finalize at init; each round the exchange
505 // imports the neighbours' newly finalized boundary values, so every pending chain finalizes one
506 // cross-block hop per round — guaranteed progress, loop until no cell is pending.
507 View<Index> targetE("pnm::mpi::targetE", nExt);
508 {
509 Kokkos::deep_copy(targetE, Index(-1));
510 kn::forestInit(
511 space, geo, sdfE, targetE, KOKKOS_LAMBDA(Index ci) { return sdfE(ci) > 0.0f; },
512 KOKKOS_LAMBDA(Index, Index cgid) { return ~cgid; }, // solids: self-root (unused)
513 KOKKOS_LAMBDA(Index, Index cgid) { return ~cgid; }); // peak: finalized self-root
514 dm::resolveToFixpoint(space, geo, haloI, targetE, comm, "gradient-root resolution");
515 }
516
517 // ---- global renumbering (single-rank first-encounter order == ascending min-appearance gid) --
518 View<int> segE("pnm::mpi::segE", nExt);
519 {
520 // Pore roots: per-rank (root -> min appearance gid), then a global min-reduce.
521 auto locPores = dm::minByKey(
522 nInner, KOKKOS_LAMBDA(std::size_t o, Index & k, Index & v) {
523 int ix, iy, iz;
524 g.ownedCoords(Index(o), ix, iy, iz);
525 const Index e = g.cell(ix + 1, iy + 1, iz + 1);
526 if (sdfE(e) <= 0.0f)
527 return false;
528 k = ~targetE(e); // finalized state stores ~root
529 v = g.gid(ix + 1, iy + 1, iz + 1);
530 return true;
531 });
532 // Solid labels: min appearance gid over ALL labelled voxels (a flood-filled shallow voxel can
533 // precede the component's min marker in gid order, so the label value alone is NOT the first
534 // appearance).
535 auto locSolids = dm::minByKey(
536 nInner, KOKKOS_LAMBDA(std::size_t o, Index & k, Index & v) {
537 int ix, iy, iz;
538 g.ownedCoords(Index(o), ix, iy, iz);
539 const Index e = g.cell(ix + 1, iy + 1, iz + 1);
540 if (sdfE(e) > 0.0f || labelE(e) == -1)
541 return false;
542 k = labelE(e);
543 v = g.gid(ix + 1, iy + 1, iz + 1);
544 return true;
545 });
546 auto allPores = dm::allgatherv(locPores, comm);
547 auto allSolids = dm::allgatherv(locSolids, comm);
548 // min-reduce per key, then order by min appearance (== single-rank first-encounter order).
549 auto reduceSort = [](std::vector<std::pair<Index, Index>>& v) {
550 std::map<Index, Index> m;
551 for (const auto& kv : v) {
552 auto it = m.find(kv.first);
553 if (it == m.end())
554 m.emplace(kv.first, kv.second);
555 else if (kv.second < it->second)
556 it->second = kv.second;
557 }
558 std::vector<std::pair<Index, Index>> order; // (minApp, key)
559 order.reserve(m.size());
560 for (const auto& kv : m)
561 order.push_back({kv.second, kv.first});
562 std::sort(order.begin(), order.end());
563 return order;
564 };
565 auto poreOrder = reduceSort(allPores);
566 auto solidOrder = reduceSort(allSolids);
567 // key -> dense id maps (pores 1,2,...; solids -1,-2,...), sorted by key for device bsearch.
568 std::vector<std::pair<Index, int>> pmap(poreOrder.size()), smap(solidOrder.size());
569 for (std::size_t i = 0; i < poreOrder.size(); ++i)
570 pmap[i] = {poreOrder[i].second, int(i) + 1};
571 for (std::size_t i = 0; i < solidOrder.size(); ++i)
572 smap[i] = {solidOrder[i].second, -(int(i) + 1)};
573 std::sort(pmap.begin(), pmap.end());
574 std::sort(smap.begin(), smap.end());
575 std::vector<Index> pk(pmap.size()), sk(smap.size());
576 std::vector<int> pv(pmap.size()), sv(smap.size());
577 for (std::size_t i = 0; i < pmap.size(); ++i) {
578 pk[i] = pmap[i].first;
579 pv[i] = pmap[i].second;
580 }
581 for (std::size_t i = 0; i < smap.size(); ++i) {
582 sk[i] = smap[i].first;
583 sv[i] = smap[i].second;
584 }
585 View<Index> dpk("pnm::mpi::dpk", pk.size()), dsk("pnm::mpi::dsk", sk.size());
586 Kokkos::View<int*, Mem> dpv("pnm::mpi::dpv", pv.size()), dsv("pnm::mpi::dsv", sv.size());
587 uploadVec(pk, dpk);
588 uploadVec(sk, dsk);
589 uploadVec(pv, dpv);
590 uploadVec(sv, dsv);
591 const Index npk = Index(pk.size()), nsk = Index(sk.size());
592 Kokkos::deep_copy(segE, 0);
593 Kokkos::parallel_for(
594 "pnm::mpi::seg_assign", inner, KOKKOS_LAMBDA(int ix, int iy, int iz) {
595 const Index e = g.cell(ix + 1, iy + 1, iz + 1);
596 int id = 0;
597 if (sdfE(e) > 0.0f) {
598 const Index k = kn::bsearchKey(dpk, npk, ~targetE(e));
599 id = (k >= 0) ? dpv(k) : 0;
600 } else if (labelE(e) != -1) {
601 const Index k = kn::bsearchKey(dsk, nsk, labelE(e));
602 id = (k >= 0) ? dsv(k) : 0;
603 }
604 segE(e) = id;
605 });
606 space.fence();
607 }
608 haloS.exchange(segE);
609
610 // ---- topology: local (+x/+y/+z) pairs against exchanged seg ghosts, global sort/unique ----
611 {
612 const Index maxPairs = Index(nInner) * 3;
613 Kokkos::View<int*, Mem> pairs("pnm::mpi::pairs", std::size_t(2 * maxPairs));
614 Kokkos::View<int, Mem> cnt("pnm::mpi::tcnt");
615 Kokkos::deep_copy(cnt, 0);
616 kn::boundaryPairs(space, geo, segE, pairs, cnt, maxPairs);
617 auto loc = kn::uniquePairs(pairs, std::size_t(std::min<Index>(readScalar(cnt), maxPairs)));
618 auto all = dm::allgatherv(loc, comm);
619 dm::sortUnique(all);
620 out.connections = std::move(all);
621 }
622
623 // ---- download this rank's inner seg block ----
624 out.seg = dm::packInner(space, geo, segE);
625 return out;
626}
627
628// ---- distributed network flow: throat flow rates + pore pressures from a MAC field ------------
629// The distributed counterpart of extract_network_flow_k (see pore_extraction.hpp for the method).
630// Composes with the validated distributed segmentation: runs extract_pore_network_mpi for the
631// labels, then re-stages the fields on a g=1 extended block and resolves the FLOW-BASIN label of
632// every cell by propagating the label itself with the same hold-at-ghost finalization scheme as
633// the pore roots (finalized = -label stored, pending = an in-block gid): no remote root-to-label
634// lookup ever needed. Fluxes/areas/centroids/residuals accumulate rank-locally over owned +faces
635// and are Allreduce-summed; the pore list (with trilinearly interpolated pressures, needing a
636// g=2 halo of p around each owned peak) is allgathered. EVERY rank returns the identical global
637// network. Bit-exact to the single-rank extract_network_flow_k up to float-sum ordering
638// (atomics) and the CUDA FMA-contraction wobble on pore centroids.
640 const std::vector<float>& sdf_local, std::array<int, 3> gdims, std::array<float, 3> origin,
641 std::array<float, 3> spacing, const std::vector<double>& u_h, const std::vector<double>& v_h,
642 const std::vector<double>& w_h, const std::vector<double>& p_h, const std::vector<double>& ox_h,
643 const std::vector<double>& oy_h, const std::vector<double>& oz_h, std::array<double, 3> grad_p,
644 MPI_Comm comm) {
645 namespace dm = detail_mpi;
646 namespace kn = kernels;
647 using peclet::core::View;
648 using peclet::core::halo::GridHalo;
649 using peclet::core::halo::GridHaloTopology;
650 NetworkFlow out;
651
652 // 1. distributed segmentation (validated pipeline) -> this rank's dense labels
653 MpiPoreNetwork base = extract_pore_network_mpi(sdf_local, gdims, origin, spacing, comm);
654
655 int rank = 0, nranks = 1;
656 MPI_Comm_rank(comm, &rank);
657 MPI_Comm_size(comm, &nranks);
658 peclet::core::decomp::BlockDecomposer<3> dec(static_cast<std::size_t>(nranks),
659 IVec<3>{gdims[0], gdims[1], gdims[2]});
660 constexpr int G = 1;
661 GridHaloTopology<3> topo;
662 topo.buildTopology(dec, rank, G, {true, true, true}, comm);
663 GridHalo<float> haloF;
664 haloF.init(topo);
665 GridHalo<Index> haloI;
666 haloI.init(topo);
667 GridHalo<int> haloS;
668 haloS.init(topo);
669 GridHalo<double> haloD;
670 haloD.init(topo);
671
672 const BlockGeo geo = dm::blockGeoOf(topo, gdims, G);
673 const BlockGeo g = geo;
674 const std::size_t nInner = geo.numOwned();
675 const std::size_t nExt = geo.numExt();
676 Exec space;
677 using R1 = kn::R1;
678 const auto inner = kn::ownedRange(space, geo);
679
680 // 2. stage sdf / seg / fields onto the extended block + exchange
681 View<float> sdfE = dm::stageInner(space, geo, sdf_local, "nf::mpi::f");
682 haloF.exchange(sdfE);
683 View<int> segE = dm::stageInner(space, geo, base.seg, "nf::mpi::s");
684 haloS.exchange(segE);
685 auto stageD = [&](const std::vector<double>& h, bool required) {
686 if (h.empty())
687 return View<double>("nf::mpi::d", required ? nExt : 0);
688 View<double> e = dm::stageInner(space, geo, h, "nf::mpi::d");
689 haloD.exchange(e);
690 return e;
691 };
692 View<double> uE = stageD(u_h, true), vE = stageD(v_h, true), wE = stageD(w_h, true);
693 const bool hasOpen = !ox_h.empty();
694 View<double> oxE = stageD(ox_h, false), oyE = stageD(oy_h, false), ozE = stageD(oz_h, false);
695
696 // 3. flow-basin labels for EVERY cell: propagate the LABEL along the steepest-ascent forest.
697 // State per cell (Index field): finalized = -(label+1) (label 0 = enclosed solid basin, no
698 // pore); pending = the in-block gid currently held. Pore cells finalize immediately with their
699 // own seg label; solid maxima finalize as 0; everything else chases, adopting only finalized
700 // ghost values (never storing an out-of-block gid).
701 View<Index> labWork("pnm::nfmpi::labWork", nExt);
702 {
703 Kokkos::deep_copy(labWork, Index(-1)); // ghosts: "finalized, label 0" until exchanged
704 kn::forestInit(
705 space, geo, sdfE, labWork, KOKKOS_LAMBDA(Index ci) { return sdfE(ci) <= 0.0f; },
706 KOKKOS_LAMBDA(Index ci, Index) { return -(Index(segE(ci)) + 1); }, // pore: own label
707 KOKKOS_LAMBDA(Index, Index) { return Index(-1); }); // solid max: label 0 (excluded)
708 dm::resolveToFixpoint(space, geo, haloI, labWork, comm, "flow-basin resolution");
709 }
710 View<int> flowLab("pnm::nfmpi::flowLab", nExt);
711 Kokkos::parallel_for(
712 "pnm::nfmpi::basin_label", R1(space, 0, nExt),
713 KOKKOS_LAMBDA(std::size_t e) { flowLab(e) = int(-labWork(e) - 1); });
714 space.fence();
715 haloS.exchange(flowLab);
716
717 // 4. global pore list: each owned basin peak emits (id, Pore, trilinear pressure). The 2x2x2
718 // interpolation cube around a peak can reach 2 cells out, so p gets its own g=2 halo.
719 int np = 0;
720 {
721 const int npLoc = kn::maxLabel(space, geo, segE);
722 MPI_Allreduce(&npLoc, &np, 1, MPI_INT, MPI_MAX, comm);
723 }
724 if (np == 0)
725 return out;
726
727 GridHaloTopology<3> topo2;
728 topo2.buildTopology(dec, rank, 2, {true, true, true}, comm);
729 GridHalo<double> haloP2;
730 haloP2.init(topo2);
731 const BlockGeo g2 = dm::blockGeoOf(topo2, gdims, 2);
732 View<double> pE2 = dm::stageInner(space, g2, p_h, "pnm::nfmpi::pE2");
733 haloP2.exchange(pE2);
734
735 std::vector<PoreRec> recs;
736 {
737 Kokkos::View<PoreRec*, Mem> buf("pnm::nfmpi::precs", nInner ? nInner : 1);
738 Kokkos::View<int, Mem> cnt("pnm::nfmpi::pcnt");
739 Kokkos::deep_copy(cnt, 0);
740 const float oxo = origin[0], oyo = origin[1], ozo = origin[2];
741 const float sx = spacing[0], sy = spacing[1], sz = spacing[2];
742 const BlockGeo gg = g2;
743 Kokkos::parallel_for(
744 "pnm::nfmpi::pores", inner, KOKKOS_LAMBDA(int ix, int iy, int iz) {
745 const int lx = ix + 1, ly = iy + 1, lz = iz + 1;
746 const Index ci = g.cell(lx, ly, lz);
747 const float cv = sdfE(ci);
748 if (cv <= 0.0f)
749 return;
750 if (!kn::isPeak(g, sdfE, lx, ly, lz, cv, g.gid(lx, ly, lz)))
751 return;
752 PoreRec rec;
753 rec.id = segE(ci);
754 g.gcoord(lx, ly, lz, rec.pgx, rec.pgy, rec.pgz);
755 rec.po = kn::poreAt(g, sdfE, lx, ly, lz, oxo, oyo, ozo, sx, sy, sz);
756 // trilinear p at the refined position: base cells are within peak +- 2 (g2 ring)
757 const double gp3[3] = {(rec.po.x - oxo) / sx, (rec.po.y - oyo) / sy,
758 (rec.po.z - ozo) / sz};
759 rec.press = kn::trilinear(gg, pE2, gp3);
760 const int s0 = Kokkos::atomic_fetch_add(&cnt(), 1);
761 buf(s0) = rec;
762 });
763 space.fence();
764 recs = downloadN(buf, std::size_t(readScalar(cnt)));
765 }
766 auto allRecs = dm::allgatherv(recs, comm);
767 out.pores.resize(np);
768 out.pore_pressure.assign(np, 0.0);
769 std::vector<double> ancX(np), ancY(np), ancZ(np); // peak-voxel min-image anchors
770 for (const auto& r0 : allRecs) {
771 out.pores[r0.id - 1] = r0.po;
772 out.pore_pressure[r0.id - 1] = r0.press;
773 ancX[r0.id - 1] = origin[0] + r0.pgx * double(spacing[0]);
774 ancY[r0.id - 1] = origin[1] + r0.pgy * double(spacing[1]);
775 ancZ[r0.id - 1] = origin[2] + r0.pgz * double(spacing[2]);
776 }
777 View<double> ancXD("pnm::nfmpi::ancX", np), ancYD("pnm::nfmpi::ancY", np),
778 ancZD("pnm::nfmpi::ancZ", np);
779 uploadVec(ancX, ancXD);
780 uploadVec(ancY, ancYD);
781 uploadVec(ancZ, ancZD);
782
783 // 5. per-patch throats: distributed CCL over the interface faces. Local union-find on owned
784 // faces (fid = 3*gid(cell)+d, same identity/adjacency as single-rank), per-component min fid,
785 // then ONE global merge of the cross-block face-adjacency graph — the marker-CCL recipe. The
786 // patch root (min global fid) is decomposition-independent, so the throat list matches the
787 // single-rank order exactly.
788 const std::size_t nfL = 3 * nInner;
789 Kokkos::View<Index*, Mem> faceLab("pnm::nfmpi::faceLab", nfL); // final: patch label (min fid)
790 kn::ThroatSlots slots;
791 constexpr Index kSent = kn::kSent; // film awaiting attachment
792 {
793 Kokkos::View<int*, Mem> parent("pnm::nfmpi::fparent", nfL);
794 Kokkos::View<Index*, Mem> fpairL("pnm::nfmpi::fpairL", nfL);
795 Kokkos::View<char*, Mem> fcoreL("pnm::nfmpi::fcoreL", nfL);
796 Kokkos::View<int, Mem> changed("pnm::nfmpi::fch");
797 Kokkos::View<Index*, Mem> mg("pnm::nfmpi::fmg", nfL); // component root -> min global fid
798 auto rootOf = KOKKOS_LAMBDA(int p) {
799 return mg(p);
800 };
801 // per-direction extended fields of the face labels / pair keys: exchanged so a boundary face
802 // can see its ghost-side neighbours (the marker CCL exchanges one cell field; a face field is
803 // three of them)
804 View<Index> flabE[3], fpairE[3];
805 for (int d = 0; d < 3; ++d) {
806 flabE[d] = View<Index>("pnm::nfmpi::flabE", nExt);
807 fpairE[d] = View<Index>("pnm::nfmpi::fpairE", nExt);
808 Kokkos::deep_copy(flabE[d], Index(-1));
809 Kokkos::deep_copy(fpairE[d], Index(-1));
810 }
811 const kn::SplitFaces<View<Index>> nbLab{flabE[0], flabE[1], flabE[2]};
812 const kn::SplitFaces<View<Index>> nbPair{fpairE[0], fpairE[1], fpairE[2]};
813 // scatter the owned face labels of direction d (all attached labels, or only the faces
814 // parent >= 0 — the tier currently being merged) and exchange
815 auto scatterLabels = [&](bool byParent) {
816 for (int d = 0; d < 3; ++d) {
817 auto fl = flabE[d];
818 const int dd0 = d;
819 Kokkos::parallel_for(
820 "pnm::nfmpi::face_scatter", inner, KOKKOS_LAMBDA(int ix, int iy, int iz) {
821 const Index f = 3 * g.owned(ix, iy, iz) + dd0;
822 const Index lbl = faceLab(f);
823 const bool take = byParent ? (parent(f) >= 0) : (lbl >= 0 && lbl < kSent);
824 fl(g.cell(ix + 1, iy + 1, iz + 1)) = take ? lbl : Index(-1);
825 });
826 space.fence();
827 haloI.exchange(flabE[d]);
828 }
829 };
830 // cross-block adjacency pairs (owned face with parent >= 0 vs ghost-cell face of the same
831 // pair), then ONE global merge applied to the owned labels of that tier
832 const std::int64_t maxbp = std::int64_t(nInner) * 8 + 1024;
833 Kokkos::View<Index*, Mem> bpairs("pnm::nfmpi::fbp", std::size_t(2 * maxbp));
834 Kokkos::View<int, Mem> bcnt("pnm::nfmpi::fbpcnt");
835 auto boundaryMerge = [&]() {
836 Kokkos::deep_copy(bcnt, 0);
837 Kokkos::parallel_for(
838 "pnm::nfmpi::face_bpairs", inner, KOKKOS_LAMBDA(int ix, int iy, int iz) {
839 if (!g.onSurface(ix, iy, iz))
840 return; // only faces within one cell of a block face can touch a ghost face
841 const Index o = g.owned(ix, iy, iz);
842 for (int d = 0; d < 3; ++d) {
843 if (parent(3 * o + d) < 0)
844 continue; // this tier's faces only
845 const Index myl = faceLab(3 * o + d);
846 const Index pk = fpairL(3 * o + d);
847 for (int dz = -1; dz <= 1; ++dz)
848 for (int dy = -1; dy <= 1; ++dy)
849 for (int dx = -1; dx <= 1; ++dx) {
850 if (g.inOwned(ix + dx, iy + dy, iz + dz))
851 continue;
852 const Index e2 = g.cell(ix + 1 + dx, iy + 1 + dy, iz + 1 + dz);
853 for (int d2 = 0; d2 < 3; ++d2) {
854 const Index ol = nbLab(e2, d2);
855 if (ol < 0 || ol == myl)
856 continue;
857 if (nbPair(e2, d2) != pk)
858 continue;
859 if (!kn::facesAdjacent(dx, dy, dz, d, d2))
860 continue;
861 const int s0 = Kokkos::atomic_fetch_add(&bcnt(), 1);
862 if (s0 < maxbp) {
863 bpairs(2 * s0) = myl < ol ? myl : ol;
864 bpairs(2 * s0 + 1) = myl < ol ? ol : myl;
865 }
866 }
867 }
868 }
869 });
870 space.fence();
871 const auto loc =
872 dm::localPairs(bpairs, std::size_t(std::min<std::int64_t>(readScalar(bcnt), maxbp)));
873 const dm::MergeRemap merge = dm::globalMerge(loc, comm);
874 dm::applyRemap(
875 space, merge, faceLab, nfL, KOKKOS_LAMBDA(std::size_t f) { return parent(f) >= 0; });
876 };
877 // one local CCL tier: fixpoint over the faces with parent >= 0, then their min global fid
878 auto cclTier = [&]() {
879 kn::cclFixpoint(space, parent, nfL, changed,
880 [&]() { kn::faceMerge(space, geo, parent, fpairL, changed); });
881 Kokkos::deep_copy(mg, Index(0x7fffffffffffffffLL));
882 kn::faceMinGid(space, geo, parent, mg);
883 };
884
885 // interface predicate + CORE tier (both cells fluid-centered — see single-rank rationale)
886 kn::faceInit(space, geo, flowLab, sdfE, oxE, oyE, ozE, hasOpen, fcoreL, parent, fpairL);
887 cclTier();
888 kn::faceLabelCore(space, nfL, parent, fpairL, faceLab, rootOf);
889 // pair keys onto the extended fields (once) + the core labels, exchange, boundary merge
890 for (int d = 0; d < 3; ++d) {
891 auto fq = fpairE[d];
892 const int dd0 = d;
893 Kokkos::parallel_for(
894 "pnm::nfmpi::pair_scatter", inner, KOKKOS_LAMBDA(int ix, int iy, int iz) {
895 fq(g.cell(ix + 1, iy + 1, iz + 1)) = fpairL(3 * g.owned(ix, iy, iz) + dd0);
896 });
897 space.fence();
898 haloI.exchange(fpairE[d]);
899 }
900 scatterLabels(false);
901 boundaryMerge();
902 // film attachment: min reachable core-patch label, propagated through films across ranks
903 // (scatter+exchange the current labels each sweep; Jacobi min => deterministic fixpoint)
904 {
905 int rounds2 = 0;
906 for (;;) {
907 scatterLabels(false);
908 Kokkos::deep_copy(changed, 0);
909 kn::filmAttachSweep(space, geo, faceLab, fpairL, fcoreL, nbLab, nbPair, changed);
910 if (!dm::allreduceMaxInt(readScalar(changed), comm))
911 break;
912 if (++rounds2 > 100000) {
913 // collective: the round count and the Allreduce'd stop are identical on every rank
914 throw std::runtime_error("[pnm::mpi] film attachment did not converge");
915 }
916 }
917 }
918 // leftover films (no core patch reachable anywhere): own patches — local CCL + one boundary
919 // merge, exactly like the core tier but restricted to still-kSent faces
920 kn::leftoverInit(space, nfL, faceLab, parent);
921 cclTier();
922 kn::faceLabelLeftover(space, nfL, parent, faceLab, rootOf);
923 scatterLabels(true);
924 boundaryMerge();
925 // global unique (patch root, pair) -> throat list, ordered by (pair, root fid)
926 {
927 auto flabH = downloadN(faceLab, nfL);
928 auto fpairH = downloadN(fpairL, nfL);
929 std::vector<std::pair<Index, Index>> locR; // (root, pair)
930 for (std::size_t f = 0; f < nfL; ++f)
931 if (flabH[f] >= 0)
932 locR.push_back({flabH[f], fpairH[f]});
933 dm::sortUnique(locR);
934 auto allR = dm::allgatherv(locR, comm);
935 dm::sortUnique(allR);
936 slots = kn::throatSlots(std::move(allR));
937 }
938 }
939 out.throats = slots.throats;
940 const std::size_t nt = out.throats.size();
941 Kokkos::View<Index*, Mem> keyD("pnm::nfmpi::keys", nt);
942 Kokkos::View<int*, Mem> slotD("pnm::nfmpi::slots", nt);
943 uploadVec(slots.keySorted, keyD);
944 uploadVec(slots.slotOf, slotD);
945
946 // 6. rank-local accumulation over owned +faces, then a global sum-reduce
947 std::vector<double> Qh(nt, 0.0), Ah(nt, 0.0), Cxh(nt, 0.0), Cyh(nt, 0.0), Czh(nt, 0.0),
948 Rh(np, 0.0);
949 {
950 View<double> Q("pnm::nfmpi::Q", nt), A("pnm::nfmpi::A", nt), Cx("pnm::nfmpi::Cx", nt),
951 Cy("pnm::nfmpi::Cy", nt), Cz("pnm::nfmpi::Cz", nt), resid("pnm::nfmpi::res", np);
952 kn::throatFlux(space, geo, flowLab, uE, vE, wE, oxE, oyE, ozE, hasOpen, faceLab, keyD, slotD,
953 nt, ancXD, ancYD, ancZD, origin, spacing, gdims, Q, A, Cx, Cy, Cz, resid);
954 auto red = [&](const View<double>& d, std::vector<double>& h) {
955 auto loc = downloadN(d, h.size());
956 MPI_Allreduce(loc.data(), h.data(), int(h.size()), MPI_DOUBLE, MPI_SUM, comm);
957 };
958 red(Q, Qh);
959 red(A, Ah);
960 red(Cx, Cxh);
961 red(Cy, Cyh);
962 red(Cz, Czh);
963 red(resid, Rh);
964 }
965 out.throat_flow = Qh;
966 out.throat_area = Ah;
967 out.pore_residual = Rh;
968
969 // 7. dp: identical on every rank (throat-anchored two-leg min-image, as single-rank)
970 out.throat_dp =
971 kn::throatPressureDrops(out.throats, out.pores, out.pore_pressure, out.throat_area, Cxh, Cyh,
972 Czh, ancX, ancY, ancZ, spacing, gdims, grad_p);
973 return out;
974}
975
976} // namespace pnm
977
978#endif // PECLET_PNM_PORE_EXTRACTION_MPI_HPP
void applyRemap(const Exec &space, const MergeRemap &m, const FieldView &field, std::size_t n, const PredFn &pred)
Apply a MergeRemap to the entries of field selected by pred(i): label -> its class root.
void sortUnique(std::vector< T > &v)
Sort + unique in place.
std::vector< std::pair< Index, Index > > minByKey(std::size_t n, const KeyValFn &kv)
Per-rank (key -> min value) reduction on device via UnorderedMap, with capacity-retry.
void resolveToFixpoint(const Exec &space, const BlockGeo &geo, Halo &halo, const TargetView &target, MPI_Comm comm, const char *what)
Cross-rank fixpoint of the hold-at-ghost pointer jumping (kernels::resolveHoldAtGhost): exchange the ...
peclet::core::View< T > stageInner(const Exec &space, const BlockGeo &geo, const std::vector< T > &h, const char *name)
Stage a host inner block (x-fastest, no ghosts) onto a fresh extended device field (ghosts zero-initi...
int allreduceMaxInt(int v, MPI_Comm comm)
MergeRemap globalMerge(const std::vector< std::pair< Index, Index > > &loc, MPI_Comm comm)
BlockGeo blockGeoOf(const Topo &topo, std::array< int, 3 > gdims, int G)
The extended-block geometry of a halo topology (ghost width G) on the global grid gdims.
std::vector< std::pair< Index, Index > > localPairs(const PairView &d, std::size_t count)
Interleaved device (a,b) Index pairs -> host list, locally sorted/unique.
std::vector< T > packInner(const Exec &space, const BlockGeo &geo, const peclet::core::View< T > &e)
Pack the owned box of an extended device field into a host vector.
std::vector< T > allgatherv(const std::vector< T > &local, MPI_Comm comm)
Allgatherv a local vector of T (POD) — every rank returns the concatenation.
Kokkos::RangePolicy< Exec > R1
MD3 ownedRange(const Exec &space, const Geo &geo)
The owned box of a geometry as an MDRange policy.
Index bsearchKey(const KeyView &keys, Index nkeys, typename KeyView::non_const_value_type key)
Device binary search over a sorted key view; returns the position or -1.
int resolveHoldAtGhost(const Exec &space, const Geo &geo, const TargetView &target)
One round of pointer jumping with the hold-at-ghost invariant: chase within the field,...
std::int64_t Index
Global voxel / face id (== peclet::core::Index; asserted where core is included).
V::value_type readScalar(const V &d)
Read a device scalar (the changed / counter flags) back to the host.
void uploadVec(const std::vector< T > &h, const Kokkos::View< T *, Mem > &d)
Bulk host->device upload of a whole std::vector via one deep_copy over an unmanaged host view.
std::vector< typename V::value_type > downloadN(const V &d, std::size_t count)
Download the first count elements of a device view into a fresh std::vector via one deep_copy — only ...
MpiPoreNetwork extract_pore_network_mpi(const std::vector< float > &sdf_local, std::array< int, 3 > gdims, std::array< float, 3 > origin, std::array< float, 3 > spacing, MPI_Comm comm)
Distributed fused extraction.
NetworkFlow extract_network_flow_mpi(const std::vector< float > &sdf_local, std::array< int, 3 > gdims, std::array< float, 3 > origin, std::array< float, 3 > spacing, const std::vector< double > &u_h, const std::vector< double > &v_h, const std::vector< double > &w_h, const std::vector< double > &p_h, const std::vector< double > &ox_h, const std::vector< double > &oy_h, const std::vector< double > &oz_h, std::array< double, 3 > grad_p, MPI_Comm comm)
Kokkos::DefaultExecutionSpace Exec
void mpi_block_of(std::array< int, 3 > gdims, MPI_Comm comm, std::array< int, 3 > &offset, std::array< int, 3 > &size)
This rank's ORB block of the global grid (deterministic; same partition as flow/dem): offset = the bl...
peclet.pnm — portable (Kokkos) single-rank pore-network extraction from an SDF.
Extended-block geometry policy (the MPI pipeline): ghost-inclusive local box <-> global periodic grid...
std::size_t numExt() const
std::size_t numOwned() const
Result of one distributed extraction.
std::vector< Pore > pores
std::array< int, 3 > block_origin
std::vector< std::pair< int, int > > connections
std::array< int, 3 > block_size
std::vector< double > throat_flow
std::vector< double > pore_pressure
std::vector< double > throat_area
std::vector< std::pair< int, int > > throats
std::vector< double > throat_dp
std::vector< double > pore_residual
std::vector< Pore > pores
One owned pore-peak record of the distributed network-flow extraction (namespace-scope: nvcc forbids ...
ONE global merge of a cross-block equivalence graph: the local (a,b) pairs are allgathered and union-...
peclet::core::View< Index > roots
peclet::core::View< Index > keys