flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
verify_poiseuille_flow.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Verification (flow): plane Poiseuille flow through an SDF-defined channel, driven by a body force, with
3Robust-Scaled cut-cell IBM no-slip at the (non-grid-aligned) walls. The steady profile is the parabola
4u(y) = F/(2 mu) (y - ylo)(yhi - y); because it is exactly quadratic and a second-order scheme is exact on
5quadratics, the cut-cell solution must match it AT EVERY GRID NODE to solver tolerance -- at every
6resolution and on both the staggered and collocated meshes.
7
8We therefore verify POINTWISE: max_node |u - u_analytic|. (The old version of this script compared the
9discrete u.max() to the continuum peak U_max = F H^2/(8 mu); with half-integer walls the channel centre
10sits 0.5h from the nearest node, so u.max() is a fixed amount below U_max and dividing by U_max fabricated
11a "converging" error -- a sampling artifact, not discretization error. The pointwise metric below actually
12tests method order and would catch a genuine first-order regression.)
13
14Uses the canonical `peclet.flow` module (one GPU as plain `python`, or multi-rank under `mpirun -np N`).
15Physical units: set_rho/set_mu, body force F is a force per unit volume (= -dp/dx). Grid spacing = 1.
16"""
17import os
18import sys
19
20import numpy as np
21
22sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", os.environ.get("SDFLOW_BUILD", "build_mpi"))))
23from peclet import flow # noqa: E402
24
25
26def channel_sdf(nx, ny, nz, ylo, yhi):
27 """Global SDF as a 3-D array sdf[x,y,z]; negative inside the solid walls."""
28 gy = np.arange(ny, dtype=np.float64)
29 sdf = np.empty((nx, ny, nz))
30 sdf[:, :, :] = np.minimum(gy - ylo, yhi - gy)[None, :, None]
31 return sdf
32
33
34def run(N, Solver, rho=1.0, mu=0.1, dt=50.0, F=0.01, max_steps=400):
35 """Solve one channel case at wall-to-wall resolution N; return the pointwise node error vs the parabola."""
36 nx, nz = 8, 8
37 ny = N
38 ylo = round(0.30 * ny) + 0.5 # non-integer walls -> cut cells
39 yhi = round(0.70 * ny) + 0.5
40 H = yhi - ylo
41
42 s = Solver(nx, ny, nz)
43 s.set_rho(rho)
44 s.set_mu(mu)
45 s.set_dt(dt)
46 s.set_body_force(F, 0.0, 0.0) # force per unit volume (= -dp/dx)
47 s.set_velocity_solver_params(200) # IBM RB-GS velocity solve
48 s.set_pressure_solver_params(1) # x-independent flow is divergence-free -> projection is a no-op
49 s.set_solid(channel_sdf(nx, ny, nz, ylo, yhi), cutcell_pressure=False) # Robust-Scaled no-slip walls
50
51 prev = 0.0
52 for it in range(max_steps):
53 s.step()
54 u = s.get_u() # collective gather: ALL ranks must call it
55 stop = False
56 if s.rank() == 0:
57 u_now = float(u.max())
58 stop = it > 5 and abs(u_now - prev) < 1e-10 * (abs(u_now) + 1e-12)
59 prev = u_now
60 if s.bcast_from_root(stop):
61 break
62
63 u = s.get_u()
64 _ = s.get_p() # exercise the pressure path (collective)
65 if s.rank() != 0:
66 return None
67 prof = u[nx // 2, :, nz // 2]
68 gy = np.arange(ny, dtype=np.float64)
69 fluid = (gy > ylo) & (gy < yhi)
70 u_ana = (F / (2.0 * mu)) * (gy - ylo) * (yhi - gy)
71 node_err = float(np.max(np.abs(prof[fluid] - u_ana[fluid])))
72 return ny, H, node_err
73
74
75def main():
76 """Run staggered + collocated at a few N, print the pointwise node error, exit non-zero on failure."""
77 print("=== flow: Poiseuille through an SDF channel -- POINTWISE node error vs the exact parabola ===")
78 print(f"{'mesh':>11} {'Ny':>5} {'H':>7} {'max|u - u_analytic|':>22}")
79 worst = 0.0
80 for name, Solver in (("staggered", flow.Solver), ("collocated", flow.SolverColocated)):
81 for N in (16, 32, 64):
82 r = run(N, Solver)
83 if r is None:
84 return # non-root rank
85 ny, H, err = r
86 print(f"{name:>11} {ny:5d} {H:7.1f} {err:22.3e}")
87 worst = max(worst, err)
88 # A 2nd-order scheme is exact on a quadratic: the node error is pure solver tolerance, ~1e-7 at these
89 # resolutions. A first-order regression (or a broken cut-cell closure) would blow this to O(1e-2)+.
90 ok = worst < 1e-4
91 print(f" worst-case node error = {worst:.3e} (exact-on-quadratic -> solver tolerance)")
92 print(f" result: {'PASS' if ok else 'FAIL'} (cut-cell IBM reproduces the parabola pointwise, both meshes)")
93 sys.exit(0 if ok else 1)
94
95
96if __name__ == "__main__":
97 main()
run(N, Solver, rho=1.0, mu=0.1, dt=50.0, F=0.01, max_steps=400)
channel_sdf(nx, ny, nz, ylo, yhi)