flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
vof_blocks_overlap.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Overlapping markers in the block VoF container: what actually goes wrong when two bubbles'
3markers interpenetrate (the channel_18 blow-up, WO-W3 section 7).
4
5 static two spheres at rest, zero gravity, centre distance d swept from separated to
6 overlapping by several cells. If the summed per-marker CSF were NOT balanced
7 against the union-colour projection (the W3 hypothesis), the parasitic current
8 would jump at d < 2R. If it stays at the single-bubble level, the static balance
9 holds and the blow-up has to be dynamic.
10 squeeze the same pair driven into each other by opposite body forces on the two markers'
11 regions -- the controlled version of a turbulent collision.
12
13The case numbers mirror channel_18 in cell units: D = 10 cells, rho_g/rho_l = 0.1,
14mu_g = mu_l, sigma = 320 (S^3 * 5e-3 at S = 40 cells per unit length).
15
16Usage: PYTHONPATH=<build> python tests/study/vof_blocks_overlap.py [static|squeeze ...]
17"""
18import math
19import sys
20import time
21
22import numpy as np
23
24import os
25
26import peclet.flow as pf
27
28sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
29
30GATES = [a for i, a in enumerate(sys.argv[1:], 1)
31 if not a.startswith("--") and not sys.argv[i - 1].startswith("--")] or ["static"]
32
33
34def arg(name, default, cast=float):
35 if name in sys.argv:
36 return cast(sys.argv[sys.argv.index(name) + 1])
37 return default
38
39
40R = arg("--R", 5.0)
41RHO_L, RHO_G = 1.0, arg("--rho-g", 0.1)
42MU = arg("--mu", 0.5333)
43MU_G = arg("--mu-g", MU) # gas viscosity (default: the liquid's, as channel_18)
44TENT = "--tent" in sys.argv # vof_overlap_design 5.6: the opt-in overlap density
45REPICK = "--repick" in sys.argv # re-pick dt every step from vof_step_limits() (static gate)
46SIGMA = arg("--sigma", 320.0)
47STEPS = arg("--steps", 400, int)
48
49
50def solver(n, seeds, blocks=True, csf=True):
51 nx, ny, nz = n
52 s = pf.Solver(nx, ny, nz)
53 s.set_rho(RHO_L)
54 s.set_mu(MU)
55 s.set_pressure_geometry(np.full((nx, ny, nz), 10.0, order="F"))
56 s.set_pressure_chebyshev(True, 800, 1e-12)
57 s.enable_vof()
58 s.set_vof(np.zeros((nx, ny, nz), order="F"))
59 s.set_property_model("rho", "linear", "C", [RHO_L, RHO_G - RHO_L])
60 s.set_property_model("mu", "linear", "C", [MU, MU_G - MU])
61 s.set_surface_tension(SIGMA)
62 s.enable_vof_blocks(seeds)
63 if csf:
64 s.enable_vof_block_csf()
65 if TENT:
66 s.diagnostics.set_vof_block_overlap_density(True)
67 return s
68
69
70def counters(s):
71 """The overlap-design counters: clip (block cascade), debris census, overlap census."""
72 st = s.diagnostics.vof_block_stats()
73 cs = s.diagnostics.vof_block_curvature_stats()
74 return {"clipped": cs.get("clipped", 0),
75 "debris_cells": sum(b.get("debris_cells", 0) for b in st),
76 "debris_volume": sum(b.get("debris_volume", 0.0) for b in st),
77 "returned": [b.get("debris_returned", 0.0) for b in st],
78 "lost": sum(b.get("debris_lost", 0.0) for b in st),
79 "unresolved": sum(b.get("debris_unresolved", 0) for b in st),
80 "overlap_cells": st[0].get("overlap_cells", 0) if st else 0,
81 "overlap_excess": st[0].get("overlap_excess", 0.0) if st else 0.0,
82 "bound": bool(st[0].get("overlap_bound_active", False)) if st else False}
83
84
85def umax(s):
86 return max(np.abs(s.get_u()).max(), np.abs(s.get_v()).max(), np.abs(s.get_w()).max())
87
88
89RUNSTAT = {}
90
91
92def run(s, steps, dt=None, every=50, tag="", safety=None):
93 L = s.vof_step_limits()
94 if dt is None:
95 dt = 0.25 * min(L["cfl_dt"], L["capillary_dt"])
96 s.set_dt(dt)
97 hist = []
98 RUNSTAT.update(bound_steps=0, dt_min=dt, clipped=0, debris=0, ov_cells=0)
99 for i in range(steps):
100 if REPICK and safety is not None:
101 L = s.vof_step_limits()
102 dt = safety * L["capillary_dt"]
103 s.set_dt(dt)
104 RUNSTAT["dt_min"] = min(RUNSTAT["dt_min"], dt)
105 try:
106 s.step()
107 if hasattr(s.diagnostics, "vof_block_stats"):
108 k = counters(s)
109 RUNSTAT["bound_steps"] += int(k["bound"])
110 RUNSTAT["clipped"] += k["clipped"]
111 RUNSTAT["debris"] = max(RUNSTAT["debris"], k["debris_cells"])
112 RUNSTAT["ov_cells"] = max(RUNSTAT["ov_cells"], k["overlap_cells"])
113 except Exception as exc:
114 print(f" {tag} step {i}: THREW {exc}")
115 hist.append(float("nan"))
116 break
117 u = umax(s)
118 hist.append(u)
119 if not math.isfinite(u):
120 break
121 return dt, np.array(hist)
122
123
125 print("=" * 96)
126 print("STATIC — two spheres at rest, no gravity: parasitic current vs centre distance d")
127 print("=" * 96)
128 n = (int(8 * R), int(5 * R), int(5 * R))
129 cy, cz = n[1] / 2, n[2] / 2
130 cx = n[0] / 2
131 cap = math.sqrt((RHO_L + RHO_G) / (4 * math.pi * SIGMA))
132 print(f" R = {R} cells, box {n}, rho_g/rho_l = {RHO_G}, mu = {MU}, sigma = {SIGMA}; "
133 f"Brackbill dt = {cap:.4e}; Laplace dp = {2*SIGMA/R:.2f}")
134 rows = []
135 ds = [float(x) for x in arg("--d", "14,10.5,10,9,8,6,4", str).split(",")]
136 cases = ([("single", None)] if "--no-single" not in sys.argv else []) + \
137 [(f"d={d:g}", d) for d in ds]
138 for name, d in cases:
139 if d is None:
140 seeds = [(cx, cy, cz, R)]
141 else:
142 seeds = [(cx - d / 2, cy, cz, R), (cx + d / 2, cy, cz, R)]
143 s = solver(n, seeds)
144 v0 = [b["volume"] for b in s.diagnostics.vof_block_stats()]
145 t0 = time.time()
146 dt, h = run(s, STEPS, dt=arg("--cap-safety", 0.25) * cap, tag=name,
147 safety=arg("--cap-safety", 0.25))
148 v1 = [b["volume"] for b in s.diagnostics.vof_block_stats()]
149 C = s.get_field("C")
150 dv = max(abs(a / b - 1) for a, b in zip(v1, v0))
151 rows.append((name, h))
152 print(f" {name:8s} max|u| @50 {h[min(49,len(h)-1)]:.3e} @end {h[-1]:.3e} peak {np.nanmax(h):.3e} "
153 f"maxC {float(C.max()):.6f} dV {dv:.1e} ({time.time()-t0:.0f} s) "
154 f"bound-steps {RUNSTAT['bound_steps']} dt_min/Brackbill {RUNSTAT['dt_min']/cap:.3f} "
155 f"clipped {RUNSTAT['clipped']} debris<= {RUNSTAT['debris']} "
156 f"ov_cells<= {RUNSTAT['ov_cells']}", flush=True)
157 if "--dump" in sys.argv:
158 np.savez(f"{arg('--dump', 'static', str)}_{name}.npz", h=h, u=s.get_u(), v=s.get_v(),
159 w=s.get_w(), C=C)
160 return rows
161
162
164 """Two bubbles in a Couette flow, offset in y by b < D so they collide glancingly -- the
165 deterministic stand-in for the channel's turbulent collisions. Per step: max|u|, union max C,
166 marker overlap (sum V_k - sum C_union), and the step at which anything goes non-finite."""
167 print("=" * 96)
168 print("SHEAR — two bubbles colliding in Couette flow (walls at +-y moving at +-U)")
169 print("=" * 96)
170 U = arg("--U", 16.0)
171 b = arg("--b", 6.0)
172 gap0 = arg("--gap", 20.0)
173 blocks = "--single-field" not in sys.argv
174 n = (int(round(13 * R)), int(round(8 * R)), int(round(6 * R)))
175 nx, ny, nz = n
176 s = pf.Solver(nx, ny, nz)
177 s.set_rho(RHO_L)
178 s.set_mu(MU)
179 s.set_domain_bc("-y", "wall", (-U, 0, 0))
180 s.set_domain_bc("+y", "wall", (U, 0, 0))
181 s.set_pressure_geometry(np.full(n, 10.0, order="F"))
182 s.set_pressure_chebyshev(True, 800, 1e-12)
183 y = (np.arange(ny) + 0.5) / ny
184 u0 = np.broadcast_to((-U + 2 * U * y)[None, :, None], n)
185 s.set_velocity(0, np.asfortranarray(u0))
186 s.enable_vof()
187 s.set_vof(np.zeros(n, order="F"))
188 s.set_property_model("rho", "linear", "C", [RHO_L, RHO_G - RHO_L])
189 s.set_property_model("mu", "linear", "C", [MU, MU_G - MU])
190 s.set_surface_tension(SIGMA)
191 seeds = [(nx / 2 - gap0 / 2, ny / 2 + b / 2, nz / 2, R), (nx / 2 + gap0 / 2, ny / 2 - b / 2, nz / 2, R)]
192 if blocks:
193 s.enable_vof_blocks(seeds)
194 s.enable_vof_block_csf()
195 if TENT:
196 s.diagnostics.set_vof_block_overlap_density(True)
197 else:
198 from vof_surface_tension import sphere_fractions
199 C = np.zeros(n)
200 for q in seeds:
201 C = np.maximum(C, sphere_fractions(n, q[3], q[:3]))
202 s.set_vof(np.asfortranarray(C))
203 rel = 2 * U / ny * b
204 print(f" box {n}, R = {R}, b = {b}, U = {U} -> approach speed {rel:.2f} cells/time; "
205 f"We = {RHO_L*rel**2*2*R/SIGMA:.2f}; {'BLOCKS' if blocks else 'single field'}")
206 T = arg("--T", 3.0 * gap0 / max(rel, 1e-9))
207 t, i = 0.0, 0
208 t0 = time.time()
209 V0 = [q["volume"] for q in s.diagnostics.vof_block_stats()] if blocks else None
210 agg = dict(umax=0.0, bound=0, contact=0, ovmax=0.0, clipped=0, debris=0, dvol=0.0,
211 first_contact=None, last_contact=None, debris_steps=0, debris_vol_max=0.0)
212 while t < T:
213 L = s.vof_step_limits()
214 dt = 0.25 * min(L["cfl_dt"], L["capillary_dt"])
215 if not (dt > 0 and math.isfinite(dt)):
216 print(f" step {i}: limits non-finite {L}")
217 break
218 s.set_dt(dt)
219 try:
220 s.step()
221 except Exception as exc:
222 print(f" step {i} t={t:.3f}: THREW {exc}")
223 break
224 t += dt
225 i += 1
226 if blocks and hasattr(s.diagnostics, "vof_block_curvature_stats"):
227 k = counters(s)
228 agg["umax"] = max(agg["umax"], umax(s))
229 agg["bound"] += int(k["bound"])
230 if k["overlap_cells"] > 0:
231 agg["contact"] += 1
232 agg["first_contact"] = agg["first_contact"] or i
233 agg["last_contact"] = i
234 agg["ovmax"] = max(agg["ovmax"], k["overlap_excess"])
235 agg["clipped"] += k["clipped"]
236 agg["debris"] = max(agg["debris"], k["debris_cells"])
237 agg["debris_steps"] += int(k["debris_cells"] > 0)
238 agg["debris_vol_max"] = max(agg["debris_vol_max"], k["debris_volume"])
239 if i % 20 == 0:
240 C = s.get_field("C")
241 um = umax(s)
242 if blocks:
243 st = s.diagnostics.vof_block_stats()
244 vols = [q["volume"] for q in st]
245 ov = sum(vols) - float(C.sum())
246 cen = [q["centroid"] for q in st]
247 dx = cen[1][0] - cen[0][0]
248 extra = f"V {vols[0]:.4f}/{vols[1]:.4f} overlap {ov:8.4f} dx {dx:6.2f}"
249 else:
250 extra = f"sumC {float(C.sum()):.4f}"
251 print(f" step {i:5d} t={t:7.3f} dt={dt:.2e} max|u| {um:8.3f} maxC {float(C.max()):.6f} "
252 f"{extra} ({time.time()-t0:.0f} s)", flush=True)
253 if not math.isfinite(um) or um > 20 * U:
254 print(" BLOW-UP")
255 break
256 if blocks:
257 V1 = [q["volume"] for q in s.diagnostics.vof_block_stats()]
258 agg["dvol"] = max(abs(a / b - 1) for a, b in zip(V1, V0))
259 print(f" SUMMARY steps {i} t {t:.3f}/{T:.3f} max|u| {agg['umax']:.3f} "
260 f"max rel dV {agg['dvol']:.2e} contact steps {agg['contact']} "
261 f"({agg['first_contact']}..{agg['last_contact']}) max overlap excess "
262 f"{agg['ovmax']:.4f} phantom-bound steps {agg['bound']} clipped cells (sum) "
263 f"{agg['clipped']} debris: max cells {agg['debris']}, steps with debris "
264 f"{agg['debris_steps']}, max volume {agg['debris_vol_max']:.3e}; ledger returned per "
265 f"marker {[f'{r:.3e}' for r in k['returned']]}, lost {k['lost']:.3e}, unresolved "
266 f"{k['unresolved']}", flush=True)
267
268
269ALL = {"static": gate_static, "shear": gate_shear}
270
271if __name__ == "__main__":
272 for g in GATES:
273 ALL[g]()
arg(name, default, cast=float)
run(s, steps, dt=None, every=50, tag="", safety=None)