flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
wall_slip.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""WO-V6b gate 1 + the float floor — the Navier slip length in the cut-cell IBM wall closure.
3
4Two analytically exact steady profiles over FLAT SDF walls, both driven without moving geometry:
5
6 `poiseuille` a slit of width H between two SDF walls, driven by a uniform body force G.
7 With Navier slip lambda at BOTH walls the exact solution is
8 u(z) = (G / 2mu) * ( (z - z0)(z1 - z) + lambda H ), H = z1 - z0,
9 a QUADRATIC that satisfies the Robin condition exactly, so the Robust-Scaled
10 closure (which fits a quadratic through u_m, u_c, u_g and imposes the Robin
11 condition at the crossing) must reproduce it to the storage floor.
12
13 `couette` the work order's own gate: an SDF wall with slip at z0 and the +z domain face
14 driven at U (as an INFLOW face -- a type-1 domain WALL ignores its tangential
15 velocity, measured), giving the linear profile
16 u(z) = U (z - z0 + lambda) / (H + lambda), H = nz - z0.
17
18 `floor` the poiseuille case over a lambda ladder down to 1e-10, to MEASURE the float
19 storage floor below which the Robin datum is indistinguishable from no-slip
20 (the closure is stored in float: D = theta(1+theta) + lambda(1+2 theta)).
21
22Wall placement is QUARTER-INTEGER throughout (WO-S finding 5 / WO-V7 finding 1).
23
24Usage: PYTHONPATH=$PWD/build_omp python tests/study/wall_slip.py [poiseuille|couette|floor|all]
25"""
26import math
27import sys
28
29import numpy as np
30
31import peclet.flow as pf
32
33NX, NY = 8, 8
34MU = 1.0
35G = 1e-3 # body force per unit volume
36
37
38def slab_sdf(nz, z0, z1):
39 """Exact signed distance to the slab boundary; > 0 in the fluid (z0 < z < z1)."""
40 z = (np.arange(nz) + 0.5)[None, None, :]
41 d = np.minimum(z - z0, z1 - z)
42 return np.asfortranarray(np.broadcast_to(d, (NX, NY, nz)).astype(float).copy())
43
44
45def _solve(s, steps, tol=1e-13):
46 prev = None
47 for i in range(steps):
48 s.step()
49 u = np.asarray(s.get_u())[NX // 2, NY // 2, :].copy()
50 if prev is not None:
51 d = np.abs(u - prev).max() / max(np.abs(u).max(), 1e-300)
52 if d < tol:
53 return i + 1, d
54 prev = u
55 return steps, float("nan")
56
57
58def poiseuille(nz, z0, z1, lam, steps=4000, verbose=True):
59 H = z1 - z0
60 s = pf.Solver(NX, NY, nz)
61 s.set_rho(1.0)
62 s.set_mu(MU)
63 s.set_dt(50.0)
64 s.set_advection(False) # Stokes: the exact profile is a steady Stokes solution
65 s.set_body_force((G, 0.0, 0.0))
66 s.set_pressure_multigrid(True, levels=3)
67 s.set_pressure_solver_params(400)
68 s.diagnostics.set_velocity_solver_params(400)
69 s.set_solid(slab_sdf(nz, z0, z1), cutcell_pressure=True)
70 if lam > 0:
71 s.set_wall_slip_length(lam)
72 n, d = _solve(s, steps)
73 u = np.asarray(s.get_u())[NX // 2, NY // 2, :]
74 zc = np.arange(nz) + 0.5
75 fluid = (zc > z0) & (zc < z1)
76 ue = (G / (2 * MU)) * ((zc - z0) * (z1 - zc) + lam * H)
77 err = np.abs(u[fluid] - ue[fluid]).max()
78 scale = np.abs(ue[fluid]).max()
79 # the analytic slip velocity AT the wall, and the one the discrete profile extrapolates to
80 us = (G / (2 * MU)) * lam * H
81 if verbose:
82 print(f" lambda {lam:<10.4g} H {H:g} steps {n:5d} (dU {d:.2e}) "
83 f"max|u - u_exact| {err:.4e} rel {err/scale:.4e} u_slip(exact) {us:.6e} "
84 f"u_centre {u[fluid][len(u[fluid])//2]:.8e}")
85 return dict(lam=lam, err=err, rel=err / scale, u=u.copy(), ue=ue, fluid=fluid,
86 iters=s.diagnostics.last_pressure_iterations(), sandwich=s.diagnostics.wall_slip_sandwich_cells(),
87 div=s.max_open_divergence_projected(), u_slip=us, steps=n)
88
89
90def couette(nz, z0, lam, U=1.0, steps=200):
91 """The work order's gate 1: plane COUETTE over a flat SDF wall carrying the Navier slip.
92
93 The bottom boundary is an SDF wall at `z0` with slip lambda; the top is the +z DOMAIN face
94 driven at U. A type-1 domain wall IGNORES its tangential velocity (measured: a plain channel
95 with `set_domain_bc(5, 1, (U, 0, 0))` stays identically 0 after 400 steps) -- the moving lid has
96 to be an INFLOW face (type 2) whose velocity is purely tangential, which is what
97 `tests/kokkos_mpi/test_varmu_mpi.cpp` uses. Exact steady Stokes profile:
98
99 u(z) = U (z - z0 + lambda) / (H + lambda), H = nz - z0
100
101 (no-slip at the lid, Navier slip at the SDF wall), a LINEAR profile, so the closure has to be
102 exact on it.
103 """
104 H = float(nz) - z0
105 s = pf.Solver(NX, NY, nz)
106 s.set_rho(1.0)
107 s.set_mu(MU)
108 s.set_dt(100.0)
109 s.set_advection(False)
110 s.set_domain_bc("-z", "wall", (0.0, 0.0, 0.0)) # -z: wall, buried in the solid
111 s.set_domain_bc("+z", "inflow", (U, 0.0, 0.0)) # +z: the moving lid
112 s.set_pressure_pcg(True, 400, 1e-12)
113 s.diagnostics.set_velocity_solver_params(400)
114 z = (np.arange(nz) + 0.5)[None, None, :]
115 d = np.broadcast_to(z - z0, (NX, NY, nz)).astype(float).copy()
116 s.set_solid(np.asfortranarray(d), cutcell_pressure=True)
117 if lam > 0:
118 s.set_wall_slip_length(lam)
119 n, dd = _solve(s, steps)
120 u = np.asarray(s.get_u())[NX // 2, NY // 2, :]
121 zc = np.arange(nz) + 0.5
122 fluid = zc > z0
123 ue = U * (zc - z0 + lam) / (H + lam)
124 err = np.abs(u[fluid] - ue[fluid]).max()
125 print(f" lambda {lam:<10.4g} H {H:g} steps {n:5d} (dU {dd:.2e}) "
126 f"max|u - u_exact| {err:.4e} rel {err/U:.4e} "
127 f"u(wall,exact) {U*lam/(H+lam):.6e} u(first fluid cell) {u[fluid][0]:.8e} "
128 f"vs exact {ue[fluid][0]:.8e} press {s.diagnostics.last_pressure_iterations()}")
129 return dict(lam=lam, err=err, rel=err / U)
130
131
132def main():
133 which = sys.argv[1] if len(sys.argv) > 1 else "all"
134 nz, z0, z1 = 40, 8.25, 32.25
135 if which in ("poiseuille", "all"):
136 print("=" * 96)
137 print(f"GATE 1a slip POISEUILLE, {NX}x{NY}x{nz}, SDF walls at z = {z0} / {z1} "
138 f"(quarter-integer), H = {z1-z0:g}, mu = {MU:g}, G = {G:g}")
139 print(" exact: u(z) = (G/2mu)((z-z0)(z1-z) + lambda H); theta = 0.25 (low wall) / "
140 "0.75 (high wall)")
141 print("=" * 96)
142 base = None
143 for lam in (0.0, 0.02, 0.05, 0.1, 0.3, 0.5, 1.0):
144 r = poiseuille(nz, z0, z1, lam)
145 if lam == 0.0:
146 base = r
147 print(f" sandwich cells (must be 0,0,0): {base['sandwich']}, "
148 f"max|div| {base['div']:.2e}, pressure iterations {base['iters']}")
149 if which in ("couette", "all"):
150 print("\n" + "=" * 96)
151 print(f"GATE 1b slip COUETTE, SDF wall at z = {z0}, +z domain wall moving at U")
152 print("=" * 96)
153 for lam in (0.0, 0.02, 0.05, 0.1, 0.3, 0.5):
154 couette(nz, z0, lam)
155 if which in ("floor", "all"):
156 print("\n" + "=" * 96)
157 print("THE FLOAT FLOOR: the closure is stored in FLOAT, so lambda vanishes from")
158 print("D = theta(1+theta) + lambda(1+2 theta) once lambda(1+2 theta) < eps_f32 * D.")
159 print("Reported as the measured slip velocity u(z0) - the analytic no-slip profile,")
160 print("i.e. how much of the imposed slip actually survives the storage.")
161 print("=" * 96)
162 r0 = poiseuille(nz, z0, z1, 0.0, verbose=False)
163 for lam in (1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10):
164 r = poiseuille(nz, z0, z1, lam, verbose=False)
165 d = np.abs(r["u"][r["fluid"]] - r0["u"][r0["fluid"]]).max()
166 expect = (G / (2 * MU)) * lam * (z1 - z0)
167 print(f" lambda {lam:<9.1e} max|u(lambda) - u(0)| {d:.6e} "
168 f"expected slip {expect:.6e} ratio {d/expect if expect else float('nan'):8.4f}")
169
170
171if __name__ == "__main__":
172 main()
_solve(s, steps, tol=1e-13)
Definition wall_slip.py:45
slab_sdf(nz, z0, z1)
Definition wall_slip.py:38
poiseuille(nz, z0, z1, lam, steps=4000, verbose=True)
Definition wall_slip.py:58
couette(nz, z0, lam, U=1.0, steps=200)
Definition wall_slip.py:90