flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
zh_wallband_diff.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""WHERE does the collocated-vs-staggered difference live, and does its amplitude grow?
3
4The permeability gap does not vanish under refinement, and the constraint operator was exonerated
5a-priori (net flux defect ~1e-5). If the surviving difference is localised in a wall band, the
6arithmetic is forced: the band's VOLUME fraction shrinks as O(h), so for its contribution to the
7volume-averaged velocity to stay constant the AMPLITUDE in the band must grow as O(1/h).
8
9So: run both solvers on identical geometry, compare the alpha-weighted FACE fluxes (the conserved
10quantity, and a like-for-like basis -- staggered get_u IS the face field, collocated get_uf is its
11projected one), and split the difference by distance to the wall in CELL units.
12
13 amplitude ~ 1/h => mechanism found: a gauge/scaling error in the near-wall reconstruction
14 amplitude ~ O(1) => the band contributes O(h) and the gap must come from somewhere else
15
16 SDFLOW_BUILD=build_ge python tests/study/zh_wallband_diff.py [N ...]
17"""
18import os
19import sys
20
21import numpy as np
22
23sys.path.insert(0, os.path.abspath(os.path.join(
24 os.path.dirname(__file__), "..", "..", os.environ.get("SDFLOW_BUILD", "build"))))
25from peclet import flow # noqa: E402
26
27PHI0 = 0.125
28BED = os.environ.get("BED", "") # pack_bed.py npz (cubic box) -> use a sphere BED, not Z&H
29
30
31def bed_sdf(N, npz):
32 """Periodic union-of-spheres SDF from a packing, sampled on an N^3 grid (cubic box only)."""
33 pk = np.load(npz)
34 box = np.asarray(pk["box"], float)
35 assert np.allclose(box, box[0]), f"{npz}: box {box} is not cubic"
36 Rc = N / box[0] # cells per sphere radius
37 c = np.asarray(pk["centers"]) * Rc
38 r = np.asarray(pk["scales"]) * Rc
39 ax = np.arange(N) + 0.5
40 S = np.full((N, N, N), 1e30)
41 for sh in np.stack(np.meshgrid(*[[-1., 0., 1.]] * 3, indexing="ij"), -1).reshape(-1, 3):
42 cs = c + sh * N
43 keep = np.all((cs + (r + 3)[:, None] > 0) & (cs - (r + 3)[:, None] < N), axis=1)
44 for (cx, cy, cz), rr in zip(cs[keep], r[keep]):
45 i0, i1 = np.searchsorted(ax, [cx - rr - 3, cx + rr + 3])
46 j0, j1 = np.searchsorted(ax, [cy - rr - 3, cy + rr + 3])
47 k0, k1 = np.searchsorted(ax, [cz - rr - 3, cz + rr + 3])
48 if i0 >= i1 or j0 >= j1 or k0 >= k1:
49 continue
50 d = np.sqrt((ax[i0:i1, None, None] - cx) ** 2 + (ax[None, j0:j1, None] - cy) ** 2
51 + (ax[None, None, k0:k1] - cz) ** 2) - rr
52 np.minimum(S[i0:i1, j0:j1, k0:k1], d, out=S[i0:i1, j0:j1, k0:k1])
53 return np.asfortranarray(np.clip(S, -1e3, 1e3)), Rc
54
55
56def solve(N, kind, mu=0.1, F=1e-3, dt=80.0, warm_tol=1e-7, tail=40, max_steps=4000):
57 if BED:
58 sdf, R = bed_sdf(N, BED)
59 else:
60 R = (3 * PHI0 / (4 * np.pi)) ** (1 / 3) * N
61 g = np.arange(N) + 0.5
62 X, Y, Z = np.meshgrid(g, g, g, indexing="ij")
63 d = lambda A: A - 0.5 * N - N * np.round((A - 0.5 * N) / N) # noqa: E731
64 sdf = np.asfortranarray(np.sqrt(d(X) ** 2 + d(Y) ** 2 + d(Z) ** 2) - R)
65 s = flow.Solver(N, N, N) if kind == "stag" else flow.SolverColocated(N, N, N)
66 s.set_rho(1.0); s.set_mu(mu); s.set_dt(dt)
67 s.set_body_force(F, 0, 0); s.set_advection(False)
68 s.set_velocity_solver_params(150)
69 s.set_pressure_multigrid(True, max(2, int(np.log2(N)) - 1))
70 s.set_pressure_pcg(True, 200, 1e-8)
71 if kind != "stag":
72 # set_collocated_scheme is new; fall back to the integer form so this runs against older
73 # builds too (the Snellius module predates it).
74 if hasattr(s, "set_collocated_scheme"):
75 s.set_collocated_scheme(kind)
76 else:
77 s.set_face_interp({"gauge-exact": 9, "plain": 0}[kind])
78 s.set_solid(sdf, cutcell_pressure=True, pressure_coarse="rediscretized")
79 prev, warm = 0.0, None
80 for it in range(max_steps):
81 s.step()
82 um = float(s.get_u().mean())
83 if warm is None:
84 if it % 10 == 9:
85 if it > 10 and abs(um - prev) < warm_tol * (abs(um) + 1e-30):
86 warm = it
87 prev = um
88 elif it - warm >= tail:
89 break
90 uf = np.asarray(s.get_u() if kind == "stag" else s.get_uf())
91 return uf, np.asarray(s.get_ox()), sdf, R
92
93
94if __name__ == "__main__":
95 Ns = [int(x) for x in (sys.argv[1:] or [32, 64, 128])]
96 if BED:
97 print(f"geometry: BED {os.path.basename(BED)}")
98 print(f"{'N':>5} | {'<dFlux>':>11} {'ord':>6} | {'band<=2h share':>15} | "
99 f"{'max|d| band':>12} {'ord':>6} | {'rms|d| band':>12} {'ord':>6}")
100 prev = None
101 for N in Ns:
102 fs, ox, sdf, R = solve(N, "stag")
103 fc, _, _, _ = solve(N, "gauge-exact")
104 # face-centred sdf along x (the face between cell i-1 and i)
105 sf = 0.5 * (sdf + np.roll(sdf, 1, axis=0))
106 d = ox * (fc - fs) # difference in the conserved face flux
107 tot = float(d.mean())
108 band = np.abs(sf) <= 2.0 # within 2 CELLS of the wall
109 share = float(d[band].sum() / d.sum()) if abs(d.sum()) > 0 else float("nan")
110 mx = float(np.abs(d[band]).max())
111 rms = float(np.sqrt((d[band] ** 2).mean()))
112 o = {}
113 if prev:
114 lr = np.log(N / prev[0])
115 for k, (a, b) in enumerate(zip(("tot", "mx", "rms"), (tot, mx, rms))):
116 pass
117 o["tot"] = f"{np.log(abs(prev[1] / tot)) / lr:+.2f}"
118 o["mx"] = f"{np.log(abs(prev[2] / mx)) / lr:+.2f}"
119 o["rms"] = f"{np.log(abs(prev[3] / rms)) / lr:+.2f}"
120 print(f"{N:>5} | {tot:>+11.4e} {o.get('tot',''):>6} | {share:>15.3f} | "
121 f"{mx:>12.4e} {o.get('mx',''):>6} | {rms:>12.4e} {o.get('rms',''):>6}", flush=True)
122 prev = (N, tot, mx, rms)
123 print("\nOrders are of the QUANTITY, so a POSITIVE order means it decays under refinement.")
124 print("An amplitude order near -1 (growing like 1/h) is the mechanism this test is looking for;")
125 print("near 0 or positive means the wall band cannot sustain a constant gap on its own.")
solve(N, kind, mu=0.1, F=1e-3, dt=80.0, warm_tol=1e-7, tail=40, max_steps=4000)