45 """The ISSUES reproducer: a half-filled box, uniform u = 1, body force in z."""
46 s = pf.Solver(n, n, n)
50 s.set_pressure_geometry(np.full((n, n, n), 1e30, order=
"F"))
51 s.set_body_force((0.0, 0.0, 1e-3))
53 C = np.zeros((n, n, n), order=
"F")
54 C[:, :, : n // 2] = 1.0
56 u = np.zeros((n, n, n), order=
"F")
65 print(
"ISSUES item 1 -- step() atomic across the Weymouth-Yue boundedness throw")
72 except RuntimeError
as e:
73 threw = str(e).split(
"\n")[0]
74 print(f
" WY throw: {threw[:110]}")
76 check(
"WY: step() threw", bool(threw), threw[:60])
81 check(
"WY: retry at a smaller dt runs",
True)
85 s = pf.Solver(n, n, n)
88 s.set_pressure_geometry(np.full((n, n, n), 1e30, order=
"F"))
89 s.set_body_force((0.0, 0.0, 1e-3))
91 x, y, z = np.meshgrid(*[np.arange(n) + 0.5] * 3, indexing=
"ij")
92 r = np.sqrt((x - n / 2) ** 2 + (y - n / 2) ** 2 + (z - n / 2) ** 2)
93 s.set_vof(np.asfortranarray(np.clip(4.0 - r, 0.0, 1.0)))
94 s.set_surface_tension(1.0)
95 s.set_dt(0.2 * s.capillary_dt())
99 s.set_dt(10.0 * s.capillary_dt())
103 except RuntimeError
as e:
104 threw = str(e).split(
"\n")[0]
105 print(f
" capillary throw: {threw[:110]}")
107 check(
"sigma: step() threw", bool(threw), threw[:60])
108 check(
"sigma: state bitwise unchanged",
same(s0,
state(s)), str(d))
113 s = pf.Solver(nx, 4, nz)
116 s.set_domain_bc(
"-z",
"wall"); s.set_domain_bc(
"+z",
"wall")
117 s.set_pressure_geometry(np.full((nx, 4, nz), 1e30, order=
"F"))
119 x, y, z = np.meshgrid(np.arange(nx) + 0.5, np.arange(4) + 0.5,
120 np.arange(nz) + 0.5, indexing=
"ij")
121 r = np.sqrt((x - nx / 2) ** 2 + (z - nz / 4) ** 2)
122 C = np.asfortranarray(np.clip(0.5 + (r - 0.25 * nx / 2), 0.0, 1.0) * 0 + (r > 0.25 * nx).astype(float))
124 s.set_property_model(
"rho",
"linear",
"C", [1.0, 9.0])
125 s.set_property_model(
"mu",
"linear",
"C", [0.005, 0.045])
126 s.set_pressure_fcg(
True, 600, 1e-10)
128 s.set_surface_tension(sigma)
129 s.set_body_force((0.0, 0.0, -5e-2))
133 ww = np.zeros((nx, 4, nz), order=
"F")
134 ww += (0.3 * np.sin(2 * np.pi * (np.arange(nx) + 0.5) / nx))[:,
None,
None]
136 s.set_dt(min(1.0, 0.4 * s.capillary_dt()))
141 print(
"ISSUES item 2 -- step_adaptive reproduces the gallery driver bitwise")
145 for cfl, cap, sig, dmax
in ((0.4, 0.4, 0.02, math.inf), (0.2, 0.4, 0.0, 1.0)):
146 print(f
" -- cfl_target = {cfl}, capillary_cfl = {cap}, sigma = {sig}, dt_max = {dmax}"
147 f
" ({'capillary' if sig else 'CFL/dt_max'}-bound)")
153 dts_a, ncap, ncfl = [], 0, 0
154 for _
in range(nsteps):
155 L = a.vof_step_limits()
156 dt = min(cflT * L[
"cfl_dt"], capT * L[
"capillary_dt"], dmax)
160 ncap += int(capT * L[
"capillary_dt"] <= min(cflT * L[
"cfl_dt"], dmax))
161 ncfl += int(cflT * L[
"cfl_dt"] < min(capT * L[
"capillary_dt"], dmax))
163 dts_b = [b.step_adaptive(cflT, capT, dmax)
for _
in range(nsteps)]
165 check(
" dt sequence identical", dts_a == dts_b,
166 f
"max|d dt| = {max(abs(x - y) for x, y in zip(dts_a, dts_b)):.3e}")
168 print(f
" {nsteps} steps, dt {dts_a[0]:.6g} -> {dts_a[-1]:.6g}, "
169 f
"pressure {a.diagnostics.last_pressure_iterations()}/600, failed={a.diagnostics.pressure_solve_failed()}; "
170 f
"capillary bound {ncap} steps, CFL bound {ncfl}; "
171 f
"dt range [{min(dts_a):.6g}, {max(dts_a):.6g}]")
176 print(
"ISSUES item 4 -- vof_geometry() on an all-fluid VoF solver")
178 s = pf.Solver(n, n, n)
179 s.set_rho(1.0); s.set_mu(0.1)
180 s.set_pressure_geometry(np.full((n, n, n), 10.0, order=
"F"))
182 eps = np.asarray(s.diagnostics.vof_geometry(0))
183 kind = np.asarray(s.diagnostics.vof_geometry(4))
184 check(
"vof_has_geometry() is False", s.vof_has_geometry()
is False)
185 check(
"eps == 1 everywhere", eps.shape == (n, n, n)
and np.all(eps == 1.0),
186 f
"min {eps.min()} max {eps.max()}")
187 check(
"openness == 1 everywhere",
188 all(np.all(np.asarray(s.diagnostics.vof_geometry(k)) == 1.0)
for k
in (1, 2, 3)))
189 check(
"classification == 0 everywhere", np.all(kind == 0.0))
191 gas = np.asarray(s.diagnostics.vof_geometry(0)) * (1.0 - s.get_vof())
192 check(
"E7 dual-use expression runs", gas.shape == (n, n, n))
197 """Cell-centre-sampled LeVeque deformation field (u, v, w) on an n^3 unit box."""
198 c = (np.arange(n) + 0.5) / n
199 X, Y, Z = np.meshgrid(c, c, c, indexing=
"ij")
200 f = math.cos(math.pi * t / T)
201 u = 2.0 * np.sin(np.pi * X) ** 2 * np.sin(2 * np.pi * Y) * np.sin(2 * np.pi * Z) * f
202 v = -np.sin(2 * np.pi * X) * np.sin(np.pi * Y) ** 2 * np.sin(2 * np.pi * Z) * f
203 w = -np.sin(2 * np.pi * X) * np.sin(2 * np.pi * Y) * np.sin(np.pi * Z) ** 2 * f
204 return (np.asfortranarray(u * n), np.asfortranarray(v * n), np.asfortranarray(w * n))
208 c = (np.arange(n) + 0.5) / n
209 X, Y, Z = np.meshgrid(c, c, c, indexing=
"ij")
210 d = np.sqrt((X - cx) ** 2 + (Y - cx) ** 2 + (Z - cx) ** 2)
211 return np.asfortranarray(np.clip(0.5 + (r - d) * n, 0.0, 1.0))
215 print(
"ISSUES item 5 -- SolverColocated: set_state + advect_vof")
217 s = pf.SolverColocated(n, n, n)
218 s.set_rho(1.0); s.set_mu(0.0)
219 s.set_pressure_geometry(np.full((n, n, n), 10.0, order=
"F"))
225 print(f
" max|u| = {np.abs(u).max():.4g}, max|uf| after set_state = "
226 f
"{np.abs(np.asarray(s.get_uf())).max():.4g}")
227 check(
"set_state seeds the face field", np.abs(np.asarray(s.get_uf())).max() > 1.0)
228 div = s.max_open_divergence_projected()
229 print(f
" max|div(open uf)| of the seeded field = {div:.4g}")
230 dt = 0.2 / np.abs(np.asarray(s.get_uf())).max()
233 moved = float(np.abs(s.get_vof() - C0).max())
234 vol = float(s.get_vof().sum())
235 check(
"advect_vof no longer a silent no-op", moved > 0.0, f
"max|C-C0| = {moved:.3e}")
237 r = pf.Solver(n, n, n)
238 r.set_rho(1.0); r.set_mu(0.0)
239 r.set_pressure_geometry(np.full((n, n, n), 10.0, order=
"F"))
240 r.enable_vof(); r.set_vof(C0)
241 for nm, fa
in zip(
"uvw", (s.get_uf(), s.get_vf(), s.get_wf())):
242 r.set_field(nm, np.asfortranarray(np.asarray(fa)))
245 dC = float(np.abs(r.get_vof() - s.get_vof()).max())
246 print(f
" dt = {dt:.6g}, 10 steps: max|C-C0| = {moved:.4g}, sum C = {vol:.12g} (C0: {float(C0.sum()):.12g})")
247 check(
"collocated colour == the staggered run on the SAME face field", dC == 0.0,
248 f
"max|dC| = {dC:.3e}")
250 s2 = pf.SolverColocated(n, n, n)
251 s2.set_rho(1.0); s2.set_mu(0.0)
252 s2.set_pressure_geometry(np.full((n, n, n), 10.0, order=
"F"))
253 s2.enable_vof(); s2.set_vof(C0)
257 except RuntimeError
as e:
258 m2 = str(e).split(
"\n")[0]
259 check(
"a never-built face field is refused",
"never been built" in m2, m2[:70])
264 s = pf.Solver(4, 4, nz)
265 s.set_rho(1.0); s.set_mu(mu); s.set_dt(dt)
266 s.set_domain_bc(
"-z",
"wall")
267 s.set_domain_bc(
"+z", top_type)
268 s.set_pressure_geometry(np.full((4, 4, nz), 1e30, order=
"F"))
269 s.set_body_force((F, 0.0, 0.0))
270 for _
in range(steps):
276 print(
"ISSUES item 7 -- domain BC type 4 = free slip")
284 u = s.get_u()[0, 0, :]
285 zc = np.arange(nz) + 0.5
287 cont = F / (2 * mu) * (H ** 2 - (H - zc) ** 2)
288 disc = cont + F / (2 * mu) * 0.25
289 e_cont = float(np.abs(u - cont).max() / cont.max())
290 e_disc = float(np.abs(u - disc).max() / disc.max())
291 print(f
" u_max {u.max():.8f}; continuum parabola {cont.max():.8f} (rel {e_cont:.3e}), "
292 f
"discrete parabola {disc.max():.8f} (rel {e_disc:.3e}); "
293 f
"pressure {s.diagnostics.last_pressure_iterations()}, failed={s.diagnostics.pressure_solve_failed()}")
294 check(
"half-channel == the scheme's own discrete parabola (solver tol)", e_disc < 1e-8, f
"{e_disc:.3e}")
295 check(
"half-channel within h^2 of the continuum parabola", e_cont < 1e-3, f
"{e_cont:.3e}")
299 u2 = f2.get_u()[0, 0, :nz]
300 d = float(np.abs(u - u2).max() / u.max())
301 print(f
" vs the lower half of a {2*nz}-cell no-slip channel: max rel difference {d:.3e}")
302 check(
"free slip == the symmetry plane of a full channel", d < 1e-9, f
"{d:.3e}")
305 check(
"type 4 unused -> nothing moves (sanity)", np.isfinite(a.get_u()).all())
312MAIN = dict(atomic=gate_atomic, adaptive=gate_adaptive, geometry=gate_geometry,