1"""Network-flow extraction on a real sphere packing: DNS (peclet.flow Stokes) -> pore network.
3Reads a packing SDF (VTI), solves body-force-driven Stokes flow through it, then extracts the
4pore network with throat flow rates and pore pressures (pnm.extract_network_flow) and reports
5network statistics + the built-in mass-balance check.
7Run: PYTHONPATH=<pnm/build>:<flow/build> python scripts/demo_network_flow_packing.py [sdf.vti]
15import peclet.pnm
as pnm
17vti = sys.argv[1]
if len(sys.argv) > 1
else "../flow/data/packing_128.vti"
18sdf_zyx, origin_zyx, spacing_zyx = pnm.SDFReader.read_vti(vti)
19nz, ny, nx = sdf_zyx.shape
20print(f
"{vti}: grid {nx}x{ny}x{nz}, spacing {spacing_zyx[::-1]}, "
21 f
"porosity {(sdf_zyx > 0).mean():.3f}")
25sdf_xyz = np.asfortranarray(sdf_zyx.T.astype(np.float64))
26s = peclet.flow.Solver(nx, ny, nz)
30s.set_body_force((FX, 0.0, 0.0))
31s.set_solid(sdf_xyz, cutcell_pressure=
True)
35u, v, w, p = s.get_uf(), s.get_vf(), s.get_wf(), s.get_p()
36ox, oy, oz = s.get_ox(), s.get_oy(), s.get_oz()
37F = np.array([(ox[i] * u[i]).sum()
for i
in range(nx)]) * spacing_zyx[1] * spacing_zyx[0]
38print(f
"DNS: {time.time()-t0:.1f} s, flux F = {F.mean():.4e} "
39 f
"(plane dev {np.abs(F-F.mean()).max()/abs(F.mean()):.1e})")
43net = pnm.extract_network_flow(
44 sdf_zyx, list(origin_zyx), list(spacing_zyx),
45 np.ascontiguousarray(u.T), np.ascontiguousarray(v.T), np.ascontiguousarray(w.T),
46 np.ascontiguousarray(p.T), np.ascontiguousarray(ox.T), np.ascontiguousarray(oy.T),
47 np.ascontiguousarray(oz.T), grad_p_zyx=[0.0, 0.0, -FX])
48Q = np.array(net[
"throat_flow"])
49dp = np.array(net[
"throat_dp"])
50A = np.array(net[
"throat_area"])
51res = np.array(net[
"pore_residual"])
53print(f
"extraction: {time.time()-t0:.1f} s")
54print(f
"network: {len(net['pores'])} pores, {len(Q)} throats")
55print(f
"mass residuals: max |res| = {np.abs(res).max():.3e} "
56 f
"({np.abs(res).max()/np.abs(Q).mean():.2e} of mean |Q|)")
57print(f
"throat |Q|: mean {np.abs(Q).mean():.3e} max {np.abs(Q).max():.3e}")
58print(f
"throat dp: mean |dp| {np.abs(dp).mean():.3e}")
59print(f
"g = Q/dp > 0 on {100.0*(g > 0).mean():.1f}% of throats")
60print(
" NOTE: on loose packings (porosity ~0.6) the intra-pore viscous pressure variation is\n"
61 " comparable to the throat drops, so point-sampled pore dp does not resolve individual\n"
62 " throat resistances (g scatters; the Voronoi-PNM workflow fitted geometric conductances\n"
63 " for the same reason). On constricted geometry g>0 holds throat-by-throat — see\n"
64 " scripts/verify_network_flow.py (chamber-tube chain + asymmetric tube lattice).")
66ok = np.abs(res).max() < 1e-6 * np.abs(Q).max()
and np.isfinite(g).all()
67print(
"PASS" if ok
else "FAIL")
69sys.exit(0
if ok
else 1)