85 print(
"\n" +
"=" * 96)
86 print(
"G1 COLOUR BUDGET, COUPLED (WO-R gate G1 driven by step(), not by a prescribed field)")
88 print(
" A liquid slug is injected through a uniform -z inlet and flushed out of the +z")
89 print(
" outlet, with the full momentum + projection step in between. The gate is")
90 print(
" sum(C)(t) - integral(inflow liquid) + integral(outflow liquid) = const,")
91 print(
" the fluxes being the ADVECTOR's own boundary face fluxes (vof_bc_volumes). Unlike the")
92 print(
" kinematic ctest twin, the advecting field here is the projection's output, so the")
93 print(
" floor is the projection's discrete divergence residual (WO-J finding 3).\n")
94 nx = ny = 32
if not QUICK
else 16
95 nz = 64
if not QUICK
else 32
96 nslug, nafter = (60, 740)
if not QUICK
else (20, 120)
98 s = pf.Solver(nx, ny, nz)
102 for f
in (
"-x",
"+x",
"-y",
"+y"):
103 s.set_domain_bc(f,
"wall", (0, 0, 0))
104 s.set_domain_bc(
"-z",
"inflow", (0.0, 0.0, W))
105 s.set_domain_bc(
"+z",
"outflow", (0, 0, 0))
110 s.diagnostics.set_velocity_solver_params(60)
111 s.set_pressure_multigrid(
True, levels=8)
112 s.set_pressure_solver_params(80)
113 s.set_pressure_geometry(np.full((nx, ny, nz), 1e30, order=
"F"))
114 s.set_pressure_chebyshev(
True, 400, 1e-12)
116 s.set_vof(np.zeros((nx, ny, nz), order=
"F"))
117 s.set_vof_inflow(
"-z", 1.0)
118 s.set_vof_backflow(
"+z", 0.0)
120 ledger, budget0, drift = 0.0,
None, 0.0
121 cmin, cmax = 1.0, 0.0
123 for i
in range(nslug + nafter):
125 s.set_vof_inflow(
"-z", 0.0)
128 d = s.diagnostics.vof_diagnostics()
129 ledger += sum(s.vof_bc_volumes())
130 b = d[
"sum"] - ledger
133 drift = max(drift, abs(b - budget0))
134 cmin, cmax = min(cmin, d[
"min"]), max(cmax, d[
"max"])
135 tot = s.vof_bc_volumes_total()
136 injected, left = tot[4], -tot[5]
137 d = s.diagnostics.vof_diagnostics()
138 print(f
" injected {injected:.10g}, left {left:.10g}, remaining sum(C) {d['sum']:.3e}")
139 print(f
" |budget drift| {drift:.3e} (relative to the injected volume {drift/injected:.3e})")
140 print(f
" C in [{cmin:.3e}, {cmax:.17g}]")
141 print(f
" {h} ({time.time()-t0:.0f} s)")
142 ok = (drift / injected < 1e-10)
and h.valid
and cmin > -1e-12
and cmax < 1 + 1e-12
143 print(f
" GATE: {'PASS' if ok else 'FAIL'} (budget <= 1e-10 relative, C bounded, no cap)")
153 100: dict(rho_l=100.0, rho_g=1.0, mu_l=50.0, mu_g=1.0, dt=0.5, mom=
True, cap=400),
154 1000: dict(rho_l=100.0, rho_g=0.1, mu_l=50.0, mu_g=1.0, dt=0.25, mom=
True, cap=2000),
160 nx, ny, nz = 32, 4, 64
162 drho = p[
"rho_l"] - p[
"rho_g"]
163 p = dict(p, g=4.93e-3 * 99.0 / drho)
164 umax = drho * p[
"g"] * delta ** 2 / (2 * p[
"mu_l"])
165 Q = drho * p[
"g"] * delta ** 3 / (3 * p[
"mu_l"])
166 Re = p[
"rho_l"] * umax * delta / p[
"mu_l"]
167 steps = steps
or (900
if QUICK
else 2400)
168 print(
"\n" +
"=" * 96)
169 print(f
"G2 NUSSELT FALLING FILM, density ratio {ratio}, mu ratio {p['mu_l']/p['mu_g']:.0f}"
170 f
"{'' if outflow_rho else ' [ABLATION: --no-outflow-rho]'}")
172 print(
" A liquid film of thickness delta = 8 cells runs down the -x wall of a quasi-2D duct,")
173 print(
" fed at the +z inlet with the exact Nusselt profile and leaving through the -z outlet.")
174 print(
" The body force is the REDUCED gravity -(rho_l - rho_g) g C: the gas is then exactly")
175 print(
" force-free and hydrostatic, which is what the Nusselt similarity solution assumes")
176 print(
" (with gravity on the gas too, the Dirichlet p=0 outlet would accelerate the gas")
177 print(
" column instead of supporting it — a different problem).")
178 print(f
" u_max {umax:.5g} cells/s, Q {Q:.5g} cells^2/s, film Reynolds {Re:.2f}\n")
179 s = pf.Solver(nx, ny, nz)
180 s.set_rho(p[
"rho_l"])
183 s.set_domain_bc(
"-x",
"wall", (0, 0, 0))
184 s.set_domain_bc(
"+x",
"wall", (0, 0, 0))
185 s.set_domain_bc(
"-z",
"outflow", (0, 0, 0))
187 xc = np.arange(nx) + 0.5
188 wprof = np.where(xc < delta, -(drho * p[
"g"] / p[
"mu_l"]) * (delta * xc - 0.5 * xc ** 2), 0.0)
189 prof = np.zeros((nx, ny, 3))
190 prof[:, :, 2] = wprof[:,
None]
191 s.set_domain_bc_profile(
"+z", np.ascontiguousarray(prof))
196 s.diagnostics.set_velocity_solver_params(60)
197 s.set_pressure_multigrid(
True, levels=8)
198 s.set_pressure_solver_params(80)
199 s.set_pressure_geometry(np.full((nx, ny, nz), 1e30, order=
"F"))
202 C = np.zeros((nx, ny, nz), order=
"F")
203 frac = np.clip(delta - np.arange(nx), 0.0, 1.0)
204 C[:, :, :] = frac[:,
None,
None]
205 s.set_vof(np.asfortranarray(C))
206 s.set_property_model(
"rho",
"linear",
"C", [p[
"rho_g"], p[
"rho_l"] - p[
"rho_g"]])
207 s.set_property_model(
"mu",
"linear",
"C", [p[
"mu_g"], p[
"mu_l"] - p[
"mu_g"]])
208 s.set_property_model(
"force_z",
"linear",
"C", [0.0, -drho * p[
"g"]])
210 s.enable_vof_momentum(p[
"rho_g"], p[
"rho_l"])
216 s.set_pressure_fcg(
True, p[
"cap"], 1e-11)
218 s.set_vof_inflow_profile(
"+z", np.ascontiguousarray(np.repeat(frac[:,
None], ny, axis=1)))
219 s.set_vof_backflow(
"-z", 0.0)
221 w0 = np.zeros((nx, ny, nz), order=
"F")
222 w0[:, :, :] = wprof[:,
None,
None]
223 s.set_field(
"w", np.asfortranarray(w0))
234 for i
in range(steps):
236 L = s.vof_step_limits()
237 dt = min(p[
"dt"], 0.4 * L[
"cfl_dt"])
if L[
"cfl_dt"] > 0
else p[
"dt"]
239 ncap += int(dt < p[
"dt"])
242 hist.append(s.diagnostics.vof_diagnostics()[
"sum"])
246 dmeas = float(C[:, :, zmid].sum() / ny)
247 qmeas = float(-(C[:, :, zmid] * w[:, :, zmid]).sum() / ny)
248 tail = np.array(hist[-200:])
249 steady = float(np.abs(tail - tail.mean()).max() / max(tail.mean(), 1e-300))
250 print(f
" at z = nz/2: film thickness {dmeas:.4f} cells (target {delta}) "
251 f
"[error {dmeas-delta:+.4f}]")
252 print(f
" flow rate {qmeas:.6g} (Nusselt {Q:.6g}) "
253 f
"[{100*(qmeas/Q-1):+.2f} %]")
254 print(f
" sum(C) steadiness over the last 200 steps: {steady:.3e} relative")
255 print(f
" dt: nominal {p['dt']}, throttled by the WY cap on {ncap} of the "
256 f
"{(steps + 19)//20} re-picks; final {dt:.4g}")
257 print(f
" {h} ({time.time()-t0:.0f} s)")
258 ok = (abs(qmeas / Q - 1) < 0.03
and abs(dmeas - delta) < 0.5
and steady < 1e-4
and h.valid)
259 print(f
" GATE: {'PASS' if ok else 'FAIL'} (Q within 3 %, delta within 0.5 cell, "
260 f
"sum(C) steady to 1e-4, no cap)")
261 return ok, qmeas / Q - 1, dmeas - delta, steady, h.iters
266 print(
"\n" +
"=" * 96)
267 print(
"G3 GAS OVER A RESTING POOL, density ratio 1000, momentum-consistent transport")
269 print(
" Gas enters at -x above a liquid pool that fills the lower half, and leaves at +x. The")
270 print(
" pool must not move and must not lose volume, and the inflow ghost DENSITY must be the")
271 print(
" INLET fluid's — gas above the pool, liquid at the pool's own inlet plane.")
273 print(
" NOTE ON THE GATE AS WRITTEN. WO-R asks for the ghost density 'through get_field(rho)")
274 print(
" on the first inner plane'. That measures the wrong quantity: get_field returns the")
275 print(
" INNER cells, whose rho is rho(C_inner) whatever the ghost policy does. The ghost is")
276 print(
" what the inlet FACE density is built from, and it is reachable only through")
277 print(
" field_view('rho'), which returns the padded buffer. That is what is gated below.\n")
278 nx = 64
if not QUICK
else 32
280 nz = 32
if not QUICK
else 16
281 steps = 500
if not QUICK
else 150
282 rho_l, rho_g = 1000.0, 1.0
283 mu_l, mu_g = 10.0, 0.1
285 s = pf.Solver(nx, ny, nz)
289 s.set_domain_bc(
"-z",
"wall", (0, 0, 0))
290 s.set_domain_bc(
"+z",
"wall", (0, 0, 0))
291 s.set_domain_bc(
"+x",
"outflow", (0, 0, 0))
293 zc = np.arange(nz) + 0.5
295 prof = np.zeros((ny, nz, 3))
296 prof[:, :, 0] = np.where(gas, U, 0.0)[
None, :]
297 s.set_domain_bc_profile(
"-x", np.ascontiguousarray(prof))
302 s.diagnostics.set_velocity_solver_params(60)
303 s.set_pressure_multigrid(
True, levels=8)
304 s.set_pressure_solver_params(80)
305 s.set_pressure_geometry(np.full((nx, ny, nz), 1e30, order=
"F"))
307 C = np.zeros((nx, ny, nz), order=
"F")
308 C[:, :, : nz // 2] = 1.0
309 s.set_vof(np.asfortranarray(C))
310 s.set_property_model(
"rho",
"linear",
"C", [rho_g, rho_l - rho_g])
311 s.set_property_model(
"mu",
"linear",
"C", [mu_g, mu_l - mu_g])
312 s.enable_vof_momentum(rho_g, rho_l)
313 s.set_pressure_fcg(
True, 800, 1e-11)
315 cprof = np.where(gas, 0.0, 1.0)
316 s.set_vof_inflow_profile(
"-x", np.ascontiguousarray(np.repeat(cprof[
None, :], ny, axis=0)))
317 s.set_vof_backflow(
"+x", 0.0)
320 v0 = s.diagnostics.vof_diagnostics()[
"sum"]
322 for _
in range(steps):
325 v1 = s.diagnostics.vof_diagnostics()[
"sum"]
327 u, v, w = s.get_u(), s.get_v(), s.get_w()
329 umax_liq = float(np.max([np.abs(f[liq]).max()
for f
in (u, v, w)]))
330 rho =
to_host(s.diagnostics.field_view(
"rho"))
331 G = (rho.shape[0] - nx) // 2
332 ghost = rho[G - 1, G + ny // 2, :]
333 gh_gas = float(np.mean(ghost[G + nz // 2: G + nz]))
334 gh_liq = float(np.mean(ghost[G: G + nz // 2]))
335 print(f
" pool volume {v0:.10g} -> {v1:.10g} (relative change {abs(v1-v0)/v0:.3e})")
336 print(f
" max|u| in the liquid {umax_liq:.4e} ({umax_liq/U:.3e} of the gas inlet speed)")
337 print(f
" inflow ghost rho: {gh_gas:.6g} above the pool (want {rho_g}), "
338 f
"{gh_liq:.6g} at the pool (want {rho_l})")
339 print(f
" {h} ({time.time()-t0:.0f} s)")
340 ok = (abs(v1 - v0) / v0 < 1e-10
and umax_liq / U < 1e-3
and h.valid
341 and abs(gh_gas - rho_g) < 1e-9 * rho_l
and abs(gh_liq - rho_l) < 1e-9 * rho_l)
342 print(f
" GATE: {'PASS' if ok else 'FAIL'}")
357 rows.append((r, dq, dd, st, it))