pnm 1.0.3
peclet.pnm — pore-network extraction from SDF geometry (Kokkos)
Loading...
Searching...
No Matches
test_pnm.cpp
Go to the documentation of this file.
1// peclet-pnm — the single-rank extraction contract on synthetic SDFs (any Kokkos backend).
2//
3// Every expected number below was derived by hand from the geometry and then confirmed against
4// the implementation; a change in any of them is a change of the extraction's behaviour.
5//
6// one spherical pore (16^3, R = 5.5 at voxel (8,8,8))
7// -> 1 pore, radius exactly R, at the integer centre; 1 solid; no throat.
8// two overlapping spherical pores (24x16x16, centres (8,8,8) and (16,8,8), R = 5.5)
9// -> 2 pores, radius R each, at their centres; 1 solid; ONE throat (1,2) at the mid-plane.
10// 2x2x2 periodic lattice of solid spheres (36x30x24, the tests/kokkos_mpi geometry)
11// -> 8 pores at the cell body centres x in {0,18}, y in {0,15}, z in {0,12} with radius
12// sqrt(9^2 + 7.5^2 + 6^2) - 0.22*24 = 7.882 (the SDF value at the peak voxel); 12 throats
13// = the face-adjacent body-centre pairs (8 pores x 3 axes / 2); 8 solid spheres each
14// touching all 8 pores (64 pore-solid contacts) => 76 connections; no debris (label 0).
15//
16// Also: the staged path (extract_pores -> segment_volume -> extract_topology) equals the fused
17// extract_pore_network stage for stage; two runs are bitwise equal (seg + connections bitwise,
18// pores as a sorted set — the emission order is an atomic-slot race); origin/spacing map the pore
19// position; empty input -> empty output.
20#include <algorithm>
21#include <array>
22#include <cmath>
23#include <cstdio>
24#include <Kokkos_Core.hpp>
25#include <tuple>
26#include <utility>
27#include <vector>
28
29#include "../synthetic_sdf.hpp"
30#include "pore_extraction.hpp"
31
32using pnm::Pore;
34
35namespace {
36
37int failures = 0;
38
39#define CHECK(cond) \
40 do { \
41 if (!(cond)) { \
42 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
43 ++failures; \
44 } \
45 } while (0)
46
47using Pair = std::pair<int, int>;
48
49bool poreLess(const Pore& a, const Pore& b) {
50 return std::tie(a.x, a.y, a.z, a.radius) < std::tie(b.x, b.y, b.z, b.radius);
51}
52std::vector<Pore> sorted(std::vector<Pore> p) {
53 std::sort(p.begin(), p.end(), poreLess);
54 return p;
55}
56bool poreBitwise(const Pore& a, const Pore& b) {
57 return a.x == b.x && a.y == b.y && a.z == b.z && a.radius == b.radius;
58}
59bool poresBitwise(const std::vector<Pore>& a, const std::vector<Pore>& b) {
60 return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), poreBitwise);
61}
62
64std::vector<Pair> throats(const std::vector<Pair>& conns) {
65 std::vector<Pair> t;
66 for (const auto& c : conns)
67 if (c.first > 0 && c.second > 0)
68 t.push_back(c);
69 return t;
70}
71int minLabel(const std::vector<int>& seg) {
72 return *std::min_element(seg.begin(), seg.end());
73}
74int maxLabel(const std::vector<int>& seg) {
75 return *std::max_element(seg.begin(), seg.end());
76}
77std::size_t countLabel(const std::vector<int>& seg, int l) {
78 return std::size_t(std::count(seg.begin(), seg.end(), l));
79}
80
83bool poreAt(const Pore& p, std::array<float, 3> xyz, std::array<float, 3> spc) {
84 return std::fabs(p.x - xyz[0]) <= 1e-5f * spc[0] && std::fabs(p.y - xyz[1]) <= 1e-5f * spc[1] &&
85 std::fabs(p.z - xyz[2]) <= 1e-5f * spc[2];
86}
87
88void report(const char* name, const PoreNetwork& net) {
89 std::printf("[%s] %zu pores, %zu connections (%zu throats), labels %d..%d\n", name,
90 net.pores.size(), net.connections.size(), throats(net.connections).size(),
91 minLabel(net.seg), maxLabel(net.seg));
92}
93
94// staged == fused, stage for stage; two fused runs bitwise equal ------------------------------
95void checkStagedAndDeterministic(const char* name, const std::vector<float>& sdf,
96 std::array<int, 3> gd, std::array<float, 3> org,
97 std::array<float, 3> spc, const PoreNetwork& net) {
98 const auto pores = pnm::extract_pores_k(sdf, gd, org, spc);
99 const auto seg = pnm::segment_volume_k(sdf, gd, spc);
100 const auto conns = pnm::extract_topology_k(seg, gd);
101 CHECK(poresBitwise(sorted(pores), sorted(net.pores)));
102 CHECK(seg == net.seg);
103 CHECK(conns == net.connections);
104
105 const auto again = pnm::extract_pore_network_k(sdf, gd, org, spc);
106 CHECK(poresBitwise(sorted(again.pores), sorted(net.pores)));
107 CHECK(again.seg == net.seg);
108 CHECK(again.connections == net.connections);
109 std::printf("[%s] staged == fused, two runs bitwise equal\n", name);
110}
111
112// 1. one spherical pore --------------------------------------------------------------------------
113void testOneSphere() {
114 const std::array<int, 3> gd{16, 16, 16}, c{8, 8, 8};
115 const float R = 5.5f;
116 const std::array<float, 3> org{0.f, 0.f, 0.f}, spc{1.f, 1.f, 1.f};
117 const auto sdf = pnm::test::sampleGrid(
118 gd, [&](int x, int y, int z) { return pnm::test::spherePoreSdf(x, y, z, c, R); });
119 const auto net = pnm::extract_pore_network_k(sdf, gd, org, spc);
120 report("one_sphere", net);
121
122 CHECK(net.pores.size() == 1);
123 if (net.pores.size() == 1) {
124 CHECK(net.pores[0].radius == R); // a straight copy of the peak SDF value
125 CHECK(poreAt(net.pores[0], {8.f, 8.f, 8.f}, spc));
126 }
127 CHECK(maxLabel(net.seg) == 1);
128 CHECK(minLabel(net.seg) == -1);
129 CHECK(countLabel(net.seg, 0) == 0);
130 CHECK((net.connections == std::vector<Pair>{{-1, 1}}));
131 CHECK(throats(net.connections).empty());
132 // pore voxels are exactly the SDF > 0 voxels, all labelled 1
133 std::size_t nPore = 0;
134 for (std::size_t i = 0; i < sdf.size(); ++i) {
135 if (sdf[i] > 0.f)
136 ++nPore;
137 CHECK((sdf[i] > 0.f) == (net.seg[i] == 1));
138 }
139 CHECK(countLabel(net.seg, 1) == nPore);
140 checkStagedAndDeterministic("one_sphere", sdf, gd, org, spc, net);
141
142 // origin/spacing: the pore position is origin + voxel*spacing (anisotropic, shifted origin);
143 // the count contract is unchanged.
144 const std::array<float, 3> org2{-1.f, 2.f, 0.5f}, spc2{0.5f, 1.f, 2.f};
145 const auto net2 = pnm::extract_pore_network_k(sdf, gd, org2, spc2);
146 CHECK(net2.pores.size() == 1);
147 if (net2.pores.size() == 1) {
148 CHECK(net2.pores[0].radius == R);
149 CHECK(poreAt(net2.pores[0],
150 {org2[0] + 8 * spc2[0], org2[1] + 8 * spc2[1], org2[2] + 8 * spc2[2]}, spc2));
151 }
152 CHECK((net2.connections == std::vector<Pair>{{-1, 1}}));
153}
154
155// 2. two overlapping spherical pores -------------------------------------------------------------
156void testTwoSpheres() {
157 const std::array<int, 3> gd{24, 16, 16}, c1{8, 8, 8}, c2{16, 8, 8};
158 const float R = 5.5f;
159 const std::array<float, 3> org{0.f, 0.f, 0.f}, spc{1.f, 1.f, 1.f};
160 const auto sdf = pnm::test::sampleGrid(
161 gd, [&](int x, int y, int z) { return pnm::test::twoSpherePoresSdf(x, y, z, c1, c2, R); });
162 const auto net = pnm::extract_pore_network_k(sdf, gd, org, spc);
163 report("two_spheres", net);
164
165 CHECK(net.pores.size() == 2);
166 const auto p = sorted(net.pores);
167 if (p.size() == 2) {
168 CHECK(p[0].radius == R && p[1].radius == R);
169 CHECK(poreAt(p[0], {8.f, 8.f, 8.f}, spc));
170 CHECK(poreAt(p[1], {16.f, 8.f, 8.f}, spc));
171 }
172 CHECK(maxLabel(net.seg) == 2);
173 CHECK(minLabel(net.seg) == -1);
174 CHECK(countLabel(net.seg, 0) == 0);
175 CHECK(net.connections == (std::vector<Pair>{{-1, 1}, {-1, 2}, {1, 2}}));
176 CHECK((throats(net.connections) == std::vector<Pair>{{1, 2}}));
177 // pore 1 owns the peak at c1 (first appearance in x-fastest order), pore 2 the peak at c2; the
178 // mid-plane x = 12 belongs to one of them (tie broken on voxel index) — every pore voxel is
179 // labelled and the two basins split the union.
180 const auto idx = [&](int x, int y, int z) { return (std::size_t(z) * gd[1] + y) * gd[0] + x; };
181 CHECK(net.seg[idx(8, 8, 8)] == 1);
182 CHECK(net.seg[idx(16, 8, 8)] == 2);
183 CHECK(net.seg[idx(10, 8, 8)] == 1);
184 CHECK(net.seg[idx(14, 8, 8)] == 2);
185 for (std::size_t i = 0; i < sdf.size(); ++i)
186 CHECK((sdf[i] > 0.f) == (net.seg[i] > 0));
187 checkStagedAndDeterministic("two_spheres", sdf, gd, org, spc, net);
188}
189
190// 3. 2x2x2 periodic lattice of solid spheres ----------------------------------------------------
191void testSphereLattice() {
192 const std::array<int, 3> gd{36, 30, 24};
193 const std::array<float, 3> org{0.f, 0.f, 0.f}, spc{1.f, 1.f, 1.f};
194 const auto sdf = pnm::test::sampleGrid(
195 gd, [&](int x, int y, int z) { return pnm::test::sphereLatticeSdf(x, y, z, gd); });
196 const auto net = pnm::extract_pore_network_k(sdf, gd, org, spc);
197 report("sphere_lattice", net);
198
199 // 8 body-centre pores, radius = the peak SDF value = sqrt(9^2+7.5^2+6^2) - 0.22*24.
200 CHECK(net.pores.size() == 8);
201 const float Rpeak = pnm::test::sphereLatticeSdf(0, 0, 0, gd);
202 CHECK(std::fabs(Rpeak - (std::sqrt(9.f * 9.f + 7.5f * 7.5f + 6.f * 6.f) - 0.22f * 24.f)) < 1e-5f);
203 const auto p = sorted(net.pores);
204 std::size_t k = 0;
205 for (float x : {0.f, 18.f})
206 for (float y : {0.f, 15.f})
207 for (float z : {0.f, 12.f}) { // sorted by (x, y, z)
208 if (k < p.size()) {
209 CHECK(p[k].radius == Rpeak);
210 CHECK(poreAt(p[k], {x, y, z}, spc));
211 }
212 ++k;
213 }
214
215 // 8 solid spheres (labels -1..-8), no debris, pore ids 1..8.
216 CHECK(maxLabel(net.seg) == 8);
217 CHECK(minLabel(net.seg) == -8);
218 CHECK(countLabel(net.seg, 0) == 0);
219 for (std::size_t i = 0; i < sdf.size(); ++i)
220 CHECK((sdf[i] > 0.f) == (net.seg[i] > 0));
221
222 // 12 face throats between body centres; ids follow first appearance in x-fastest order:
223 // 1:(0,0,0) 2:(18,0,0) 3:(0,15,0) 4:(18,15,0) 5..8: the same at z = 12.
224 const std::vector<Pair> expectedThroats{{1, 2}, {1, 3}, {1, 5}, {2, 4}, {2, 6}, {3, 4},
225 {3, 7}, {4, 8}, {5, 6}, {5, 7}, {6, 8}, {7, 8}};
226 CHECK(throats(net.connections) == expectedThroats);
227 // every sphere touches every pore: 64 pore-solid contacts, 76 connections in total.
228 CHECK(net.connections.size() == 76);
229 std::vector<Pair> expected;
230 for (int s = -8; s <= -1; ++s)
231 for (int q = 1; q <= 8; ++q)
232 expected.push_back({s, q});
233 expected.insert(expected.end(), expectedThroats.begin(), expectedThroats.end());
234 CHECK(net.connections == expected);
235
236 checkStagedAndDeterministic("sphere_lattice", sdf, gd, org, spc, net);
237}
238
239// 4. empty input ---------------------------------------------------------------------------------
240void testEmpty() {
241 const std::array<int, 3> gd{0, 0, 0};
242 const std::array<float, 3> z{0.f, 0.f, 0.f}, one{1.f, 1.f, 1.f};
243 const std::vector<float> none;
244 CHECK(pnm::extract_pores_k(none, gd, z, one).empty());
245 CHECK(pnm::segment_volume_k(none, gd, one).empty());
246 CHECK(pnm::extract_topology_k(std::vector<int>{}, gd).empty());
247 const auto net = pnm::extract_pore_network_k(none, gd, z, one);
248 CHECK(net.pores.empty() && net.seg.empty() && net.connections.empty());
249}
250
251} // namespace
252
253int main(int argc, char** argv) {
254 Kokkos::ScopeGuard kokkos(argc, argv);
255 std::printf("peclet-pnm single-rank tests on %s\n", Kokkos::DefaultExecutionSpace::name());
256 testOneSphere();
257 testTwoSpheres();
258 testSphereLattice();
259 testEmpty();
260 if (failures)
261 std::fprintf(stderr, "%d CHECK(s) failed\n", failures);
262 else
263 std::printf("all checks passed\n");
264 return failures ? 1 : 0;
265}
int maxLabel(const Exec &space, const Geo &geo, const SegView &seg)
The largest label over the owned box (= the number of pore labels).
Pore poreAt(const Geo &g, const SdfView &sdf, int lx, int ly, int lz, float ox, float oy, float oz, float sx, float sy, float sz)
The pore record at a peak: sub-voxel position by the SDF^2-weighted centroid of the 3^3 neighbourhood...
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.
std::vector< float > sampleGrid(std::array< int, 3 > gd, const SdfFn &fn)
Sample fn(gx, gy, gz) over the whole gd grid in pnm's flat x-fastest layout.
float twoSpherePoresSdf(int gx, int gy, int gz, std::array< int, 3 > c1, std::array< int, 3 > c2, float radius)
Two overlapping spherical pores (the union = max of the two SDFs); a throat at the mid-plane when the...
float spherePoreSdf(int gx, int gy, int gz, std::array< int, 3 > c, float radius)
One spherical PORE (SDF > 0 inside) of radius radius centred on the integer voxel c; solid everywhere...
std::vector< int > segment_volume_k(const std::vector< float > &sdf_h, std::array< int, 3 > resolution, std::array< float, 3 > spacing)
std::vector< Pore > extract_pores_k(const std::vector< float > &sdf_h, std::array< int, 3 > resolution, std::array< float, 3 > origin, std::array< float, 3 > spacing)
std::vector< std::pair< int, int > > extract_topology_k(const int *seg_h, std::size_t n, std::array< int, 3 > resolution)
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)
throats(conns)
Definition test_smoke.py:29
peclet.pnm — portable (Kokkos) single-rank pore-network extraction from an SDF.
The full pore network from one extraction: pores, the per-voxel segmentation (flat),...
int main(int argc, char **argv)
Definition test_pnm.cpp:253
#define CHECK(cond)
Definition test_pnm.cpp:39