flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
vof_collocated.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""VoF rung V8 (WO-T) — the staggered/collocated physics battery, both columns.
3
4The collocated (`SolverColocated`) path is the **ABC approximate projection**: average the cell
5velocities onto a MAC face field, project THAT exactly, correct the cell field. Rung V8 adds
6
7 * variable density — the face coefficient ``c_f = o_f rho0/rho_f`` and ``projectCorrectVar`` on the
8 FACE field, with the CELL correction the AVERAGE OF THE TWO FACE CORRECTIONS of each axis;
9 * forces as FACE accelerations ``a_f = dt (f_f - (P(i)-P(i-s)))/rho_f`` added after
10 ``centerToFace``, the cell taking the average of the two faces' total increment (Basilisk
11 ``centered.h``; Popinet JCP 2009 §3);
12 * colour advection from ``uf_/vf_/wf_``, the field the approximate projection makes exactly
13 divergence-free.
14
15What this script measures, staggered vs collocated:
16
17 G3 static droplet with the COMPUTED curvature — the spurious capillary number
18 ``Ca = mu max|u| / sigma`` at D/dx = 16 (and 8, 24).
19 G4 Hysing case 1 (density ratio 10) — max rise velocity and y_c(3). The collocated column runs
20 WITHOUT momentum consistency (`enable_vof_momentum` is staggered-only at this rung — the
21 collocated construction needs Favre face states), so the honest staggered comparison is the
22 one with it OFF; both staggered columns are printed.
23 G5 capillary wave at 32 cells/lambda — the frequency against the exact viscous two-fluid mode.
24
25Every gate records the pressure iteration count against its cap (preamble rule 3b: a capped solve
26makes the run INVALID) and, on the collocated grid, BOTH the cell max|u| and the FACE max|uf|: the
27cell field carries the approximate projection's invisible checkerboard, which `centerToFace`
28annihilates and the projection therefore cannot remove.
29
30Usage: PYTHONPATH=<build> python tests/study/vof_collocated.py [static|hysing1|wave] [--quick]
31"""
32import math
33import os
34import sys
35import time
36
37import numpy as np
38
39import peclet.flow as pf
40
41sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
42from vof_capillary_references import wave_mode # noqa: E402
43from vof_surface_tension import (HYSING, Scale, Solve, cylinder_fractions, # noqa: E402
44 maxvel, sphere_fractions, zero_crossings)
45
46QUICK = "--quick" in sys.argv
47GATES = [a for a in sys.argv[1:] if not a.startswith("--")]
48
49GRIDS = (("staggered ", pf.Solver), ("collocated", pf.SolverColocated))
50
51
53 return max(np.abs(s.get_uf()).max(), np.abs(s.get_vf()).max(), np.abs(s.get_wf()).max())
54
55
56# --------------------------------------------------------------------------- G3 static droplet
58 print("\n" + "=" * 100)
59 print("G3 STATIC DROPLET, computed curvature — Ca = mu max|u| / sigma, both grids")
60 print("=" * 100)
61 print(" The balanced-force identity is exact on both grids (the ctest's T2: the FACE field")
62 print(" stays at machine zero with a constant kappa). What is left is the curvature error.")
63 print(" On the collocated grid the CELL number additionally carries the invisible")
64 print(" checkerboard, so both are printed.\n")
65 rungs = [(16, 4.0), (32, 8.0)] if QUICK else [(16, 4.0), (32, 8.0), (48, 12.0)]
66 for n, R in rungs:
67 row = {}
68 for label, cls in GRIDS:
69 s = cls(n, n, n)
70 s.set_rho(1.0)
71 s.set_mu(0.1)
72 s.set_pressure_geometry(np.full((n, n, n), 10.0, order="F"))
73 s.set_pressure_chebyshev(True, 500, 1e-14)
74 s.enable_vof()
75 s.set_vof(sphere_fractions((n, n, n), R, (n / 2 + 0.13, n / 2 + 0.27, n / 2 + 0.11)))
76 s.set_property_model("rho", "linear", "C", [1.0, 0.0])
77 s.set_surface_tension(1.0)
78 s.set_dt(0.5 * s.capillary_dt())
79 h = Solve(500)
80 for _ in range(60):
81 s.step()
82 h.sample(s)
83 ca = 0.1 * maxvel(s) / 1.0
84 caf = 0.1 * maxfacevel(s) / 1.0
85 row[label] = (ca, caf, h)
86 print(f" D/dx = {2*R:5.1f} {label} Ca(cell) = {ca:.3e} Ca(face) = {caf:.3e}")
87 print(f" {h}")
88 st, co = row["staggered "][0], row["collocated"][0]
89 print(f" D/dx = {2*R:5.1f} collocated / staggered (cell Ca) = {co/st:.2f} x\n")
90
91
92# --------------------------------------------------------------------------- G5 capillary wave
94 print("\n" + "=" * 100)
95 print("G5 CAPILLARY WAVE, 32 cells/lambda — omega against the EXACT viscous two-fluid mode")
96 print("=" * 100)
97 rungs = [(32, 0.005)] if QUICK else [(32, 0.005), (64, 0.005)]
98 for nx, nu in rungs:
99 nz, ny, rho, sigma = nx, 4, 1.0, 1.0
100 lam = float(nx)
101 k = 2 * math.pi / lam
102 a0 = lam / 100.0
103 omega_th = math.sqrt(sigma * k ** 3 / (2 * rho))
104 s_ex, _ = wave_mode(k, nu, sigma, rho, H=nz / 2.0)
105 omega_ex, gamma_ex = s_ex.imag, -s_ex.real
106 for label, cls in GRIDS:
107 s = cls(nx, ny, nz)
108 s.set_rho(rho)
109 s.set_mu(nu * rho)
110 s.set_domain_bc("-z", "wall", (0, 0, 0))
111 s.set_domain_bc("+z", "wall", (0, 0, 0))
112 s.set_pressure_geometry(np.full((nx, ny, nz), 10.0, order="F"))
113 s.set_pressure_chebyshev(True, 500, 1e-11)
114 s.enable_vof()
115 xe = np.arange(nx + 1)
116 zi = nz / 2.0 + a0 * (np.sin(k * xe[1:]) - np.sin(k * xe[:-1])) / (k * 1.0)
117 C = np.zeros((nx, ny, nz))
118 for kk in range(nz):
119 C[:, :, kk] = np.clip(zi - kk, 0.0, 1.0)[:, None]
120 s.set_vof(np.asfortranarray(C))
121 s.set_property_model("rho", "linear", "C", [rho, 0.0])
122 s.set_surface_tension(sigma)
123 dt = 0.5 * s.capillary_dt()
124 s.set_dt(dt)
125 nsteps = int(2.5 * (2 * math.pi / omega_th) / dt)
126 h = Solve(500)
127 t, amp = [], []
128 for i in range(nsteps):
129 s.step()
130 h.sample(s)
131 col = s.get_vof().sum(axis=2)[:, 0]
132 t.append((i + 1) * dt)
133 amp.append(float(np.dot(col - nz / 2.0,
134 np.cos(k * (np.arange(nx) + 0.5))) * 2.0 / nx))
135 t, amp = np.array(t), np.array(amp)
136 zc = zero_crossings(t, amp)
137 omega_num = math.pi / np.mean(np.diff(zc)) if len(zc) >= 3 else float("nan")
138 pk = [i for i in range(1, len(amp) - 1)
139 if abs(amp[i]) > abs(amp[i - 1]) and abs(amp[i]) > abs(amp[i + 1])]
140 gamma_num = (-math.log(abs(amp[pk[-1]]) / abs(amp[pk[0]])) / (t[pk[-1]] - t[pk[0]])
141 if len(pk) >= 2 else float("nan"))
142 print(f" nx = {nx} {label} omega {omega_num:.5f} vs inviscid {omega_th:.5f} "
143 f"({100*(omega_num/omega_th-1):+.2f} %) vs EXACT viscous {omega_ex:.5f} "
144 f"({100*(omega_num/omega_ex-1):+.2f} %)")
145 print(f" gamma {gamma_num:.3e} vs exact {gamma_ex:.3e} "
146 f"({100*(gamma_num/gamma_ex-1):+.1f} %); {h}")
147
148
149# --------------------------------------------------------------------------- G4 Hysing case 1
150def gate_hysing1(nx=64):
151 p = HYSING[1]
152 print("\n" + "=" * 100)
153 print(f"G4 HYSING RISING BUBBLE case 1 (ratio 10), nx = {nx} — both grids")
154 print("=" * 100)
155 print(" `enable_vof_momentum` is STAGGERED-ONLY at this rung (the collocated construction")
156 print(" needs Favre face states, AMR-Wind's pattern), so the collocated column runs without")
157 print(" it and the honest staggered comparison is the momentum-consistency-OFF one. Both")
158 print(" staggered columns are printed; V2b is worth ~14 % here.\n")
159 Lx = 1.0
160 nz, ny = 2 * nx, 4
161 sc = Scale(nx / Lx)
162 R = sc.len_to_cells(0.25)
163 runs = [("staggered (V2b ON )", pf.Solver, True),
164 ("staggered (V2b OFF)", pf.Solver, False),
165 ("COLLOCATED (V2b n/a)", pf.SolverColocated, False)]
166 out = {}
167 for label, cls, vmom in runs:
168 s = cls(nx, ny, nz)
169 s.set_rho(p["rho1"])
170 s.set_mu(sc.mu(p["mu1"]))
171 s.set_domain_bc("-z", "wall", (0, 0, 0))
172 s.set_domain_bc("+z", "wall", (0, 0, 0))
173 s.set_pressure_geometry(np.full((nx, ny, nz), 10.0, order="F"))
174 s.set_pressure_chebyshev(True, 600, 1e-12)
175 s.enable_vof()
176 C = 1.0 - cylinder_fractions((nx, ny, nz), R, nx / 2.0, sc.len_to_cells(0.5))
177 s.set_vof(np.asfortranarray(C))
178 s.set_property_model("rho", "linear", "C", [p["rho2"], p["rho1"] - p["rho2"]])
179 s.set_property_model("mu", "linear", "C",
180 [sc.mu(p["mu2"]), sc.mu(p["mu1"] - p["mu2"])])
181 s.set_surface_tension(sc.sigma(p["sigma"]))
182 if vmom:
183 s.enable_vof_momentum(p["rho2"], p["rho1"])
184 s.set_property_model("force_z", "linear", "C",
185 [-sc.bodyforce(p["rho2"] * p["g"]),
186 -sc.bodyforce((p["rho1"] - p["rho2"]) * p["g"])])
187 T = 3.0
188 dt = 0.5 * s.capillary_dt()
189 s.set_dt(dt)
190 zs = (np.arange(nz) + 0.5) / sc.s
191 h = Solve(600)
192 vmax, tvmax, yc, t, i = 0.0, 0.0, 0.0, 0.0, 0
193 t0 = time.time()
194 while t < T:
195 if i % 10 == 0:
196 L = s.vof_step_limits()
197 dt = 0.4 * min(L["cfl_dt"], L["capillary_dt"])
198 s.set_dt(dt)
199 t += dt
200 i += 1
201 s.step()
202 h.sample(s)
203 B = 1.0 - s.get_vof()
204 vb = B.sum()
205 yc = float((B.sum(axis=(0, 1)) * zs).sum() / vb)
206 vrise = float((B * s.get_w()).sum() / vb) / sc.s
207 if vrise > vmax:
208 vmax, tvmax = vrise, t
209 out[label] = (vmax, tvmax, yc)
210 print(f" {label} v_rise max {vmax:.4f} at t = {tvmax:.3f} y_c(3) = {yc:.4f} "
211 f"({i} steps, {time.time()-t0:.0f} s)")
212 print(f" {h}")
213 print(f"\n published reference (Hysing 2009): v_rise max {p['ref_vmax']:.4f} at "
214 f"t = {p['ref_tvmax']:.4f}, y_c(3) = {p['ref_yc']:.4f}")
215 a = out["staggered (V2b OFF)"]
216 b = out["COLLOCATED (V2b n/a)"]
217 print(f" collocated vs staggered (both V2b off): v_rise {100*(b[0]/a[0]-1):+.2f} %, "
218 f"y_c(3) {100*(b[2]/a[2]-1):+.2f} %")
219
220
221ALL = {"static": gate_static, "wave": gate_wave,
222 "hysing1": lambda: gate_hysing1(32 if QUICK else 64)}
223
224if __name__ == "__main__":
225 run = GATES or list(ALL)
226 for g in run:
227 if g not in ALL:
228 print(f"unknown gate {g!r}; known: {', '.join(ALL)}")
229 sys.exit(2)
230 for g in run:
231 ALL[g]()
232 print("\ndone.")