flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_cutcell.cpp
Go to the documentation of this file.
1// Correctness of the Kokkos cut-cell face openness (peclet::flow::buildOpenness) vs a host
2// replication on a sphere SDF, plus properties: an all-fluid SDF gives openness 1, an all-solid SDF
3// gives 0. SDF sign convention: negative inside solid, positive in fluid. Runs on whatever backend
4// Kokkos was built for.
5#include <cmath>
6#include <cstdio>
7#include <Kokkos_Core.hpp>
8#include <vector>
9
10#include "mac_cutcell.hpp"
11#include "mac_cutcell_mg.hpp" // MReal / FPV / FPC (the operator storage type under test)
12#include "mac_pressure.hpp"
13
14using namespace peclet::flow;
15
16int main(int argc, char** argv) {
17 Kokkos::initialize(argc, argv);
18 int status = 0;
19 {
20 C3 ext{16, 16, 16};
21 const double dx = 1.0, dy = 1.0, dz = 1.0;
22 const std::size_t n = (std::size_t)ext.x * ext.y * ext.z;
23 auto IDX = [&](int x, int y, int z) {
24 return (std::size_t)x + (std::size_t)y * ext.x + (std::size_t)z * (std::size_t)ext.x * ext.y;
25 };
26
27 // solid sphere radius 5 at (8,8,8): sdf = dist - R (>0 fluid outside, <0 solid inside)
28 std::vector<double> hsdf(n);
29 const double cx = 8, cy = 8, cz = 8, R = 5;
30 for (int z = 0; z < ext.z; ++z)
31 for (int y = 0; y < ext.y; ++y)
32 for (int x = 0; x < ext.x; ++x)
33 hsdf[IDX(x, y, z)] =
34 std::sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)) - R;
35
36 auto upC = [&](const char* nm, std::vector<double>& h) {
37 CCField v(nm, n);
38 auto m = Kokkos::create_mirror_view(v);
39 for (std::size_t i = 0; i < n; ++i)
40 m(i) = h[i];
41 Kokkos::deep_copy(v, m);
42 return v;
43 };
44 auto getC = [&](CCField v) {
45 std::vector<double> o(n);
46 auto m = Kokkos::create_mirror_view(v);
47 Kokkos::deep_copy(m, v);
48 for (std::size_t i = 0; i < n; ++i)
49 o[i] = m(i);
50 return o;
51 };
52
53 CCField sdf = upC("sdf", hsdf), ox("ox", n), oy("oy", n), oz("oz", n);
54 buildOpenness(ox, oy, oz, sdf, ext, dx, dy, dz);
55 auto gox = getC(ox), goy = getC(oy), goz = getC(oz);
56
57 // host reference (same functions)
58 auto close = [](double a, double b) {
59 return std::fabs(a - b) <= 1e-10 * (1.0 + std::fabs(b));
60 };
61 int bad = 0;
62 // Host reference replicates the same math on the host SDF vector.
63 auto sampleH = [&](double X, double Y, double Z) {
64 double fx = std::floor(X), fy = std::floor(Y), fz = std::floor(Z);
65 double wx = X - fx, wy = Y - fy, wz = Z - fz;
66 int x0 = (int)fx, y0 = (int)fy, z0 = (int)fz;
67 auto cl = [&](int v, int nn) { return v < 0 ? 0 : (v >= nn ? nn - 1 : v); };
68 int x1 = cl(x0 + 1, ext.x), y1 = cl(y0 + 1, ext.y), z1 = cl(z0 + 1, ext.z);
69 x0 = cl(x0, ext.x);
70 y0 = cl(y0, ext.y);
71 z0 = cl(z0, ext.z);
72 auto F = [&](int xx, int yy, int zz) { return hsdf[IDX(xx, yy, zz)]; };
73 double c00 = F(x0, y0, z0) * (1 - wx) + F(x1, y0, z0) * wx,
74 c10 = F(x0, y1, z0) * (1 - wx) + F(x1, y1, z0) * wx;
75 double c01 = F(x0, y0, z1) * (1 - wx) + F(x1, y0, z1) * wx,
76 c11 = F(x0, y1, z1) * (1 - wx) + F(x1, y1, z1) * wx;
77 double c0 = c00 * (1 - wy) + c10 * wy, c1 = c01 * (1 - wy) + c11 * wy;
78 return c0 * (1 - wz) + c1 * wz;
79 };
80 auto faceH = [&](double X, double Y, double Z, int type) {
81 double sd = sampleH(X, Y, Z);
82 if (sd <= 0)
83 return 0.0;
84 double e = 1.0;
85 return ccFractionCore(sd, sampleH(X + e, Y, Z), sampleH(X - e, Y, Z), sampleH(X, Y + e, Z),
86 sampleH(X, Y - e, Z), sampleH(X, Y, Z + e), sampleH(X, Y, Z - e), type,
87 dx, dy, dz);
88 };
89 for (int z = 0; z < ext.z; ++z)
90 for (int y = 0; y < ext.y; ++y)
91 for (int x = 0; x < ext.x; ++x) {
92 std::size_t i = IDX(x, y, z);
93 if (!close(gox[i], faceH(x - 0.5, y, z, 1)))
94 ++bad;
95 if (!close(goy[i], faceH(x, y - 0.5, z, 2)))
96 ++bad;
97 if (!close(goz[i], faceH(x, y, z - 0.5, 3)))
98 ++bad;
99 }
100 if (bad) {
101 std::fprintf(stderr, "FAIL: %d openness faces differ\n", bad);
102 status = 1;
103 }
104
105 // count cut faces (0<open<1) — the sphere surface must produce some
106 int cut = 0, open1 = 0, closed0 = 0;
107 for (std::size_t i = 0; i < n; ++i) {
108 double v = gox[i];
109 if (v > 1e-9 && v < 1 - 1e-9)
110 ++cut;
111 else if (v > 1 - 1e-9)
112 ++open1;
113 else
114 ++closed0;
115 }
116
117 // properties
118 std::vector<double> allf(n, 100.0), alls(n, -100.0);
119 CCField sf = upC("sf", allf), so = upC("so", alls), a("a", n), b("b", n), c("c", n);
120 buildOpenness(a, b, c, sf, ext, dx, dy, dz);
121 auto ga = getC(a);
122 for (std::size_t i = 0; i < n; ++i)
123 if (!close(ga[i], 1.0))
124 ++bad;
125 buildOpenness(a, b, c, so, ext, dx, dy, dz);
126 auto ga2 = getC(a);
127 for (std::size_t i = 0; i < n; ++i)
128 if (!close(ga2[i], 0.0))
129 ++bad;
130 if (bad && !status) {
131 std::fprintf(stderr, "FAIL: all-fluid/all-solid property\n");
132 status = 1;
133 }
134
135 if (!status)
136 std::printf(
137 "[mac_cutcell] PASS: openness matches host (%d cut / %d open / %d closed x-faces) + "
138 "properties (exec: %s)\n",
139 cut, open1, closed0, CCExec::name());
140 }
141
142 // ---------------------------------------------------------------------------------------
143 // P1 (docs/DEFECT_CORRECTION_PLAN.md) — the exact flux-form level-0 apply annihilates the
144 // constant vector BITWISE, where the stored (float) bands do not.
145 //
146 // A = -div(open grad) is singular by construction: its null space is the constants, and the
147 // pressure solve deflates exactly that mode. buildCutcellOp forms AC as the float-rounded sum
148 // of the six face terms, so A*1 comes out at eps_f32 * max|AC| instead of 0 -- the defect WO-M
149 // measured as a resolution-independent residual floor at ~5e-9 with a 1e4-1e5 rebound.
150 // applyCutcellOpExact re-derives t_f = open_f * gf from the double openness and applies
151 // sum_f t_f (x_i - x_nbr), in which every difference vanishes identically for constant x.
152 //
153 // Beds: the two the regression suite ships (tests/regression/sdflow_regression.py) -- the
154 // jittered 2x2x2 sphere packing (centres lifted from that generator's fixed seed 12345) and the
155 // three Raschig rings -- at N = 32 and 64, because what matters here is the aperture spectrum,
156 // and those beds span ~3 decades of it.
157 {
158 // random_spheres: fractional centres of sdf_random_spheres(N, seed=12345); N-independent.
159 static const double kSphereC[8][3] = {
160 {0.16457049781272215, 0.32582370748774664, 0.19776029572245485},
161 {0.23444960590393615, 0.24547940157936873, 0.70554692087486348},
162 {0.16793243789302340, 0.78893356813158244, 0.27166348678329372},
163 {0.13282821621926860, 0.89084457926273108, 0.80810981434511542},
164 {0.70443676917452958, 0.30413189645273508, 0.22198280960076699},
165 {0.74635862887577831, 0.29733066067115205, 0.67459991201161940},
166 {0.78455145086375566, 0.83393873968342314, 0.32933788364396716},
167 {0.73201808908205368, 0.80417516048550364, 0.65270503594906770}};
168 // hollow_rings: (centre fraction, axis) of sdf_hollow_rings.
169 static const double kRingC[3][3] = {{0.30, 0.32, 0.30}, {0.70, 0.68, 0.55}, {0.45, 0.50, 0.78}};
170 static const int kRingA[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
171
172 auto minimg = [](double d, double N) { return d - N * std::round(d / N); };
173
174 // SDF of the two beds at a point, in cell units on the periodic N^3 box.
175 auto sdfSpheres = [&](double X, double Y, double Z, double N) {
176 const double R = 0.18 * N;
177 double best = 1e30;
178 for (int s = 0; s < 8; ++s) {
179 const double dx = minimg(X - kSphereC[s][0] * N, N), dy = minimg(Y - kSphereC[s][1] * N, N),
180 dz = minimg(Z - kSphereC[s][2] * N, N);
181 best = std::min(best, std::sqrt(dx * dx + dy * dy + dz * dz) - R);
182 }
183 return best;
184 };
185 auto sdfRings = [&](double X, double Y, double Z, double N) {
186 const double rO = 0.22 * N, rI = 0.12 * N, H = 0.34 * N;
187 double best = 1e30;
188 for (int r = 0; r < 3; ++r) {
189 const double ax = kRingA[r][0], ay = kRingA[r][1], az = kRingA[r][2]; // already unit
190 const double dx = minimg(X - kRingC[r][0] * N, N), dy = minimg(Y - kRingC[r][1] * N, N),
191 dz = minimg(Z - kRingC[r][2] * N, N);
192 const double zc = dx * ax + dy * ay + dz * az; // axial coordinate
193 const double rx = dx - zc * ax, ry = dy - zc * ay, rz = dz - zc * az;
194 const double rho = std::sqrt(rx * rx + ry * ry + rz * rz);
195 best = std::min(best, std::max(std::max(rI - rho, rho - rO), std::fabs(zc) - 0.5 * H));
196 }
197 return best;
198 };
199
200 const char* bedName[2] = {"random_spheres", "hollow_rings"};
201 const int grids[2] = {32, 64};
202 for (int bed = 0; bed < 2 && !status; ++bed)
203 for (int gi = 0; gi < 2 && !status; ++gi) {
204 const int N = grids[gi];
205 const int G = 1; // CutcellMG::G — the level-0 ghost width
206 const C3 e{N + 2 * G, N + 2 * G, N + 2 * G};
207 const std::size_t nn = (std::size_t)e.x * e.y * e.z;
208 // The bed SDFs are periodic, so evaluating them on the ghosted block directly IS the
209 // periodic ghost fill CutcellMG::fillOpenness performs (up to last-bit differences in the
210 // minimum image, which affect both operators identically and cannot affect the identity).
211 std::vector<double> hs(nn);
212 for (int z = 0; z < e.z; ++z)
213 for (int y = 0; y < e.y; ++y)
214 for (int x = 0; x < e.x; ++x) {
215 const double X = (x - G) + 0.5, Y = (y - G) + 0.5, Z = (z - G) + 0.5;
216 hs[(std::size_t)x + (std::size_t)y * e.x + (std::size_t)z * (std::size_t)e.x * e.y] =
217 bed == 0 ? sdfSpheres(X, Y, Z, (double)N) : sdfRings(X, Y, Z, (double)N);
218 }
219 CCField sdfB("sdf_bed", nn);
220 {
221 auto m = Kokkos::create_mirror_view(sdfB);
222 for (std::size_t i = 0; i < nn; ++i)
223 m(i) = hs[i];
224 Kokkos::deep_copy(sdfB, m);
225 }
226 CCField box("box", nn), boy("boy", nn), boz("boz", nn);
227 buildOpenness(box, boy, boz, sdfB, e, 1.0, 1.0, 1.0);
228
229 FPV AC("AC", nn), AW("AW", nn), AE("AE", nn), AS("AS", nn), AN("AN", nn), AB("AB", nn),
230 AT("AT", nn);
231 buildCutcellOp(AC, AW, AE, AS, AN, AB, AT, CCConst(box), CCConst(boy), CCConst(boz), e, G,
232 1.0, 1.0, 1.0);
233
234 // x == 1 (the null vector). A*1 must be 0; the flux form must give it BITWISE.
235 CCField one("one", nn), yBand("yBand", nn), yEx("yEx", nn);
236 Kokkos::deep_copy(one, 1.0);
237 applyCutcellOp(yBand, CCConst(one), FPC(AC), FPC(AW), FPC(AE), FPC(AS), FPC(AN), FPC(AB),
238 FPC(AT), e, G);
239 applyCutcellOpExact(yEx, CCConst(one), CCConst(box), CCConst(boy), CCConst(boz), e, G, 1.0,
240 1.0, 1.0);
241
242 auto hb = Kokkos::create_mirror_view(yBand), hx = Kokkos::create_mirror_view(yEx);
243 auto hAC = Kokkos::create_mirror_view(AC);
244 auto hox = Kokkos::create_mirror_view(box), hoy = Kokkos::create_mirror_view(boy),
245 hoz = Kokkos::create_mirror_view(boz);
246 Kokkos::deep_copy(hb, yBand);
247 Kokkos::deep_copy(hx, yEx);
248 Kokkos::deep_copy(hAC, AC);
249 Kokkos::deep_copy(hox, box);
250 Kokkos::deep_copy(hoy, boy);
251 Kokkos::deep_copy(hoz, boz);
252
253 double maxBand = 0.0, maxExact = 0.0, maxAC = 0.0;
254 long nSolid = 0, nSolidBad = 0, nCut = 0;
255 for (int z = G; z < e.z - G; ++z)
256 for (int y = G; y < e.y - G; ++y)
257 for (int x = G; x < e.x - G; ++x) {
258 const std::size_t i =
259 (std::size_t)x + (std::size_t)y * e.x + (std::size_t)z * (std::size_t)e.x * e.y;
260 maxBand = std::max(maxBand, std::fabs(hb(i)));
261 maxExact = std::max(maxExact, std::fabs(hx(i)));
262 maxAC = std::max(maxAC, (double)hAC(i));
263 const double sum = hox(i) + hox(i + 1) + hoy(i) + hoy(i + e.x) + hoz(i) +
264 hoz(i + (std::size_t)e.x * e.y);
265 if (sum == 0.0) { // fully closed (solid): both forms must be exactly 0
266 ++nSolid;
267 if (hx(i) != 0.0 || hb(i) != 0.0)
268 ++nSolidBad;
269 } else if (sum < 6.0) {
270 ++nCut;
271 }
272 }
273
274 // THE GATE: bitwise zero. Not a tolerance — every term of the flux form is t_f*(1-1) = 0.
275 if (maxExact != 0.0) {
276 std::fprintf(stderr, "FAIL: %s N=%d exact A*1 = %.17g, expected bitwise 0\n",
277 bedName[bed], N, maxExact);
278 status = 1;
279 }
280 if (nSolidBad) {
281 std::fprintf(stderr, "FAIL: %s N=%d %ld solid cells with nonzero A*1\n", bedName[bed], N,
282 nSolidBad);
283 status = 1;
284 }
285 std::printf(
286 "[cutcell/P1] %-15s N=%2d max|A_exact*1| = %.3e (bitwise 0) "
287 "max|A_bands*1| = %.3e (rel to max AC %.3e: %.2e) %ld cut / %ld solid cells\n",
288 bedName[bed], N, maxExact, maxBand, maxAC, maxBand / maxAC, nCut, nSolid);
289
290 // Same operator, not just a null-space trick: on a non-constant field the two forms must
291 // agree to the float rounding of the stored bands (this is what "the bands are a rounded
292 // copy of the exact operator" means quantitatively).
293 CCField xr("xr", nn);
294 {
295 auto m = Kokkos::create_mirror_view(xr);
296 unsigned s32 = 22801u + 7919u * (unsigned)(bed * 4 + gi);
297 for (std::size_t i = 0; i < nn; ++i) {
298 s32 = s32 * 1664525u + 1013904223u; // LCG: deterministic across backends
299 m(i) = (double)(s32 >> 8) / (double)(1u << 24) - 0.5;
300 }
301 Kokkos::deep_copy(xr, m);
302 }
303 applyCutcellOp(yBand, CCConst(xr), FPC(AC), FPC(AW), FPC(AE), FPC(AS), FPC(AN), FPC(AB),
304 FPC(AT), e, G);
306 1.0, 1.0);
307 Kokkos::deep_copy(hb, yBand);
308 Kokkos::deep_copy(hx, yEx);
309 double dmax = 0.0, ymax = 0.0;
310 for (int z = G; z < e.z - G; ++z)
311 for (int y = G; y < e.y - G; ++y)
312 for (int x = G; x < e.x - G; ++x) {
313 const std::size_t i =
314 (std::size_t)x + (std::size_t)y * e.x + (std::size_t)z * (std::size_t)e.x * e.y;
315 dmax = std::max(dmax, std::fabs(hb(i) - hx(i)));
316 ymax = std::max(ymax, std::fabs(hx(i)));
317 }
318 const double rel = dmax / ymax;
319 // sizeof(MReal) picks the bound: a double build stores the exact coefficient, so the two
320 // forms then differ only by summation order.
321 const double tol = sizeof(MReal) == sizeof(float) ? 1e-5 : 1e-13;
322 if (!(rel < tol)) {
323 std::fprintf(stderr, "FAIL: %s N=%d bands vs exact rel diff %.3e >= %.1e\n", bedName[bed],
324 N, rel, tol);
325 status = 1;
326 }
327 std::printf("[cutcell/P1] %-15s N=%2d bands vs exact on random x: rel %.3e (< %.0e)\n",
328 bedName[bed], N, rel, tol);
329 }
330 if (!status)
331 std::printf("[cutcell/P1] PASS: exact flux-form apply annihilates the constants bitwise\n");
332 }
333
334 Kokkos::finalize();
335 return status;
336}
int main()
Definition dbg_scene.cpp:13
flow — portable (Kokkos) cut-cell pressure-operator face openness from an SDF.
flow — portable (Kokkos) geometric multigrid for the cut-cell (variable-openness) pressure Poisson.
flow — portable (Kokkos) cut-cell pressure operator + Chorin projection.
Kokkos::View< const MReal *, CCMem > FPC
double ccFractionCore(double sd, double sxp, double sxm, double syp, double sym, double szp, double szm, int type, double dx, double dy, double dz)
void applyCutcellOpExact(CCField y, CCConst x, CCConst ox, CCConst oy, CCConst oz, C3 e, int g, double gfx, double gfy, double gfz)
void applyCutcellOp(CCField y, CCConst x, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, int g)
void buildCutcellOp(OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, CCConst ox, CCConst oy, CCConst oz, C3 e, int g, double gfx, double gfy, double gfz)
Kokkos::View< double *, CCMem > CCField
Kokkos::View< MReal *, CCMem > FPV
void buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx, double dy, double dz, int order=1)
Kokkos::View< const double *, CCMem > CCConst
static constexpr int N
static constexpr int G
static constexpr double AC
static constexpr double F