flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_freeslip.cpp
Go to the documentation of this file.
1// FREE-SLIP / SYMMETRY domain BC (set_domain_bc type 4) -- analytic gates on BOTH grids.
2//
3// Type 4 = zero normal velocity + zero normal derivative of the tangential components + pressure
4// Neumann (a wall for the pressure, a mirror for the tangential velocity). A symmetric problem cut
5// in half by a type-4 face must reproduce the full symmetric problem POINTWISE, which is what every
6// gate below is built on:
7// A. Half Poiseuille channel. Cut-cell SDF wall at a half-integer (pointwise EXACT on the
8// quadratic profile, the fact scripts/verify_poiseuille_flow.py rests on) at one side, a
9// type-4 face at the other. Gate: max_node |u - parabola| at solver tolerance, and
10// max_node |u_half - u_full| against the full channel (both cut-cell walls, periodic) at
11// solver tolerance. Both sides of the axis (s = 0 and s = 1), staggered AND collocated.
12// B. Uniform flow along free-slip faces (four type-4 faces, body-force driven, advection ON):
13// u stays uniform and equals F t / rho to round-off; v, w stay 0.
14// C. The pressure solve: Stokes flow through a sphere in a box with type-4 faces at +-y against
15// the periodic box of twice the height holding the sphere AND its mirror image (whose
16// symmetry planes are exactly the type-4 faces). Pointwise agreement at the pressure
17// solver's tolerance -- the projection, the cut-cell MG's Neumann rows on a type-4 face and
18// the momentum stencil's ghosts all have to be right for this to hold.
19#include <algorithm>
20#include <cmath>
21#include <cstdio>
22#include <Kokkos_Core.hpp>
23#include <string>
24#include <vector>
25
26#include "flow_ibm.hpp"
27
28namespace {
29int failures = 0;
30#define CHECK(cond) \
31 do { \
32 if (!(cond)) { \
33 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
34 ++failures; \
35 } \
36 } while (0)
37
38double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
39 double m = 0.0;
40 for (std::size_t i = 0; i < a.size(); ++i) {
41 const double d = std::fabs(a[i] - b[i]);
42 if (!(d <= m))
43 m = d; // NaN-propagating
44 }
45 return m;
46}
47double maxAbs(const std::vector<double>& a) {
48 double m = 0.0;
49 for (double v : a)
50 if (!(std::fabs(v) <= m))
51 m = std::fabs(v);
52 return m;
53}
54
55// ---------------------------------------------------------------------------------------------
56// A. Poiseuille: body-force-driven flow along x between a cut-cell wall and a symmetry face.
57// Node y = j (the u/collocated cell centre), the +y domain face at y = ny - 0.5, the -y face
58// at y = -0.5. ylo/yhi are the SDF wall positions (half-integers -> cut cells).
59// ---------------------------------------------------------------------------------------------
60constexpr double RHO = 1.0, MU = 0.1, DT = 50.0, F = 0.01;
61
62template <class Grid>
63std::vector<double> runChannel(int nx, int ny, int nz, double ylo, double yhi, int slipFace,
64 bool cutcellPressure, int& stepsTaken) {
65 // slipFace = -1: both walls are SDF walls (the full channel); 2 / 3: that face is type 4 and
66 // only the OTHER wall is an SDF wall.
67 peclet::flow::Solver<Grid> s(nx, ny, nz);
68 s.setRho(RHO);
69 s.setMu(MU);
70 s.setDt(DT);
71 s.setBodyForce(F, 0.0, 0.0);
72 s.setVelocityIterations(400);
73 s.setVelocityResidualTolerance(1e-12);
74 s.setPressurePcg(true, 50, 1e-11);
75 if (slipFace >= 0)
76 s.setDomainBc(slipFace, 4, 0.0, 0.0, 0.0);
77 std::vector<double> sdf((std::size_t)nx * ny * nz);
78 for (int z = 0; z < nz; ++z)
79 for (int y = 0; y < ny; ++y)
80 for (int x = 0; x < nx; ++x) {
81 double d;
82 if (slipFace < 0)
83 d = std::min(y - ylo, yhi - y);
84 else if (slipFace == 3)
85 d = y - ylo; // wall below, symmetry face above
86 else
87 d = yhi - y; // wall above, symmetry face below
88 sdf[(std::size_t)x + (std::size_t)y * nx + (std::size_t)z * nx * ny] = d;
89 }
90 s.setSolid(sdf, cutcellPressure);
91 double prev = 0.0;
92 stepsTaken = 0;
93 for (int it = 0; it < 800; ++it) {
94 s.step();
95 ++stepsTaken;
96 const double now = maxAbs(s.getVelocity(0));
97 if (it > 5 && std::fabs(now - prev) < 1e-12 * (now + 1e-300))
98 break;
99 prev = now;
100 }
101 std::vector<double> out = s.getVelocity(0);
102 std::vector<double> v = s.getVelocity(1);
103 // the wall-normal component must vanish identically (x-independent flow)
104 CHECK(maxAbs(v) < 1e-12);
105 return out;
106}
107
108template <class Grid>
109void gateChannel(const char* grid, bool cutcellPressure) {
110 const int nx = 8, nz = 8, nyH = 16, nyF = 32;
111 const double ylo = 3.5, yhi = 2.0 * (nyH - 0.5) - ylo; // 27.5: the mirror of ylo about 15.5
112 int stF = 0, stT = 0, stB = 0;
113 // full channel (periodic y, two cut-cell walls) -- the reference
114 const std::vector<double> uF = runChannel<Grid>(nx, nyF, nz, ylo, yhi, -1, cutcellPressure, stF);
115 // top-slip half channel: wall at ylo, type-4 face at +y (= the full channel's centre 15.5)
116 const std::vector<double> uT = runChannel<Grid>(nx, nyH, nz, ylo, yhi, 3, cutcellPressure, stT);
117 // bottom-slip half channel: type-4 face at -y (y' = -0.5 <-> y = 15.5), wall at yhi - 16 = 11.5
118 const std::vector<double> uB =
119 runChannel<Grid>(nx, nyH, nz, ylo, yhi - 16.0, 2, cutcellPressure, stB);
120 auto at = [&](const std::vector<double>& u, int ny, int x, int y, int z) {
121 return u[(std::size_t)x + (std::size_t)y * nx + (std::size_t)z * nx * ny];
122 };
123 const double c = F / (2.0 * MU);
124 double errF = 0, errT = 0, errB = 0, dT = 0, dB = 0, umax = 0;
125 for (int z = 0; z < nz; ++z)
126 for (int x = 0; x < nx; ++x) {
127 for (int y = 0; y < nyF; ++y) {
128 if (y <= ylo || y >= yhi)
129 continue; // fluid nodes only
130 const double ana = c * (y - ylo) * (yhi - y);
131 errF = std::max(errF, std::fabs(at(uF, nyF, x, y, z) - ana));
132 umax = std::max(umax, ana);
133 if (y < nyH) { // lower half <-> top-slip half channel, node for node
134 errT = std::max(errT, std::fabs(at(uT, nyH, x, y, z) - ana));
135 dT = std::max(dT, std::fabs(at(uT, nyH, x, y, z) - at(uF, nyF, x, y, z)));
136 } else { // upper half <-> bottom-slip half channel, y' = y - 16
137 errB = std::max(errB, std::fabs(at(uB, nyH, x, y - 16, z) - ana));
138 dB = std::max(dB, std::fabs(at(uB, nyH, x, y - 16, z) - at(uF, nyF, x, y, z)));
139 }
140 }
141 }
142 std::printf(
143 "[A %s] u_max %.4f node error vs parabola: full %.2e (steps %d) top-slip half %.2e (%d) "
144 "bottom-slip half %.2e (%d); half-vs-full pointwise: top %.2e bottom %.2e\n",
145 grid, umax, errF, stF, errT, stT, errB, stB, dT, dB);
146 // Exact on the quadratic -> the closure's float floor (the cut-cell closure computes in float,
147 // so the node error scales with u_max: verify_poiseuille_flow.py reads 6.5e-8 / 1.1e-6 /
148 // 2.5e-5 at u_max 0.45 / 1.8 / 8.5). Gate RELATIVE to u_max with a 10x margin. A first-order
149 // treatment of the symmetry face (a Neumann copy, or a wall) would read O(1e-2 .. 1e-1).
150 const double tol = 3e-5 * umax;
151 CHECK(errF < tol);
152 CHECK(errT < tol);
153 CHECK(errB < tol);
154 CHECK(dT < tol);
155 CHECK(dB < tol);
156}
157
158// ---------------------------------------------------------------------------------------------
159// B. Uniform tangential flow: free-slip on all four y/z faces, periodic x, body force along x,
160// advection on. The exact solution is u = F t / rho everywhere, v = w = 0.
161// ---------------------------------------------------------------------------------------------
162template <class Grid>
163void gateUniform(const char* grid, bool advection) {
164 const int n = 8, steps = 20;
165 const double dt = 1.0, f = 0.01;
167 s.setRho(RHO);
168 s.setMu(MU);
169 s.setDt(dt);
170 s.setBodyForce(f, 0.0, 0.0);
171 s.setAdvection(advection);
172 s.setVelocityIterations(200);
173 s.setPressurePcg(true, 50, 1e-11);
174 for (int face = 2; face < 6; ++face)
175 s.setDomainBc(face, 4, 0.0, 0.0, 0.0);
176 s.setPressureGeometry(std::vector<double>((std::size_t)n * n * n, 1e30)); // all fluid
177 for (int it = 0; it < steps; ++it)
178 s.step();
179 const std::vector<double> u = s.getVelocity(0), v = s.getVelocity(1), w = s.getVelocity(2);
180 const double uex = steps * dt * f / RHO;
181 double du = 0.0;
182 for (double x : u)
183 if (!(std::fabs(x - uex) <= du))
184 du = std::fabs(x - uex);
185 const double div = s.maxOpenDivergence();
186 std::printf(
187 "[B %s advection=%d] max|u - F t/rho| = %.2e (u = %.3f) max|v| = %.2e max|w| = %.2e "
188 "div %.1e\n",
189 grid, (int)advection, du, uex, maxAbs(v), maxAbs(w), div);
190 // Staggered, advection OFF: the all-fluid domain-BC momentum solve is the DOUBLE
191 // const-coefficient fold smoother -> round-off over 20 steps (measured 4.6e-12). Advection ON
192 // (the domain-BC path solves advection implicitly, i.e. the FLOAT-stored FOU stencil,
193 // Solver::FV) and the collocated grid (its domain-BC smoother is the float all-fluid IBM stencil
194 // either way): the per-row rounding of A.1 is ~1e-7 relative (the WO-M storage-precision axis,
195 // not the BC; measured 9.8e-8 and 3.1e-8) -> a relative float gate.
196 const bool dbl = !Grid::collocated && !advection;
197 CHECK(du < (dbl ? 1e-10 : 1e-6 * uex));
198 CHECK(maxAbs(v) < 1e-12);
199 CHECK(maxAbs(w) < 1e-12);
200}
201
202// ---------------------------------------------------------------------------------------------
203// C. Stokes flow through a sphere: type-4 faces at +-y vs the mirror-periodic twin.
204// Half box nx x ny x nz with the sphere at (cx, cy, cz); the twin is nx x 2ny x nz periodic
205// with the sphere and its image at y = 2ny - 1 - cy (mirror about the +y face y = ny - 0.5;
206// the -y face y = -0.5 is then a symmetry plane too, through the periodic image).
207// ---------------------------------------------------------------------------------------------
208void gateSphere() {
209 const int nx = 16, ny = 16, nz = 16, steps = 8;
210 const double R = 4.3, cx = 8.0, cy = 8.0, cz = 8.0;
211 auto sphereSdf = [&](int NY, bool twin) {
212 std::vector<double> sdf((std::size_t)nx * NY * nz);
213 for (int z = 0; z < nz; ++z)
214 for (int y = 0; y < NY; ++y)
215 for (int x = 0; x < nx; ++x) {
216 const double dx = x - cx, dz = z - cz;
217 double d = std::sqrt(dx * dx + (y - cy) * (y - cy) + dz * dz) - R;
218 if (twin) {
219 const double yi = 2.0 * ny - 1.0 - cy;
220 d = std::min(d, std::sqrt(dx * dx + (y - yi) * (y - yi) + dz * dz) - R);
221 }
222 sdf[(std::size_t)x + (std::size_t)y * nx + (std::size_t)z * nx * NY] = d;
223 }
224 return sdf;
225 };
226 auto configure = [&](peclet::flow::IbmSolver& s) {
227 s.setRho(1.0);
228 s.setMu(1.0);
229 s.setDt(1.0);
230 s.setBodyForce(1.0, 0.0, 0.0);
231 s.setAdvection(false);
232 s.setVelocityIterations(2000);
233 s.setVelocityResidualTolerance(1e-12);
234 s.setPressureLevels(3);
235 s.setPressurePcg(true, 200, 1e-12);
236 };
237 peclet::flow::IbmSolver sh(nx, ny, nz);
238 configure(sh);
239 sh.setDomainBc(2, 4, 0.0, 0.0, 0.0);
240 sh.setDomainBc(3, 4, 0.0, 0.0, 0.0);
241 sh.setSolid(sphereSdf(ny, false), true);
242 peclet::flow::IbmSolver sf(nx, 2 * ny, nz);
243 configure(sf);
244 sf.setSolid(sphereSdf(2 * ny, true), true);
245 for (int it = 0; it < steps; ++it) {
246 sh.step();
247 sf.step();
248 }
249 double d[3] = {0, 0, 0}, umax = 0;
250 for (int c = 0; c < 3; ++c) {
251 const std::vector<double> uh = sh.getVelocity(c), uf = sf.getVelocity(c);
252 for (int z = 0; z < nz; ++z)
253 for (int y = 0; y < ny; ++y)
254 for (int x = 0; x < nx; ++x) {
255 const std::size_t ih = (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * nx * ny;
256 const std::size_t iF =
257 (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * nx * (2 * ny);
258 const double dd = std::fabs(uh[ih] - uf[iF]);
259 if (!(dd <= d[c]))
260 d[c] = dd;
261 umax = std::max(umax, std::fabs(uf[iF]));
262 }
263 }
264 const double divh = sh.maxOpenDivergence(), divf = sf.maxOpenDivergence();
265 std::printf(
266 "[C] sphere in a slip box vs mirror-periodic twin: max|du| %.2e |dv| %.2e |dw| %.2e "
267 "(u_max %.3e, rel %.2e) div %.1e / %.1e\n",
268 d[0], d[1], d[2], umax, std::max({d[0], d[1], d[2]}) / umax, divh, divf);
269 CHECK(std::max({d[0], d[1], d[2]}) / umax < 1e-7);
270 // the divergence floor is the float-stored pressure operator's (A.1 rounding), ~5e-9 relative
271 CHECK(divh / umax < 1e-6 && divf / umax < 1e-6);
272}
273} // namespace
274
275int main(int argc, char** argv) {
276 Kokkos::initialize(argc, argv);
277 {
278 std::printf("[freeslip] exec %s\n", Kokkos::DefaultExecutionSpace::name());
279 gateChannel<peclet::flow::Staggered>("staggered", /*cutcellPressure=*/true);
280 gateChannel<peclet::flow::Colocated>("collocated", /*cutcellPressure=*/false);
281 gateUniform<peclet::flow::Staggered>("staggered", false);
282 gateUniform<peclet::flow::Staggered>("staggered", true);
283 gateUniform<peclet::flow::Colocated>("collocated", false);
284 gateUniform<peclet::flow::Colocated>("collocated", true);
285 gateSphere();
286 if (failures == 0)
287 std::printf("[freeslip] PASS: type-4 free-slip faces reproduce the symmetric problems\n");
288 else
289 std::fprintf(stderr, "[freeslip] FAILED (%d checks)\n", failures);
290 }
291 Kokkos::finalize();
292 return failures == 0 ? 0 : 1;
293}
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
x
Definition cmp.py:24
y
Definition cmp.py:24
static constexpr int NY
static std::vector< double > sphereSdf()
static void configure(IbmSolver &s)
static double maxAbs(const std::vector< double > &a)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static constexpr double DT
#define CHECK(cond)
static constexpr double RHO
static constexpr double MU
static constexpr double F