40def cylinder_fractions(shape, R, cx, cz, sub=32):
41 """Volume fraction of a cylinder with its axis along y (a 2-D disc), exact in z."""
43 ax = (np.arange(nx)[:,
None] + (np.arange(sub)[
None, :] + 0.5) / sub).ravel()
44 half = np.sqrt(np.maximum(R * R - (ax - cx) ** 2, 0.0))
45 z0, z1 = cz - half, cz + half
46 C = np.zeros((nx, ny, nz))
48 seg = np.maximum(np.minimum(z1, k + 1) - np.maximum(z0, k), 0.0)
49 C[:, :, k] = seg.reshape(nx, sub).
mean(axis=1)[:,
None]
50 return np.asfortranarray(C)
53def sphere_fractions(shape, R, c, sub=24):
54 """Liquid volume fraction of a sphere: exact in z, sub x sub subsampled in (x,y)."""
56 ax = (np.arange(nx)[:,
None] + (np.arange(sub)[
None, :] + 0.5) / sub).ravel()
57 ay = (np.arange(ny)[:,
None] + (np.arange(sub)[
None, :] + 0.5) / sub).ravel()
58 X, Y = ax[:,
None], ay[
None, :]
59 half = np.sqrt(np.maximum(R * R - (X - c[0]) ** 2 - (Y - c[1]) ** 2, 0.0))
60 z0, z1 = c[2] - half, c[2] + half
61 C = np.zeros((nx, ny, nz))
63 seg = np.maximum(np.minimum(z1, k + 1) - np.maximum(z0, k), 0.0)
64 C[:, :, k] = seg.reshape(nx, sub, ny, sub).
mean(axis=(1, 3))
65 return np.asfortranarray(C)
69 """C = liquid fraction; the gas bubble is the C = 0 sphere at (nx/2, nx/2, cz)."""
70 q = (np.arange(sub) + 0.5) / sub
71 C = np.zeros((nx, nx, nz))
75 acc = np.zeros((nx, nx))
77 px = (np.arange(nx) + a)[:,
None]
79 py = (np.arange(nx) + b)[
None, :]
80 r2 = (px - c) ** 2 + (py - c) ** 2
81 acc += ((r2[:, :,
None] + (pz - cz) ** 2) < R * R).sum(axis=2)
82 C[:, :, k] = 1.0 - acc / sub ** 3
83 return np.asfortranarray(C)
86def bed_sdf(centres, nx, nz, Rg, periodic_xy=True):
87 """Signed distance to a union of spheres, positive in the fluid; periodic in x and y."""
89 g, gz = np.arange(nx) + 0.5, np.arange(nz) + 0.5
90 X, Y, Z = np.meshgrid(g, g, gz, indexing=
"ij")
91 phi = np.full((nx, nx, nz), 1e30)
92 for k
in range(len(centres)):
93 dx = X - centres[k, 0]
94 dy = Y - centres[k, 1]
96 dx -= W * np.round(dx / W)
97 dy -= W * np.round(dy / W)
98 dz = Z - centres[k, 2]
99 phi = np.minimum(phi, np.sqrt(dx * dx + dy * dy + dz * dz) - Rg)
100 return np.asfortranarray(phi)
104 """A deterministic random-sequential-addition sphere bed in a laterally periodic column.
106 A stand-in for the gallery pages' DEM-settled packings: same grain radius, same column, same
107 number of grains, and no `dem` import (which would put two Kokkos backends in one process).
108 The PROFILE does not depend on which loose packing it is -- only on the cut-cell count, which
109 is set by the grain count and radius.
111 rng = np.random.default_rng(seed)
115 while len(pts) < n
and tries < 400000:
117 p = np.array([rng.uniform(0, W), rng.uniform(0, W), rng.uniform(z0 + R, z0 + R + 3.6 * R)])
121 d[0] -= W * round(d[0] / W)
122 d[1] -= W * round(d[1] / W)
123 if d @ d < (2 * R * (1 + gap)) ** 2:
149def profile(s, nsteps, pick_dt, cap, label, extra=None):
150 """`pick_dt(s, i)` sets dt for step i and returns which limit bound it ('cfl'|'cap'|'fixed')."""
151 s.diagnostics.set_vof_timing(
True)
152 binds = {
"cfl": 0,
"cap": 0,
"fixed": 0}
156 for i
in range(nsteps):
157 binds[pick_dt(s, i)] += 1
162 except RuntimeError
as ex:
163 if "Weymouth-Yue boundedness cap" not in str(ex)
or retries > 20:
166 set_dt(s, 0.5 * _LAST_DT[
"dt"])
167 it = s.diagnostics.last_pressure_iterations()
168 itmax = max(itmax, it)
169 capped += int(it >= cap)
170 wall = time.time() - t0
171 tm = dict(s.diagnostics.vof_timing())
172 s.diagnostics.set_vof_timing(
False)
175 tm[
"ms_per_step"] = 1e3 * wall / nsteps
176 tm[
"press_max"] = itmax
177 tm[
"press_cap"] = cap
178 tm[
"capped"] = capped
179 tm[
"binds_cfl"] = binds[
"cfl"]
180 tm[
"binds_capillary"] = binds[
"cap"]
181 tm[
"binds_fixed"] = binds[
"fixed"]
182 tm[
"dt_retries"] = retries
214HYSING1 = dict(rho1=1000.0, rho2=100.0, mu1=10.0, mu2=1.0, g=0.98, sigma=24.5)
222 s = flow.Solver(nx, ny, nz)
224 s.set_mu(sc.mu(p[
"mu1"]))
225 s.set_domain_bc(
"-z",
"wall", (0, 0, 0))
226 s.set_domain_bc(
"+z",
"wall", (0, 0, 0))
227 s.set_pressure_geometry(np.full((nx, ny, nz), 10.0, order=
"F"))
228 s.set_pressure_chebyshev(
True, 600, 1e-12)
230 s.diagnostics.set_vof_worklist(worklist)
231 s.set_vof(np.asfortranarray(
232 1.0 - cylinder_fractions((nx, ny, nz), R, nx / 2.0, sc.s * 0.5)))
233 s.set_property_model(
"rho",
"linear",
"C", [p[
"rho2"], p[
"rho1"] - p[
"rho2"]])
234 s.set_property_model(
"mu",
"linear",
"C",
235 [sc.mu(p[
"mu2"]), sc.mu(p[
"mu1"] - p[
"mu2"])])
236 s.set_surface_tension(sc.sigma(p[
"sigma"]))
238 s.enable_vof_momentum(p[
"rho2"], p[
"rho1"])
239 s.set_property_model(
"force_z",
"linear",
"C",
240 [-sc.bodyforce(p[
"rho2"] * p[
"g"]),
241 -sc.bodyforce((p[
"rho1"] - p[
"rho2"]) * p[
"g"])])
242 s.set_dt(0.5 * s.capillary_dt())
247 """E7: a gas bubble released under a sphere bed in a closed, laterally periodic column."""
248 NG, RJ, FSDF = 26, 0.171875 * nx, 0.85
250 ZBED, RBUB, ZBUB = 0.5625 * nx, 0.15625 * nx, 0.28125 * nx
251 ctr =
cached(f
"bed_e7_{nx}_{nz}",
lambda:
rsa_bed(NG, float(nx), RJ, ZBED, seed=11))
252 SDF =
cached(f
"sdf_e7_{nx}_{nz}",
lambda:
bed_sdf(ctr, nx, nz, RG))
253 ratio, Eo, Mo, g, theta = 100.0, 10.0, 1e-3, 2e-4, 60.0
255 rho_l, rho_g = 1.0, 1.0 / ratio
257 sigma = drho * g * D * D / Eo
258 mu_l = (Mo * rho_l ** 2 * sigma ** 3 / (g * drho)) ** 0.25
260 s = flow.Solver(nx, nx, nz)
263 s.set_domain_bc(
"-z",
"wall", (0, 0, 0))
264 s.set_domain_bc(
"+z",
"wall", (0, 0, 0))
265 s.set_solid(np.asfortranarray(SDF), cutcell_pressure=
True)
267 s.diagnostics.set_vof_worklist(worklist)
269 s.set_property_model(
"rho",
"linear",
"C", [rho_g, rho_l - rho_g])
270 s.set_property_model(
"mu",
"linear",
"C", [mu_g, mu_l - mu_g])
271 s.set_surface_tension(sigma)
272 s.set_contact_angle(theta)
273 s.enable_vof_momentum(rho_g, rho_l)
274 s.set_property_model(
"force_z",
"linear",
"C", [-rho_g * g, -(rho_l - rho_g) * g])
275 s.set_pressure_chebyshev(
True, 600, 1e-12)
276 s.set_dt(0.4 * s.capillary_dt())
281 """E6: a liquid distributor over a dry sphere bed, outflow at the bottom."""
282 RHO_L, MU_L, MU_RATIO, BO, THETA = 100.0, 50.0, 50.0, 1.2, 60.0
283 R48, ZBED0, NG = 7.7, 22.0, 34
286 ctr =
cached(f
"bed_e6_{nx}_{nz}",
lambda:
rsa_bed(NG, float(nx), R, ZBED0 * (nz / 96.0), 7))
287 SDF =
cached(f
"sdf_e6_{nx}_{nz}",
lambda:
bed_sdf(ctr, nx, nz, R))
289 rho_g, mu_g = RHO_L / ratio, MU_L / MU_RATIO
291 delta, umax = 3.0 * sc, 0.3 * sc
292 g = 2 * MU_L * umax / (drho * delta ** 2)
294 sigma = drho * g * dp ** 2 / BO
296 s = flow.Solver(nx, nx, nz)
299 for f
in (
"-x",
"+x",
"-y",
"+y"):
300 s.set_domain_bc(f,
"periodic", (0, 0, 0))
301 s.set_domain_bc(
"-z",
"outflow", (0, 0, 0))
302 xc = (np.arange(nx) + 0.5)[:,
None]
303 yc = (np.arange(nx) + 0.5)[
None, :]
304 disc = ((xc - nx / 2) ** 2 + (yc - nx / 2) ** 2) < rd ** 2
305 prof = np.zeros((nx, nx, 3))
306 prof[:, :, 2] = np.where(disc, -umax, 0.0)
307 s.set_domain_bc_profile(
"+z", np.ascontiguousarray(prof))
308 s.diagnostics.set_velocity_solver_params(60)
309 s.set_pressure_multigrid(
True, levels=6)
310 s.set_pressure_solver_params(80)
311 s.set_solid(np.asfortranarray(SDF), cutcell_pressure=
True)
313 s.diagnostics.set_vof_worklist(worklist)
314 s.set_vof(np.zeros((nx, nx, nz), order=
"F"))
315 s.set_surface_tension(sigma)
316 s.set_contact_angle(THETA)
317 s.set_property_model(
"rho",
"linear",
"C", [rho_g, RHO_L - rho_g])
318 s.set_property_model(
"mu",
"linear",
"C", [mu_g, MU_L - mu_g])
319 s.set_property_model(
"force_z",
"linear",
"C", [0.0, -drho * g])
320 s.enable_vof_momentum(rho_g, RHO_L)
321 s.set_pressure_fcg(
True, 400, 1e-11)
322 s.set_vof_inflow_profile(
"+z", np.ascontiguousarray(disc.astype(float)))
323 s.set_vof_backflow(
"-z", 0.0)
324 dt_cap = 0.5 * s.capillary_dt()
328 L = sv.vof_step_limits()
329 dt = min(dt_cap, 0.4 * L[
"cfl_dt"])
if L[
"cfl_dt"] > 0
else dt_cap
331 return "cap" if dt >= dt_cap - 1e-30
else "cfl"
333 return s, pick, 400, f
"trickle {nx}x{nx}x{nz}"
337 """A stationary droplet, D/dx = n/2, in an n^3 periodic box (the parasitic-currents case)."""
340 off = (0.13, 0.27, 0.11)
341 s = flow.Solver(n, n, n)
344 s.set_pressure_geometry(np.full((n, n, n), 10.0, order=
"F"))
345 s.set_pressure_chebyshev(
True, 500, 1e-14)
347 s.diagnostics.set_vof_worklist(worklist)
348 s.set_vof(
cached(f
"drop_{n}",
lambda: sphere_fractions(
349 (n, n, n), R, (n / 2 + off[0], n / 2 + off[1], n / 2 + off[2]))))
350 s.set_property_model(
"rho",
"linear",
"C", [1.0, 0.0])
351 s.set_surface_tension(SIGMA)
352 dt = 0.5 * s.capillary_dt()
359 return s, pick, 500, f
"droplet {n}^3"
363 """WO-P23 rung P3: a Scriven bubble growing in superheated liquid (the phase-change pipeline)."""
364 sys.path.insert(0, HERE)
365 import vof_scriven
as vs
368 beta = vs.scriven_beta(ja, rr)
369 t0 = (r0 / (2 * beta)) ** 2
370 rho_l, rho_v, cpl = 1.0, rr, 1.0
372 k_l = alpha_l * rho_l * cpl
375 h_lv = rho_l * cpl * dT / (rho_v * ja)
377 s = flow.Solver(n, n, n)
380 for f
in (
"-x",
"+x",
"-y",
"+y",
"-z",
"+z"):
381 s.set_domain_bc(f,
"outflow")
382 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
384 s.diagnostics.set_vof_worklist(worklist)
385 s.set_vof(
cached(f
"scriven_c_{n}_{r0}",
lambda: vs.sphere_colour_chunked(n, ctr, r0, 4)))
386 s.set_property_model(
"rho",
"linear",
"C", [rho_v, rho_l - rho_v])
387 s.set_pressure_fcg(
True, 600, 1e-10)
388 s.add_scalar(
"T", k_l / (rho_l * cpl), 1, 200)
389 for f
in (
"-x",
"+x",
"-y",
"+y",
"-z",
"+z"):
390 s.set_scalar_bc(
"T", f,
"dirichlet", dT)
391 s.set_field(
"T", np.asfortranarray(
cached(
392 f
"scriven_T_{n}_{r0}_{ja}",
393 lambda: vs.initial_temperature(n, ctr, r0, beta, rr, dT, mode=
"similarity"))))
394 s.enable_phase_change(rho_v, rho_l, h_lv)
395 s.set_phase_change_thermal(
True,
"T", 0.0, k_v, k_l, 0.0)
396 s.diagnostics.set_phase_change_plane_dirichlet(
True)
397 s.set_phase_change_energy(
True, rho_v * cpl, rho_l * cpl)
398 s.diagnostics.set_phase_change_quadratic_fit(
True)
399 Rd = beta * math.sqrt(alpha_l / t0)
400 dt = 0.4 * 0.2 / max(Rd * (1.0 - rr), 1e-30)
406 st = {
"dt": 0.25 * dt}
410 c = sv.diagnostics.vof_last_courant()
411 st[
"dt"] *= min(1.2, max(0.5, 0.2 / max(c, 1e-30)))
415 return s, pick, 600, f
"scriven {n}^3 Ja={ja}"
472 ap = argparse.ArgumentParser()
473 ap.add_argument(
"--case", default=
"all")
474 ap.add_argument(
"--steps", type=int, default=20)
475 ap.add_argument(
"--warm", type=int, default=5)
476 ap.add_argument(
"--worklist", choices=(
"on",
"off",
"both"), default=
"on")
477 ap.add_argument(
"--curv-worklist", choices=(
"on",
"off",
"both"), default=
"on")
478 ap.add_argument(
"--json", default=
"")
480 names = list(CASES)
if a.case ==
"all" else a.case.split(
",")
481 wls = [
True,
False]
if a.worklist ==
"both" else [a.worklist ==
"on"]
482 cwls = [
True,
False]
if a.curv_worklist ==
"both" else [a.curv_worklist ==
"on"]
487 s, pick, cap, label = CASES[nm](worklist=wl)
488 s.diagnostics.set_vof_curvature_worklist(cwl)
489 label +=
"" if cwl
else " [curv worklist OFF]"
490 for i
in range(a.warm):
493 tm =
profile(s, a.steps, pick, cap,
494 label + (
"" if wl
else " [worklist OFF]"),
495 extra={
"case": nm,
"worklist": wl,
"curv_worklist": cwl})
500 with open(a.json,
"w")
as f:
501 json.dump(out, f, indent=1)
502 print(f
"\nwrote {a.json}")