1"""Byte gate for the peclet.pnm public surface: SHA-256 of every output of every entry path.
3Run: PYTHONPATH=<pnm build dir> python tests/regression/state_hash.py [packing_ring.vti]
4(single-threaded: the script pins OMP_NUM_THREADS=1 before the module is imported; with an MPI
5build it also re-launches itself under `mpirun -np 2` for the `*_mpi` collectives).
7Two inputs: (i) the 2x2x2 periodic solid-sphere lattice of tests/synthetic_sdf.hpp
8(regenerated in numpy, grid 36x30x24, non-unit origin/spacing/grad_p exactly as in
9tests/kokkos_mpi/test_pnm_flow_mpi.cpp) and (ii) ../flow/data/packing_ring.vti (256^3; skipped
10with a message when absent). The MAC fields fed to extract_network_flow are the deterministic
11trigonometric fields of that MPI test — not a physical flow, the bookkeeping only needs
12consistency, and the gate only needs reproducibility.
14Hashed, per entry path: the pores as an (N, 4) float64 array [x, y, z, radius] sorted by
15(z, y, x) (extract_pores returns device-completion order), the segmentation as int32 bytes,
16the connections as an (M, 2) int32 array in the returned order, and every array of the
17network-flow dict (pores in label order, throats (M, 2) int32, the five float64 arrays).
18A refactor of the bindings or the kernels must reproduce every line of this output bitwise
19(QUALITY_PLAN §6: "a byte-comparison of the outputs against the pre-refactor build, not just
20a green battery"). Hashes recorded at each gate live in the commit messages.
28os.environ[
"OMP_NUM_THREADS"] =
"1"
29os.environ.setdefault(
"OMP_PROC_BIND",
"false")
33import peclet.pnm
as pnm
35HERE = os.path.dirname(os.path.abspath(__file__))
36PACKING_RING = os.path.join(HERE,
"..",
"..",
"..",
"flow",
"data",
"packing_ring.vti")
39LATTICE_GD_XYZ = (36, 30, 24)
40LATTICE_ORG_XYZ = (0.5, -1.0, 2.0)
41LATTICE_SPC_XYZ = (0.5, 1.0, 1.5)
42LATTICE_GP_XYZ = (1.0e-3, -2.0e-3, 0.5e-3)
46 return hashlib.sha256(np.ascontiguousarray(a).tobytes()).hexdigest()
51 z, y, x = np.meshgrid(np.arange(nz), np.arange(ny), np.arange(nx), indexing=
"ij")
56 """tests/synthetic_sdf.hpp sphereLatticeSdf, in float32 like the C++."""
57 x, y, z = (a.astype(np.float32)
for a
in grid_xyz(gd))
58 R = np.float32(0.22 * min(gd))
59 sdf = np.full(x.shape, 1e30, np.float32)
63 cc = [gd[i] * (2 * c + 1) / 4.0
for i, c
in enumerate((cx, cy, cz))]
64 dx, dy, dz = np.abs(x - cc[0]), np.abs(y - cc[1]), np.abs(z - cc[2])
65 dx = np.minimum(dx, gd[0] - dx)
66 dy = np.minimum(dy, gd[1] - dy)
67 dz = np.minimum(dz, gd[2] - dz)
68 sdf = np.minimum(sdf, (np.sqrt(dx * dx + dy * dy + dz * dz) - R).astype(np.float32))
73 """The seven deterministic float64 fields (u, v, w, p, ox, oy, oz) of test_pnm_flow_mpi.cpp
74 on the global grid `gd`, evaluated on the block [offset, offset + shape) (x-y-z)."""
75 shape_xyz = shape_xyz
or gd
77 gx, gy, gz = (a + o
for a, o
in zip((x, y, z), offset_xyz))
80 X, Y, Z = (2.0 * np.pi * g / n
for g, n
in zip((gx, gy, gz), gd))
82 return np.sin(X) * np.cos(Y) + 0.3 * np.cos(2 * Z)
84 return np.cos(X) * np.sin(Z) - 0.2 * np.sin(Y)
86 return np.sin(Y) * np.sin(Z) + 0.1 * np.cos(X)
88 return np.cos(X + Y) + 0.5 * np.sin(Z - X)
89 s = 0.5 + 0.5 * np.sin(X + 2 * Y - Z)
90 return np.where(s < 0.02, 0.0, s)
92 out = [fld(c, gx)
for c
in range(4)] + [fld(4, gx + c)
for c
in (4, 5, 6)]
93 return [np.ascontiguousarray(f, dtype=np.float64)
for f
in out]
97 a = np.array([(p.x, p.y, p.z, p.radius)
for p
in pores], np.float64).reshape(-1, 4)
99 a = a[np.lexsort((a[:, 0], a[:, 1], a[:, 2]))]
104 return np.ascontiguousarray(np.asarray(x, np.int32)).reshape(shape)
108 print(f
"{tag + '.pores':<44}{sha(pores_array(net['pores'], sort=False))} n={len(net['pores'])}")
109 th = np.asarray(net[
"throats"], np.int32).reshape(-1, 2)
110 assert isinstance(net[
"throats"], np.ndarray)
and net[
"throats"].dtype == np.int32
111 print(f
"{tag + '.throats':<44}{sha(th)} m={len(th)}")
112 for k
in (
"pore_pressure",
"pore_residual",
"throat_flow",
"throat_area",
"throat_dp"):
113 assert isinstance(net[k], np.ndarray)
and net[k].dtype == np.float64
114 print(f
"{tag + '.' + k:<44}{sha(np.asarray(net[k], np.float64))}")
117def run_case(tag, sdf, origin_zyx, spacing_zyx, grad_p_zyx):
118 nz, ny, nx = sdf.shape
120 pores = pnm.extract_pores(sdf, origin_zyx, spacing_zyx)
121 print(f
"{tag + '.extract_pores':<44}{sha(pores_array(pores))} n={len(pores)}")
122 seg = pnm.segment_volume(sdf, spacing_zyx)
123 assert isinstance(seg, np.ndarray)
and seg.shape == sdf.shape
and seg.dtype == np.int32
124 seg =
as_i32(seg, sdf.shape)
125 print(f
"{tag + '.segment_volume':<44}{sha(seg)}")
126 conns = pnm.extract_topology(seg, shape_zyx=sdf.shape)
127 assert isinstance(conns, np.ndarray)
and conns.dtype == np.int32
and conns.ndim == 2
128 conns =
as_i32(conns, (-1, 2))
129 print(f
"{tag + '.extract_topology':<44}{sha(conns)} m={len(conns)}")
130 fpores, fseg, fconns = pnm.extract_pore_network(sdf, origin_zyx, spacing_zyx)
131 assert isinstance(fseg, np.ndarray)
and fseg.shape == sdf.shape
and fseg.dtype == np.int32
132 assert isinstance(fconns, np.ndarray)
and fconns.dtype == np.int32
and fconns.ndim == 2
133 print(f
"{tag + '.fused.pores':<44}{sha(pores_array(fpores))} n={len(fpores)}")
134 print(f
"{tag + '.fused.seg':<44}{sha(as_i32(fseg, sdf.shape))}")
135 print(f
"{tag + '.fused.connections':<44}{sha(as_i32(fconns, (-1, 2)))} m={len(fconns)}")
136 net = pnm.extract_network_flow(sdf, origin_zyx, spacing_zyx, *fields[:4],
137 ox=fields[4], oy=fields[5], oz=fields[6],
138 grad_p_zyx=grad_p_zyx)
140 net = pnm.extract_network_flow(sdf, origin_zyx, spacing_zyx, *fields[:4],
141 grad_p_zyx=grad_p_zyx)
146 from mpi4py
import MPI
148 comm = MPI.COMM_WORLD
150 offset_zyx, shape_zyx = pnm.mpi_block(gshape)
151 oz, oy, ox = offset_zyx
152 sz, sy, sx = shape_zyx
153 local = np.ascontiguousarray(sdf[oz:oz + sz, oy:oy + sy, ox:ox + sx])
155 fields =
mac_fields((nx, ny, nz), offset_xyz=(ox, oy, oz), shape_xyz=(sx, sy, sz))
157 assert isinstance(seg, np.ndarray)
and seg.shape == local.shape
and seg.dtype == np.int32
158 assert isinstance(conns, np.ndarray)
and conns.dtype == np.int32
and conns.ndim == 2
159 segs = comm.gather(
as_i32(seg, local.shape).tobytes(), root=0)
160 pores_all = comm.gather(
pores_array(pores).tobytes(), root=0)
162 ox=fields[4], oy=fields[5], oz=fields[6],
163 grad_p_zyx=grad_p_zyx)
166 print(f
"{tag + f'.mpi{np_}.pores':<44}{hashlib.sha256(b''.join(pores_all)).hexdigest()}")
167 print(f
"{tag + f'.mpi{np_}.seg':<44}{hashlib.sha256(b''.join(segs)).hexdigest()}")
168 print(f
"{tag + f'.mpi{np_}.connections':<44}{sha(as_i32(conns, (-1, 2)))} m={len(conns)}")
174 list(LATTICE_SPC_XYZ[::-1]), list(LATTICE_GP_XYZ[::-1]))
175 if os.path.isfile(vti):
176 sdf, origin_zyx, spacing_zyx = pnm.SDFReader.read_vti(vti)
177 yield (
"packing_ring", sdf, list(origin_zyx), list(spacing_zyx), [0.0, 0.0, -1.0e-3])
179 print(f
"packing_ring: SKIPPED ({vti} not present)")
183 paths = [a
for a
in argv
if not a.startswith(
"--")]
184 vti = os.path.abspath(paths[0])
if paths
else os.path.normpath(PACKING_RING)
186 from mpi4py
import MPI
188 for c
in cases(vti, quiet=MPI.COMM_WORLD.rank != 0):
191 print(f
"peclet.pnm on {pnm.execution_space}, OMP_NUM_THREADS={os.environ['OMP_NUM_THREADS']}")
192 for c
in cases(vti, quiet=
False):
194 if hasattr(pnm,
"mpi_block"):
195 cmd = [
"mpirun",
"-np",
"2", sys.executable, os.path.abspath(__file__),
"--mpi", vti]
197 return subprocess.call(cmd, env=os.environ)
198 print(
"mpi: SKIPPED (module built without PECLET_PNM_MPI)")
202if __name__ ==
"__main__":
203 sys.exit(
main(sys.argv[1:]))
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.
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)
run_case_mpi(tag, sdf, origin_zyx, spacing_zyx, grad_p_zyx)
pores_array(pores, sort=True)
run_case(tag, sdf, origin_zyx, spacing_zyx, grad_p_zyx)
mac_fields(gd, offset_xyz=(0, 0, 0), shape_xyz=None)