50def solver(n, seeds, blocks=True, csf=True):
52 s = pf.Solver(nx, ny, nz)
55 s.set_pressure_geometry(np.full((nx, ny, nz), 10.0, order=
"F"))
56 s.set_pressure_chebyshev(
True, 800, 1e-12)
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)
64 s.enable_vof_block_csf()
66 s.diagnostics.set_vof_block_overlap_density(
True)
92def run(s, steps, dt=None, every=50, tag="", safety=None):
93 L = s.vof_step_limits()
95 dt = 0.25 * min(L[
"cfl_dt"], L[
"capillary_dt"])
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"]
104 RUNSTAT[
"dt_min"] = min(RUNSTAT[
"dt_min"], dt)
107 if hasattr(s.diagnostics,
"vof_block_stats"):
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"))
119 if not math.isfinite(u):
121 return dt, np.array(hist)
126 print(
"STATIC — two spheres at rest, no gravity: parasitic current vs centre distance d")
128 n = (int(8 * R), int(5 * R), int(5 * R))
129 cy, cz = n[1] / 2, n[2] / 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}")
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:
140 seeds = [(cx, cy, cz, R)]
142 seeds = [(cx - d / 2, cy, cz, R), (cx + d / 2, cy, cz, R)]
144 v0 = [b[
"volume"]
for b
in s.diagnostics.vof_block_stats()]
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()]
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(),
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."""
168 print(
"SHEAR — two bubbles colliding in Couette flow (walls at +-y moving at +-U)")
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)))
176 s = pf.Solver(nx, ny, nz)
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))
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)]
193 s.enable_vof_blocks(seeds)
194 s.enable_vof_block_csf()
196 s.diagnostics.set_vof_block_overlap_density(
True)
198 from vof_surface_tension
import sphere_fractions
201 C = np.maximum(C, sphere_fractions(n, q[3], q[:3]))
202 s.set_vof(np.asfortranarray(C))
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))
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)
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}")
221 except Exception
as exc:
222 print(f
" step {i} t={t:.3f}: THREW {exc}")
226 if blocks
and hasattr(s.diagnostics,
"vof_block_curvature_stats"):
228 agg[
"umax"] = max(agg[
"umax"],
umax(s))
229 agg[
"bound"] += int(k[
"bound"])
230 if k[
"overlap_cells"] > 0:
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"])
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}"
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:
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)