pnm 1.0.3
peclet.pnm — pore-network extraction from SDF geometry (Kokkos)
Loading...
Searching...
No Matches
pnm_bindings.cpp
Go to the documentation of this file.
1
30#include <nanobind/nanobind.h>
31#include <nanobind/ndarray.h>
32#include <nanobind/stl/optional.h> // optional openness arrays, optional shape_zyx
33#include <nanobind/stl/string.h>
34#include <nanobind/stl/vector.h> // list[Pore] and the z-y-x triples
35
36#include <array>
37#include <cstddef>
38#include <cstdint>
39#include <cstdio>
40#include <cstdlib>
41#include <Kokkos_Core.hpp>
42#include <optional>
43#include <type_traits>
44#include <utility>
45#include <vector>
46
47#include "peclet/core/python/kokkos_teardown.hpp"
48#include "peclet/core/python/ndarray_interop.hpp"
49#include "pore_extraction.hpp"
50#include "sdf_reader.h"
51
52#ifdef PECLET_PNM_MPI
53#include <mpi.h>
54
56#endif
57
58namespace nb = nanobind;
59using pnm::Pore;
60
61#ifdef PECLET_PNM_MPI
62// Ensure MPI_Init has been called (mirrors the flow/dem idiom); safe to call repeatedly. If WE
63// initialized MPI (no mpi4py in the driver), also finalize it at exit so mpirun sees a clean
64// shutdown; when mpi4py did the init, its own atexit hook finalizes.
65static void ensure_mpi_init() {
66 int inited = 0;
67 MPI_Initialized(&inited);
68 if (!inited) {
69 int argc = 0;
70 char** argv = nullptr;
71 MPI_Init(&argc, &argv);
72 std::atexit([]() {
73 int fin = 0;
74 MPI_Finalized(&fin);
75 if (!fin)
76 MPI_Finalize();
77 });
78 }
79}
80#endif
81
82// C-order (Nz,Ny,Nx) float SDF -> flat x-fastest vector + res = (Nx,Ny,Nz).
83static std::vector<float> to_sdf(nb::ndarray<float, nb::c_contig> a, std::array<int, 3>& res) {
84 if (a.ndim() != 3)
85 throw std::runtime_error("SDF array must be 3D (Nz,Ny,Nx)");
86 res = {(int)a.shape(2), (int)a.shape(1), (int)a.shape(0)};
87 return peclet::core::python::ndarray_to_vector<float>(nb::ndarray<>(a));
88}
89
90// C-order (Nz,Ny,Nx) double field -> flat x-fastest vector, shape-checked against the SDF's res.
91static std::vector<double> to_field(nb::ndarray<double, nb::c_contig> a,
92 const std::array<int, 3>& res, const char* name) {
93 if (a.ndim() != 3 || (int)a.shape(2) != res[0] || (int)a.shape(1) != res[1] ||
94 (int)a.shape(0) != res[2])
95 throw std::runtime_error(std::string(name) + " must be (Nz,Ny,Nx) matching the SDF");
96 return peclet::core::python::ndarray_to_vector<double>(nb::ndarray<>(a));
97}
98
99// ---- outputs: host vectors moved into capsule-owned NumPy arrays (no element-wise boxing) ----
100
101// Flat x-fastest labels of an (Nx,Ny,Nz) grid -> int32 (Nz,Ny,Nx) C-order array over the SAME
102// memory (an (Nz,Ny,Nx) C-order buffer is x-fastest, so this is a re-shape, not a transpose).
103static nb::ndarray<nb::numpy, int> labels_to_ndarray(std::vector<int>&& seg,
104 const std::array<int, 3>& res) {
105 const std::size_t nx = res[0], ny = res[1], nz = res[2];
106 if (seg.size() != nx * ny * nz)
107 throw std::runtime_error("internal: segmentation size does not match the grid");
108 return peclet::core::python::vector_to_ndarray<int>(std::move(seg), {nz, ny, nx},
109 {std::int64_t(ny * nx), std::int64_t(nx), 1});
110}
111
112// Label pairs -> (M,2) int32 array over the pair vector's own storage.
113static nb::ndarray<nb::numpy, int> pairs_to_ndarray(std::vector<std::pair<int, int>>&& v) {
114 using P = std::pair<int, int>;
115 static_assert(sizeof(P) == 2 * sizeof(int) && std::is_standard_layout_v<P>,
116 "std::pair<int,int> must be two adjacent ints to alias it as an (M,2) array");
117 const std::size_t m = v.size();
118 auto* held = new std::vector<P>(std::move(v));
119 nb::capsule owner(held, [](void* p) noexcept { delete static_cast<std::vector<P>*>(p); });
120 return nb::ndarray<nb::numpy, int>(reinterpret_cast<int*>(held->data()), {m, 2}, owner, {2, 1},
121 nb::dtype<int>(), nb::device::cpu::value, 0);
122}
123
124// Per-pore / per-throat scalars -> 1-D float64 array.
125static nb::ndarray<nb::numpy, double> doubles_to_ndarray(std::vector<double>&& v) {
126 const std::size_t n = v.size();
127 return peclet::core::python::vector_to_ndarray<double>(std::move(v), {n}, {1});
128}
129
130// The network-flow result as the documented dict (pores: list[Pore]; everything else ndarray).
131static nb::dict network_flow_dict(pnm::NetworkFlow&& net) {
132 nb::dict d;
133 d["pores"] = net.pores;
134 d["pore_pressure"] = doubles_to_ndarray(std::move(net.pore_pressure));
135 d["pore_residual"] = doubles_to_ndarray(std::move(net.pore_residual));
136 d["throats"] = pairs_to_ndarray(std::move(net.throats));
137 d["throat_flow"] = doubles_to_ndarray(std::move(net.throat_flow));
138 d["throat_area"] = doubles_to_ndarray(std::move(net.throat_area));
139 d["throat_dp"] = doubles_to_ndarray(std::move(net.throat_dp));
140 return d;
141}
142
143// Common docstring tail of extract_network_flow / extract_network_flow_mpi (nanobind keeps the
144// doc POINTER, so the assembled strings below are function-local statics).
145static constexpr const char* kNetworkFlowDictDoc =
146 "\n\nReturns a dict: 'pores' (list[Pore], ordered by label: pores[k] is label k+1), "
147 "'pore_pressure' and 'pore_residual' (float64 (N,)), 'throats' ((M,2) int32 label pairs "
148 "a < b, per PATCH), 'throat_flow', 'throat_area' and 'throat_dp' (float64 (M,)).\n\n"
149 "PRECONDITION (cross-repo, not checkable here): the openness arrays must be the face "
150 "openness the velocity field was projected with. For peclet.flow's cut-cell IBM that means "
151 "the solid was set with set_solid(..., cutcell_pressure=True) — without it every openness "
152 "flow reports is 0, so every throat_flow and throat_area comes back 0; for the ghost-cell "
153 "IBM (set_ghost_projection) pass get_ox_proj()/get_oy_proj()/get_oz_proj() instead.";
154
155NB_MODULE(_pnm, m) {
156 m.attr("__doc__") =
157 "pnm — pore-network extraction from SDF geometry (Kokkos).\n\n"
158 "Arrays are (Nz,Ny,Nx) C-order and every describing triple is z-y-x with a `_zyx` suffix. "
159 "Precision: the SDF is float32 and the geometry kernels compute in float32; origin_zyx / "
160 "spacing_zyx are doubles narrowed to float32 (pore centres and radii are float32 in the "
161 "input "
162 "unit system); the network-flow MAC fields (u, v, w, p, ox, oy, oz) are float64 and their "
163 "sums are double. SDF sign: negative inside the solid, positive in the pore space.";
164 // Kokkos init + the release-then-finalize atexit hook + finalize() + execution_space: the
165 // suite-wide teardown pattern (peclet/core/python/kokkos_teardown.hpp). pnm's functions hold no
166 // Kokkos state between calls and return host-vector-backed arrays, so its registry stays empty;
167 // the atexit Kokkos::finalize is still REQUIRED on CUDA (else cudaErrorCudartUnloading at exit).
168 peclet::core::python::install(m);
169
170 // VTI reader (pure C++; sdf_reader.cpp). Returns (sdf_3d[nz,ny,nx], origin_zyx, spacing_zyx).
171 nb::class_<SDFReader>(m, "SDFReader",
172 "VTI (VTK ImageData) reader for a signed-distance-field volume. Stateless: "
173 "the class exists only to hold read_vti as a static method, not to be "
174 "instantiated — call SDFReader.read_vti(filename) directly.")
175 .def_static(
176 "read_vti",
177 [](const std::string& filename) {
178 auto* data = new SDFData(SDFReader::read_vti(filename));
179 std::size_t shape[3]{(std::size_t)data->resolution[2], (std::size_t)data->resolution[1],
180 (std::size_t)data->resolution[0]};
181 std::vector<double> org{data->origin[2], data->origin[1], data->origin[0]};
182 std::vector<double> spc{data->spacing[2], data->spacing[1], data->spacing[0]};
183 // C-contiguous (nz,ny,nx) float array referencing the reader's buffer; the capsule
184 // keeps the SDFData alive for exactly as long as Python holds the array.
185 nb::capsule owner(data, [](void* p) noexcept { delete static_cast<SDFData*>(p); });
186 nb::ndarray<nb::numpy, float> sdf_3d(data->sdf_values.data(), 3, shape, owner);
187 return nb::make_tuple(sdf_3d, org, spc);
188 },
189 "Reads a VTI (VTK ImageData) SDF volume. Returns the tuple (sdf, origin_zyx, "
190 "spacing_zyx), in that order: sdf a float32 (Nz,Ny,Nx) C-order array over the "
191 "reader's buffer (no copy), origin_zyx and spacing_zyx the z-y-x lists of the grid "
192 "origin and cell size (float64).");
193
194 nb::class_<Pore>(m, "Pore",
195 "One detected pore: a strict local maximum of the SDF over its 26-neighbourhood "
196 "(periodic in all three directions). The centre is the peak voxel's position "
197 "plus a squared-SDF-weighted sub-voxel offset over the 3x3x3 stencil, in the "
198 "unit system of origin_zyx / spacing_zyx; the radius is the SDF value at the "
199 "peak, i.e. the physical inscribed-sphere radius. All fields are float32. "
200 "The centre is exposed as three self-named scalars x, y, z (not a triple), so "
201 "no axis order is implied and no _zyx suffix applies — the documented "
202 "exception to NAMING.md 1.7.")
203 .def(nb::init<>())
204 .def(
205 "__init__",
206 [](Pore* p, float x, float y, float z, float radius) { new (p) Pore{x, y, z, radius}; },
207 nb::arg("x"), nb::arg("y"), nb::arg("z"), nb::arg("radius"))
208 .def_rw("x", &Pore::x, "Centre x coordinate (physical units of origin_zyx / spacing_zyx).")
209 .def_rw("y", &Pore::y, "Centre y coordinate.")
210 .def_rw("z", &Pore::z, "Centre z coordinate.")
211 .def_rw("radius", &Pore::radius,
212 "Inscribed-sphere radius = the SDF value at the peak voxel (physical units).")
213 .def("__repr__", [](const Pore& p) {
214 char buf[128];
215 std::snprintf(buf, sizeof buf, "Pore(x=%g, y=%g, z=%g, radius=%g)", (double)p.x,
216 (double)p.y, (double)p.z, (double)p.radius);
217 return std::string(buf);
218 });
219
220 m.def(
221 "extract_pores",
222 [](nb::ndarray<float, nb::c_contig> sdf, std::vector<double> origin_zyx,
223 std::vector<double> spacing_zyx) {
224 std::array<int, 3> res;
225 auto v = to_sdf(sdf, res);
226 std::array<float, 3> org{(float)origin_zyx[2], (float)origin_zyx[1], (float)origin_zyx[0]};
227 std::array<float, 3> spc{(float)spacing_zyx[2], (float)spacing_zyx[1],
228 (float)spacing_zyx[0]};
229 return pnm::extract_pores_k(v, res, org, spc);
230 },
231 nb::arg("sdf"), nb::arg("origin_zyx"), nb::arg("spacing_zyx"),
232 "Pore detection: every voxel with sdf > 0 that is a strict local maximum of the SDF over its "
233 "26 neighbours (periodic wrap in x, y, z; ties broken towards the higher flat index) becomes "
234 "a Pore. sdf is a float32 (Nz,Ny,Nx) C-order array; origin_zyx / spacing_zyx are the grid's "
235 "z-y-x origin and cell size (narrowed to float32). Returns the list of Pore(x, y, z, radius) "
236 "in the input unit system, in device-completion order (NOT sorted; sort by (z, y, x) for a "
237 "reproducible order). Capped at 1e6 pores.");
238
239 m.def(
240 "segment_volume",
241 [](nb::ndarray<float, nb::c_contig> sdf, std::vector<double> spacing_zyx) {
242 std::array<int, 3> res;
243 auto v = to_sdf(sdf, res);
244 std::array<float, 3> spc{(float)spacing_zyx[2], (float)spacing_zyx[1],
245 (float)spacing_zyx[0]};
246 return labels_to_ndarray(pnm::segment_volume_k(v, res, spc), res);
247 },
248 nb::arg("sdf"), nb::arg("spacing_zyx"),
249 "Marker-controlled watershed segmentation of the SDF grid. Returns an int32 (Nz,Ny,Nx) "
250 "C-order label array (sdf.shape; the same memory as the kernels' flat x-fastest label "
251 "vector — no copy, no reshape needed): pore voxels (sdf > 0) carry the "
252 "id of the pore basin they belong to, 1, 2, ... in first-encounter (flat-index) order of the "
253 "basin peaks — the same peaks extract_pores finds — assigned by a gradient-ascent path to "
254 "the local SDF maximum; solid voxels (sdf <= 0) carry the NEGATIVE id -1, -2, ... of their "
255 "connected solid grain (26-connected components of the deep solid, sdf < -1.5 * min "
256 "spacing, flooded outwards by a Jacobi min-label sweep), and 0 marks solid debris no grain "
257 "reached. Periodic in all three directions. spacing_zyx only sets the deep-solid marker "
258 "threshold.");
259
260 m.def(
261 "extract_topology",
262 [](nb::ndarray<const int, nb::c_contig> segmentation,
263 std::optional<std::vector<int>> shape_zyx) {
264 std::array<int, 3> res;
265 if (segmentation.ndim() == 3) {
266 res = {(int)segmentation.shape(2), (int)segmentation.shape(1),
267 (int)segmentation.shape(0)};
268 if (shape_zyx && (shape_zyx->size() != 3 || (*shape_zyx)[0] != res[2] ||
269 (*shape_zyx)[1] != res[1] || (*shape_zyx)[2] != res[0]))
270 throw std::runtime_error("shape_zyx does not match the (Nz,Ny,Nx) segmentation");
271 } else if (segmentation.ndim() == 1) {
272 if (!shape_zyx)
273 throw std::runtime_error("a flat segmentation needs shape_zyx = (Nz, Ny, Nx)");
274 if (shape_zyx->size() != 3)
275 throw std::runtime_error("shape_zyx must be (Nz, Ny, Nx)");
276 res = {(*shape_zyx)[2], (*shape_zyx)[1], (*shape_zyx)[0]};
277 if (segmentation.size() != std::size_t(res[0]) * res[1] * res[2])
278 throw std::runtime_error("segmentation length does not match shape_zyx");
279 } else {
280 throw std::runtime_error("segmentation must be (Nz,Ny,Nx) or flat");
281 }
282 return pairs_to_ndarray(
283 pnm::extract_topology_k(segmentation.data(), segmentation.size(), res));
284 },
285 nb::arg("segmentation"), nb::arg("shape_zyx") = nb::none(),
286 "Label adjacency of a segment_volume result: the sorted, unique (a, b) pairs with a < b of "
287 "labels that share a voxel face (+x, +y, +z, periodic wrap), as an (M,2) int32 array. "
288 "segmentation is the int32 (Nz,Ny,Nx) array segment_volume returned (read in place, no "
289 "copy); a flat x-fastest label vector is accepted too, then shape_zyx = (Nz, Ny, Nx) of "
290 "the grid it was made on (= sdf.shape) is required. Rows with a label <= 0 are pore-solid "
291 "(or grain-grain / debris) contacts; the pore-to-pore throats are the rows with both "
292 "labels > 0. One row per label pair (a per-PATCH throat list, which can repeat a pair, is "
293 "what extract_network_flow returns).");
294
295 // Fused pipeline (F1): SDF uploaded once, segmentation device-resident across all three stages.
296 m.def(
297 "extract_pore_network",
298 [](nb::ndarray<float, nb::c_contig> sdf, std::vector<double> origin_zyx,
299 std::vector<double> spacing_zyx) {
300 std::array<int, 3> res;
301 auto v = to_sdf(sdf, res);
302 std::array<float, 3> org{(float)origin_zyx[2], (float)origin_zyx[1], (float)origin_zyx[0]};
303 std::array<float, 3> spc{(float)spacing_zyx[2], (float)spacing_zyx[1],
304 (float)spacing_zyx[0]};
305 pnm::PoreNetwork net = pnm::extract_pore_network_k(v, res, org, spc);
306 return nb::make_tuple(net.pores, labels_to_ndarray(std::move(net.seg), res),
307 pairs_to_ndarray(std::move(net.connections)));
308 },
309 nb::arg("sdf"), nb::arg("origin_zyx"), nb::arg("spacing_zyx"),
310 "Fused extraction (SDF uploaded once, segmentation device-resident across stages): returns "
311 "the tuple (pores, segmentation, connections), in that order, each element exactly what "
312 "extract_pores, segment_volume and extract_topology return (list[Pore]; int32 (Nz,Ny,Nx) "
313 "array; (M,2) int32 array).\n\n"
314 "UNITS: everything is in the system `origin_zyx` / `spacing_zyx` and the SDF are stated in "
315 "(the VTI's own, straight from SDFReader.read_vti) — pore centres are physical coordinates "
316 "and a pore radius is the physical inscribed radius, not a voxel count. Pass "
317 "spacing_zyx = [1,1,1] and origin_zyx = [0,0,0] to work in voxels. Measured on "
318 "flow/data/packing_ring.vti: doubling the spacing, the origin and the SDF together leaves "
319 "the pore count (7199) and the throat-connection count (53020) unchanged and doubles every "
320 "radius BITWISE. One sub-voxel caveat: the centre's intra-cell offset is guarded by an "
321 "absolute `sw > 1e-6` on a squared-distance weight sum, so a near-degenerate peak's centre "
322 "can shift by a fraction of a cell (measured up to 0.37 cells) under a change of unit "
323 "system. Radii, counts and topology are unaffected.");
324
325 // Network flow: throat flow rates + pore-center pressures from a peclet.flow MAC field (the
326 // method transferred from the Voronoi PNM). Pass flow's fields transposed to (Nz,Ny,Nx):
327 // u = s.get_uf().T etc. (zero-copy view of the [x,y,z] Fortran array).
328 static const std::string kNetworkFlowDoc =
329 std::string(
330 "Pore-network FLOW data from a MAC field on the same grid as the SDF: segments the SDF, "
331 "then returns per-pore-label centers/pressures (trilinear p at the basin peak, periodic) "
332 "and per-throat flow rates (sum of openness-weighted MAC face fluxes o*u*A over the "
333 "label-interface faces; positive from the lower to the higher label). All arrays are "
334 "(Nz,Ny,Nx) C-order on the SDF grid: pass flow's fields as get_uf().T, get_p().T, "
335 "get_ox().T, ... u(i,j,k) is the -x face velocity of cell (i,j,k) (flow's MAC layout); "
336 "omit ox/oy/oz for a fully open grid. grad_p_zyx adds the macroscopic gradient along "
337 "the min-image pore-to-pore vector to throat_dp (= P_i - P_j, drives flow i->j when "
338 "positive). pore_residual is the signed flux sum over each pore's whole boundary — "
339 "~solver tolerance when u is flow's projected divergence-free field. Throats are "
340 "PER-PATCH (a connected patch of interface faces): two disjoint interfaces between the "
341 "same two pores are separate parallel throats, so the throat list can repeat a label "
342 "pair.") +
344 m.def(
345 "extract_network_flow",
346 [](nb::ndarray<float, nb::c_contig> sdf, std::vector<double> origin_zyx,
347 std::vector<double> spacing_zyx, nb::ndarray<double, nb::c_contig> u,
348 nb::ndarray<double, nb::c_contig> v, nb::ndarray<double, nb::c_contig> w,
349 nb::ndarray<double, nb::c_contig> p, std::optional<nb::ndarray<double, nb::c_contig>> ox,
350 std::optional<nb::ndarray<double, nb::c_contig>> oy,
351 std::optional<nb::ndarray<double, nb::c_contig>> oz, std::vector<double> grad_p_zyx) {
352 std::array<int, 3> res;
353 auto sv = to_sdf(sdf, res);
354 if ((bool)ox != (bool)oy || (bool)ox != (bool)oz)
355 throw std::runtime_error("pass all three openness arrays (ox,oy,oz) or none");
356 std::array<float, 3> org{(float)origin_zyx[2], (float)origin_zyx[1], (float)origin_zyx[0]};
357 std::array<float, 3> spc{(float)spacing_zyx[2], (float)spacing_zyx[1],
358 (float)spacing_zyx[0]};
359 std::array<double, 3> gp{grad_p_zyx[2], grad_p_zyx[1], grad_p_zyx[0]};
361 sv, res, org, spc, to_field(u, res, "u"), to_field(v, res, "v"), to_field(w, res, "w"),
362 to_field(p, res, "p"), ox ? to_field(*ox, res, "ox") : std::vector<double>{},
363 oy ? to_field(*oy, res, "oy") : std::vector<double>{},
364 oz ? to_field(*oz, res, "oz") : std::vector<double>{}, gp);
365 return network_flow_dict(std::move(net));
366 },
367 nb::arg("sdf"), nb::arg("origin_zyx"), nb::arg("spacing_zyx"), nb::arg("u"), nb::arg("v"),
368 nb::arg("w"), nb::arg("p"), nb::arg("ox") = nb::none(), nb::arg("oy") = nb::none(),
369 nb::arg("oz") = nb::none(), nb::arg("grad_p_zyx") = std::vector<double>{0.0, 0.0, 0.0},
370 kNetworkFlowDoc.c_str());
371
372#ifdef PECLET_PNM_MPI
373 // Distributed path (built with -DPECLET_PNM_MPI=ON): the SDF is decomposed over ranks by the
374 // shared core ORB (same deterministic partition as flow/dem). Query this rank's block with
375 // mpi_block, pass the LOCAL block's SDF to extract_pore_network_mpi. Bit-exact to single-rank.
376 // Rank / size queries are mpi4py's job (MPI.COMM_WORLD.rank / .size), not this module's.
377 m.def(
378 "mpi_block",
379 [](std::vector<int> global_shape_zyx) {
380 ensure_mpi_init();
381 if (global_shape_zyx.size() != 3)
382 throw std::runtime_error("global_shape_zyx must be (Nz, Ny, Nx)");
383 std::array<int, 3> gd{global_shape_zyx[2], global_shape_zyx[1], global_shape_zyx[0]};
384 std::array<int, 3> o{}, s{};
385 pnm::mpi_block_of(gd, MPI_COMM_WORLD, o, s);
386 return nb::make_tuple(nb::make_tuple(o[2], o[1], o[0]), nb::make_tuple(s[2], s[1], s[0]));
387 },
388 nb::arg("global_shape_zyx"),
389 "This rank's ORB block of the global (Nz,Ny,Nx) grid (collective; MPI_Init is called if "
390 "needed): returns the tuple (offset_zyx, shape_zyx), in that order — two int 3-tuples, the "
391 "block's first VOXEL index per axis and its voxel count per axis. offset_zyx is an integer "
392 "grid offset, NOT the physical origin_zyx the extraction functions take (that stays the "
393 "global grid's). Slice the global SDF as sdf[oz:oz+sz, oy:oy+sy, ox:ox+sx] and pass the "
394 "block to extract_pore_network_mpi / extract_network_flow_mpi.");
395 m.def(
396 "extract_pore_network_mpi",
397 [](nb::ndarray<float, nb::c_contig> sdf_local, std::vector<int> global_shape_zyx,
398 std::vector<double> origin_zyx, std::vector<double> spacing_zyx) {
399 ensure_mpi_init();
400 std::array<int, 3> res;
401 auto v = to_sdf(sdf_local, res);
402 std::array<int, 3> gd{global_shape_zyx[2], global_shape_zyx[1], global_shape_zyx[0]};
403 std::array<int, 3> bo{}, bs{};
404 pnm::mpi_block_of(gd, MPI_COMM_WORLD, bo, bs);
405 if (res != bs)
406 throw std::runtime_error("sdf_local shape does not match this rank's mpi_block");
407 std::array<float, 3> org{(float)origin_zyx[2], (float)origin_zyx[1], (float)origin_zyx[0]};
408 std::array<float, 3> spc{(float)spacing_zyx[2], (float)spacing_zyx[1],
409 (float)spacing_zyx[0]};
410 auto net = pnm::extract_pore_network_mpi(v, gd, org, spc, MPI_COMM_WORLD);
411 return nb::make_tuple(net.pores, labels_to_ndarray(std::move(net.seg), res),
412 pairs_to_ndarray(std::move(net.connections)));
413 },
414 nb::arg("sdf_local"), nb::arg("global_shape_zyx"), nb::arg("origin_zyx"),
415 nb::arg("spacing_zyx"),
416 "Distributed fused extraction (collective over MPI_COMM_WORLD). sdf_local is this rank's "
417 "ORB block (Nz,Ny,Nx C-order, from mpi_block); origin_zyx / spacing_zyx are the GLOBAL "
418 "grid's. Returns the tuple (pores, segmentation, connections), in that order: the pores "
419 "whose peak this rank owns (list[Pore]), this rank's block of the segmentation (int32 "
420 "array of sdf_local.shape, global label ids), and the GLOBAL connection list ((M,2) int32, "
421 "identical on every rank). Bit-exact to the single-rank extract_pore_network on the "
422 "gathered grid.");
423 static const std::string kNetworkFlowMpiDoc =
424 std::string(
425 "Distributed network-flow extraction (collective over MPI_COMM_WORLD): all arrays are "
426 "this rank's ORB block (Nz,Ny,Nx C-order, from mpi_block; MAC fields from a distributed "
427 "peclet.flow run on the SAME BlockDecomposer). Returns the same dict as "
428 "extract_network_flow, GLOBAL and identical on every rank.") +
430 m.def(
431 "extract_network_flow_mpi",
432 [](nb::ndarray<float, nb::c_contig> sdf_local, std::vector<int> global_shape_zyx,
433 std::vector<double> origin_zyx, std::vector<double> spacing_zyx,
434 nb::ndarray<double, nb::c_contig> u, nb::ndarray<double, nb::c_contig> v,
435 nb::ndarray<double, nb::c_contig> w, nb::ndarray<double, nb::c_contig> p,
436 std::optional<nb::ndarray<double, nb::c_contig>> ox,
437 std::optional<nb::ndarray<double, nb::c_contig>> oy,
438 std::optional<nb::ndarray<double, nb::c_contig>> oz, std::vector<double> grad_p_zyx) {
439 ensure_mpi_init();
440 std::array<int, 3> res;
441 auto sv = to_sdf(sdf_local, res);
442 std::array<int, 3> gd{global_shape_zyx[2], global_shape_zyx[1], global_shape_zyx[0]};
443 std::array<int, 3> bo{}, bs{};
444 pnm::mpi_block_of(gd, MPI_COMM_WORLD, bo, bs);
445 if (res != bs)
446 throw std::runtime_error("sdf_local shape does not match this rank's mpi_block");
447 if ((bool)ox != (bool)oy || (bool)ox != (bool)oz)
448 throw std::runtime_error("pass all three openness arrays (ox,oy,oz) or none");
449 std::array<float, 3> org{(float)origin_zyx[2], (float)origin_zyx[1], (float)origin_zyx[0]};
450 std::array<float, 3> spc{(float)spacing_zyx[2], (float)spacing_zyx[1],
451 (float)spacing_zyx[0]};
452 std::array<double, 3> gp{grad_p_zyx[2], grad_p_zyx[1], grad_p_zyx[0]};
454 sv, gd, org, spc, to_field(u, res, "u"), to_field(v, res, "v"), to_field(w, res, "w"),
455 to_field(p, res, "p"), ox ? to_field(*ox, res, "ox") : std::vector<double>{},
456 oy ? to_field(*oy, res, "oy") : std::vector<double>{},
457 oz ? to_field(*oz, res, "oz") : std::vector<double>{}, gp, MPI_COMM_WORLD);
458 return network_flow_dict(std::move(net));
459 },
460 nb::arg("sdf_local"), nb::arg("global_shape_zyx"), nb::arg("origin_zyx"),
461 nb::arg("spacing_zyx"), nb::arg("u"), nb::arg("v"), nb::arg("w"), nb::arg("p"),
462 nb::arg("ox") = nb::none(), nb::arg("oy") = nb::none(), nb::arg("oz") = nb::none(),
463 nb::arg("grad_p_zyx") = std::vector<double>{0.0, 0.0, 0.0}, kNetworkFlowMpiDoc.c_str());
464#endif
465}
static SDFData read_vti(const std::string &filename)
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)
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.
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)
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)
std::vector< std::pair< int, int > > extract_topology_k(const int *seg_h, std::size_t n, std::array< int, 3 > resolution)
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)
NB_MODULE(_pnm, m)
static std::vector< float > to_sdf(nb::ndarray< float, nb::c_contig > a, std::array< int, 3 > &res)
static std::vector< double > to_field(nb::ndarray< double, nb::c_contig > a, const std::array< int, 3 > &res, const char *name)
static nb::ndarray< nb::numpy, int > pairs_to_ndarray(std::vector< std::pair< int, int > > &&v)
static nb::dict network_flow_dict(pnm::NetworkFlow &&net)
static nb::ndarray< nb::numpy, int > labels_to_ndarray(std::vector< int > &&seg, const std::array< int, 3 > &res)
static nb::ndarray< nb::numpy, double > doubles_to_ndarray(std::vector< double > &&v)
static constexpr const char * kNetworkFlowDictDoc
peclet.pnm — portable (Kokkos) single-rank pore-network extraction from an SDF.
peclet.pnm — distributed (MPI) pore-network extraction on the core block decomposition.
Reader for SDF / VTI geometry (interface).
The full pore network from one extraction: pores, the per-voxel segmentation (flat),...