pnm 1.0.3
peclet.pnm — pore-network extraction from SDF geometry (Kokkos)
Loading...
Searching...
No Matches
pore_extraction.hpp
Go to the documentation of this file.
1
12#ifndef PECLET_PNM_PORE_EXTRACTION_HPP
13#define PECLET_PNM_PORE_EXTRACTION_HPP
14
15#include <algorithm>
16#include <array>
17#include <cstdint>
18#include <Kokkos_Core.hpp>
19#include <utility>
20#include <vector>
21
22#include "pore_kernels.hpp"
23
24namespace pnm {
25
26// ---- pore detection (local maxima of the SDF + weight-centroid sub-voxel position) ----
27// Device core: operates on an already-uploaded device SDF, so a fused pipeline uploads the SDF
28// once.
29inline std::vector<Pore> extractPoresView(const Kokkos::View<float*, Mem>& sdf,
30 std::array<int, 3> resolution,
31 std::array<float, 3> origin,
32 std::array<float, 3> spacing) {
33 const GridGeo geo = GridGeo::of(I3{resolution[0], resolution[1], resolution[2]});
34 const int max_pores = 1000000;
35 Kokkos::View<Pore*, Mem> pores("pores", max_pores);
36 Kokkos::View<int, Mem> counter("counter");
37 Kokkos::deep_copy(counter, 0);
38 Exec space;
39 kernels::detectPores(space, geo, sdf, origin, spacing, pores, counter, max_pores);
40 const int h_count = std::min(readScalar(counter), max_pores);
41 return downloadN(pores, static_cast<std::size_t>(h_count));
42}
43
44// Host wrapper: upload the SDF, then run the device core.
45inline std::vector<Pore> extract_pores_k(const std::vector<float>& sdf_h,
46 std::array<int, 3> resolution, std::array<float, 3> origin,
47 std::array<float, 3> spacing) {
48 if (sdf_h.empty())
49 return {};
50 Kokkos::View<float*, Mem> sdf("sdf", sdf_h.size());
51 uploadVec(sdf_h, sdf);
52 return extractPoresView(sdf, resolution, origin, spacing);
53}
54
55// ---- marker-controlled watershed segmentation of the solid + gradient-path pore basins ----
56// Device core: takes an uploaded device SDF, returns the (device-resident) segmentation View.
57// `rootsOut` (optional) receives the gradient-path pore roots (roots(i)==i at the pore peaks) —
58// the network-flow extraction uses them to locate pore centers per label.
59inline Kokkos::View<int*, Mem> segmentVolumeView(const Kokkos::View<float*, Mem>& sdf,
60 std::array<int, 3> resolution,
61 std::array<float, 3> spacing,
62 Kokkos::View<int*, Mem>* rootsOut = nullptr) {
63 const GridGeo geo = GridGeo::of(I3{resolution[0], resolution[1], resolution[2]});
64 const std::size_t n = sdf.extent(0);
65 const float min_sp = std::min(spacing[0], std::min(spacing[1], spacing[2]));
66 const float thr = -1.5f * min_sp;
67
68 Kokkos::View<int*, Mem> labels("labels", n), roots("roots", n);
69 Kokkos::View<int, Mem> changed("changed");
70 Exec space;
71
72 // 1. init markers (deep solid -> own index, else -1)
73 kernels::initMarkers(space, geo, sdf, thr, labels);
74
75 // 2. union-find CCL on markers (26-connectivity, 13 forward neighbours) + path compression, to
76 // fixpoint. Labels are voxel ids, so the fixpoint label of a component is its min voxel id.
77 kernels::cclFixpoint(space, labels, n, changed,
78 [&]() { kernels::cclMergeMarkers(space, geo, labels, changed); });
79
80 // 3. flood-fill the remaining (shallow) solid voxels (26-connectivity, smallest neighbour label),
81 // to fixpoint. Jacobi (double-buffered) — deterministic, and sweep-for-sweep identical to the
82 // distributed flood, which is what makes the multi-rank segmentation bit-exact to this path.
83 {
84 Kokkos::View<int*, Mem> labelsN("labelsN", n);
85 kernels::floodFixpoint(space, geo, sdf, labels, labelsN, changed, [](int c) { return c; });
86 }
87
88 // 4. gradient-path pore basins (ascent for pores, descent for solids; 26-connectivity, tie-break
89 // on index)
91 space, geo, sdf, false, KOKKOS_LAMBDA(Index i, Index walker) { roots(i) = int(walker); });
92
93 // 5. combine + renumber ON DEVICE (pores >0 ascending, solids <0 descending, debris 0), matching
94 // the host first-encounter relabel exactly (F2). A label's id is its rank in voxel-index order of
95 // first appearance — which equals the exclusive prefix sum of a "first-occurrence" flag, so it
96 // parallelises without the host std::map + the two full-volume D2Hs (labels/roots stay on
97 // device). The root/label values ARE voxel indices, so per-label scratch is size-n arrays indexed
98 // by that value.
99 Kokkos::View<int*, Mem> seg("seg", n);
100 Kokkos::View<int*, Mem> minPoreIdx("minPoreIdx", n), minSolidIdx("minSolidIdx", n);
101 Kokkos::View<int*, Mem> poreFirst("poreFirst", n), solidFirst("solidFirst", n);
102 Kokkos::View<int*, Mem> poreRank("poreRank", n), solidRank("solidRank", n);
103 Kokkos::View<int*, Mem> poreId("poreId", n), solidId("solidId", n);
104 constexpr int kBig = 0x7fffffff;
105 Kokkos::deep_copy(minPoreIdx, kBig);
106 Kokkos::deep_copy(minSolidIdx, kBig);
107 const std::size_t nn = n;
108 using R1 = kernels::R1;
109 // (a) per-root/label min voxel index of first appearance (pores use `roots`, solids use
110 // `labels`).
111 Kokkos::parallel_for(
112 "pnm::relabel_min", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
113 if (sdf(i) > 0.0f)
114 Kokkos::atomic_min(&minPoreIdx(roots(i)), static_cast<int>(i));
115 else if (labels(i) != -1)
116 Kokkos::atomic_min(&minSolidIdx(labels(i)), static_cast<int>(i));
117 });
118 space.fence();
119 // (b) flag the voxel that is the first appearance of its label.
120 Kokkos::parallel_for(
121 "pnm::relabel_first", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
122 poreFirst(i) = (sdf(i) > 0.0f && minPoreIdx(roots(i)) == static_cast<int>(i)) ? 1 : 0;
123 solidFirst(i) =
124 (sdf(i) <= 0.0f && labels(i) != -1 && minSolidIdx(labels(i)) == static_cast<int>(i))
125 ? 1
126 : 0;
127 });
128 space.fence();
129 // (c) exclusive prefix sums ⇒ the 0-based rank (= first-encounter order) of each first voxel.
130 Kokkos::parallel_scan(
131 "pnm::relabel_porescan", R1(space, 0, nn),
132 KOKKOS_LAMBDA(std::size_t i, int& upd, const bool fin) {
133 const int v = poreFirst(i);
134 if (fin)
135 poreRank(i) = upd;
136 upd += v;
137 });
138 Kokkos::parallel_scan(
139 "pnm::relabel_solidscan", R1(space, 0, nn),
140 KOKKOS_LAMBDA(std::size_t i, int& upd, const bool fin) {
141 const int v = solidFirst(i);
142 if (fin)
143 solidRank(i) = upd;
144 upd += v;
145 });
146 space.fence();
147 // (d) assign each label its signed id at its first voxel (pores 1,2,…; solids −1,−2,…).
148 Kokkos::parallel_for(
149 "pnm::relabel_assign", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
150 if (poreFirst(i))
151 poreId(roots(i)) = poreRank(i) + 1;
152 if (solidFirst(i))
153 solidId(labels(i)) = -(solidRank(i) + 1);
154 });
155 space.fence();
156 // (e) scatter ids to every voxel: pore→poreId, labelled solid→solidId, unlabelled solid
157 // (debris)→0.
158 Kokkos::parallel_for(
159 "pnm::relabel_seg", R1(space, 0, nn), KOKKOS_LAMBDA(std::size_t i) {
160 seg(i) = (sdf(i) > 0.0f) ? poreId(roots(i)) : (labels(i) == -1 ? 0 : solidId(labels(i)));
161 });
162 space.fence();
163 if (rootsOut)
164 *rootsOut = roots;
165 return seg; // device-resident; the fused pipeline feeds it straight to the topology stage
166}
167
168// Host wrapper: upload the SDF, segment on device, download the segmentation.
169inline std::vector<int> segment_volume_k(const std::vector<float>& sdf_h,
170 std::array<int, 3> resolution,
171 std::array<float, 3> spacing) {
172 if (sdf_h.empty())
173 return {};
174 Kokkos::View<float*, Mem> sdf("sdf", sdf_h.size());
175 uploadVec(sdf_h, sdf);
176 return downloadN(segmentVolumeView(sdf, resolution, spacing), sdf_h.size());
177}
178
179// ---- boundary-pair topology (unique adjacent-label pairs across +x/+y/+z faces) ----
180// Device core: takes the (device-resident) segmentation View directly — no re-upload.
181inline std::vector<std::pair<int, int>> extractTopologyView(const Kokkos::View<int*, Mem>& seg,
182 std::array<int, 3> resolution) {
183 const GridGeo geo = GridGeo::of(I3{resolution[0], resolution[1], resolution[2]});
184 const std::size_t n = seg.extent(0);
185 const Index max_pairs = Index(n) * 3;
186 Kokkos::View<int*, Mem> pairs("pairs",
187 (std::size_t)max_pairs * 2); // flattened (l1,l2) interleaved
188 Kokkos::View<int, Mem> cnt("cnt");
189 Kokkos::deep_copy(cnt, 0);
190 Exec space;
191 kernels::boundaryPairs(space, geo, seg, pairs, cnt, max_pairs);
192 const Index h_count = std::min<Index>(readScalar(cnt), max_pairs);
193 return kernels::uniquePairs(pairs, static_cast<std::size_t>(h_count)); // only the used slots
194}
195
196// Host wrapper: upload the segmentation from a caller-owned buffer (the binding hands over the
197// NumPy array's memory directly), then run the device core.
198inline std::vector<std::pair<int, int>> extract_topology_k(const int* seg_h, std::size_t n,
199 std::array<int, 3> resolution) {
200 if (n == 0)
201 return {};
202 Kokkos::View<int*, Mem> seg("seg", n);
203 Kokkos::deep_copy(
204 seg, Kokkos::View<const int*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
205 seg_h, n));
206 return extractTopologyView(seg, resolution);
207}
208
209inline std::vector<std::pair<int, int>> extract_topology_k(const std::vector<int>& seg_h,
210 std::array<int, 3> resolution) {
211 return extract_topology_k(seg_h.data(), seg_h.size(), resolution);
212}
213
214// ---- network flow: throat flow rates + pore-center pressures from a MAC flow field ----
215// The method transferred from the Voronoi PNM (pnm_voronoi/pnm_voro): there, the throat flow was
216// the integral of u·n over the Voronoi facet (facet sliced against the grid, MAC velocities
217// interpolated) and the pore pressure a trilinear interpolation of the cell-centered periodic p
218// at the pore center (Voronoi vertex), with the macroscopic gradient added along the min-image
219// pore-to-pore vector. On the segmentation network both transfer EXACTLY and more simply:
220// * a throat (label pair) is a set of grid-aligned voxel faces, and on flow's staggered MAC
221// grid the openness-weighted face velocity IS the discrete flux carrier (div = sum of
222// o·u·A over the cell faces) — so Q_ij = sum over interface faces of o·u·A, no slicing or
223// interpolation, and the per-pore signed boundary sum reproduces the solver's divergence
224// (~0 to solve tolerance) — reported as `pore_residual`, the built-in correctness check.
225// * the pore center is the basin's SDF peak (the gradient-path root) with the same sub-voxel
226// centroid refinement as extract_pores; p is trilinearly interpolated there (periodic).
227// Conventions (flow's MAC layout): u(i,j,k) lives on the -x face of cell (i,j,k); ox is that
228// face's openness (fluid area fraction); cell centers sit at origin + i*spacing (pnm convention).
229// Throats are PER-PATCH: a throat is a CONNECTED patch of interface faces, so two disjoint
230// interfaces between the same two pores (e.g. once directly and once through the periodic wrap)
231// are separate parallel throats — `throats` can repeat the same label pair. Like the Voronoi
232// network, where each throat was its own facet object.
234 std::vector<Pore> pores; // ordered by pore label id (pores[k] <-> label k+1)
235 std::vector<double> pore_pressure; // periodic-part p at the pore center (trilinear)
236 std::vector<double> pore_residual; // net signed outflow over the pore's WHOLE boundary
237 std::vector<std::pair<int, int>> throats; // pore-pore label pairs (li < lj)
238 std::vector<double> throat_flow; // Q through the interface, positive li -> lj
239 std::vector<double> throat_area; // open area of the interface (sum o·A over faces)
240 std::vector<double> throat_dp; // total-pressure drop P_i - P_j =
241 // (p_i - p_j) - grad_p · minimage(x_j - x_i)
242};
243
245 const std::vector<float>& sdf_h, std::array<int, 3> resolution, std::array<float, 3> origin,
246 std::array<float, 3> spacing, const std::vector<double>& u_h, const std::vector<double>& v_h,
247 const std::vector<double>& w_h, const std::vector<double>& p_h, const std::vector<double>& ox_h,
248 const std::vector<double>& oy_h,
249 const std::vector<double>& oz_h, // pass empty vectors for a fully-open (non-cut-cell) grid
250 std::array<double, 3> grad_p) {
251 namespace kn = kernels;
252 NetworkFlow out;
253 if (sdf_h.empty())
254 return out;
255 const I3 res{resolution[0], resolution[1], resolution[2]};
256 const GridGeo geo = GridGeo::of(res);
257 const std::size_t n = sdf_h.size();
258 Exec space;
259 using R1 = kn::R1;
260
261 Kokkos::View<float*, Mem> sdf("nf::sdf", n);
262 uploadVec(sdf_h, sdf);
263 Kokkos::View<int*, Mem> roots;
264 Kokkos::View<int*, Mem> seg = segmentVolumeView(sdf, resolution, spacing, &roots);
265
266 // number of pore labels
267 const int np = kn::maxLabel(space, geo, seg);
268 if (np == 0)
269 return out;
270
271 // pore centers: the basin peak (gradient-path root) per label + centroid refinement
272 Kokkos::View<int*, Mem> peak("nf::peak", np);
273 Kokkos::parallel_for(
274 "nf::peaks", R1(space, 0, n), KOKKOS_LAMBDA(std::size_t i) {
275 if (sdf(i) > 0.0f && roots(i) == static_cast<int>(i))
276 peak(seg(i) - 1) = static_cast<int>(i);
277 });
278 space.fence();
279 Kokkos::View<Pore*, Mem> poresD("nf::pores", np);
280 {
281 const float oxo = origin[0], oyo = origin[1], ozo = origin[2];
282 const float sx = spacing[0], sy = spacing[1], sz = spacing[2];
283 const GridGeo g = geo;
284 Kokkos::parallel_for(
285 "nf::centers", R1(space, 0, np), KOKKOS_LAMBDA(int k) {
286 int ix, iy, iz;
287 g.ownedCoords(peak(k), ix, iy, iz);
288 poresD(k) = kn::poreAt(g, sdf, ix, iy, iz, oxo, oyo, ozo, sx, sy, sz);
289 });
290 space.fence();
291 }
292 out.pores = downloadN(poresD, static_cast<std::size_t>(np));
293
294 // fields on device
295 auto upl = [&](const std::vector<double>& h, const char* nm, bool required) {
296 Kokkos::View<double*, Mem> d(nm, required || !h.empty() ? n : 0);
297 if (!h.empty())
298 uploadVec(h, d);
299 return d;
300 };
301 Kokkos::View<double*, Mem> u = upl(u_h, "nf::u", true), v = upl(v_h, "nf::v", true),
302 w = upl(w_h, "nf::w", true), p = upl(p_h, "nf::p", true);
303 const bool hasOpen = !ox_h.empty();
304 Kokkos::View<double*, Mem> ox = upl(ox_h, "nf::ox", false), oy = upl(oy_h, "nf::oy", false),
305 oz = upl(oz_h, "nf::oz", false);
306
307 // pore-center pressures: trilinear interpolation of the cell-centered periodic p
308 Kokkos::View<double*, Mem> ppres("nf::ppres", np);
309 {
310 const float oxo = origin[0], oyo = origin[1], ozo = origin[2];
311 const float sx = spacing[0], sy = spacing[1], sz = spacing[2];
312 const GridGeo g = geo;
313 Kokkos::parallel_for(
314 "nf::pore_pressure", R1(space, 0, np), KOKKOS_LAMBDA(int k) {
315 const Pore po = poresD(k);
316 const double gp[3] = {(po.x - oxo) / sx, (po.y - oyo) / sy, (po.z - ozo) / sz};
317 ppres(k) = kn::trilinear(g, p, gp);
318 });
319 space.fence();
320 }
321 {
322 auto hv = downloadN(ppres, static_cast<std::size_t>(np));
323 out.pore_pressure.assign(hv.begin(), hv.end());
324 }
325
326 // FLOW-basin labels for every cell: gradient ascent from ALL cells (not just sdf>0). Cut cells
327 // whose CENTER is inside the solid still carry real openness-weighted flux (the near-wall
328 // staircase); keyed on seg alone that flux bypasses the pore-pore interface (measured: 6% of
329 // the tube flux through the wall annulus, exactly the pore residual). Ascent assigns each such
330 // cell to the pore basin whose flow it carries; deep solid ends at a solid peak (negative
331 // label) and carries no flux.
332 Kokkos::View<int*, Mem> flowLab("nf::flowLab", n);
333 kn::gradientWalk(
334 space, geo, sdf, true, KOKKOS_LAMBDA(Index i, Index walker) {
335 flowLab(i) = seg(walker); // seg at a pore peak == its pore label id
336 });
337
338 // ---- per-patch throats: CCL over the interface FACES -------------------------------------
339 // A throat is a CONNECTED patch of open interface faces, not the whole label pair: two pores
340 // touching at two disjoint places (e.g. once directly and once through the periodic wrap) are
341 // two parallel throats — pair-keyed, their fluxes would merge and can cancel. Faces are
342 // identified by fid = 3*cell + d (the +d face of `cell`); adjacency is kernels::facesAdjacent.
343 // Each patch is keyed by its minimum fid, so the throat list is grid-deterministic.
344 // Two-tier patching: CORE faces — both cells FLUID-centered (sdf > 0) — define the patches by
345 // CCL; FILM faces (at least one solid-centered cell; they exist only because the flow basins
346 // extend into the cut-cell staircase) then ATTACH to the minimum reachable core patch by label
347 // propagation, so a film can never bridge two core patches into one. The fluid-centered
348 // criterion is geometric and parameter-free: a wall-hugging film cannot contain a core face by
349 // construction (measured: an openness threshold could not separate two capsule throats bridged
350 // by a wall film whose staircase faces reach openness ~0.7). Films reaching no core patch
351 // become their own patches. Every interface face lands in exactly one patch — exact bookkeeping.
352 const std::size_t nf = 3 * n;
353 Kokkos::View<Index*, Mem> fpar("nf::fpar", nf); // final patch label per face, -1 = none
354 Kokkos::View<Index*, Mem> fpair("nf::fpair", nf); // (lo<<32|hi) pair key per face
355 {
356 Kokkos::View<Index*, Mem> par("nf::fccl", nf); // CCL parents (core, then leftover)
357 Kokkos::View<char*, Mem> core("nf::fcore", nf);
358 Kokkos::View<int, Mem> changed("nf::fchanged");
359 kn::faceInit(space, geo, flowLab, sdf, ox, oy, oz, hasOpen, core, par, fpair);
360 // one CCL fixpoint over the faces with par >= 0 (used twice: core tier, then leftover films)
361 auto cclPass = [&]() {
362 kn::cclFixpoint(space, par, nf, changed,
363 [&]() { kn::faceMerge(space, geo, par, fpair, changed); });
364 };
365 // single-rank: the CCL root IS the min fid of the patch (parents are fids, union-by-min)
366 auto rootOf = KOKKOS_LAMBDA(Index parent) {
367 return parent;
368 };
369 cclPass(); // core tier
370 kn::faceLabelCore(space, nf, par, fpair, fpar, rootOf);
371 // film attachment: min reachable core-patch label, propagated through films (Jacobi min)
372 const kn::FlatFaces<Kokkos::View<Index*, Mem>> nbLab{fpar}, nbPair{fpair};
373 int h_changed = 1;
374 while (h_changed) {
375 Kokkos::deep_copy(changed, 0);
376 kn::filmAttachSweep(space, geo, fpar, fpair, core, nbLab, nbPair, changed);
377 h_changed = readScalar(changed);
378 }
379 // leftover films (no core patch reachable): their own patches by a second CCL tier
380 kn::leftoverInit(space, nf, fpar, par);
381 cclPass();
382 kn::faceLabelLeftover(space, nf, par, fpar, rootOf);
383 }
384 // unique patch roots -> throat slots, ordered by (pair, root fid)
385 kn::ThroatSlots slots;
386 {
387 Kokkos::View<Index*, Mem> rbuf("nf::rbuf", nf ? nf : 1), pbuf("nf::pbuf", nf ? nf : 1);
388 Kokkos::View<int, Mem> rcnt("nf::rcnt");
389 Kokkos::deep_copy(rcnt, 0);
390 Kokkos::parallel_for(
391 "nf::face_roots", R1(space, 0, nf), KOKKOS_LAMBDA(std::size_t f) {
392 if (fpar(f) == Index(f)) {
393 const int s0 = Kokkos::atomic_fetch_add(&rcnt(), 1);
394 rbuf(s0) = f;
395 pbuf(s0) = fpair(f);
396 }
397 });
398 space.fence();
399 const std::size_t nr = std::size_t(readScalar(rcnt));
400 auto hr = downloadN(rbuf, nr);
401 auto hp = downloadN(pbuf, nr);
402 std::vector<std::pair<Index, Index>> rootPair(nr);
403 for (std::size_t i = 0; i < nr; ++i)
404 rootPair[i] = {hr[i], hp[i]};
405 slots = kn::throatSlots(std::move(rootPair));
406 }
407 out.throats = slots.throats;
408 const std::size_t nt = out.throats.size();
409 Kokkos::View<Index*, Mem> keyD("nf::keys", nt);
410 Kokkos::View<int*, Mem> slotD("nf::slots", nt);
411 uploadVec(slots.keySorted, keyD);
412 uploadVec(slots.slotOf, slotD);
413
414 // Min-image anchor per pore: its PEAK VOXEL center — integer coordinates, so the periodic-image
415 // branch is deterministic (the refined float centroid wobbles ~1e-8 under CUDA FMA
416 // contraction, which flips the image of faces exactly half a period away — measured on a
417 // symmetric sphere lattice).
418 const auto peakH = downloadN(peak, static_cast<std::size_t>(np));
419 std::vector<double> ancX(np), ancY(np), ancZ(np);
420 for (int k = 0; k < np; ++k) {
421 const int pk = peakH[k];
422 ancX[k] = origin[0] + (pk % res.x) * double(spacing[0]);
423 ancY[k] = origin[1] + ((pk / res.x) % res.y) * double(spacing[1]);
424 ancZ[k] = origin[2] + (pk / (res.x * res.y)) * double(spacing[2]);
425 }
426 Kokkos::View<double*, Mem> ancXD("nf::ancX", np), ancYD("nf::ancY", np), ancZD("nf::ancZ", np);
427 uploadVec(ancX, ancXD);
428 uploadVec(ancY, ancYD);
429 uploadVec(ancZ, ancZD);
430
431 // accumulate: openness-weighted MAC face fluxes over every flow-basin boundary face, plus the
432 // area-weighted throat centroid (min-imaged relative to the lower pore's anchor).
433 Kokkos::View<double*, Mem> Q("nf::Q", nt), A("nf::A", nt), resid("nf::resid", np);
434 Kokkos::View<double*, Mem> Cx("nf::Cx", nt), Cy("nf::Cy", nt), Cz("nf::Cz", nt);
435 kn::throatFlux(space, geo, flowLab, u, v, w, ox, oy, oz, hasOpen, fpar, keyD, slotD, nt, ancXD,
436 ancYD, ancZD, origin, spacing, resolution, Q, A, Cx, Cy, Cz, resid);
437 std::vector<double> hcx = downloadN(Cx, nt), hcy = downloadN(Cy, nt), hcz = downloadN(Cz, nt);
438 {
439 auto hq = downloadN(Q, nt);
440 auto ha = downloadN(A, nt);
441 auto hr = downloadN(resid, static_cast<std::size_t>(np));
442 out.throat_flow.assign(hq.begin(), hq.end());
443 out.throat_area.assign(ha.begin(), ha.end());
444 out.pore_residual.assign(hr.begin(), hr.end());
445 }
446
447 // total-pressure drop per throat (throat-anchored two-leg min-image path)
448 out.throat_dp =
449 kn::throatPressureDrops(out.throats, out.pores, out.pore_pressure, out.throat_area, hcx, hcy,
450 hcz, ancX, ancY, ancZ, spacing, resolution, grad_p);
451 return out;
452}
453
457 std::vector<Pore> pores;
458 std::vector<int> seg;
459 std::vector<std::pair<int, int>> connections;
460};
461
462// ---- fused pipeline (F1): upload the SDF ONCE, keep it + the segmentation device-resident across
463// all three stages (extract_pores → segment_volume → extract_topology), so neither the SDF nor seg
464// is re-uploaded or round-tripped between stages. Only the final results cross back to the host.
465// Each stage's result is identical to calling the three functions separately. ----
466inline PoreNetwork extract_pore_network_k(const std::vector<float>& sdf_h,
467 std::array<int, 3> resolution,
468 std::array<float, 3> origin,
469 std::array<float, 3> spacing) {
470 PoreNetwork out;
471 if (sdf_h.empty())
472 return out;
473 Kokkos::View<float*, Mem> sdf("sdf", sdf_h.size());
474 uploadVec(sdf_h, sdf);
475 out.pores = extractPoresView(sdf, resolution, origin, spacing);
476 Kokkos::View<int*, Mem> seg = segmentVolumeView(sdf, resolution, spacing); // stays on device
477 out.connections = extractTopologyView(seg, resolution);
478 out.seg = downloadN(seg, sdf_h.size());
479 return out;
480}
481
482} // namespace pnm
483
484#endif // PECLET_PNM_PORE_EXTRACTION_HPP
Kokkos::RangePolicy< Exec > R1
void boundaryPairs(const Exec &space, const Geo &geo, const SegView &seg, const PairView &pairs, const CountView &cnt, Index maxPairs)
Boundary pairs: for every owned cell, the (min,max) label pairs across its +x/+y/+z faces where the l...
void cclFixpoint(const Exec &space, const ParentView &parent, std::size_t n, const FlagView &changed, const MergeFn &merge)
Union-find to fixpoint: merge() launches one union sweep (setting changed), then a flatten; repeat un...
void detectPores(const Exec &space, const Geo &geo, const SdfView &sdf, std::array< float, 3 > origin, std::array< float, 3 > spacing, const PoreView &pores, const CountView &counter, int maxPores)
Emit the owned peaks (local SDF maxima, sdf > 0) as Pore records into pores (atomic slot counter coun...
void floodFixpoint(const Exec &space, const Geo &geo, const SdfView &sdf, LabelView &labels, LabelView &labelsN, const FlagView &changed, const SyncFn &sync)
Jacobi flood to fixpoint.
void gradientWalk(const Exec &space, const GridGeo &geo, const SdfView &sdf, bool ascentAll, const EmitFn &emit)
Single-rank gradient walk: from every owned cell follow the steepest neighbour (ascent for pores,...
std::vector< std::pair< int, int > > uniquePairs(const PairView &pairs, std::size_t count)
Interleaved device pairs -> host (l1,l2) list, sorted and unique.
void initMarkers(const Exec &space, const Geo &geo, const SdfView &sdf, float thr, const ParentView &parent)
Deep-solid markers: parent(o) = o where sdf < thr, else -1 (owned-dense).
void cclMergeMarkers(const Exec &space, const Geo &geo, const ParentView &parent, const FlagView &changed)
One union sweep of the marker CCL (26-connectivity, 13 forward neighbours, union-by-min via atomic_mi...
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< 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 — only ...
NetworkFlow extract_network_flow_k(const std::vector< float > &sdf_h, std::array< int, 3 > resolution, 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)
Kokkos::View< int *, Mem > segmentVolumeView(const Kokkos::View< float *, Mem > &sdf, std::array< int, 3 > resolution, std::array< float, 3 > spacing, Kokkos::View< int *, Mem > *rootsOut=nullptr)
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< Pore > extractPoresView(const Kokkos::View< float *, Mem > &sdf, 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)
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)
peclet.pnm — the ONE set of stage kernels shared by the single-rank (pore_extraction....
Single-rank geometry policy: the field IS the whole periodic grid, owned == field coords,...
static GridGeo of(I3 res)
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
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