flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
collocated_s1_reconciliation.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""S1 probe (doc/collocated_accuracy_ceiling.md #5): does the ABC cell/face reconciliation carry a
3steady-state defect that could hold the ~0.3 % flow-excess plateau?
4
5Structural background (this is what the test checks, not assumes): at the JOINT fixed point of the
6mode-9 step -- velocity AND accumulated pressure both stationary -- the rotational update
7P += (rho/dt)*phi - mu*div(u*) together with A phi = -div(u*) forces (A + rho/(mu dt)) phi = 0,
8i.e. phi = 0 and div(u*) = 0. Consequences, each measured here at the march's stopping point:
9
10 m1 |uf - halfavg(u)| / <u> the projected face field vs the plain 1/2-1/2 average of the
11 cell field. projectCorrect writes uf = halfavg(u*) - grad_f(phi)
12 and the cell correction writes u = u* - gpCenterGrad(phi), so a
13 persistent gap is EXACTLY the un-converged gauge (phi != 0) plus
14 the gradient-pair mismatch: the S1 channel. If m1 ~ 0 the ABC
15 two-field loop closes and S1 is dead.
16 m2 alpha-div(halfavg(u)) h/<u> the constraint residual of the CELL field (the face field's is
17 pinned by the solve; the cell field's is the approximate
18 projection's O(h^2) remainder -- it should CONVERGE, not floor).
19 m3 |u_cell - cellavg(uf)| / <u> the doc's S1 quantity, split by wall distance (near = |sdf|<=2h).
20 This is a smoothing difference (h^2 D2u/4 in the bulk); the
21 interesting part is whether the NEAR-WALL part floors.
22
23Usage: SDFLOW_BUILD=build_ge BED=<packing.npz> python collocated_s1_reconciliation.py [N ...]
24 (defaults N = 96 128 192 -> R = 6, 8, 12 on the 16^3-box beds)
25"""
26import os
27import sys
28
29import numpy as np
30
31sys.path.insert(0, os.path.abspath(os.path.join(
32 os.path.dirname(__file__), "..", "..", os.environ.get("SDFLOW_BUILD", "build"))))
33from peclet import flow # noqa: E402
34
35BED = os.environ.get("BED", "")
36if not BED:
37 raise SystemExit("BED=<packing.npz> is required (use the phi=0.60 s3 bed)")
38MARCH_TOL = float(os.environ.get("MARCH_TOL", "1e-8"))
39MAXS = int(os.environ.get("MARCH_MAX", "4000"))
40DT = float(os.environ.get("DT", "60.0"))
41
42
43def bed_sdf(N, npz):
44 pk = np.load(npz)
45 box = np.asarray(pk["box"], float)
46 assert np.allclose(box, box[0]), f"{npz}: box {box} is not cubic"
47 Rc = N / box[0]
48 c = np.asarray(pk["centers"]) * Rc
49 r = np.asarray(pk["scales"]) * Rc
50 ax = np.arange(N) + 0.5
51 S = np.full((N, N, N), 1e30)
52 for sh in np.stack(np.meshgrid(*[[-1., 0., 1.]] * 3, indexing="ij"), -1).reshape(-1, 3):
53 cs = c + sh * N
54 keep = np.all((cs + (r + 3)[:, None] > 0) & (cs - (r + 3)[:, None] < N), axis=1)
55 for (cx, cy, cz), rr in zip(cs[keep], r[keep]):
56 i0, i1 = np.searchsorted(ax, [cx - rr - 3, cx + rr + 3])
57 j0, j1 = np.searchsorted(ax, [cy - rr - 3, cy + rr + 3])
58 k0, k1 = np.searchsorted(ax, [cz - rr - 3, cz + rr + 3])
59 if i0 >= i1 or j0 >= j1 or k0 >= k1:
60 continue
61 d = np.sqrt((ax[i0:i1, None, None] - cx) ** 2 + (ax[None, j0:j1, None] - cy) ** 2
62 + (ax[None, None, k0:k1] - cz) ** 2) - rr
63 np.minimum(S[i0:i1, j0:j1, k0:k1], d, out=S[i0:i1, j0:j1, k0:k1])
64 return np.asfortranarray(np.clip(S, -1e3, 1e3)), Rc
65
66
67def march(N):
68 sdf, R = bed_sdf(N, BED)
69 s = flow.SolverColocated(N, N, N)
70 s.set_rho(1.0); s.set_mu(0.1); s.set_dt(DT)
71 s.set_body_force(1e-3, 0, 0); s.set_advection(False)
72 s.set_velocity_solver_params(150)
73 s.set_pressure_multigrid(True, max(2, int(np.log2(N)) - 2))
74 s.set_pressure_pcg(True, 300, 1e-8)
75 if hasattr(s, "set_collocated_scheme"):
76 s.set_collocated_scheme("gauge-exact")
77 else:
78 s.set_face_interp(9)
79 s.set_solid(sdf, cutcell_pressure=True, pressure_coarse="rediscretized")
80 prev = 0.0
81 for it in range(MAXS):
82 s.step()
83 if it % 10 == 9:
84 um = float(np.asarray(s.get_u()).mean())
85 if it > 10 and abs(um - prev) < MARCH_TOL * (abs(um) + 1e-30):
86 break
87 prev = um
88 return s, sdf, R, it + 1
89
90
91def stats(name, v, scale):
92 v = np.abs(v) / scale
93 if v.size == 0:
94 print(f" {name:>34}: (empty)")
95 return None
96 q = np.quantile(v, [0.5, 0.99])
97 print(f" {name:>34}: max {v.max():.3e} p99 {q[1]:.3e} med {q[0]:.3e} "
98 f"rms {np.sqrt((v**2).mean()):.3e}", flush=True)
99 return float(np.sqrt((v ** 2).mean()))
100
101
102prev = {}
103for N in [int(x) for x in (sys.argv[1:] or [96, 128, 192])]:
104 s, sdf, R, steps = march(N)
105 U = [np.asarray(s.get_u()), np.asarray(s.get_v()), np.asarray(s.get_w())]
106 UF = [np.asarray(s.get_uf()), np.asarray(s.get_vf()), np.asarray(s.get_wf())]
107 OX = [np.asarray(s.get_ox()), np.asarray(s.get_oy()), np.asarray(s.get_oz())]
108 fluid = sdf >= 0.0
109 uscale = float(np.abs(U[0][fluid]).mean())
110 print(f"\nN={N} R={R:.1f} steps={steps} <|u|>_fluid={uscale:.4e} "
111 f"k/R^2={float(U[0].mean())/1e-3*0.1/R**2:.6e}")
112 rms = {}
113 for a in range(3):
114 half = 0.5 * (U[a] + np.roll(U[a], 1, axis=a)) # halfavg at the low-a face of cell i
115 openf = OX[a] > 0.0
116 rms[f"m1_ax{a}"] = stats(f"m1 |uf-halfavg(u)| ax{a} (open faces)",
117 (UF[a] - half)[openf], uscale)
118 # m2: alpha-divergence of the CELL field via the halfavg fluxes (x-fastest, low-face layout:
119 # div_i = sum_a o_a(i+e_a)*f_a(i+e_a) - o_a(i)*f_a(i))
120 div = np.zeros_like(U[0])
121 for a in range(3):
122 half = 0.5 * (U[a] + np.roll(U[a], 1, axis=a))
123 flx = OX[a] * half
124 div += np.roll(flx, -1, axis=a) - flx
125 stats("m2 alpha-div(halfavg(u)) fluid", div[fluid], uscale) # per-cell, h=1 units
126 # face-field residual for reference (should be at the solve tolerance)
127 divf = np.zeros_like(U[0])
128 for a in range(3):
129 flx = OX[a] * UF[a]
130 divf += np.roll(flx, -1, axis=a) - flx
131 stats("m2f alpha-div(uf) fluid (solver)", divf[fluid], uscale)
132 # m3: cell field vs the cell-average of the face field, split by wall distance
133 for a in range(3):
134 cavg = 0.5 * (UF[a] + np.roll(UF[a], -1, axis=a)) # avg of the two a-faces of cell i
135 d = (U[a] - cavg)
136 near = fluid & (np.abs(sdf) <= 2.0)
137 bulk = fluid & (np.abs(sdf) > 2.0)
138 rms[f"m3n_ax{a}"] = stats(f"m3 |u-cellavg(uf)| ax{a} NEAR wall", d[near], uscale)
139 rms[f"m3b_ax{a}"] = stats(f"m3 |u-cellavg(uf)| ax{a} bulk", d[bulk], uscale)
140 if prev:
141 lr = np.log(N / prev["N"])
142 orders = {k: np.log(prev[k] / v) / lr for k, v in rms.items()
143 if k in prev and v and prev[k]}
144 print(" orders vs previous N (positive = decays):",
145 " ".join(f"{k}:{o:+.2f}" for k, o in orders.items()), flush=True)
146 prev = {"N": N, **{k: v for k, v in rms.items() if v}}