1"""Layer 3 rungs 2-3 gate: GALILEAN INVARIANCE of the moving-wall cut-cell solver.
3Stokes flow through a periodic sphere bed, driven by a body force, solved twice:
5 A (lab frame) spheres static, wall velocity 0 -> u_A
6 B (boosted frame) IDENTICAL geometry, every sphere given linVel=-V -> u_B
8In a frame translating at V the same physical flow reads u' = u - V and the walls move at -V, so
9the discrete claim is u_B + V == u_A in every fluid cell.
11Why this gates rung 3 and not just rung 2. The true boosted solution has
12 div_open(u_A - V) = -V_x (oE - oW) != 0 at every cut cell,
13which is exactly cancelled by the wall term u_w . A_wall with A_wall = -(oE-oW, oN-oS, oT-oB).
14Drop the wall term and the projection wrongly forces div_open(u)=0, so the run is repeated with
15set_wall_flux_divergence(False) to EXHIBIT the failure rather than assert it.
17Stokes (advection off) is required: with the convective term, a fixed geometry plus a moving wall
18is not a Galilean boost of the static problem -- the missing -(V.grad)u term is real physics, not
19a discretisation error.
23from peclet
import flow
as sdflow
25N = int(sys.argv[1])
if len(sys.argv) > 1
else 48
31MU, RHO, DT = 0.1, 1.0, 60.0
32STEPS = int(os.environ.get(
"GAL_STEPS",
"260"))
34CEN = [(0.25*N, 0.25*N, 0.25*N), (0.75*N, 0.75*N, 0.25*N),
35 (0.75*N, 0.25*N, 0.75*N), (0.25*N, 0.75*N, 0.75*N)]
37KN_R, KI_I, KI_R = 16, 2, 17
38node_ints = np.array([1, -1, -1], dtype=np.int32)
39node_reals = np.zeros(KN_R); node_reals[0] = R
40node_reals[14] = 1.0; node_reals[15] = 1.0
41inst_ints = np.zeros((len(CEN), KI_I), dtype=np.int32)
42inst_reals = np.zeros((len(CEN), KI_R))
43for m, c
in enumerate(CEN):
44 inst_ints[m] = (0, -1)
45 inst_reals[m, 0:3] = c
46 inst_reals[m, 6] = 1.0
47 inst_reals[m, 7] = 1.0
50def run(boost, wall_flux=True):
51 s = sdflow.Solver(N, N, N)
52 s.set_rho(RHO); s.set_mu(MU); s.set_dt(DT)
53 s.set_body_force(F, 0.0, 0.0)
54 s.set_advection(
False)
55 s.set_velocity_solver_params(80)
56 s.set_pressure_solver_params(20)
60 s.set_pressure_multigrid(
True, levels=4)
61 s.set_scene(node_ints, node_reals, inst_ints.ravel(), inst_reals.ravel(), periodic=
True)
63 s.set_wall_flux_divergence(wall_flux)
64 for i
in range(len(CEN)):
65 s.set_instance_motion(i, lin_vel=[-V, 0.0, 0.0], ang_vel=[0.0, 0.0, 0.0])
66 s.set_solid_from_scene(
True)
68 for it
in range(STEPS):
70 um = float(np.asarray(s.get_u()).
mean())
73 return (np.asarray(s.get_u()), np.asarray(s.get_v()), np.asarray(s.get_w()),
74 s.max_open_divergence(), s.wall_flux_imbalance(), it + 1,
75 np.asarray(s.diagnostics.hydro_force_torque_traction()),
76 np.asarray(s.hydro_force_torque_reaction()))
84 g = np.arange(N).astype(float)
85 X, Y, Z = np.meshgrid(g + offx, g + offy, g + offz, indexing=
"ij")
86 d = np.full((N, N, N), 1e30)
87 for (cx, cy, cz)
in CEN:
88 dx = np.abs(X-cx); dx = np.minimum(dx, N-dx)
89 dy = np.abs(Y-cy); dy = np.minimum(dy, N-dy)
90 dz = np.abs(Z-cz); dz = np.minimum(dz, N-dz)
91 d = np.minimum(d, np.sqrt(dx*dx+dy*dy+dz*dz) - R)
97mU, mV, mW = sdU > 0.0, sdV > 0.0, sdW > 0.0
98bU, bV, bW = mU & (sdU < 1.5), mV & (sdV < 1.5), mW & (sdW < 1.5)
99dU, dV, dW = sdU > 2.0, sdV > 2.0, sdW > 2.0
101uA, vA, wA, divA, imbA, itA, ftA, frA =
run(
False)
102print(
"A static : steps=%3d max|div|=%.3e <u>=%.6e" % (itA, divA, uA.mean()))
103print(
" sum F_hydro = %+.6e (traction diagnostic, LAB frame)"
104 % ftA[0][:, 0].sum())
105print(
" reaction F = %+.6e (route b, LAB frame)" % frA[0][:, 0].sum())
107for wf
in (
True,
False):
108 uB, vB, wB, divB, imbB, itB, ftB, frB =
run(
True, wall_flux=wf)
112 scale = max(abs(uA[mU]).max(), 1e-30)
115 return max(abs(du[masks[0]]).max(), abs(dv[masks[1]]).max(),
116 abs(dw[masks[2]]).max()) / scale
119 n = masks[0].sum() + masks[1].sum() + masks[2].sum()
120 ssq = (du[masks[0]]**2).sum() + (dv[masks[1]]**2).sum() + (dw[masks[2]]**2).sum()
121 return np.sqrt(ssq / n) / scale
123 e_all, e_band, e_deep =
worst((mU, mV, mW)),
worst((bU, bV, bW)),
worst((dU, dV, dW))
124 print(
"B boosted : wall_flux=%-5s steps=%3d max|div|=%.3e wall-flux imbalance=%.3e"
125 % (wf, itB, divB, imbB))
130 print(
" sum F_hydro = %+.6e ratio to the lab frame %.6f (traction)"
131 % (ftB[0][:, 0].sum(), ftB[0][:, 0].sum() / ftA[0][:, 0].sum()))
132 print(
" reaction F = %+.6e ratio to the lab frame %.9f (route b -- at steady "
133 "state the reaction is IDENTICAL between frames up to solver residual: Delta u and the "
134 "viscous differences of u* are boost-invariant)"
135 % (frB[0][:, 0].sum(), frB[0][:, 0].sum() / frA[0][:, 0].sum()))
136 print(
" max|u_B + V - u_A| / max|u_A|: all fluid %.3e cut band %.3e "
137 "deep %.3e L2(all) %.3e" % (e_all, e_band, e_deep,
l2((mU, mV, mW))))
141 print(
"GATE (rung 3 ON) %s [claim: Galilean invariant to solver tolerance]"
142 % (
"PASS" if ok
else "FAIL"))
144 print(
"CONTRAST (rung 3 OFF) all-fluid error %.3e = %.0fx the rung-3-ON error -- the wall "
145 "flux term is load bearing" % (e_all, e_all / max(keep, 1e-300)))
run(boost, wall_flux=True)
static double mean(const std::vector< double > &a)