flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
verify_bfs_sdflow.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Verification (sdflow): the backward-facing step (BFS) -- the canonical separated-flow benchmark, and
3the showcase for the per-face INLET PROFILE. Geometry is the Gartling (1990) expansion-ratio-2 step: a
4channel of full height H = 2S (S = step height) with no-slip walls top/bottom; flow enters ONLY the upper
5half (y in [S, 2S]) with the developed parabolic channel profile and is zero over the lower half (the step
6face); it leaves through the outflow. Behind the step a recirculation bubble forms on the bottom wall; the
7reattachment length x_r/S grows with Re. Quasi-2D (periodic z).
8
9The step is realized purely as the inlet condition via set_domain_bc_profile (no immersed solid): the
10parabola vanishes at y=S (the step lip) and y=2S (top wall), exactly the developed upper-channel profile.
11
12We de-risk in the laminar regime (Re_S = U_in*S/nu = 100, 200) -- a bubble must form, x_r/S must grow with
13Re, mass is conserved and the flow stays divergence-free -- then push toward Gartling Re=800 and report the
14reattachment for comparison (Gartling lower-wall reattachment ~6.1 H = ~12 S). Re_S = U_in * S / nu, with
15U_in the mean inlet velocity over the open upper half.
16"""
17import os
18import sys
19import time
20
21import numpy as np
22
23sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..",
24 os.environ.get("SDFLOW_BUILD", "build_mpi"))))
25from peclet import flow as sdflow # noqa: E402
26
27
28def inlet_profile(H, S, nz, U_in):
29 """Developed parabola over the open upper half y in [S, 2S], zero over the step face y in [0, S]."""
30 prof = np.zeros((H, nz, 3))
31 yc = np.arange(H) + 0.5
32 eta = (yc - S) / S # 0 at the step lip, 1 at the top wall
33 up = (yc > S)
34 prof[up, :, 0] = (6.0 * U_in * eta * (1.0 - eta))[up, None]
35 return prof
36
37
38def reattachment(u_bottom):
39 """End of the primary bubble = the first - -> + crossing of the near-bottom-wall streamwise velocity
40 AFTER the flow has actually reversed behind the step (skip any thin positive sliver at the step lip)."""
41 reversed_yet = False
42 for i in range(1, len(u_bottom)):
43 if u_bottom[i] < 0.0:
44 reversed_yet = True
45 elif reversed_yet and u_bottom[i] >= 0.0:
46 return (i - 1) + u_bottom[i - 1] / (u_bottom[i - 1] - u_bottom[i])
47 return 0.0
48
49
50def run(Re, S=16, Lr=12, U_in=1.0, nz=4, dt=0.2, max_steps=24000):
51 """Run the backward-facing step at Reynolds number `Re` (Re_S = U_in*S/nu) and return its diagnostics.
52
53 Step height S, full channel height 2S, channel length Lr*S; a partial parabolic inlet feeds the upper
54 half, the lower half is the step face. Marches to steady state and returns a dict with the lower-wall
55 reattachment length (x_r/S, x_r/H), bubble presence, mass-conservation error and max divergence.
56 Returns None off root.
57 """
58 # dt=0.2 (was 0.4): the step-lip shear layer is stiff for the explicit (SOU) deferred-correction
59 # advection -- at dt=0.4 the anti-diffusive correction is marginally unstable (a roundoff-sensitive
60 # transient divergence spike that can tip to NaN, worse on finer grids). Halving dt keeps it well
61 # under that explicit stability limit while retaining full 2nd-order accuracy (x_r/S unchanged).
62 # (Alternatives if speed matters: set_deferred_correction(False) for robust 1st-order, or
63 # set_outer_iterations(>1) to converge the correction -- both leave the SOU default intact.)
64 H = 2 * S
65 L = Lr * S
66 nu = U_in * S / Re
67 s = sdflow.Solver(L, H, nz)
68 s.set_rho(1.0); s.set_mu(nu); s.set_dt(dt); s.set_advection(True)
69 s.set_domain_bc_profile(0, inlet_profile(H, S, nz, U_in)) # -x partial parabolic inlet (-> inflow)
70 s.set_domain_bc(1, 3) # +x outflow
71 s.set_domain_bc(2, 1); s.set_domain_bc(3, 1) # -y, +y no-slip walls
72 s.set_velocity_solver_params(60)
73 if os.environ.get("SDFLOW_BFS_VMG") == "1": # opt-in: velocity-MG (diffusion-only) on the BFS
74 s.set_velocity_multigrid(True, 8, 4) # -- exercises the outflow -beta fold + inflow
75 s.set_pressure_multigrid(True, levels=8) # semi-coarsening MG (z frozen, x/y deep; capped)
76 s.set_pressure_solver_params(80)
77 s.set_pressure_geometry(np.full((L, H, nz), 1e30)) # all-fluid + BC pressure faces
78
79 verbose = os.environ.get("SDFLOW_BFS_VERBOSE") == "1"
80 t0 = time.time()
81 prev = 0.0
82 steps = max_steps
83 for it in range(max_steps):
84 s.step()
85 if it % 100 == 99:
86 u = s.get_u()
87 xr = reattachment(u[:, 0, nz // 2]) if s.rank() == 0 else 0.0
88 # converge only once a real bubble has formed (x_r > S) AND its length has stopped drifting --
89 # early in the transient x_r sits near 0 ("stable" but undeveloped), which must NOT trigger.
90 done = it > 3000 and xr > S and abs(xr - prev) < 1e-3 * S
91 if verbose and s.rank() == 0:
92 print(f" [Re={Re} it={it+1} x_r/S={xr/S:.2f} t={time.time()-t0:.0f}s]", flush=True)
93 prev = xr
94 if s.bcast_from_root(done):
95 steps = it + 1
96 break
97 u = s.get_u(); div = s.max_open_divergence()
98 if s.rank() != 0:
99 return None
100 xr = reattachment(u[:, 0, nz // 2])
101 flux_in = float(u[2, :, nz // 2].sum())
102 flux_out = float(u[L - 3, :, nz // 2].sum())
103 mass_err = abs(flux_out - flux_in) / (abs(flux_in) + 1e-30)
104 has_bubble = bool((u[1:S, 0, nz // 2] < 0).any()) # reverse flow just behind the step
105 return dict(Re=Re, S=S, H=H, L=L, steps=steps, secs=time.time() - t0, xr_S=xr / S, xr_H=xr / H,
106 div=div, mass_err=mass_err, bubble=has_bubble)
107
108
109def main():
110 """Run the laminar de-risk (Re_S 100, 200) and optional Re=800 push; print results and set exit code."""
111 print("=== sdflow: backward-facing step (Gartling expansion ratio 2) ===")
112 # laminar de-risk: a bubble must form, x_r/S must grow with Re, flow stays mass-conserving & div-free
113 laminar = [run(100), run(200)]
114 ok = True
115 xr_prev = -1.0
116 for r in laminar:
117 if r is None:
118 return
119 print(f" Re_S={r['Re']:<4d} S={r['S']} L={r['L']} x_r/S={r['xr_S']:.2f} bubble={r['bubble']} "
120 f"mass_err={r['mass_err']:.1e} div={r['div']:.1e} ({r['steps']} steps, {r['secs']:.0f}s)")
121 ok &= (r["bubble"] and r["mass_err"] < 1e-3 and r["div"] < 1e-6 and 0.5 < r["xr_S"] < 12.0
122 and r["xr_S"] > xr_prev)
123 xr_prev = r["xr_S"]
124 # push toward Re=800 (long: ~30 min, opt in with SDFLOW_BFS_RE800=1). NB our Re_S = U_in*S/nu, whereas
125 # Gartling's Re=800 uses the full height H=2S (~ our Re_S=400, where x_r/H ~ 6 would match his ~6.1).
126 # At Re_S=800 the bubble is large and slow: measured x_r/H ~ 7.3, still creeping at 36k steps -- a
127 # textbook slow open-flow transient near the steady/unsteady boundary. The laminar points above are the
128 # quantitative validation; this is a stress/illustration run (PASS gate = monotone growth + invariants).
129 if os.environ.get("SDFLOW_BFS_RE800") == "1":
130 r = run(800, S=32, Lr=22, dt=0.3, max_steps=36000)
131 if r is not None:
132 print(f" Re_S={r['Re']:<4d} S={r['S']} L={r['L']} x_r/S={r['xr_S']:.2f} x_r/H={r['xr_H']:.2f}"
133 f" (Gartling ~6.1 H) mass_err={r['mass_err']:.1e} div={r['div']:.1e} "
134 f"({r['steps']} steps, {r['secs']:.0f}s)")
135 ok &= (r["bubble"] and r["mass_err"] < 1e-3 and r["div"] < 1e-6 and r["xr_S"] > xr_prev)
136 print(f" result: {'PASS' if ok else 'FAIL'} (recirculation grows with Re, mass-conserving, div-free)")
137 sys.exit(0 if ok else 1)
138
139
140if __name__ == "__main__":
141 main()
inlet_profile(H, S, nz, U_in)
run(Re, S=16, Lr=12, U_in=1.0, nz=4, dt=0.2, max_steps=24000)