78 """A plane at a generic angle, with exact PLIC fractions from the analytic plane->volume map."""
79 from itertools
import product
81 m = np.array([mx, my, mz], dtype=float)
83 alpha = 0.5 * m.sum() * n
84 c = np.zeros((n, n, n), order=
"F")
87 for i, j, k
in product(range(n), repeat=3):
88 c[i, j, k] =
_plic_volume(m[0], m[1], m[2], alpha - (m[0] * i + m[1] * j + m[2] * k))
93 """Fluid fraction of the unit cube under mx x + my y + mz z < alpha (independent oracle)."""
94 m = np.abs(np.array([mx, my, mz], dtype=float))
97 if [mx, my, mz][k] < 0:
101 return 1.0
if a > 0
else 0.0
110 return x * x * x
if x > 0
else 0.0
112 n1, n2, n3 = np.sort(m)
114 v -= cub(a - n1) + cub(a - n2) + cub(a - n3)
115 v += cub(a - n1 - n2) + cub(a - n1 - n3) + cub(a - n2 - n3)
116 v -= cub(a - n1 - n2 - n3)
117 den = 6.0 * n1 * n2 * n3
if n1 * n2 * n3 > 0
else 0.0
120 nz = [x
for x
in (n1, n2, n3)
if x > 1e-300]
122 return min(1.0, max(0.0, a / nz[0]))
125 f += (a * a) / (2 * p * q)
127 f -= ((a - p) ** 2) / (2 * p * q)
129 f -= ((a - q) ** 2) / (2 * p * q)
131 f += ((a - p - q) ** 2) / (2 * p * q)
132 return min(1.0, max(0.0, f))
133 return min(1.0, max(0.0, v / den))
137 """A sphere, sampled by 5-level octree subdivision (exact to ~1e-6 per cell)."""
139 cx = cy = cz = n * 0.5
140 c = np.zeros((n, n, n), order=
"F")
142 off = (np.arange(2**L) + 0.5) / 2**L
145 X = (i + off)[:,
None,
None] - cx
146 Y = (j + off)[
None, :,
None] - cy
148 Z = (np.arange(2**L) + 0.5) / 2**L + k - cz
149 d2 = X * X + Y * Y + Z[
None,
None, :] ** 2
150 c[i, j, k] = float((d2 < r * r).
mean())
158def make_solver(n, C, rho_g, rho_l, mu, dt, U, momentum, rho0=None):
159 s = pf.Solver(n, n, n)
160 s.set_rho(rho0
if rho0
is not None else rho_l)
164 s.set_pressure_chebyshev(
True, PRESS_MAXIT, PRESS_RTOL)
167 s.set_property_model(
"rho",
"linear",
"C", [rho_g, rho_l - rho_g])
169 s.enable_vof_momentum(rho_g, rho_l)
170 one = np.ones((n, n, n), order=
"F")
171 s.set_state(U[0] * one, U[1] * one, U[2] * one)
184def gate_uniform(n=32, steps=200, scenes=(
"tilted",), ratios=(10.0, 1e2, 1e3, 1e4)):
185 """THE gate: uniform velocity at four density ratios, consistent vs inconsistent."""
187 dt = 0.2 / max(abs(x)
for x
in U)
188 print(
"\n=== GATE 1 — uniform-velocity consistency (n=%d, %d steps, dt=%g, CFL=%.2f) ==="
189 % (n, steps, dt, dt * max(abs(x)
for x
in U)))
190 print(
" U = %s, mu = 0, g = 0; error = max|u-U|/|U|_inf" % (U,))
193 print(
"\n scene: %s (sum C = %.6f)" % (scene, C.sum()))
194 print(
" %-10s | %-14s | %-14s | %s" % (
"ratio",
"consistent",
"inconsistent",
"pressure"))
198 for mom
in (
True,
False):
201 for _
in range(steps):
207 flag =
"" if all(x.ok()
for x
in watches)
else " ** INVALID (capped) **"
208 print(
" %-10g | %-14.4e | %-14.4e | %s%s"
209 % (R, row[0], row[1], watches[0], flag))
213 """The same identity on the ADVECTION alone — projection and momentum solve out of the picture.
214 This isolates whether the two advections truly share fluxes."""
216 dt = 0.2 / max(abs(x)
for x
in U)
217 print(
"\n=== GATE 1b — the consistency identity on the advection alone ===")
218 for scene
in (
"slab",
"tilted",
"droplet"):
223 for _
in range(steps):
226 for c, uc
in enumerate(U):
227 f = s.diagnostics.vof_advected_velocity(c)
228 e = max(e, float(np.max(np.abs(f - uc))))
229 errs.append(e / max(abs(x)
for x
in U))
230 d = s.diagnostics.vof_momentum_diagnostics()
231 print(
" %-8s R=%-8g err %.3e C^c in [%.4e, %.6f] floored %d min rho^c %.4g"
232 % (scene, R, errs[-1], min(d[
"min_Cc"]), max(d[
"max_Cc"]), d[
"floored"],
235 print(
" %-8s " % scene +
" ".join(
"R=%-6g %.3e" % (R, e)
for R, e
in zip(ratios, errs)))
239 """With C == const the consistent advection must be a SINGLE-PHASE transport: bitwise
240 independent of the density ratio (the rho_g/rho_l factors cancel), and volume-preserving."""
241 print(
"\n=== GATE 2 — C == const: the consistent scheme is density-ratio independent ===")
244 for val, name
in ((1.0,
"C=1 (all liquid)"), (0.0,
"C=0 (all gas)")):
245 C = np.full((n, n, n), val, order=
"F")
247 for R
in (1.0, 1e2, 1e4):
250 for _
in range(steps):
253 u = np.stack([s.get_u(), s.get_v(), s.get_w()])
256 print(
" %-18s R=%-8g reference %s" % (name, R, w))
258 d = float(np.max(np.abs(u - ref))) / max(abs(x)
for x
in U)
259 print(
" %-18s R=%-8g d=%.4e %s" % (name, R, d, w))
264 """Total momentum sum(rho_f u) in a periodic box, no forces: conserved to round-off."""
265 print(
"\n=== GATE 3 — momentum conservation in a periodic box (ratio %g) ===" % R)
269 x = (np.arange(n) + 0.5) / n
270 ux = 1.0 + 0.3 * np.sin(2 * math.pi * x)[
None,
None, :] * np.ones((n, n, n), order=
"F")
271 uy = np.full((n, n, n), 0.2, order=
"F")
272 uz = np.full((n, n, n), -0.1, order=
"F")
273 for mom
in (
True,
False):
274 s =
make_solver(n, C, 1.0, R, 0.0, 0.15, (1.3, 0.2, -0.1), mom)
275 s.set_state(np.asfortranarray(ux), uy, uz)
279 rho = s.get_field(
"rho")
281 comps = [s.get_u(), s.get_v(), s.get_w()]
283 rf = 0.5 * (rho + np.roll(rho, 1, axis=c))
284 p += float((rf * comps[c]).sum())
288 for _
in range(steps):
292 print(
" %-14s sum(rho_f u): %.16e -> %.16e rel %.3e %s"
293 % (
"consistent" if mom
else "inconsistent", m0, m1, abs(m1 - m0) / abs(m0), w))
298 """The rho^c floor: how often it bites and how much it matters."""
299 print(
"\n=== GATE 4 — the rho^c floor (ratio %g, %d steps) ===" % (R, steps))
303 for frac
in (1e-3, 1e-6, 1e-9, 1e-12):
305 s.diagnostics.set_vof_rho_floor(frac)
306 for _
in range(steps):
308 d = s.diagnostics.vof_momentum_diagnostics()
309 u = np.stack([s.get_u(), s.get_v(), s.get_w()])
310 dd = 0.0
if ref
is None else float(np.max(np.abs(u - ref)))
313 print(
" frac %-8g floor %-11.4g floored CVs %-6d min rho^c %.6f "
314 "C^c in [%.3e, %.6f] du vs frac=1e-3: %.3e"
315 % (frac, s.diagnostics.vof_rho_floor(), d[
"floored"], d[
"min_rho_c"],
316 min(d[
"min_Cc"]), max(d[
"max_Cc"]), dd))
321 """A viscous drop falling under buoyancy at density ratio ~800 and 15 cells/diameter.
323 WO-K asks for the Arrufat raindrop (ratio 831.8, 15 cells/D, terminal velocity within 15%).
324 That case is held together by SURFACE TENSION, which is rung V4 — without it a raindrop at its
325 terminal Weber number flattens and breaks up, so the literal case cannot be run at V2b and a
326 "terminal velocity" would not exist to measure. The surface-tension-free case with the same
327 payoff structure is a VISCOUS drop in the Stokes regime, whose terminal velocity is the
328 Hadamard-Rybczynski result
330 U_HR = (2/3) g R^2 (rho_d - rho_c)/mu_c * (mu_c + mu_d)/(2 mu_c + 3 mu_d)
332 (-> the rigid-sphere Stokes value (2/9) g R^2 drho/mu_c as mu_d/mu_c -> infinity). In a periodic
333 box the drop feels its own images, so the measured plateau sits below U_HR by the usual
334 Sangani-Acrivos-type factor at this volume fraction; the number to trust is the CONSISTENT vs
335 INCONSISTENT comparison at identical settings, with U_HR quoted as the scale.
337 Buoyancy is imposed as f_z = -(rho - <rho>) g through a LinearMix force closure, so the box
338 carries no net force and the drop falls relative to a quiescent mean.
343 rho_g, rho_l = 1.0, R
344 rho_mean = rho_g + (rho_l - rho_g) * vol
349 mu_l = mu_g * mu_ratio
352 f0 = -(rho_g - rho_mean) * g
353 f1 = -(rho_l - rho_g) * g
354 U_hr = (2.0 / 3.0) * g * R_drop**2 * (rho_l - rho_g) / mu_g * (mu_g + mu_l) / (
356 print(
"\n=== GATE 5 — falling viscous drop, ratio %g, D/h = %d, mu_l/mu_g = %g ==="
358 tau = 2.0 * R_drop**2 * rho_l / (9.0 * mu_g)
361 K = 1.0 + 1.76 * vol ** (1.0 / 3.0)
362 print(
" Hadamard-Rybczynski U_t = %.5e (unbounded), /K = %.5e with the periodic correction "
363 "K = %.3f at phi = %.4f; tau = %.0f, box %d^3, Re = %.3f"
364 % (U_hr, U_hr / K, K, vol, tau, n, rho_g * (U_hr / K) * D / mu_g))
365 print(
" %-14s | %-12s | %-12s | %-9s | %s"
366 % (
"scheme",
"U_drop",
"U/(U_HR/K)",
"max|u|",
"pressure"))
368 for mom
in (
True,
False):
369 s = pf.Solver(n, n, n)
376 s.set_pressure_chebyshev(
True, PRESS_MAXIT, 1e-11)
379 s.set_property_model(
"rho",
"linear",
"C", [rho_g, rho_l - rho_g])
380 s.set_property_model(
"mu",
"linear",
"C", [mu_g, mu_l - mu_g])
381 s.set_property_model(
"force_z",
"linear",
"C", [f0, f1])
383 s.enable_vof_momentum(rho_g, rho_l)
387 for k
in range(steps):
390 except RuntimeError
as ex:
391 print(
" %-14s BLEW UP at step %d: %s" % (
"consistent" if mom
else "inconsistent",
400 hist.append(float((c * uz).sum() / m)
if m > 0
else 0.0)
403 cfl = s.diagnostics.vof_last_courant()
407 elif cfl < 0.06
and dt < dt0:
408 dt = min(dt0, dt * 1.5)
411 u_drop = np.mean(hist[-4:])
if len(hist) >= 4
else (hist[-1]
if hist
else 0.0)
412 mx = max(float(np.max(np.abs(s.get_u()))), float(np.max(np.abs(s.get_v()))),
413 float(np.max(np.abs(s.get_w()))))
414 print(
" %-14s | %-12.5e | %-12.4f | %-9.3e | %s"
415 % (
"consistent" if mom
else "inconsistent", u_drop, u_drop / (U_hr / K), mx, w))
416 print(
" plateau history (every 20 steps, last 10): %s"
417 %
" ".join(
"%.4e" % h
for h
in hist[-10:]))
418 out[
"mom" if mom
else "no"] = (u_drop, hist)
423def gate_rt(res=(24, 48), R=1000.0, steps=300):
424 """Rayleigh-Taylor at two resolutions — the WO-K near-Nyquist check (Arrufat section 5).
426 A momentum-consistent scheme can excite growth near the grid scale on under-resolved shear
427 layers. Reported here as the interface amplitude growth AND the peak |u| relative to the
428 inconsistent run at the same resolution; a near-Nyquist artefact shows up as the finer grid
429 being WORSE, which is the opposite of a convergence signature.
431 print(
"\n=== GATE 6 — Rayleigh-Taylor at two resolutions (near-Nyquist check, ratio %g) ==="
433 print(
" %-5s | %-13s | %-13s | %-13s | %s"
434 % (
"n",
"amp consistent",
"amp inconsist",
"max|u| cons",
"pressure (consistent)"))
437 for mom
in (
True,
False):
439 C = np.zeros((nx, nx, nz), order=
"F")
440 x = (np.arange(nx) + 0.5) / nx
441 z = (np.arange(nz) + 0.5) / nz
445 h = 0.5 + 0.01 * math.cos(2 * math.pi * x[i]) * math.cos(2 * math.pi * x[j])
448 C[i, j, k] = min(1.0, max(0.0, hz - k))
449 rho_g, rho_l = 1.0, R
451 vol = C.sum() / (nx * nx * nz)
452 rho_mean = rho_g + (rho_l - rho_g) * vol
453 s = pf.Solver(nx, nx, nz)
459 s.set_pressure_geometry(np.full((nx, nx, nz), 10.0, order=
"F"))
460 s.set_pressure_chebyshev(
True, PRESS_MAXIT, 1e-11)
463 s.set_property_model(
"rho",
"linear",
"C", [rho_g, rho_l - rho_g])
464 s.set_property_model(
"force_z",
"linear",
"C",
465 [-(rho_g - rho_mean) * g, -(rho_l - rho_g) * g])
467 s.enable_vof_momentum(rho_g, rho_l)
470 for k
in range(steps):
477 cfl = s.diagnostics.vof_last_courant()
481 elif cfl < 0.06
and dt < dt0:
482 dt = min(dt0, dt * 1.5)
485 row.append((float(
"nan"), float(
"nan"), w))
488 col = cf.sum(axis=(0, 1)) / (nx * nx)
490 amp = float(np.max(cf.sum(axis=2)) - np.min(cf.sum(axis=2))) / nz
491 mx = max(float(np.max(np.abs(s.get_u()))), float(np.max(np.abs(s.get_w()))))
492 row.append((amp, mx, w))
494 print(
" %-5d | %-13.5e | %-13.5e | %-13.3e | %s"
495 % (n, row[0][0], row[1][0], row[0][1], row[0][2]))