pnm 1.0.3
peclet.pnm — pore-network extraction from SDF geometry (Kokkos)
Loading...
Searching...
No Matches
state_hash.py
Go to the documentation of this file.
1"""Byte gate for the peclet.pnm public surface: SHA-256 of every output of every entry path.
2
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).
6
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.
13
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.
21"""
22
23import hashlib
24import os
25import subprocess
26import sys
27
28os.environ["OMP_NUM_THREADS"] = "1"
29os.environ.setdefault("OMP_PROC_BIND", "false")
30
31import numpy as np # noqa: E402
32
33import peclet.pnm as pnm # noqa: E402
34
35HERE = os.path.dirname(os.path.abspath(__file__))
36PACKING_RING = os.path.join(HERE, "..", "..", "..", "flow", "data", "packing_ring.vti")
37
38# The sphere-lattice case of tests/kokkos_mpi/test_pnm_flow_mpi.cpp, in x-y-z (C++) order …
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)
43
44
45def sha(a):
46 return hashlib.sha256(np.ascontiguousarray(a).tobytes()).hexdigest()
47
48
49def grid_xyz(gd_xyz):
50 nx, ny, nz = gd_xyz
51 z, y, x = np.meshgrid(np.arange(nz), np.arange(ny), np.arange(nx), indexing="ij")
52 return x, y, z # each (Nz, Ny, Nx), x fastest
53
54
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)
60 for cz in (0, 1):
61 for cy in (0, 1):
62 for cx in (0, 1):
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))
69 return sdf
70
71
72def mac_fields(gd, offset_xyz=(0, 0, 0), shape_xyz=None):
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
76 x, y, z = grid_xyz(shape_xyz)
77 gx, gy, gz = (a + o for a, o in zip((x, y, z), offset_xyz))
78
79 def fld(c, gx):
80 X, Y, Z = (2.0 * np.pi * g / n for g, n in zip((gx, gy, gz), gd))
81 if c == 0:
82 return np.sin(X) * np.cos(Y) + 0.3 * np.cos(2 * Z)
83 if c == 1:
84 return np.cos(X) * np.sin(Z) - 0.2 * np.sin(Y)
85 if c == 2:
86 return np.sin(Y) * np.sin(Z) + 0.1 * np.cos(X)
87 if c == 3:
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)
91
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]
94
95
96def pores_array(pores, sort=True):
97 a = np.array([(p.x, p.y, p.z, p.radius) for p in pores], np.float64).reshape(-1, 4)
98 if sort and len(a):
99 a = a[np.lexsort((a[:, 0], a[:, 1], a[:, 2]))]
100 return a
101
102
103def as_i32(x, shape):
104 return np.ascontiguousarray(np.asarray(x, np.int32)).reshape(shape)
105
106
107def report_network(tag, net):
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 # NEWAPI
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 # NEWAPI
114 print(f"{tag + '.' + k:<44}{sha(np.asarray(net[k], np.float64))}")
115
116
117def run_case(tag, sdf, origin_zyx, spacing_zyx, grad_p_zyx):
118 nz, ny, nx = sdf.shape
119 fields = mac_fields((nx, ny, nz))
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 # NEWAPI
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 # NEWAPI
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 # NEWAPI
132 assert isinstance(fconns, np.ndarray) and fconns.dtype == np.int32 and fconns.ndim == 2 # NEWAPI
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)
139 report_network(f"{tag}.network_flow", net)
140 net = pnm.extract_network_flow(sdf, origin_zyx, spacing_zyx, *fields[:4],
141 grad_p_zyx=grad_p_zyx)
142 report_network(f"{tag}.network_flow.open", net)
143
144
145def run_case_mpi(tag, sdf, origin_zyx, spacing_zyx, grad_p_zyx):
146 from mpi4py import MPI
147
148 comm = MPI.COMM_WORLD
149 gshape = sdf.shape
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])
154 nz, ny, nx = gshape
155 fields = mac_fields((nx, ny, nz), offset_xyz=(ox, oy, oz), shape_xyz=(sx, sy, sz))
156 pores, seg, conns = pnm.extract_pore_network_mpi(local, gshape, origin_zyx, spacing_zyx)
157 assert isinstance(seg, np.ndarray) and seg.shape == local.shape and seg.dtype == np.int32 # NEWAPI
158 assert isinstance(conns, np.ndarray) and conns.dtype == np.int32 and conns.ndim == 2 # NEWAPI
159 segs = comm.gather(as_i32(seg, local.shape).tobytes(), root=0)
160 pores_all = comm.gather(pores_array(pores).tobytes(), root=0)
161 net = pnm.extract_network_flow_mpi(local, gshape, origin_zyx, spacing_zyx, *fields[:4],
162 ox=fields[4], oy=fields[5], oz=fields[6],
163 grad_p_zyx=grad_p_zyx)
164 if comm.rank == 0:
165 np_ = comm.size
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)}")
169 report_network(f"{tag}.mpi{np_}.network_flow", net)
170
171
172def cases(vti, quiet):
173 yield ("lattice", sphere_lattice_sdf(LATTICE_GD_XYZ), list(LATTICE_ORG_XYZ[::-1]),
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])
178 elif not quiet:
179 print(f"packing_ring: SKIPPED ({vti} not present)")
180
181
182def main(argv):
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)
185 if "--mpi" in argv:
186 from mpi4py import MPI
187
188 for c in cases(vti, quiet=MPI.COMM_WORLD.rank != 0):
189 run_case_mpi(*c)
190 return 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):
193 run_case(*c)
194 if hasattr(pnm, "mpi_block"):
195 cmd = ["mpirun", "-np", "2", sys.executable, os.path.abspath(__file__), "--mpi", vti]
196 sys.stdout.flush()
197 return subprocess.call(cmd, env=os.environ)
198 print("mpi: SKIPPED (module built without PECLET_PNM_MPI)")
199 return 0
200
201
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)
cases(vti, quiet)
pores_array(pores, sort=True)
Definition state_hash.py:96
as_i32(x, shape)
sphere_lattice_sdf(gd)
Definition state_hash.py:55
run_case(tag, sdf, origin_zyx, spacing_zyx, grad_p_zyx)
grid_xyz(gd_xyz)
Definition state_hash.py:49
mac_fields(gd, offset_xyz=(0, 0, 0), shape_xyz=None)
Definition state_hash.py:72
report_network(tag, net)