39 """Periodic array of spheres; > 0 in fluid (flow's SDF convention), cell centres at i + 1/2."""
40 ax = (np.arange(n) + 0.5)[:,
None,
None]
41 ay = (np.arange(n) + 0.5)[
None, :,
None]
42 az = (np.arange(n) + 0.5)[
None,
None, :]
43 best = np.full((n, n, n), 1e30)
44 for cx, cy, cz
in centres:
48 d = np.sqrt((ax - (cx + px)) ** 2 + (ay - (cy + py)) ** 2
49 + (az - (cz + pz)) ** 2) - radius
50 best = np.minimum(best, d)
51 return np.asfortranarray(best)
54DEFAULT_CENTRES = [(6.0, 7.0, 8.0), (20.0, 9.0, 23.0), (11.0, 24.0, 19.0), (26.0, 22.0, 6.0)]
77def g2(n=48, nadv=500, cfl=0.2):
79 div = s.max_open_divergence()
81 c0 = np.zeros((n, n, n), order=
"F")
82 c0[:, :, : n // 2] = 1.0
84 d0 = s.diagnostics.vof_diagnostics()
85 dt = cfl / s.vof_max_courant()
91 d = s.diagnostics.vof_diagnostics()
92 clip += d[
"clipped_volume"]
93 mn = min(mn, d[
"min_fluid"])
94 mx = max(mx, d[
"max_fluid"])
95 solid = max(solid, abs(d[
"solid_sum"]))
96 d1 = s.diagnostics.vof_diagnostics()
97 drift = (d1[
"volume"] - d0[
"volume"]) / d0[
"volume"]
98 raw = (d1[
"raw_volume"] - d0[
"raw_volume"]) / d0[
"raw_volume"]
99 print(f
"G2 packing {n}^3, {nadv} kinematic steps at interface CFL {cfl} (dt = {dt:.5g})")
100 print(f
" max|div(open u)| {div:.3e} pressure iters {it}")
101 print(f
" sum eps_eff C {d0['volume']:.15e} -> {d1['volume']:.15e} drift {drift:.3e}")
102 print(f
" sum eps C {d0['raw_volume']:.15e} -> {d1['raw_volume']:.15e} drift {raw:.3e}")
103 print(f
" solid cells {d1['solid_cells']}, cut cells {d1['cut_cells']}, "
104 f
"clamped faces last step {d1['clamped_faces']}")
105 print(f
" max|sum C over solid cells| {solid:.3e} (must be exactly 0)")
106 print(f
" min/max C over UNCUT fluid cells {mn:.3e} / {mx:.17g}")
107 print(f
" clipped liquid volume over the run {clip:.6e} "
108 f
"({clip / d0['volume']:.3e} of the liquid volume)")
109 ok = abs(drift) <= 1e-11
and solid == 0.0
and mn >= -1e-12
and mx <= 1 + 1e-12
110 print(f
" G2 {'PASS' if ok else 'FAIL'}")
115def g4(n=48, steps=400, ratio=10.0, grav=2e-3, momentum=True):
116 s = pf.Solver(n, n, n)
120 s.set_solid(
packing(n), cutcell_pressure=
True)
122 c0 = np.zeros((n, n, n), order=
"F")
123 c0[:, :, 3 * n // 4:] = 1.0
126 rho_mean = liq * ratio + (1.0 - liq)
127 s.set_property_model(
"rho",
"linear",
"C", [1.0, ratio - 1.0])
128 s.set_property_model(
"mu",
"linear",
"C", [0.05, 0.45])
131 s.set_property_model(
"force_z",
"linear",
"rho", [grav * rho_mean, -grav])
133 s.enable_vof_momentum(1.0, ratio)
134 v0 = s.diagnostics.vof_diagnostics()[
"volume"]
135 maxit, capped, clip = 0, 0, 0.0
136 for _
in range(steps):
138 it = s.diagnostics.last_pressure_iterations()
139 maxit = max(maxit, it)
140 capped += it >= PRESS_MAXIT
141 clip += s.diagnostics.vof_diagnostics()[
"clipped_volume"]
142 d1 = s.diagnostics.vof_diagnostics()
143 umax = max(abs(np.asarray(g())).max()
for g
in (s.get_u, s.get_v, s.get_w))
144 drift = (d1[
"volume"] - v0) / v0
145 print(f
"G4 packing {n}^3 draining, ratio {ratio}, {steps} coupled steps, "
146 f
"momentum consistency {'ON' if momentum else 'off'}")
147 print(f
" colour drift {drift:.3e} ({drift / steps:.3e} per step); "
148 f
"max|div(open u)| {s.max_open_divergence():.3e}")
149 print(f
" pressure iters max {maxit} (cap {PRESS_MAXIT}, capped steps {capped})")
150 print(f
" min/max C over uncut fluid {d1['min_fluid']:.3e} / {d1['max_fluid']:.17g}; "
151 f
"clipped volume total {clip:.3e}")
152 print(f
" max|u| {umax:.4e}; sum C over solid cells {d1['solid_sum']:.3e}")
153 ok = (
not math.isnan(umax)
and capped == 0
and abs(drift) / steps <= 1e-10
154 and d1[
"solid_sum"] == 0.0)
155 print(f
" G4 {'PASS' if ok else 'FAIL (a capped solve makes the run INVALID)'}")
160def g5(diam=24, steps=200, sigma=1.0, mu=0.05, cfl=0.5):
161 nx = ny = int(1.7 * diam)
165 z = (np.arange(nz) + 0.5)[
None,
None, :]
166 sdf = np.asfortranarray(np.broadcast_to(z - zw, (nx, ny, nz)).astype(float).copy())
167 cx, cy = nx * 0.5, ny * 0.5
169 sub = (np.arange(4) + 0.5) / 4.0
170 c0 = np.zeros((nx, ny, nz))
174 px = (np.arange(nx) + a)[:,
None,
None]
175 py = (np.arange(ny) + b)[
None, :,
None]
176 pz = (np.arange(nz) + c)[
None,
None, :]
178 inside = ((px - cx) ** 2 + (py - cy) ** 2 + (pz - zw) ** 2 < r * r) & fluid
179 c0 += np.where(inside, 1.0, 0.0)
180 tot = np.zeros((nx, ny, nz))
182 pz = (np.arange(nz) + c)[
None,
None, :]
183 tot += np.broadcast_to(np.where(pz >= zw, 16.0, 0.0), (nx, ny, nz))
184 c0 = np.asfortranarray(np.where(tot > 0, c0 / np.maximum(tot, 1e-30), 0.0))
186 s = pf.Solver(nx, ny, nz)
189 s.set_solid(sdf, cutcell_pressure=
True)
192 s.set_surface_tension(sigma)
193 dts = s.capillary_dt()
195 v0 = s.diagnostics.vof_diagnostics()[
"volume"]
197 for _
in range(steps):
199 it = s.diagnostics.last_pressure_iterations()
200 maxit = max(maxit, it)
201 capped += it >= PRESS_MAXIT
202 cc = np.asarray(s.get_vof())
203 d1 = s.diagnostics.vof_diagnostics()
204 z0 = int(math.ceil(zw))
206 frac = np.clip(np.arange(nz) + 1 - zw, 0.0, 1.0)
207 h = float((cc[nx // 2, ny // 2, :] * frac).sum())
208 a = math.sqrt(float(cc[:, :, z0].sum()) / math.pi)
209 theta = math.degrees(2.0 * math.atan2(h, a))
210 rc = (a * a + h * h) / (2.0 * h)
211 p = np.asarray(s.get_field(
"p"))
212 px = (np.arange(nx) + 0.5)[:,
None,
None] - cx
213 py = (np.arange(ny) + 0.5)[
None, :,
None] - cy
214 pz = (np.arange(nz) + 0.5)[
None,
None, :] - zw
215 rr = np.sqrt(px ** 2 + py ** 2 + pz ** 2)
216 inner = (rr < 0.55 * r) & (pz > 0.5)
217 outer = (rr > 1.5 * r) & (rr < 1.8 * r) & (pz > 0.5)
218 dp = float(p[inner].
mean() - p[outer].
mean())
219 umax_open = max(abs(np.asarray(g())[:, :, z0 + 1:]).max()
for g
in (s.get_u, s.get_v, s.get_w))
220 umax_all = max(abs(np.asarray(g())).max()
for g
in (s.get_u, s.get_v, s.get_w))
221 print(f
"G5 liquid cap on a CUT flat wall, D/dx = {diam}, {nx}x{ny}x{nz}, {steps} steps at "
222 f
"{cfl} dt_sigma = {cfl * dts:.5g}")
223 print(f
" volume {v0:.6f} -> {d1['volume']:.6f} (drift {(d1['volume'] - v0) / v0:.3e}); "
224 f
"analytic hemisphere {2 * math.pi * r ** 3 / 3:.6f}")
225 print(f
" h {h:.4f} a {a:.4f} -> apparent theta {theta:.3f} deg (target 90)")
226 print(f
" cap sphere radius {rc:.4f} (target {r}); Young-Laplace dP {dp:.6f} vs "
227 f
"2 sigma/R {2 * sigma / rc:.6f} (rel {abs(dp - 2 * sigma / rc) / (2 * sigma / rc):.3e})")
228 print(f
" spurious currents: max|u| in the OPEN fluid {umax_open:.3e} -> "
229 f
"Ca = mu max|u|/sigma = {mu * umax_open / sigma:.3e}; "
230 f
"max|u| including the wall band {umax_all:.3e}")
231 print(f
" (V4 free-droplet reference at the same D/dx: Ca 1.4e-5 ... 2.6e-5)")
232 print(f
" pressure iters max {maxit} (capped steps {capped}); "
233 f
"sum C over solid cells {d1['solid_sum']:.3e}")
234 ok = (abs(theta - 90.0) <= 3.0
235 and abs(dp - 2 * sigma / rc) / (2 * sigma / rc) <= 0.01
236 and d1[
"solid_sum"] == 0.0
and capped == 0)
237 print(f
" G5 {'PASS' if ok else 'FAIL'}")