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).
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
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)]
111 node |= o | np.roll(o, 1, axis=ax)
114 m = (opn[ax] > 0.0) & node & np.roll(node, -1, axis=ax)
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
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")
175 sys.path.insert(0, os.path.join(HERE,
"..",
"..",
"scripts"))
176 from _bootstrap
import ensure_flow
179 st = SAG.settings_of(a.settings)
181 st[
"max_steps"] = a.max_steps
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}",
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)
205 sdfv = np.array(s.get_field(
"sdf"))
206 hh = float(np.abs(np.diff(sdfv, axis=0)).max())
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)
214 print(
" face convention: no cut faces (binary openness)", flush=
True)
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)
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)
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)
239 def cb(steps, phase):
240 if steps % a.sample_every == 0:
241 p = np.array(s.get_p())
244 trace.append((steps, phase, float(pm.min()), float(pm.max()), float(pp.min()),
245 float(pp.max()), float(pp.std())))
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)
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)
258 S0 = {f: np.asfortranarray(np.array(s.get_field(f)))
for f
in (
"u",
"v",
"w",
"p")}
261 dmain = float(pm.max() - pm.min())
262 pphys = np.array(s.get_p())[fluid & ~pk]
263 dmain_phys = float(pphys.max() - pphys.min())
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}",
271 ps = p0[~fluid & ~anyo]
272 print(f
" solid (closed) cells: p min {ps.min():+.4e} max {ps.max():+.4e}", flush=
True)
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,
281 for name, m
in masks.items():
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)
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)
299 """(instant change against the unperturbed state under the SAME stash, outputs after one
300 step from the perturbed state)."""
302 o0 =
outputs(s, case, a.scheme, fluid)
304 oi =
outputs(s, case, a.scheme, fluid)
306 return o0, oi,
outputs(s, case, a.scheme, fluid)
308 _, _, base_s =
run(pk, 0.0)
309 _, _, base2_s =
run(pk, 0.0)
310 keys = list(base_s.keys())
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)"):
317 return float(np.max(np.abs(o[k])))
319 def report(o0, oi, os_, mask, cfac=0.0):
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)
325 for tag, o, b
in ((
"instant", oi, o0), (
"step", os_, base_s)):
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)
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)
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",
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)
368 hi[ax] = (hi[ax] + 1) % case.N
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)