flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
pore_extraction.hpp
Go to the documentation of this file.
1
11#ifndef PECLET_FLOW_PORE_EXTRACTION_HPP
12#define PECLET_FLOW_PORE_EXTRACTION_HPP
13
14#include <algorithm>
15#include <array>
16#include <cstdint>
17#include <Kokkos_Core.hpp>
18#include <map>
19#include <utility>
20#include <vector>
21
22namespace pnm {
23
24struct Pore {
25 float x, y, z, radius;
26};
27struct I3 {
28 int x, y, z;
29};
30
31using Exec = Kokkos::DefaultExecutionSpace;
32using Mem = Exec::memory_space;
33
37template <class T>
38inline void uploadVec(const std::vector<T>& h, const Kokkos::View<T*, Mem>& d) {
39 if (h.empty())
40 return;
41 Kokkos::deep_copy(
42 d, Kokkos::View<const T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
43 h.data(), h.size()));
44}
45
49template <class V>
50inline std::vector<typename V::value_type> downloadN(const V& d, std::size_t count) {
51 std::vector<typename V::value_type> out(count);
52 if (count)
53 Kokkos::deep_copy(Kokkos::View<typename V::value_type*, Kokkos::HostSpace,
54 Kokkos::MemoryTraits<Kokkos::Unmanaged>>(out.data(), count),
55 Kokkos::subview(d, Kokkos::make_pair(std::size_t(0), count)));
56 return out;
57}
58
59KOKKOS_INLINE_FUNCTION int get_idx(int x, int y, int z, I3 res) {
60 x = (x % res.x + res.x) % res.x;
61 y = (y % res.y + res.y) % res.y;
62 z = (z % res.z + res.z) % res.z;
63 return z * res.y * res.x + y * res.x + x;
64}
65
66// ---- pore detection (local maxima of the SDF + weight-centroid sub-voxel position) ----
67// Device core: operates on an already-uploaded device SDF, so a fused pipeline uploads the SDF
68// once.
69inline std::vector<Pore> extractPoresView(const Kokkos::View<float*, Mem>& sdf,
70 std::array<int, 3> resolution,
71 std::array<float, 3> origin,
72 std::array<float, 3> spacing) {
73 const I3 res{resolution[0], resolution[1], resolution[2]};
74 const float ox = origin[0], oy = origin[1], oz = origin[2];
75 const float sx = spacing[0], sy = spacing[1], sz = spacing[2];
76 const std::size_t n = sdf.extent(0);
77 const int max_pores = 1000000;
78
79 Kokkos::View<Pore*, Mem> pores("pores", max_pores);
80 Kokkos::View<int, Mem> counter("counter");
81 Kokkos::deep_copy(counter, 0);
82
83 Exec space;
84 using MD = Kokkos::MDRangePolicy<Exec, Kokkos::Rank<3>>;
85 Kokkos::parallel_for(
86 "pnm::extract_pores", MD(space, {0, 0, 0}, {res.x, res.y, res.z}),
87 KOKKOS_LAMBDA(int ix, int iy, int iz) {
88 const int ci = get_idx(ix, iy, iz, res);
89 const float cv = sdf(ci);
90 if (cv <= 0.0f)
91 return;
92 bool peak = true;
93 for (int dz = -1; dz <= 1 && peak; ++dz)
94 for (int dy = -1; dy <= 1 && peak; ++dy)
95 for (int dx = -1; dx <= 1; ++dx) {
96 if (dx == 0 && dy == 0 && dz == 0)
97 continue;
98 const int ni = get_idx(ix + dx, iy + dy, iz + dz, res);
99 const float nv = sdf(ni);
100 if (nv > cv || (nv == cv && ni > ci)) {
101 peak = false;
102 break;
103 }
104 }
105 if (!peak)
106 return;
107 float sw = 0.0f, px = 0.0f, py = 0.0f, pz = 0.0f;
108 for (int dz = -1; dz <= 1; ++dz)
109 for (int dy = -1; dy <= 1; ++dy)
110 for (int dx = -1; dx <= 1; ++dx) {
111 const float v = sdf(get_idx(ix + dx, iy + dy, iz + dz, res));
112 float w = v > 0.0f ? v : 0.0f;
113 w = w * w;
114 sw += w;
115 px += dx * w;
116 py += dy * w;
117 pz += dz * w;
118 }
119 float fx = 0, fy = 0, fz = 0;
120 if (sw > 1e-6f) {
121 fx = px / sw;
122 fy = py / sw;
123 fz = pz / sw;
124 }
125 const int slot = Kokkos::atomic_fetch_add(&counter(), 1);
126 if (slot < max_pores)
127 pores(slot) = Pore{ox + (ix + fx) * sx, oy + (iy + fy) * sy, oz + (iz + fz) * sz, cv};
128 });
129 space.fence();
130
131 int h_count = 0;
132 {
133 auto hc = Kokkos::create_mirror_view(counter);
134 Kokkos::deep_copy(hc, counter);
135 h_count = hc();
136 }
137 if (h_count > max_pores)
138 h_count = max_pores;
139 return downloadN(pores, static_cast<std::size_t>(h_count));
140}
141
142// Host wrapper: upload the SDF, then run the device core.
143inline std::vector<Pore> extract_pores_k(const std::vector<float>& sdf_h,
144 std::array<int, 3> resolution, std::array<float, 3> origin,
145 std::array<float, 3> spacing) {
146 if (sdf_h.empty())
147 return {};
148 Kokkos::View<float*, Mem> sdf("sdf", sdf_h.size());
149 uploadVec(sdf_h, sdf);
150 return extractPoresView(sdf, resolution, origin, spacing);
151}
152
153// ---- marker-controlled watershed segmentation of the solid + gradient-path pore basins ----
154// Device core: takes an uploaded device SDF, returns the (device-resident) segmentation View.
155inline Kokkos::View<int*, Mem> segmentVolumeView(const Kokkos::View<float*, Mem>& sdf,
156 std::array<int, 3> resolution,
157 std::array<float, 3> spacing) {
158 const I3 res{resolution[0], resolution[1], resolution[2]};
159 const std::size_t n = sdf.extent(0);
160 const float min_sp = std::min(spacing[0], std::min(spacing[1], spacing[2]));
161 const float thr = -1.5f * min_sp;
162
163 Kokkos::View<int*, Mem> labels("labels", n), roots("roots", n);
164 Kokkos::View<int, Mem> changed("changed");
165
166 Exec space;
167 using MD = Kokkos::MDRangePolicy<Exec, Kokkos::Rank<3>>;
168 const auto full = MD(space, {0, 0, 0}, {res.x, res.y, res.z});
169
170 // 1. init markers (deep solid -> own index, else -1)
171 Kokkos::parallel_for(
172 "pnm::init_markers", full, KOKKOS_LAMBDA(int ix, int iy, int iz) {
173 const int i = get_idx(ix, iy, iz, res);
174 labels(i) = (sdf(i) < thr) ? i : -1;
175 });
176 space.fence();
177
178 // 2. union-find CCL on markers (26-connectivity, 13 forward neighbours) + path compression, to
179 // fixpoint
180 auto flatten = [&]() {
181 Kokkos::parallel_for(
182 "pnm::flatten", Kokkos::RangePolicy<Exec>(space, 0, n), KOKKOS_LAMBDA(std::size_t idx) {
183 int l = labels(idx);
184 if (l != -1) {
185 while (l != labels(l))
186 l = labels(l);
187 labels(idx) = l;
188 }
189 });
190 space.fence();
191 };
192 int h_changed = 1;
193 while (h_changed) {
194 Kokkos::deep_copy(changed, 0);
195 Kokkos::parallel_for(
196 "pnm::merge_markers", full, KOKKOS_LAMBDA(int ix, int iy, int iz) {
197 const int idx = get_idx(ix, iy, iz, res);
198 const int my = labels(idx);
199 if (my == -1)
200 return;
201 const int dz_l[13] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0};
202 const int dy_l[13] = {-1, -1, -1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0};
203 const int dx_l[13] = {-1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, 1};
204 for (int k = 0; k < 13; ++k) {
205 const int ni = get_idx(ix + dx_l[k], iy + dy_l[k], iz + dz_l[k], res);
206 const int nl = labels(ni);
207 if (nl != -1 && my != nl) {
208 int rm = my;
209 while (rm != labels(rm))
210 rm = labels(rm);
211 int rn = nl;
212 while (rn != labels(rn))
213 rn = labels(rn);
214 if (rm != rn) {
215 const int small = rm < rn ? rm : rn, large = rm < rn ? rn : rm;
216 Kokkos::atomic_min(&labels(large), small);
217 changed() = 1;
218 }
219 }
220 }
221 });
222 space.fence();
223 flatten();
224 auto hc = Kokkos::create_mirror_view(changed);
225 Kokkos::deep_copy(hc, changed);
226 h_changed = hc();
227 }
228
229 // 3. flood-fill the remaining (shallow) solid voxels (26-connectivity, smallest neighbour label),
230 // to fixpoint
231 h_changed = 1;
232 while (h_changed) {
233 Kokkos::deep_copy(changed, 0);
234 Kokkos::parallel_for(
235 "pnm::flood", full, KOKKOS_LAMBDA(int ix, int iy, int iz) {
236 const int idx = get_idx(ix, iy, iz, res);
237 if (sdf(idx) >= 0.0f)
238 return; // pore: ignore
239 if (labels(idx) != -1)
240 return; // already labelled
241 int best = -1;
242 for (int dz = -1; dz <= 1; ++dz)
243 for (int dy = -1; dy <= 1; ++dy)
244 for (int dx = -1; dx <= 1; ++dx) {
245 if (dx == 0 && dy == 0 && dz == 0)
246 continue;
247 const int nl = labels(get_idx(ix + dx, iy + dy, iz + dz, res));
248 if (nl != -1 && (best == -1 || nl < best))
249 best = nl;
250 }
251 if (best != -1) {
252 labels(idx) = best;
253 changed() = 1;
254 }
255 });
256 space.fence();
257 auto hc = Kokkos::create_mirror_view(changed);
258 Kokkos::deep_copy(hc, changed);
259 h_changed = hc();
260 }
261
262 // 4. gradient-path pore basins (ascent for pores, descent for solids; 26-connectivity, tie-break
263 // on index)
264 Kokkos::parallel_for(
265 "pnm::gradient_path", full, KOKKOS_LAMBDA(int ix, int iy, int iz) {
266 const int ci = get_idx(ix, iy, iz, res);
267 const bool ascent = (sdf(ci) > 0.0f);
268 int walker = ci;
269 const int MAX_STEPS = 512;
270 for (int s = 0; s < MAX_STEPS; ++s) {
271 int best = walker;
272 float bv = sdf(walker);
273 const int wx = walker % res.x, wy = (walker / res.x) % res.y,
274 wz = walker / (res.x * res.y);
275 for (int dz = -1; dz <= 1; ++dz)
276 for (int dy = -1; dy <= 1; ++dy)
277 for (int dx = -1; dx <= 1; ++dx) {
278 if (dx == 0 && dy == 0 && dz == 0)
279 continue;
280 const int ni = get_idx(wx + dx, wy + dy, wz + dz, res);
281 const float nv = sdf(ni);
282 if (ascent) {
283 if (nv > bv) {
284 bv = nv;
285 best = ni;
286 } else if (nv == bv && ni > best)
287 best = ni;
288 } else {
289 if (nv < bv) {
290 bv = nv;
291 best = ni;
292 } else if (nv == bv && ni > best)
293 best = ni;
294 }
295 }
296 if (best == walker)
297 break;
298 walker = best;
299 }
300 roots(ci) = walker;
301 });
302 space.fence();
303
304 // 5. combine + renumber ON DEVICE (pores >0 ascending, solids <0 descending, debris 0), matching
305 // the host first-encounter relabel exactly (F2). A label's id is its rank in voxel-index order of
306 // first appearance — which equals the exclusive prefix sum of a "first-occurrence" flag, so it
307 // parallelises without the host std::map + the two full-volume D2Hs (labels/roots stay on
308 // device). The root/label values ARE voxel indices, so per-label scratch is size-n arrays indexed
309 // by that value.
310 Kokkos::View<int*, Mem> seg("seg", n);
311 Kokkos::View<int*, Mem> minPoreIdx("minPoreIdx", n), minSolidIdx("minSolidIdx", n);
312 Kokkos::View<int*, Mem> poreFirst("poreFirst", n), solidFirst("solidFirst", n);
313 Kokkos::View<int*, Mem> poreRank("poreRank", n), solidRank("solidRank", n);
314 Kokkos::View<int*, Mem> poreId("poreId", n), solidId("solidId", n);
315 constexpr int kBig = 0x7fffffff;
316 Kokkos::deep_copy(minPoreIdx, kBig);
317 Kokkos::deep_copy(minSolidIdx, kBig);
318 const std::size_t nn = n;
319 using R1 = Kokkos::RangePolicy<Exec>;
320 // (a) per-root/label min voxel index of first appearance (pores use `roots`, solids use
321 // `labels`).
322 Kokkos::parallel_for(
323 "pnm::relabel_min", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
324 if (sdf(i) > 0.0f)
325 Kokkos::atomic_min(&minPoreIdx(roots(i)), static_cast<int>(i));
326 else if (labels(i) != -1)
327 Kokkos::atomic_min(&minSolidIdx(labels(i)), static_cast<int>(i));
328 });
329 space.fence();
330 // (b) flag the voxel that is the first appearance of its label.
331 Kokkos::parallel_for(
332 "pnm::relabel_first", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
333 poreFirst(i) = (sdf(i) > 0.0f && minPoreIdx(roots(i)) == static_cast<int>(i)) ? 1 : 0;
334 solidFirst(i) =
335 (sdf(i) <= 0.0f && labels(i) != -1 && minSolidIdx(labels(i)) == static_cast<int>(i))
336 ? 1
337 : 0;
338 });
339 space.fence();
340 // (c) exclusive prefix sums ⇒ the 0-based rank (= first-encounter order) of each first voxel.
341 Kokkos::parallel_scan(
342 "pnm::relabel_porescan", R1(space, 0, nn),
343 KOKKOS_LAMBDA(std::size_t i, int& upd, const bool fin) {
344 const int v = poreFirst(i);
345 if (fin)
346 poreRank(i) = upd;
347 upd += v;
348 });
349 Kokkos::parallel_scan(
350 "pnm::relabel_solidscan", R1(space, 0, nn),
351 KOKKOS_LAMBDA(std::size_t i, int& upd, const bool fin) {
352 const int v = solidFirst(i);
353 if (fin)
354 solidRank(i) = upd;
355 upd += v;
356 });
357 space.fence();
358 // (d) assign each label its signed id at its first voxel (pores 1,2,…; solids −1,−2,…).
359 Kokkos::parallel_for(
360 "pnm::relabel_assign", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
361 if (poreFirst(i))
362 poreId(roots(i)) = poreRank(i) + 1;
363 if (solidFirst(i))
364 solidId(labels(i)) = -(solidRank(i) + 1);
365 });
366 space.fence();
367 // (e) scatter ids to every voxel: pore→poreId, labelled solid→solidId, unlabelled solid
368 // (debris)→0.
369 Kokkos::parallel_for(
370 "pnm::relabel_seg", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
371 seg(i) = (sdf(i) > 0.0f) ? poreId(roots(i)) : (labels(i) == -1 ? 0 : solidId(labels(i)));
372 });
373 space.fence();
374 return seg; // device-resident; the fused pipeline feeds it straight to the topology stage
375}
376
377// Host wrapper: upload the SDF, segment on device, download the segmentation.
378inline std::vector<int> segment_volume_k(const std::vector<float>& sdf_h,
379 std::array<int, 3> resolution,
380 std::array<float, 3> spacing) {
381 if (sdf_h.empty())
382 return {};
383 Kokkos::View<float*, Mem> sdf("sdf", sdf_h.size());
384 uploadVec(sdf_h, sdf);
385 return downloadN(segmentVolumeView(sdf, resolution, spacing), sdf_h.size());
386}
387
388// ---- boundary-pair topology (unique adjacent-label pairs across +x/+y/+z faces) ----
389// Device core: takes the (device-resident) segmentation View directly — no re-upload.
390inline std::vector<std::pair<int, int>> extractTopologyView(const Kokkos::View<int*, Mem>& seg,
391 std::array<int, 3> resolution) {
392 const I3 res{resolution[0], resolution[1], resolution[2]};
393 const std::size_t n = seg.extent(0);
394 const int max_pairs = (int)(n * 3);
395
396 Kokkos::View<int*, Mem> pairs("pairs",
397 (std::size_t)max_pairs * 2); // flattened (l1,l2) interleaved
398 Kokkos::View<int, Mem> cnt("cnt");
399 Kokkos::deep_copy(cnt, 0);
400
401 Exec space;
402 using MD = Kokkos::MDRangePolicy<Exec, Kokkos::Rank<3>>;
403 Kokkos::parallel_for(
404 "pnm::boundary_pairs", MD(space, {0, 0, 0}, {res.x, res.y, res.z}),
405 KOKKOS_LAMBDA(int ix, int iy, int iz) {
406 const int idx = get_idx(ix, iy, iz, res);
407 const int my = seg(idx);
408 const int dx_l[3] = {1, 0, 0}, dy_l[3] = {0, 1, 0}, dz_l[3] = {0, 0, 1};
409 for (int k = 0; k < 3; ++k) {
410 const int nl = seg(get_idx(ix + dx_l[k], iy + dy_l[k], iz + dz_l[k], res));
411 if (my != nl) {
412 const int l1 = my < nl ? my : nl, l2 = my < nl ? nl : my;
413 const int slot = Kokkos::atomic_fetch_add(&cnt(), 1);
414 if (slot < max_pairs) {
415 pairs(2 * slot) = l1;
416 pairs(2 * slot + 1) = l2;
417 }
418 }
419 }
420 });
421 space.fence();
422
423 int h_count = 0;
424 {
425 auto hc = Kokkos::create_mirror_view(cnt);
426 Kokkos::deep_copy(hc, cnt);
427 h_count = hc();
428 }
429 if (h_count > max_pairs)
430 h_count = max_pairs;
431 std::vector<int> flat =
432 downloadN(pairs, static_cast<std::size_t>(2 * h_count)); // only the used slots
433 std::vector<std::pair<int, int>> result;
434 result.reserve(h_count);
435 for (int i = 0; i < h_count; ++i)
436 result.push_back({flat[2 * i], flat[2 * i + 1]});
437 std::sort(result.begin(), result.end());
438 result.erase(std::unique(result.begin(), result.end()), result.end());
439 return result;
440}
441
442// Host wrapper: upload the segmentation, then run the device core.
443inline std::vector<std::pair<int, int>> extract_topology_k(const std::vector<int>& seg_h,
444 std::array<int, 3> resolution) {
445 if (seg_h.empty())
446 return {};
447 Kokkos::View<int*, Mem> seg("seg", seg_h.size());
448 uploadVec(seg_h, seg);
449 return extractTopologyView(seg, resolution);
450}
451
455 std::vector<Pore> pores;
456 std::vector<int> seg;
457 std::vector<std::pair<int, int>> connections;
458};
459
460// ---- fused pipeline (F1): upload the SDF ONCE, keep it + the segmentation device-resident across
461// all three stages (extract_pores → segment_volume → extract_topology), so neither the SDF nor seg
462// is re-uploaded or round-tripped between stages. Only the final results cross back to the host.
463// Each stage's result is identical to calling the three functions separately. ----
464inline PoreNetwork extract_pore_network_k(const std::vector<float>& sdf_h,
465 std::array<int, 3> resolution,
466 std::array<float, 3> origin,
467 std::array<float, 3> spacing) {
468 PoreNetwork out;
469 if (sdf_h.empty())
470 return out;
471 Kokkos::View<float*, Mem> sdf("sdf", sdf_h.size());
472 uploadVec(sdf_h, sdf);
473 out.pores = extractPoresView(sdf, resolution, origin, spacing);
474 Kokkos::View<int*, Mem> seg = segmentVolumeView(sdf, resolution, spacing); // stays on device
475 out.connections = extractTopologyView(seg, resolution);
476 out.seg = downloadN(seg, sdf_h.size());
477 return out;
478}
479
480} // namespace pnm
481
482#endif // PECLET_FLOW_PORE_EXTRACTION_HPP
int get_idx(int x, int y, int z, I3 res)
Exec::memory_space Mem
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 — repla...
std::vector< std::pair< int, int > > extractTopologyView(const Kokkos::View< int *, Mem > &seg, std::array< int, 3 > resolution)
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 — repla...
std::vector< std::pair< int, int > > extract_topology_k(const std::vector< int > &seg_h, std::array< int, 3 > resolution)
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)
Kokkos::View< int *, Mem > segmentVolumeView(const Kokkos::View< float *, Mem > &sdf, std::array< int, 3 > resolution, std::array< float, 3 > spacing)
std::vector< Pore > extractPoresView(const Kokkos::View< float *, Mem > &sdf, std::array< int, 3 > resolution, std::array< float, 3 > origin, std::array< float, 3 > spacing)
Kokkos::DefaultExecutionSpace Exec
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)
The full pore network from one extraction: pores, the per-voxel segmentation (flat),...
std::vector< int > seg
std::vector< Pore > pores
std::vector< std::pair< int, int > > connections