flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
pocket_pressure_probe.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Does the pressure in SEALED fluid pockets of a dense bed change any physical output? A study
3instrument (not a ctest): it prints one line per (output, perturbation).
4
5A sealed pocket is a fluid component of the projection's face-aperture graph other than the main
6pore space -- nearly always one cell at a sphere near-contact, every face aperture exactly zero. Its
7pressure is a null vector of the pressure operator, uncoupled from the velocity constraint, so its
8value is whatever the projection writes into the decoupled row.
9
10Method: build the bed as an analytic SCENE (one instance per sphere, so the per-body reaction and
11traction loads exist), configure it exactly as tests/study/steady_acceleration_gates.py does
12(Case.configure, production settings), march to steady, snapshot u, v, w, p (the G5 'field'
13restore; the run is Stokes, so that is the whole state), then for each perturbation
14p -> p + c * [pocket mask] (one pocket, all pockets; c = 0 is the baseline, run twice for the
15noise floor):
16 instant -- outputs recomputed with no step (the reaction reads the stashed u* of the last step);
17 stepped -- the same after ONE step from the perturbed state.
18The outputs: per-body reaction force / torque (staggered only), per-body traction force / torque /
19pressure part, the bed totals and K from <u_x>, the velocity fields, and the pressure a user reads
20(the fluid mean, x-plane means, main-space values, the pockets themselves).
21
22 PYTHONPATH=build_cuda OMP_NUM_THREADS=8 OMP_PROC_BIND=false \
23 python tests/study/pocket_pressure_probe.py --arrangement bed.npz --scheme staggered
24
25Face convention (checked by the instrument itself): get_ox[i] is the face between cells i-1 and i,
26as the binding says. anderson_rev1_probes.py reads it as the face (i, i+1); with that reading this
27bed shows 1,085 pockets / 1,168 cells, with the right one 205 / 217 (sdf > 0 nodes) or 115 / 126
28(--graph dof, the staggered cut-cell operator's own graph, which also joins through solid-CENTRED
29cut cells).
30
31The raw field registry (get_field / set_field / diagnostics.field_view) is used here as a study
32instrument only; production code must not. The perturbation c is therefore in the registry's
33internal pressure units; the reported pressure changes are scaled by the physical (get_p) range.
34"""
35import argparse
36import os
37import sys
38import time
39
40import numpy as np
41
42HERE = os.path.dirname(os.path.abspath(__file__))
43sys.path.insert(0, HERE)
44import steady_acceleration_gates as SAG # noqa: E402 (Case, settings_of: reused, not re-derived)
45
46
47def case_args(a):
48 """The namespace steady_acceleration_gates.Case and Case.configure read."""
49 return argparse.Namespace(case="bed", arrangement=a.arrangement, mu=1.0, N=a.N, phi=0.6,
50 force=None, dt=None, beta=a.beta, advection=None,
51 implicit_advection=False)
52
53
54def set_bed_scene(s, case):
55 """One sphere node + one instance per body, periodic min-image, physical coordinates."""
56 n = case.npart
57 ni = np.tile(np.array([1, -1, -1], dtype=np.int32), n)
58 nr = np.zeros((n, 16))
59 nr[:, 0] = case.rad
60 nr[:, 14] = 1.0 # rotation w
61 nr[:, 15] = 1.0 # scale
62 ii = np.stack([np.arange(n, dtype=np.int32), -np.ones(n, dtype=np.int32)], axis=1).ravel()
63 ir = np.zeros((n, 17))
64 ir[:, 0:3] = np.mod(case.cen, case.L)
65 ir[:, 6] = 1.0 # rotation w
66 ir[:, 7] = 1.0 # scale
67 s.set_scene(ni, nr.ravel(), np.ascontiguousarray(ii), ir.ravel(), periodic=True)
68 s.set_solid_from_scene(cutcell_pressure=True)
69
70
71def build(flow, case, scheme, st, scene=True):
72 Cls = flow.Solver if scheme == "staggered" else flow.SolverColocated
73 N, L = case.N, case.L
74 s = Cls((N, N, N), extent=(L, L, L))
75 case.configure(s, st)
76 if scene:
77 set_bed_scene(s, case)
78 else:
79 cx, cy, cz = s.cell_centers()
80 s.set_solid(np.asfortranarray(case.sdf_global(cx, cy, cz)), cutcell_pressure=True)
81 return s
82
83
84def inner(v, G=2):
85 return np.array(v[G:-G, G:-G, G:-G])
86
87
88def components(s, fluid, proj=True, graph="sdf"):
89 """Components of the face-aperture graph (ox at index i couples cells i-1 and i; as in
90 anderson_rev1_probes.py, rolled so that index i couples i and i+1).
91
92 graph='sdf' -- anderson_rev1_probes.py's definition: nodes are the sdf > 0 cells, an edge needs
93 BOTH cells sdf > 0. A solid-CENTRED cut cell (sdf <= 0, open faces) is not a
94 node, so a fluid cell joined to the main space only through one looks sealed.
95 graph='dof' -- the pressure operator's own graph: every open face is an edge whatever the sdf
96 sign of its cells; nodes are the cells with an open face plus the sdf > 0 cells.
97 Returns (labels over the grid, main label, component sizes, apertures, node mask)."""
98 from scipy.sparse import coo_matrix
99 from scipy.sparse.csgraph import connected_components
100 n = fluid.shape
101 idx = np.arange(fluid.size).reshape(n, order="F")
102 get = (s.get_ox_proj, s.get_oy_proj, s.get_oz_proj) if proj else (s.get_ox, s.get_oy, s.get_oz)
103 opn = [np.roll(np.asarray(g()).reshape(n, order="F"), -1, axis=ax) for ax, g in enumerate(get)]
104 # opn[ax][i] is now the +ax face of cell i, i.e. the face between i and i+1
105 if graph == "sdf":
106 node = fluid.copy()
107 else:
108 node = fluid.copy()
109 for ax in range(3):
110 o = opn[ax] > 0.0
111 node |= o | np.roll(o, 1, axis=ax)
112 Ii, Jj = [], []
113 for ax in range(3):
114 m = (opn[ax] > 0.0) & node & np.roll(node, -1, axis=ax)
115 Ii.append(idx[m])
116 Jj.append(np.roll(idx, -1, axis=ax)[m])
117 Ii, Jj = np.concatenate(Ii), np.concatenate(Jj)
118 A = coo_matrix((np.ones(Ii.size), (Ii, Jj)), shape=(fluid.size, fluid.size))
119 _, lab = connected_components(A, directed=False)
120 lab = lab.reshape(n, order="F")
121 sizes = np.bincount(lab[node], minlength=lab.max() + 1)
122 main = int(np.argmax(sizes))
123 return lab, main, sizes, opn, node
124
125
126def outputs(s, case, scheme, fluid):
127 o = {}
128 if scheme == "staggered":
129 r = np.array(s.hydro_force_torque_reaction())
130 o["reaction F"] = r[0]
131 o["reaction T"] = r[1]
132 o["reaction sum Fx"] = np.array([r[0][:, 0].sum()])
133 t = np.array(s.diagnostics.hydro_force_torque_traction())
134 o["traction F"] = t[0]
135 o["traction T"] = t[1]
136 o["traction F_p"] = t[2]
137 o["traction sum Fx"] = np.array([t[0][:, 0].sum()])
138 o["u"] = np.array(s.get_u())
139 o["v"] = np.array(s.get_v())
140 o["w"] = np.array(s.get_w())
141 um = float(o["u"].mean())
142 o["<u_x>"] = np.array([um])
143 o["K"] = np.array([case.K(um)])
144 p = np.array(s.get_p())
145 o["p"] = p
146 o["<p> fluid"] = np.array([p[fluid].mean()])
147 # x-plane fluid means (a user's pressure profile / drop estimate)
148 cnt = fluid.sum(axis=(1, 2))
149 o["<p>_yz(x)"] = (np.where(fluid, p, 0.0).sum(axis=(1, 2)) / np.maximum(cnt, 1))
150 return o
151
152
153def rel(a, b, scale):
154 return float(np.max(np.abs(a - b))) / scale if scale > 0 else float(np.max(np.abs(a - b)))
155
156
157def main():
158 ap = argparse.ArgumentParser()
159 ap.add_argument("--arrangement", required=True)
160 ap.add_argument("--scheme", choices=("staggered", "collocated"), default="staggered")
161 ap.add_argument("--N", type=int, default=64)
162 ap.add_argument("--beta", type=float, default=6.0)
163 ap.add_argument("--settings", choices=("production", "tight"), default="production")
164 ap.add_argument("--window", type=int, default=5, help="march_to_steady window (0 = plain)")
165 ap.add_argument("--max-steps", type=int, default=None)
166 ap.add_argument("--sample-every", type=int, default=200,
167 help="record the pocket-pressure spread every this many march steps")
168 ap.add_argument("--graph", choices=("sdf", "dof"), default="dof",
169 help="pocket definition (see components()); 'sdf' = anderson_rev1_probes.py's")
170 ap.add_argument("--per-pocket", action="store_true",
171 help="perturb every pocket on its own (c = 1000 x main range), one step each")
172 ap.add_argument("--check-sdf", action="store_true",
173 help="also build the harness's set_solid(array) bed and compare apertures")
174 a = ap.parse_args()
175 sys.path.insert(0, os.path.join(HERE, "..", "..", "scripts"))
176 from _bootstrap import ensure_flow
177 flow = ensure_flow()
178
179 st = SAG.settings_of(a.settings)
180 if a.max_steps:
181 st["max_steps"] = a.max_steps
182 case = SAG.Case(case_args(a))
183 s = build(flow, case, a.scheme, st)
184 fluid = np.array(s.get_field("sdf")) > 0.0
185 lab, main, sizes, opn, node = components(s, fluid, proj=True, graph=a.graph)
186 labg, maing, sizesg, opng, nodeg = components(s, fluid, proj=False, graph=a.graph)
187 pk = node & (lab != main)
188 ids = np.unique(lab[pk])
189 print(f"[{a.scheme}] N={a.N} bodies={case.npart} L={case.L:.6f} graph={a.graph}: fluid (sdf>0) "
190 f"{fluid.sum()} cells, graph nodes {node.sum()}; projection-aperture graph "
191 f"{len(np.unique(lab[node]))} components, main {sizes[main]}, {len(ids)} pockets holding "
192 f"{pk.sum()} cells (of which sdf>0: {(pk & fluid).sum()}; pocket sizes "
193 f"{dict((int(k), int(v)) for k, v in zip(*np.unique(sizes[ids], return_counts=True)))}); "
194 f"geometric-aperture graph {len(np.unique(labg[nodeg]))} components; "
195 f"max|o_proj - o_geom| {max(float(np.abs(x - y).max()) for x, y in zip(opn, opng)):.3e}",
196 flush=True)
197 # the other definition, for the record: how many of its pockets are pockets here
198 lab2, main2, _, _, node2 = components(s, fluid, proj=True,
199 graph=("dof" if a.graph == "sdf" else "sdf"))
200 pk2 = node2 & (lab2 != main2)
201 print(f" other graph ({'dof' if a.graph == 'sdf' else 'sdf'}): {len(np.unique(lab2[pk2]))} "
202 f"pockets, {pk2.sum()} cells; cells pocket in both {(pk & pk2).sum()}", flush=True)
203 # face convention check: an open face between two cells that are both deeper than one cell
204 # inside a solid is impossible, so count them under each reading of get_ox[i]
205 sdfv = np.array(s.get_field("sdf"))
206 hh = float(np.abs(np.diff(sdfv, axis=0)).max()) # max |sdf step| along x = one cell (|grad|=1)
207 # a CUT face (0 < aperture < 1) has the wall crossing it, so the sdf at the face midpoint,
208 # ~ the mean of its two cells' sdf, is within ~0.71 h of zero; read with the wrong neighbour it
209 # is up to ~1.7 h away
210 raw = [np.asarray(g()) for g in (s.get_ox_proj, s.get_oy_proj, s.get_oz_proj)]
211 for nm, sh in (("(i-1, i)", 1), ("(i, i+1)", -1)):
212 cut = sum(int(((r > 0) & (r < 1)).sum()) for r in raw)
213 if cut == 0:
214 print(" face convention: no cut faces (binary openness)", flush=True)
215 break
216 mids = np.concatenate([np.abs(0.5 * (sdfv + np.roll(sdfv, sh, axis=ax)))[(r > 0) & (r < 1)]
217 for ax, r in enumerate(raw)]) / hh
218 print(f" face convention: get_ox[i] read as the face {nm}: |mid-face sdf| / h over cut "
219 f"faces: p99 {np.percentile(mids, 99):.3f}, max {mids.max():.3f}", flush=True)
220 anyo = np.zeros_like(fluid)
221 for ax in range(3):
222 o = opn[ax] > 0.0
223 anyo |= o | np.roll(o, 1, axis=ax)
224 print(f" solid-centred cells (sdf<=0) with an open projection face: {(anyo & ~fluid).sum()}; "
225 f"sdf>0 cells with all six faces closed: {(fluid & ~anyo).sum()}", flush=True)
226 if a.check_sdf:
227 s2 = build(flow, case, a.scheme, st, scene=False)
228 g2 = [np.roll(np.asarray(g()), -1, axis=ax)
229 for ax, g in enumerate((s2.get_ox_proj, s2.get_oy_proj, s2.get_oz_proj))]
230 f2 = np.array(s2.get_field("sdf")) > 0.0
231 print(f" scene vs harness set_solid(array): max|d o_proj| "
232 f"{max(float(np.abs(x - y).max()) for x, y in zip(opn, g2)):.3e}, fluid-mask "
233 f"mismatches {(f2 != fluid).sum()}", flush=True)
234 del s2
235
236 # ---- march, sampling the pocket pressure spread
237 trace = []
238
239 def cb(steps, phase):
240 if steps % a.sample_every == 0:
241 p = np.array(s.get_p())
242 pm = p[fluid & ~pk]
243 pp = p[pk]
244 trace.append((steps, phase, float(pm.min()), float(pm.max()), float(pp.min()),
245 float(pp.max()), float(pp.std())))
246
247 t0 = time.perf_counter()
248 res = flow.march_to_steady(s, lambda: float(s.get_u().mean()), rtol=st["rtol"],
249 max_steps=st["max_steps"], accelerate=(a.window > 0),
250 window=max(a.window, 1), callback=cb)
251 print(f" march: converged={res.converged} {res.reason} steps={res.steps} "
252 f"acc={res.accelerated_steps} K={case.K(res.monitor):.8f} "
253 f"wall={time.perf_counter() - t0:.1f}s", flush=True)
254 for t in trace:
255 print(f" step {t[0]:5d} {t[1]:10s} main p [{t[2]:+.4e}, {t[3]:+.4e}] pockets "
256 f"[{t[4]:+.4e}, {t[5]:+.4e}] std {t[6]:.4e}", flush=True)
257
258 S0 = {f: np.asfortranarray(np.array(s.get_field(f))) for f in ("u", "v", "w", "p")}
259 p0 = S0["p"]
260 pm = p0[fluid & ~pk]
261 dmain = float(pm.max() - pm.min()) # INTERNAL units (the raw registry): the perturbation scale
262 pphys = np.array(s.get_p())[fluid & ~pk]
263 dmain_phys = float(pphys.max() - pphys.min()) # physical (get_p): the scale of the p outputs
264 pp = p0[pk]
265 print(f" converged pressure: main space range {dmain:.6e} (mean {pm.mean():+.4e}); pockets "
266 f"min {pp.min():+.4e} max {pp.max():+.4e} mean {pp.mean():+.4e} std {pp.std():.4e} "
267 f"-> pocket spread / main range = {(pp.max() - pp.min()) / dmain:.3e}; "
268 f"max|p_pocket - <p_main>| / main range = {np.abs(pp - pm.mean()).max() / dmain:.3e}",
269 flush=True)
270 # solid cells' pressure too (also null rows)
271 ps = p0[~fluid & ~anyo]
272 print(f" solid (closed) cells: p min {ps.min():+.4e} max {ps.max():+.4e}", flush=True)
273
274 # the single pocket to perturb: the largest pocket (multi-cell, if any) and one single cell
275 big = int(ids[np.argmax(sizes[ids])])
276 single = int(ids[np.argmin(sizes[ids])])
277 masks = {"one pocket (largest, %d cells)" % sizes[big]: lab == big,
278 "one pocket (single cell)": lab == single,
279 "all pockets": pk}
280 # owners of the perturbed pocket cells (nearest sphere centre, min-image)
281 for name, m in masks.items():
282 if "all" in name:
283 continue
284 cx, cy, cz = s.cell_centers()
285 X = np.stack(np.meshgrid(cx, cy, cz, indexing="ij"), axis=-1)[m]
286 d = X[:, None, :] - np.mod(case.cen, case.L)[None]
287 d -= case.L * np.round(d / case.L)
288 r = np.linalg.norm(d, axis=-1) - case.rad[None]
289 near = np.argsort(r, axis=1)[:, :3]
290 print(f" {name}: cells {np.argwhere(m).tolist()}, nearest bodies "
291 f"{near.tolist()} gaps {np.take_along_axis(r, near, 1).round(4).tolist()}", flush=True)
292
293 def restore(c=0.0, mask=None):
294 for f in ("u", "v", "w", "p"):
295 s.set_field(f, S0[f] if (f != "p" or c == 0.0) else np.asfortranarray(p0 + c * mask))
296 s.diagnostics.exchange_field(f)
297
298 def run(mask, c):
299 """(instant change against the unperturbed state under the SAME stash, outputs after one
300 step from the perturbed state)."""
301 restore()
302 o0 = outputs(s, case, a.scheme, fluid)
303 restore(c, mask)
304 oi = outputs(s, case, a.scheme, fluid)
305 s.step()
306 return o0, oi, outputs(s, case, a.scheme, fluid)
307
308 _, _, base_s = run(pk, 0.0)
309 _, _, base2_s = run(pk, 0.0)
310 keys = list(base_s.keys())
311
312 def scale_of(k, o):
313 if k in ("u", "v", "w"):
314 return float(max(np.abs(o["u"]).max(), np.abs(o["v"]).max(), np.abs(o["w"]).max()))
315 if k in ("p", "<p> fluid", "<p>_yz(x)"):
316 return dmain_phys
317 return float(np.max(np.abs(o[k])))
318
319 def report(o0, oi, os_, mask, cfac=0.0):
320 for k in keys:
321 ri = rel(oi[k], o0[k], scale_of(k, o0)) if o0 is not None else 0.0
322 rs = rel(os_[k], base_s[k], scale_of(k, base_s))
323 print(f" {k:18s} instant {ri:.3e} after 1 step {rs:.3e}", flush=True)
324 # the pressure split: main space vs pockets, and what survives the step in the pockets
325 for tag, o, b in (("instant", oi, o0), ("step", os_, base_s)):
326 if b is None:
327 continue
328 d = o["p"] - b["p"]
329 print(f" p main space {tag:7s}: max|dp| / main range "
330 f"{np.abs(d[fluid & ~pk]).max() / dmain_phys:.3e} (mean shift "
331 f"{d[fluid & ~pk].mean() / dmain_phys:+.3e}); perturbed pocket(s) mean dp / c "
332 f"{d[mask].mean() / dmain_phys / (cfac if cfac else 1.0):+.4e}", flush=True)
333
334 print(" noise floor (baseline step twice):", flush=True)
335 report(None, base2_s, base2_s, pk)
336 for name, m in masks.items():
337 for c in (dmain, 1e3 * dmain):
338 print(f" perturbation: {name}, c = {c:.4e} ({c / dmain:g} x main range)", flush=True)
339 o0, oi, os_ = run(m.astype(float), c)
340 report(o0, oi, os_, m, c / dmain)
341 if a.per_pocket:
342 c = 1e3 * dmain
343 rows = []
344 for pid in ids:
345 m = lab == pid
346 o0, oi, os_ = run(m.astype(float), c)
347 du = [rel(os_[k], base_s[k], scale_of(k, base_s)) for k in ("u", "v", "w")]
348 rr = (rel(os_["reaction F"], base_s["reaction F"], scale_of("reaction F", base_s))
349 if "reaction F" in os_ else float("nan"))
350 rows.append((max(du), int(pid), rr, rel(os_["K"], base_s["K"], scale_of("K", base_s)),
351 rel(oi["traction F"], o0["traction F"], scale_of("traction F", o0))))
352 rows.sort(reverse=True)
353 dus = np.array([r[0] for r in rows])
354 floor = float(np.median(dus))
355 print(f" per pocket (c = 1000 x main range, one step): median max|du|/max|u| {floor:.3e}; "
356 f"pockets above 10 x median: {int((dus > 10 * floor).sum())} of {len(rows)}; "
357 f"instant traction change > 1e-12 in {sum(r[4] > 1e-12 for r in rows)} pockets",
358 flush=True)
359 uu = [S0[f] for f in ("u", "v", "w")]
360 for du, pid, rr, dk, dtr in rows[:8]:
361 cells = np.argwhere(lab == pid)
362 print(f" pocket {pid}: max|du| {du:.3e} reaction F {rr:.3e} K {dk:.3e} "
363 f"instant traction F {dtr:.3e} cells {cells.tolist()}", flush=True)
364 for cell in cells:
365 i = tuple(cell)
366 for ax in range(3):
367 hi = list(i)
368 hi[ax] = (hi[ax] + 1) % case.N
369 lo2 = list(i)
370 lo2[ax] = (lo2[ax] - 1) % case.N
371 print(f" cell {i} sdf {sdfv[i] / hh:+.3f} h axis {'xyz'[ax]}: "
372 f"-face aperture {opn[ax][tuple(lo2)]:.3g} (nbr sdf "
373 f"{sdfv[tuple(lo2)] / hh:+.3f} h, u_face {uu[ax][i]:+.3e}); "
374 f"+face aperture {opn[ax][i]:.3g} (nbr sdf {sdfv[tuple(hi)] / hh:+.3f} h, "
375 f"u_face {uu[ax][tuple(hi)]:+.3e})", flush=True)
376 restore()
377
378
379if __name__ == "__main__":
380 main()
components(s, fluid, proj=True, graph="sdf")
outputs(s, case, scheme, fluid)
static int run(int bc)
static double mean(const std::vector< double > &a)