flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
collocated_neutral_probe.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Neutral-mode probe: is the collocated march's stalled state an ATTRACTOR or a FROZEN defect?
3
4The steady Stokes step map is linear with a unique true fixed point (phi = 0). The measured
5stalled states (|uf-halfavg(u)| ~ 4e-2 <u>, k off by the plateau) can only be exactly stationary
6if the map has (near-)neutral modes; then the reached state depends on the initial condition and
7on any perturbation applied along the way. Test: march from IC-A (zero velocity, the protocol
8IC), then (1) perturb the converged state with a small random solenoidal-ish kick and re-march,
9(2) march independently from IC-B (a bulk plug profile). Attractor => all k agree to march
10noise. Neutral modes => they differ at the plateau scale.
11
12 SDFLOW_BUILD=build_omp3 BED=...npz N=96 python collocated_neutral_probe.py
13"""
14import os
15import sys
16
17import numpy as np
18
19sys.path.insert(0, os.path.abspath(os.path.join(
20 os.path.dirname(__file__), "..", "..", os.environ.get("SDFLOW_BUILD", "build"))))
21from peclet import flow # noqa: E402
22
23BED = os.environ["BED"]
24N = int(os.environ.get("N", "96"))
25TOL = float(os.environ.get("MARCH_TOL", "1e-8"))
26CAP = int(os.environ.get("MARCH_MAX", "6000"))
27DT = float(os.environ.get("DT", "60.0"))
28KIND = os.environ.get("KIND", "gauge-exact")
29MU, F0 = 0.1, 1e-3
30
31
32def bed_sdf(N, npz):
33 pk = np.load(npz)
34 box = np.asarray(pk["box"], float)
35 Rc = N / box[0]
36 c = np.asarray(pk["centers"]) * Rc
37 r = np.asarray(pk["scales"]) * Rc
38 ax = np.arange(N) + 0.5
39 S = np.full((N, N, N), 1e30)
40 for sh in np.stack(np.meshgrid(*[[-1., 0., 1.]] * 3, indexing="ij"), -1).reshape(-1, 3):
41 cs = c + sh * N
42 keep = np.all((cs + (r + 3)[:, None] > 0) & (cs - (r + 3)[:, None] < N), axis=1)
43 for (cx, cy, cz), rr in zip(cs[keep], r[keep]):
44 i0, i1 = np.searchsorted(ax, [cx - rr - 3, cx + rr + 3])
45 j0, j1 = np.searchsorted(ax, [cy - rr - 3, cy + rr + 3])
46 k0, k1 = np.searchsorted(ax, [cz - rr - 3, cz + rr + 3])
47 if i0 >= i1 or j0 >= j1 or k0 >= k1:
48 continue
49 d = np.sqrt((ax[i0:i1, None, None] - cx) ** 2 + (ax[None, j0:j1, None] - cy) ** 2
50 + (ax[None, None, k0:k1] - cz) ** 2) - rr
51 np.minimum(S[i0:i1, j0:j1, k0:k1], d, out=S[i0:i1, j0:j1, k0:k1])
52 return np.asfortranarray(np.clip(S, -1e3, 1e3)), Rc
53
54
55sdf, R = bed_sdf(N, BED)
56fluid = sdf >= 0.0
57
58
59def make():
60 s = (flow.Solver if KIND == "stag" else flow.SolverColocated)(N, N, N)
61 s.set_rho(1.0); s.set_mu(MU); s.set_dt(DT)
62 s.set_body_force(F0, 0, 0); s.set_advection(False)
63 s.set_velocity_solver_params(150)
64 s.set_pressure_multigrid(True, max(2, int(np.log2(N)) - 2))
65 s.set_pressure_pcg(True, 300, 1e-8)
66 if KIND != "stag":
67 if hasattr(s, "set_collocated_scheme"):
68 s.set_collocated_scheme(KIND)
69 else:
70 s.set_face_interp({"gauge-exact": 9, "plain": 0}[KIND])
71 s.set_solid(sdf, cutcell_pressure=True, pressure_coarse="rediscretized")
72 return s
73
74
75def march(s, cap=CAP):
76 prev = 0.0
77 for it in range(cap):
78 s.step()
79 if it % 10 == 9:
80 um = float(np.asarray(s.get_u()).mean())
81 if it > 20 and abs(um - prev) < TOL * (abs(um) + 1e-300):
82 return it + 1
83 prev = um
84 return cap
85
86
87def diag(s, tag, steps):
88 U = [np.asarray(s.get_u()), np.asarray(s.get_v()), np.asarray(s.get_w())]
89 us = float(np.abs(U[0][fluid]).mean()) + 1e-300
90 kc = float(U[0].mean()) * MU / F0 / R ** 2
91 m1 = 0.0
92 if KIND != "stag":
93 UF = [np.asarray(s.get_uf()), np.asarray(s.get_vf()), np.asarray(s.get_wf())]
94 OX = [np.asarray(s.get_ox()), np.asarray(s.get_oy()), np.asarray(s.get_oz())]
95 m1sq = cnt = 0.0
96 for a in range(3):
97 half = 0.5 * (U[a] + np.roll(U[a], 1, axis=a))
98 op = OX[a] > 0
99 m1sq += float(((UF[a] - half)[op] ** 2).sum()); cnt += int(op.sum())
100 m1 = np.sqrt(m1sq / cnt) / us
101 print(f"[{tag}] steps={steps} k_cell/R2={kc:.7e} m1={m1:.3e}", flush=True)
102 return kc, U
103
104
105print(f"# bed {os.path.basename(BED)} N={N} R={R:.1f} kind={KIND} dt={DT} tol={TOL}", flush=True)
106
107# A: protocol IC (zero velocity)
108sA = make()
109stA = march(sA)
110kA, UA = diag(sA, "A zero-IC ", stA)
111
112# A': perturb the converged state and re-march
113rng = np.random.default_rng(7)
114pert = [np.asfortranarray(np.where(fluid, 0.1 * np.abs(UA[0][fluid]).mean()
115 * rng.standard_normal(UA[a].shape), 0.0))
116 for a in range(3)]
117sA.set_state(np.asfortranarray(UA[0] + pert[0]), np.asfortranarray(UA[1] + pert[1]),
118 np.asfortranarray(UA[2] + pert[2]))
119stP = march(sA)
120kP, _ = diag(sA, "A' perturbed ", stP)
121
122# B: plug-profile IC (mean-flow magnitude, x only, fluid cells)
123sB = make()
124plug = np.asfortranarray(np.where(fluid, float(np.abs(UA[0][fluid]).mean()), 0.0))
125z = np.zeros_like(plug)
126sB.set_state(plug, z.copy(), z.copy())
127stB = march(sB)
128kB, _ = diag(sB, "B plug-IC ", stB)
129
130print(f"\nrel spread: (A'-A)/A = {(kP - kA) / kA:+.3e} (B-A)/A = {(kB - kA) / kA:+.3e}",
131 flush=True)
132print("attractor => spreads ~ march noise (<1e-6); neutral/frozen modes => plateau-scale (1e-3)")