flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
rayleigh_taylor.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Variable-density validation (Phase 5): hydrostatic acid tests + Rayleigh-Taylor demonstrator.
3
41. Hydrostatic balance (the acid test): a stratified two-layer fluid at rest under gravity must
5 STAY at rest, with the discrete pressure gradient exactly rho_face*g. This detects any
6 inconsistency between the momentum face density, the body-force face value, and the projection
7 face coefficient. Inviscid: max steady velocity ~1e-16, P-gradient error ~1e-16 at density
8 ratios 3 AND 1000. (The C++ ctest `vardensity_projection` runs the same case.)
9
102. Rayleigh-Taylor: heavy over light (ratio 3, Atwood 0.5) through the FULL two-phase chain — a
11 TRANSPORTED phase fraction c drives rho via a linear-mixture closure (auto-enabling the
12 variable-density path), gravity is a closure force_z = -g*rho, momentum + projection carry the
13 variable density. The interface amplitude grows ~exponentially then nonlinearly; the measured
14 growth rate is ~0.75x the inviscid sqrt(A g k) (viscous + finite-interface damping).
15
16 TWO records, same physics, reported side by side (VoF rung V2a / WO-J):
17 * DIFFUSE C — the pre-VoF baseline: a tanh profile of width 1.5 advected by the Koren TVD
18 scalar transport. Amplitude 1.50 -> 19.52 (x13.0), rate 0.75x sqrt(A g k).
19 * SHARP C — geometric VoF (PLIC + Weymouth-Yue), adaptive dt from the INTERFACE-LOCAL
20 Courant number. Amplitude 1.50 -> 20.20 (x13.5), rate 0.77x sqrt(A g k), colour volume
21 drift -6.6e-09. The sharp interface grows marginally FASTER, which is the expected sign:
22 a two-cell-wide density ramp damps the mode that a sharp jump does not.
23
24Run: PYTHONPATH=<build> python rayleigh_taylor.py
25"""
26import numpy as np
27import peclet.flow as F
28
29
30def hydrostatic(ratio, mu=0.0, steps=100, g=0.1, dt=1.0, N=8, NZ=24):
31 s = F.Solver(N, N, NZ)
32 s.set_rho(1.0); s.set_mu(mu); s.set_dt(dt)
33 s.set_domain_bc("-z", "wall", (0, 0, 0)); s.set_domain_bc("+z", "wall", (0, 0, 0)) # walls +-z
34 s.set_pressure_geometry(np.asfortranarray(np.full((N, N, NZ), 10.0)))
35 z = np.arange(NZ)
36 rz = np.where(z < NZ // 2, ratio, 1.0).astype(np.float64) # heavy below (stable)
37 s.add_field("rho")
38 s.set_field("rho", np.asfortranarray(np.broadcast_to(rz[None, None, :], (N, N, NZ)).copy()))
39 s.diagnostics.set_density_mode("variable") # Chebyshev pressure driver
40 s.set_property_model("force_z", "linear", "rho", [0.0, -g])
41 m = None
42 for _ in range(steps):
43 s.step()
44 m = max(np.abs(s.get_u()).max(), np.abs(s.get_v()).max(), np.abs(s.get_w()).max())
45 p = s.get_p()
46 dp = p[N//2, N//2, 1:] - p[N//2, N//2, :-1]
47 rf = 0.5 * (rz[1:] + rz[:-1])
48 perr = np.max(np.abs(dp + g * rf)) / (g * ratio)
49 return m, perr
50
51
52def rayleigh_taylor(N=48, NZ=96, g=0.005, mu=0.002, dt=1.0, steps=240):
53 s = F.Solver(N, 4, NZ)
54 s.set_rho(1.0); s.set_mu(mu); s.set_dt(dt)
55 s.set_advection(True)
56 s.set_domain_bc("-z", "wall", (0, 0, 0)); s.set_domain_bc("+z", "wall", (0, 0, 0))
57 s.set_pressure_geometry(np.asfortranarray(np.full((N, 4, NZ), 10.0)))
58 s.add_scalar("c", diffusivity=0.0, scheme="koren", iters=1)
59 s.set_scalar_bc("c", "-z", "neumann", 0.0); s.set_scalar_bc("c", "+z", "neumann", 0.0)
60 s.set_property_model("rho", "linear", "c", [1.0, 2.0]) # rho = 1 + 2c (ratio 3)
61 s.set_property_model("force_z", "linear", "rho", [0.0, -g])
62 x, z = np.arange(N), np.arange(NZ)
63 zi = NZ / 2 + 1.5 * np.cos(2 * np.pi * x / N)
64 c0 = np.zeros((N, 4, NZ))
65 for i in range(N):
66 c0[i, :, :] = 0.5 * (1.0 + np.tanh((z[None, :] - zi[i]) / 1.5)) # heavy on top
67 s.set_field("c", np.asfortranarray(c0))
68
69 def amp():
70 c = s.get_field("c")[:, 1, :]
71 zc = np.array([np.interp(0.5, c[i, :], z) for i in range(N)])
72 return 0.5 * (zc.max() - zc.min())
73
74 hist = [amp()]
75 for it in range(steps):
76 s.step()
77 if it % 40 == 39:
78 hist.append(amp())
79 return hist
80
81
82def rayleigh_taylor_vof(N=48, NZ=96, g=0.005, mu=0.002, steps=240, cfl=0.2, dt0=1.0):
83 """SHARP-interface Rayleigh-Taylor through the geometric VoF chain (rung V2a, WO-J).
84
85 Identical physics to `rayleigh_taylor()` above — same grid, same Atwood number (ratio 3),
86 same gravity, same viscosity, same initial interface z_i(x) = NZ/2 + 1.5 cos(2 pi x / N) —
87 with ONE difference: the phase field is a sharp geometric VoF colour field advected by
88 Weymouth-Yue instead of a tanh profile of width 1.5 advected by Koren TVD. Reporting both is
89 the point: the diffuse record is the pre-VoF baseline this rung has to reproduce.
90
91 Two deliberate departures from the diffuse driver, both forced by the method rather than
92 chosen:
93
94 * dt is ADAPTIVE. Weymouth-Yue is bounded only for an interface-local Courant number below
95 1/(2(N-1)) = 0.25 in 3D, and an RT interface accelerates, so a fixed dt = 1 that is safe at
96 t = 0 is not safe at t = 240. dt is therefore chosen from `vof_max_courant()` — the
97 INTERFACE-LOCAL Courant number, which is the quantity the bound applies to (a global max
98 would throttle on the far-field return flow; V1 measured 2x over-throttling on Zalesak, and
99 the WO-J ctest measures 22x on a jet-plus-quiescent-interface scene). Amplitudes are
100 therefore reported against PHYSICAL TIME, sampled at the same t = 40, 80, ... 240 as the
101 diffuse record.
102
103 * the amplitude comes from the COLUMN SUM of C, not from interpolating a C = 0.5 crossing.
104 For a sharp interface the column sum IS the interface height to machine precision
105 (z_i = NZ - sum_z C, heavy on top), while a 0.5-crossing interpolation of a two-cell-wide
106 profile carries an O(h) bias that the diffuse case cannot avoid and this one need not.
107 """
108 s = F.Solver(N, 4, NZ)
109 s.set_rho(1.0); s.set_mu(mu); s.set_dt(dt0)
110 s.set_advection(True)
111 s.set_domain_bc("-z", "wall", (0, 0, 0)); s.set_domain_bc("+z", "wall", (0, 0, 0))
112 s.set_pressure_geometry(np.asfortranarray(np.full((N, 4, NZ), 10.0)))
113 s.enable_vof()
114 x, z = np.arange(N), np.arange(NZ)
115 zi = NZ / 2 + 1.5 * np.cos(2 * np.pi * x / N)
116 # exact cell fractions of {z > z_i(x)} (heavy on top), i.e. a SHARP interface
117 c0 = np.clip((z[None, :] + 1.0) - zi[:, None], 0.0, 1.0)
118 s.set_vof(np.asfortranarray(np.repeat(c0[:, None, :], 4, axis=1)))
119 s.set_property_model("rho", "linear", "C", [1.0, 2.0]) # rho = 1 + 2C (ratio 3)
120 s.set_property_model("force_z", "linear", "rho", [0.0, -g])
121
122 def amp():
123 h = s.get_vof()[:, 1, :].sum(axis=1) # cells above the interface, per x-column
124 zc = NZ - h
125 return 0.5 * (zc.max() - zc.min())
126
127 v0 = s.get_vof().sum()
128 t, dt, sample, hist, tt = 0.0, dt0, 40.0, [amp()], [0.0]
129 while t < steps - 1e-9:
130 c = s.vof_max_courant() # interface-local, with the CURRENT dt
131 if c > 0: # target `cfl`, never grow dt by more than 2x
132 dt = min(2.0 * dt, dt * cfl / c, dt0)
133 dt = min(dt, sample - t)
134 s.set_dt(dt)
135 s.step()
136 t += dt
137 if t >= sample - 1e-9:
138 hist.append(amp()); tt.append(t); sample += 40.0
139 return hist, tt, (s.get_vof().sum() - v0) / v0
140
141
142if __name__ == "__main__":
143 for ratio in (3.0, 1000.0):
144 m, perr = hydrostatic(ratio)
145 print(f"hydrostatic ratio {ratio:g}: steady max|u| {m:.2e} P-grad rel-err {perr:.2e}")
146 assert m < 1e-12 and perr < 1e-11
148 growth = hist[-1] / hist[0]
149 print(f"Rayleigh-Taylor DIFFUSE C amplitude: {' -> '.join(f'{h:.2f}' for h in hist)} "
150 f"(x{growth:.1f})")
151 assert growth > 3.0 and all(hist[i+1] >= hist[i] * 0.98 for i in range(len(hist) - 1))
152
153 vh, vt, vdrift = rayleigh_taylor_vof()
154 vgrowth = vh[-1] / vh[0]
155 print(f"Rayleigh-Taylor SHARP C amplitude: {' -> '.join(f'{h:.2f}' for h in vh)} "
156 f"(x{vgrowth:.1f}) colour dV/V {vdrift:+.2e}")
157 # Linear theory: the inviscid interfacial growth rate n = sqrt(A g k), with A = (r-1)/(r+1)
158 # = 0.5 at ratio 3 and k = 2 pi / N. The rate is fitted over t in [40, 160] -- NOT over the
159 # first window. The linear solution from REST is a cosh, not a pure exponential, so the first
160 # window measures the establishment of the mode rather than its growth rate (measured: 0.30x
161 # there against 0.75x once established); by t = 160 the amplitude is ~10 cells = 0.2 lambda
162 # and the nonlinear (bubble/spike) stage is taking over. Both records use the same window.
163 A, k = 0.5, 2 * np.pi / 48
164 n_lin = np.sqrt(A * 0.005 * k)
165
166 def fit(a, t, t0=40.0, t1=160.0):
167 m = [i for i, ti in enumerate(t) if t0 - 1e-9 <= ti <= t1 + 1e-9]
168 return np.polyfit([t[i] for i in m], [np.log(a[i]) for i in m], 1)[0]
169
170 td = [40.0 * i for i in range(len(hist))]
171 n_dif, n_vof = fit(hist, td), fit(vh, vt)
172 print(f"growth rate over t in [40,160]: linear theory sqrt(A g k) = {n_lin:.5f} "
173 f"diffuse {n_dif:.5f} ({n_dif/n_lin:.2f}x) sharp {n_vof:.5f} ({n_vof/n_lin:.2f}x)")
174 assert vgrowth > 3.0 and all(vh[i+1] >= vh[i] * 0.98 for i in range(len(vh) - 1))
175 # The colour-volume floor here is the PROJECTION's divergence residual, not the advection
176 # (WO-E finding 2): most of it is deposited by the pressure driver's first solve on a fresh
177 # field, which leaves a transient velocity the advector faithfully transports once.
178 assert abs(vdrift) < 1e-7
179 print("PASS")
hydrostatic(ratio, mu=0.0, steps=100, g=0.1, dt=1.0, N=8, NZ=24)
rayleigh_taylor_vof(N=48, NZ=96, g=0.005, mu=0.002, steps=240, cfl=0.2, dt0=1.0)
fit(a, t, t0=40.0, t1=160.0)