pnm 1.0.3
peclet.pnm — pore-network extraction from SDF geometry (Kokkos)
Loading...
Searching...
No Matches
test_pnm_mpi.cpp
Go to the documentation of this file.
1// peclet-pnm — distributed extraction vs the single-rank oracle, bit-exact.
2//
3// Two synthetic periodic geometries (a solid sphere lattice with interstitial pores; a smooth
4// trigonometric SDF with wrap-around components, anisotropic spacing and a shifted origin) are
5// extracted (a) distributed over the ORB blocks via pnm::extract_pore_network_mpi and (b) on the
6// full grid by the single-rank pnm::extract_pore_network_k (rank 0). The distributed result must
7// be EXACTLY equal: per-voxel segmentation ids, the pore set (positions/radii bitwise, compared
8// sorted — the emission order is an atomic-slot race in both paths), and the global connection
9// list. Runs on any Kokkos backend (OpenMP / CUDA / HIP), np = 1, 2, 4.
10#include <mpi.h>
11
12#include <algorithm>
13#include <array>
14#include <cmath>
15#include <cstdio>
16#include <Kokkos_Core.hpp>
17#include <tuple>
18#include <vector>
19
20#include "../synthetic_sdf.hpp"
21#include "pore_extraction.hpp"
23
24using pnm::Index;
25using pnm::Pore;
26
27namespace {
28
29struct BlockMeta {
30 int o[3], s[3];
31};
32
33bool poreLess(const Pore& a, const Pore& b) {
34 return std::tie(a.x, a.y, a.z, a.radius) < std::tie(b.x, b.y, b.z, b.radius);
35}
36// Radius must be bitwise (a straight copy of the SDF peak value). Positions carry the sub-voxel
37// centroid, whose accumulation nvcc may contract into FMAs differently in the oracle vs the
38// distributed kernel (distinct kernels, same expressions) — measured ~1e-8 wobble at symmetric
39// peaks on CUDA, bitwise on OpenMP. Compare positions to 1e-5 * spacing; peaks are >= 1 voxel
40// apart so the sort stays stable.
41bool poreEq(const Pore& a, const Pore& b, std::array<float, 3> spc) {
42 return a.radius == b.radius && std::fabs(a.x - b.x) <= 1e-5f * spc[0] &&
43 std::fabs(a.y - b.y) <= 1e-5f * spc[1] && std::fabs(a.z - b.z) <= 1e-5f * spc[2];
44}
45
46// one geometry: distributed extraction on all ranks vs the rank-0 single-rank oracle ------------
47template <class SdfFn>
48int runCase(const char* name, std::array<int, 3> gd, std::array<float, 3> org,
49 std::array<float, 3> spc, const SdfFn& sdfAt, MPI_Comm comm) {
50 int rank = 0, size = 1;
51 MPI_Comm_rank(comm, &rank);
52 MPI_Comm_size(comm, &size);
53
54 std::array<int, 3> bo{}, bs{};
55 pnm::mpi_block_of(gd, comm, bo, bs);
56 std::vector<float> local(std::size_t(bs[0]) * bs[1] * bs[2]);
57 for (int z = 0; z < bs[2]; ++z)
58 for (int y = 0; y < bs[1]; ++y)
59 for (int x = 0; x < bs[0]; ++x)
60 local[(std::size_t(z) * bs[1] + y) * bs[0] + x] =
61 sdfAt(bo[0] + x, bo[1] + y, bo[2] + z, gd);
62
63 auto dist = pnm::extract_pore_network_mpi(local, gd, org, spc, comm);
64
65 // gather the distributed pieces (block metas fixed-size; seg variable-size, rank order)
66 BlockMeta meta{{bo[0], bo[1], bo[2]}, {bs[0], bs[1], bs[2]}};
67 auto metas = pnm::detail_mpi::allgatherv(std::vector<BlockMeta>{meta}, comm);
68 auto segAll = pnm::detail_mpi::allgatherv(dist.seg, comm);
69 auto poresAll = pnm::detail_mpi::allgatherv(dist.pores, comm);
70
71 int fail = 0;
72 if (rank == 0) {
73 // single-rank oracle on the full grid
74 const std::size_t n = std::size_t(gd[0]) * gd[1] * gd[2];
75 std::vector<float> full(n);
76 for (int z = 0; z < gd[2]; ++z)
77 for (int y = 0; y < gd[1]; ++y)
78 for (int x = 0; x < gd[0]; ++x)
79 full[(std::size_t(z) * gd[1] + y) * gd[0] + x] = sdfAt(x, y, z, gd);
80 auto oracle = pnm::extract_pore_network_k(full, gd, org, spc);
81
82 // seg: reassemble the blocks and compare per voxel
83 std::vector<int> segG(n, -999);
84 std::size_t off = 0;
85 for (int r = 0; r < size; ++r) {
86 const auto& m = metas[r];
87 for (int z = 0; z < m.s[2]; ++z)
88 for (int y = 0; y < m.s[1]; ++y)
89 for (int x = 0; x < m.s[0]; ++x)
90 segG[(std::size_t(m.o[2] + z) * gd[1] + (m.o[1] + y)) * gd[0] + (m.o[0] + x)] =
91 segAll[off + (std::size_t(z) * m.s[1] + y) * m.s[0] + x];
92 off += std::size_t(m.s[0]) * m.s[1] * m.s[2];
93 }
94 std::size_t nbad = 0;
95 for (std::size_t i = 0; i < n; ++i)
96 if (segG[i] != oracle.seg[i])
97 ++nbad;
98 if (nbad) {
99 std::printf("[%s] FAIL seg: %zu / %zu voxels differ\n", name, nbad, n);
100 fail = 1;
101 }
102
103 // pores: exact set equality (sorted; positions/radii must be bitwise-equal floats)
104 auto op = oracle.pores;
105 std::sort(op.begin(), op.end(), poreLess);
106 std::sort(poresAll.begin(), poresAll.end(), poreLess);
107 if (op.size() != poresAll.size()) {
108 std::printf("[%s] FAIL pores: %zu (dist) vs %zu (oracle)\n", name, poresAll.size(),
109 op.size());
110 fail = 1;
111 } else {
112 for (std::size_t i = 0; i < op.size(); ++i)
113 if (!poreEq(op[i], poresAll[i], spc)) {
114 std::printf("[%s] FAIL pore %zu: (%g,%g,%g,r=%g) vs (%g,%g,%g,r=%g)\n", name, i,
115 poresAll[i].x, poresAll[i].y, poresAll[i].z, poresAll[i].radius, op[i].x,
116 op[i].y, op[i].z, op[i].radius);
117 fail = 1;
118 break;
119 }
120 }
121
122 // connections: the distributed list is global + identical on every rank
123 if (dist.connections != oracle.connections) {
124 std::printf("[%s] FAIL connections: %zu (dist) vs %zu (oracle)\n", name,
125 dist.connections.size(), oracle.connections.size());
126 fail = 1;
127 }
128 if (!fail)
129 std::printf("[%s] PASS np=%d: %zu pores, %zu connections, seg exact on %zu voxels\n", name,
130 size, op.size(), oracle.connections.size(), n);
131 }
132 MPI_Bcast(&fail, 1, MPI_INT, 0, comm);
133 return fail;
134}
135
136} // namespace
137
138int main(int argc, char** argv) {
139 MPI_Init(&argc, &argv);
140 int fail = 0;
141 {
142 Kokkos::ScopeGuard kokkos(argc, argv);
143 fail += runCase("sphere_lattice", {36, 30, 24}, {0.f, 0.f, 0.f}, {1.f, 1.f, 1.f},
144 pnm::test::sphereLatticeSdf, MPI_COMM_WORLD);
145 fail += runCase("trig_field", {32, 32, 32}, {-1.f, 2.f, 0.5f}, {0.5f, 1.f, 2.f},
146 pnm::test::trigSdf, MPI_COMM_WORLD);
147 }
148 MPI_Finalize();
149 return fail ? 1 : 0;
150}
std::vector< T > allgatherv(const std::vector< T > &local, MPI_Comm comm)
Allgatherv a local vector of T (POD) — every rank returns the concatenation.
float sphereLatticeSdf(int gx, int gy, int gz, std::array< int, 3 > gd)
2x2x2 lattice of SOLID spheres in a periodic box (min-image distance); pores at the interstices.
float trigSdf(int gx, int gy, int gz, std::array< int, 3 > gd)
Smooth trigonometric field with wrap-around components (exercises the periodic images).
std::int64_t Index
Global voxel / face id (== peclet::core::Index; asserted where core is included).
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.
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...
PoreNetwork extract_pore_network_k(const std::vector< float > &sdf_h, std::array< int, 3 > resolution, std::array< float, 3 > origin, std::array< float, 3 > spacing)
peclet.pnm — portable (Kokkos) single-rank pore-network extraction from an SDF.
peclet.pnm — distributed (MPI) pore-network extraction on the core block decomposition.
int main(int argc, char **argv)