flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
vof_momentum_consistency.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""WO-K (rung V2b) — momentum-consistent transport: the gate battery.
3
4The decisive gate is the **uniform-velocity consistency test**: an arbitrary sharp colour field
5carried by a spatially uniform velocity, no gravity, no viscosity. A momentum-consistent scheme
6holds the velocity uniform to machine precision at ANY density ratio; an inconsistent one is wrong
7at O(delta rho), so the test is pass/fail at 1e-15 with no tuning knob — the momentum analogue of
8the hydrostatic acid test.
9
10Every gate here also asserts that **no pressure solve hit its iteration cap**. Weymouth-Yue's exact
11conservation is conditioned on a discretely divergence-free face field, so an under-solved
12projection degrades precisely the quantities these gates measure, and a capped solve can look like
13a momentum-consistency failure. A run with a capped solve is reported INVALID, not degraded.
14
15Usage:
16 PYTHONPATH=$PWD/build_wok_omp python tests/study/vof_momentum_consistency.py [gate ...]
17"""
18import math
19import sys
20import time
21
22import numpy as np
23
24import peclet.flow as pf
25
26# The pressure-driver cap in force for every run here (Chebyshev is the varRho default).
27PRESS_MAXIT = 300
28PRESS_RTOL = 1e-13
29
30
32 """Per-step record of the pressure solve, so a capped solve invalidates a run."""
33
34 def __init__(self, cap=PRESS_MAXIT):
35 self.cap = cap
36 self.max_iters = 0
37 self.max_div = 0.0
38 self.capped = 0
39
40 def sample(self, s):
41 it = s.diagnostics.last_pressure_iterations()
42 self.max_iters = max(self.max_iters, it)
43 if it >= self.cap:
44 self.capped += 1
45 try:
46 self.max_div = max(self.max_div, s.max_open_divergence())
47 except Exception:
48 pass
49
50 def ok(self):
51 return self.capped == 0
52
53 def __str__(self):
54 return "pressure: max_iters %d/%d%s, max|div(open u)| %.3g" % (
55 self.max_iters,
56 self.cap,
57 " ** CAPPED %d STEPS — RUN INVALID **" % self.capped if self.capped else "",
58 self.max_div,
59 )
60
61
62def all_fluid(n):
63 return np.full((n, n, n), 10.0, order="F")
64
65
66# --------------------------------------------------------------------------------------------
67# sharp colour scenes (exact fractions; VOF_PLAN: never initialise a diffuse profile)
68# --------------------------------------------------------------------------------------------
70 """Grid-aligned slab: no mixed cell at all (the configuration with the fewest degrees of
71 freedom, and the one the interface-local CFL band had to be redefined for in WO-J)."""
72 c = np.zeros((n, n, n), order="F")
73 c[:, :, n // 4 : 3 * n // 4] = 1.0
74 return c
75
76
77def scene_tilted(n, mx=0.6, my=-0.5, mz=0.62456):
78 """A plane at a generic angle, with exact PLIC fractions from the analytic plane->volume map."""
79 from itertools import product
80
81 m = np.array([mx, my, mz], dtype=float)
82 m /= np.abs(m).sum()
83 alpha = 0.5 * m.sum() * n # plane through the box centre, in cell units
84 c = np.zeros((n, n, n), order="F")
85 # exact fraction of the unit cell under m.x < alpha, via the same 5-case SZ relation the solver
86 # uses; implemented here independently (host, numpy) so the test does not lean on the kernel.
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))
89 return c
90
91
92def _plic_volume(mx, my, mz, alpha):
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))
95 a = alpha
96 for k in range(3):
97 if [mx, my, mz][k] < 0:
98 a += m[k]
99 s = m.sum()
100 if s == 0.0:
101 return 1.0 if a > 0 else 0.0
102 m = m / s
103 a = a / s
104 if a <= 0.0:
105 return 0.0
106 if a >= 1.0:
107 return 1.0
108 # inclusion-exclusion for the simplex volume truncated to the unit cube
109 def cub(x):
110 return x * x * x if x > 0 else 0.0
111
112 n1, n2, n3 = np.sort(m)
113 v = cub(a)
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
118 if den == 0.0:
119 # a degenerate (1D/2D) normal: fall back to the exact lower-dimensional closed form
120 nz = [x for x in (n1, n2, n3) if x > 1e-300]
121 if len(nz) == 1:
122 return min(1.0, max(0.0, a / nz[0]))
123 p, q = sorted(nz)
124 f = 0.0
125 f += (a * a) / (2 * p * q)
126 if a > p:
127 f -= ((a - p) ** 2) / (2 * p * q)
128 if a > q:
129 f -= ((a - q) ** 2) / (2 * p * q)
130 if a > 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))
134
135
136def scene_droplet(n, r=None):
137 """A sphere, sampled by 5-level octree subdivision (exact to ~1e-6 per cell)."""
138 r = r or n * 0.28
139 cx = cy = cz = n * 0.5
140 c = np.zeros((n, n, n), order="F")
141 L = 4
142 off = (np.arange(2**L) + 0.5) / 2**L
143 for i in range(n):
144 for j in range(n):
145 X = (i + off)[:, None, None] - cx
146 Y = (j + off)[None, :, None] - cy
147 for k in range(n):
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())
151 return c
152
153
154SCENES = {"slab": scene_slab, "tilted": scene_tilted, "droplet": scene_droplet}
155
156
157# --------------------------------------------------------------------------------------------
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)
161 s.set_mu(mu)
162 s.set_dt(dt)
163 s.set_pressure_geometry(all_fluid(n))
164 s.set_pressure_chebyshev(True, PRESS_MAXIT, PRESS_RTOL)
165 s.enable_vof()
166 s.set_vof(C)
167 s.set_property_model("rho", "linear", "C", [rho_g, rho_l - rho_g])
168 if momentum:
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)
172 return s
173
174
176 e = 0.0
177 for c, uc in enumerate(U):
178 f = [s.get_u, s.get_v, s.get_w][c]()
179 e = max(e, float(np.max(np.abs(f - uc))))
180 return e / max(abs(x) for x in U)
181
182
183# --------------------------------------------------------------------------------------------
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."""
186 U = (1.0, 0.6, -0.4)
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,))
191 for scene in scenes:
192 C = SCENES[scene](n)
193 print("\n scene: %s (sum C = %.6f)" % (scene, C.sum()))
194 print(" %-10s | %-14s | %-14s | %s" % ("ratio", "consistent", "inconsistent", "pressure"))
195 for R in ratios:
196 row = []
197 watches = []
198 for mom in (True, False):
199 s = make_solver(n, C, 1.0, R, 0.0, dt, U, mom)
200 w = SolveWatch()
201 for _ in range(steps):
202 s.step()
203 w.sample(s)
204 row.append(uniform_error(s, U))
205 watches.append(w)
206 del s
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))
210
211
212def gate_uniform_advection_only(n=32, steps=1, ratios=(10.0, 1e2, 1e3, 1e4)):
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."""
215 U = (1.0, 0.6, -0.4)
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"):
219 C = SCENES[scene](n)
220 errs = []
221 for R in ratios:
222 s = make_solver(n, C, 1.0, R, 0.0, dt, U, True)
223 for _ in range(steps):
224 s.step()
225 e = 0.0
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"],
233 d["min_rho_c"]))
234 del s
235 print(" %-8s " % scene + " ".join("R=%-6g %.3e" % (R, e) for R, e in zip(ratios, errs)))
236
237
238def gate_single_phase(n=32, steps=50):
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 ===")
242 U = (1.0, 0.6, -0.4)
243 dt = 0.2
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")
246 ref = None
247 for R in (1.0, 1e2, 1e4):
248 s = make_solver(n, C, 1.0, R, 0.0, dt, U, True)
249 w = SolveWatch()
250 for _ in range(steps):
251 s.step()
252 w.sample(s)
253 u = np.stack([s.get_u(), s.get_v(), s.get_w()])
254 if ref is None:
255 ref = u
256 print(" %-18s R=%-8g reference %s" % (name, R, w))
257 else:
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))
260 del s
261
262
263def gate_momentum_conservation(n=32, steps=200, R=1000.0):
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)
266 C = scene_droplet(n)
267 # a sheared, discretely divergence-free initial field would not stay uniform; use a uniform
268 # translation plus a solenoidal shear so there IS momentum transport to conserve.
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)
276 w = SolveWatch()
277
278 def total():
279 rho = s.get_field("rho")
280 p = 0.0
281 comps = [s.get_u(), s.get_v(), s.get_w()]
282 for c in range(3):
283 rf = 0.5 * (rho + np.roll(rho, 1, axis=c))
284 p += float((rf * comps[c]).sum())
285 return p
286
287 m0 = total()
288 for _ in range(steps):
289 s.step()
290 w.sample(s)
291 m1 = total()
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))
294 s = None # not `del`: the closure `total` binds this cell (ruff F821)
295
296
297def gate_rho_floor(n=32, steps=200, R=1000.0):
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))
300 C = scene_droplet(n)
301 U = (1.0, 0.6, -0.4)
302 ref = None
303 for frac in (1e-3, 1e-6, 1e-9, 1e-12):
304 s = make_solver(n, C, 1.0, R, 0.0, 0.2, U, True)
305 s.diagnostics.set_vof_rho_floor(frac)
306 for _ in range(steps):
307 s.step()
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)))
311 if ref is None:
312 ref = u
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))
317 del s
318
319
320def gate_falling_drop(n=48, D=15, R=800.0, mu_ratio=100.0, steps=500, quiet=False):
321 """A viscous drop falling under buoyancy at density ratio ~800 and 15 cells/diameter.
322
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
329
330 U_HR = (2/3) g R^2 (rho_d - rho_c)/mu_c * (mu_c + mu_d)/(2 mu_c + 3 mu_d)
331
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.
336
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.
339 """
340 R_drop = D / 2.0
341 C = scene_droplet(n, r=R_drop)
342 vol = C.sum() / n**3
343 rho_g, rho_l = 1.0, R
344 rho_mean = rho_g + (rho_l - rho_g) * vol
345 # mu_g is chosen so the Stokes response time tau = 2 R^2 rho_l / (9 mu_g) is O(200) rather than
346 # O(1e4): the drop must actually REACH its plateau inside the step budget, and at Re ~ 0.02 the
347 # case stays in the Stokes regime the Hadamard-Rybczynski formula describes.
348 mu_g = 50.0
349 mu_l = mu_g * mu_ratio
350 g = 2.5e-4
351 # f_z = -(rho - rho_mean) g = -(rho_g - rho_mean) g - (rho_l - rho_g) g * C
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) / (
355 2 * mu_g + 3 * mu_l)
356 print("\n=== GATE 5 — falling viscous drop, ratio %g, D/h = %d, mu_l/mu_g = %g ==="
357 % (R, D, mu_ratio))
358 tau = 2.0 * R_drop**2 * rho_l / (9.0 * mu_g)
359 # Periodic array: the drop feels its images. Sangani-Acrivos / Hasimoto to leading order,
360 # K = 1 + 1.76 phi^(1/3), so the expected plateau is U_HR / K, NOT U_HR.
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"))
367 out = {}
368 for mom in (True, False):
369 s = pf.Solver(n, n, n)
370 s.set_rho(rho_l)
371 s.set_mu(mu_g)
372 dt0 = 2.0
373 dt = dt0
374 s.set_dt(dt)
375 s.set_pressure_geometry(all_fluid(n))
376 s.set_pressure_chebyshev(True, PRESS_MAXIT, 1e-11)
377 s.enable_vof()
378 s.set_vof(C)
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])
382 if mom:
383 s.enable_vof_momentum(rho_g, rho_l)
384 w = SolveWatch()
385 hist = []
386 blew = False
387 for k in range(steps):
388 try:
389 s.step()
390 except RuntimeError as ex: # the WY CFL cap = the run went unstable
391 print(" %-14s BLEW UP at step %d: %s" % ("consistent" if mom else "inconsistent",
392 k, str(ex)[:90]))
393 blew = True
394 break
395 w.sample(s)
396 if k % 20 == 19:
397 c = s.get_vof()
398 uz = s.get_w()
399 m = c.sum()
400 hist.append(float((c * uz).sum() / m) if m > 0 else 0.0)
401 # keep the interface-local Courant number inside the WY cap
402 if k % 10 == 9:
403 cfl = s.diagnostics.vof_last_courant()
404 if cfl > 0.18:
405 dt *= 0.18 / cfl
406 s.set_dt(dt)
407 elif cfl < 0.06 and dt < dt0:
408 dt = min(dt0, dt * 1.5)
409 s.set_dt(dt)
410 if not blew:
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)
419 del s
420 return out
421
422
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).
425
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.
430 """
431 print("\n=== GATE 6 — Rayleigh-Taylor at two resolutions (near-Nyquist check, ratio %g) ==="
432 % R)
433 print(" %-5s | %-13s | %-13s | %-13s | %s"
434 % ("n", "amp consistent", "amp inconsist", "max|u| cons", "pressure (consistent)"))
435 for n in res:
436 row = []
437 for mom in (True, False):
438 nx, nz = n, 2 * n
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
442 # a single-mode perturbed flat interface at mid height, exact fractions in the cut cells
443 for i in range(nx):
444 for j in range(nx):
445 h = 0.5 + 0.01 * math.cos(2 * math.pi * x[i]) * math.cos(2 * math.pi * x[j])
446 hz = h * nz
447 for k in range(nz):
448 C[i, j, k] = min(1.0, max(0.0, hz - k))
449 rho_g, rho_l = 1.0, R
450 g = 1e-4
451 vol = C.sum() / (nx * nx * nz)
452 rho_mean = rho_g + (rho_l - rho_g) * vol
453 s = pf.Solver(nx, nx, nz)
454 s.set_rho(rho_l)
455 s.set_mu(0.05)
456 dt0 = 2.0
457 dt = dt0
458 s.set_dt(dt)
459 s.set_pressure_geometry(np.full((nx, nx, nz), 10.0, order="F"))
460 s.set_pressure_chebyshev(True, PRESS_MAXIT, 1e-11)
461 s.enable_vof()
462 s.set_vof(C)
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])
466 if mom:
467 s.enable_vof_momentum(rho_g, rho_l)
468 w = SolveWatch()
469 blew = False
470 for k in range(steps):
471 try:
472 s.step()
473 except RuntimeError:
474 blew = True
475 break
476 w.sample(s)
477 cfl = s.diagnostics.vof_last_courant()
478 if cfl > 0.18:
479 dt *= 0.18 / cfl
480 s.set_dt(dt)
481 elif cfl < 0.06 and dt < dt0:
482 dt = min(dt0, dt * 1.5)
483 s.set_dt(dt)
484 if blew:
485 row.append((float("nan"), float("nan"), w))
486 continue
487 cf = s.get_vof()
488 col = cf.sum(axis=(0, 1)) / (nx * nx)
489 hbar = col.sum()
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))
493 del s
494 print(" %-5d | %-13.5e | %-13.5e | %-13.3e | %s"
495 % (n, row[0][0], row[1][0], row[0][1], row[0][2]))
496
497
498GATES = {
499 "uniform": gate_uniform,
500 "uniform_adv": gate_uniform_advection_only,
501 "single_phase": gate_single_phase,
502 "momentum": gate_momentum_conservation,
503 "floor": gate_rho_floor,
504 "drop": gate_falling_drop,
505 "rt": gate_rt,
506}
507
508if __name__ == "__main__":
509 want = sys.argv[1:] or ["uniform_adv", "uniform", "single_phase", "momentum", "floor"]
510 for g in want:
511 t0 = time.time()
512 GATES[g]()
513 print(" [%s: %.1f s]" % (g, time.time() - t0))
make_solver(n, C, rho_g, rho_l, mu, dt, U, momentum, rho0=None)
scene_tilted(n, mx=0.6, my=-0.5, mz=0.62456)
gate_rho_floor(n=32, steps=200, R=1000.0)
gate_uniform(n=32, steps=200, scenes=("tilted",), ratios=(10.0, 1e2, 1e3, 1e4))
gate_rt(res=(24, 48), R=1000.0, steps=300)
gate_momentum_conservation(n=32, steps=200, R=1000.0)
gate_falling_drop(n=48, D=15, R=800.0, mu_ratio=100.0, steps=500, quiet=False)
gate_uniform_advection_only(n=32, steps=1, ratios=(10.0, 1e2, 1e3, 1e4))
static double mean(const std::vector< double > &a)