flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_scalar_mg.cpp
Go to the documentation of this file.
1// ScalarMG, the cut-cell scalar preconditioner (doc/scalar_ibm_design.md §5.2; WO-4), single rank.
2//
3// levels — the level table equals VelocityMG::init's on the same grid and metric (inner
4// dims and coarsening ratio per level), isotropic and anisotropic, odd and
5// short axes included (§5.2: "verified identical by a unit test");
6// coarse — the rediscretized coarse surrogate of a Dirichlet sphere: symmetric by
7// construction (face form), identity rows exactly at pinned cells, an M-matrix
8// (positive diagonal, non-positive faces, weakly diagonally dominant on every
9// unknown row), and the face coefficient of the constant-Lam single-phase case
10// equal to CutcellMG's rule w_a(L) <Lam a>;
11// level rule — transient with dt D / h^2 < 1 uses level 0 alone, steady the full table;
12// contraction — the guard of design Amendment A1 (the power estimate of rho(I - M^-1 S), the
13// geometric mean of the last 10 of 30): (C1) < 1 on every geometry; (C2) <= 0.35
14// on box geometries (G1, G2, G3a at every rung), Neumann geometries (G5b's array
15// on multigrid-friendly n) and no-solid geometries; (C3) <= 0.75 on a periodic
16// box whose only sink is one Dirichlet sphere. Krylov counts stay the primary
17// gate (tests/python/test_scalar_cutcell_gates.py);
18// advective — design Amendment A2 (WO-5c), the steady advective surrogate, rows u1-u6: (u1)
19// uniform flow: Qp_a = phi_a / cf_a exactly, Qm_a = 0, (2,2,1) levels included;
20// (u2) a divergence-free random field: advective row sums <= 1e-13 max Q, column
21// sums 0 to round-off, every level; (u3) the M-matrix on every level; (u4) the
22// level-0 advective applySurrogate == the operator's band matvec with SAC,
23// bitwise; (u5) Q = 0 on the domain-face planes of every non-periodic axis, open
24// faces included; (u6) C4: the C3 problem advecting at census Pe_h 0.1 / 1 / 10,
25// contraction < 1 and <= 0.90 (prov.). Prints the G-perf V-cycle time ratio.
26#include <chrono>
27#include <cmath>
28#include <cstdio>
29#include <cstring>
30#include <Kokkos_Core.hpp>
31#include <vector>
32
33#include "flow_ibm.hpp"
34
35namespace {
36int failures = 0;
37#define CHECK(cond) \
38 do { \
39 if (!(cond)) { \
40 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
41 ++failures; \
42 } \
43 } while (0)
44
50using HV = Kokkos::View<double*, Kokkos::HostSpace>;
51HV host(const CCField& f) {
52 return Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), f);
53}
54
55void testLevels() {
56 struct Case {
57 int n[3];
58 double hp[3];
59 };
60 const Case cases[] = {
61 {{64, 64, 64}, {1, 1, 1}}, {{48, 40, 12}, {1, 1, 1}}, {{128, 128, 4}, {1, 1, 1}},
62 {{30, 64, 7}, {1, 1, 1}}, {{77, 77, 77}, {1, 1, 1}}, {{64, 64, 32}, {1, 1, 2}},
63 {{20, 24, 48}, {1, 1.5, 3}}, {{96, 32, 64}, {2, 1, 1}},
64 };
65 for (const Case& c : cases) {
66 const double w[3] = {1.0 / (c.hp[0] * c.hp[0]), 1.0 / (c.hp[1] * c.hp[1]),
67 1.0 / (c.hp[2] * c.hp[2])};
68 VelocityMG vmg;
69 vmg.setMetric(w, c.hp);
70 vmg.init(c.n[0], c.n[1], c.n[2], 64);
71 ScalarMG smg;
72 smg.setMetric(w, c.hp);
73 smg.init(c.n[0], c.n[1], c.n[2], [](CCField) {});
74 bool same = vmg.levels() == smg.levels();
75 for (int L = 0; same && L < smg.levels(); ++L) {
76 const auto& a = vmg.level(L);
77 const auto& b = smg.level(L);
78 same = a.inner.x == b.inner.x && a.inner.y == b.inner.y && a.inner.z == b.inner.z &&
79 a.ratio.x == b.ratio.x && a.ratio.y == b.ratio.y && a.ratio.z == b.ratio.z &&
80 a.cfac.x == b.cfac.x && a.cfac.y == b.cfac.y && a.cfac.z == b.cfac.z;
81 }
82 std::printf("levels %dx%dx%d h' (%g,%g,%g): %d levels, bottom %dx%dx%d %s\n", c.n[0], c.n[1],
83 c.n[2], c.hp[0], c.hp[1], c.hp[2], smg.levels(),
84 smg.level(smg.levels() - 1).inner.x, smg.level(smg.levels() - 1).inner.y,
85 smg.level(smg.levels() - 1).inner.z, same ? "== VelocityMG" : "DIFFERS");
86 CHECK(same);
87 }
88}
89
90// Sphere of radius R (cells) at c in an n^3 box; solid inside.
91std::vector<double> sphereSdf(int n, double cx, double cy, double cz, double R) {
92 std::vector<double> s((std::size_t)n * n * n);
93 for (int z = 0; z < n; ++z)
94 for (int y = 0; y < n; ++y)
95 for (int x = 0; x < n; ++x) {
96 const double dx = x + 0.5 - cx, dy = y + 0.5 - cy, dz = z + 0.5 - cz;
97 s[(std::size_t)x + (std::size_t)y * n + (std::size_t)z * n * n] =
98 std::sqrt(dx * dx + dy * dy + dz * dz) - R;
99 }
100 return s;
101}
102
103// G1's configuration on an n^3 box (cells): Dirichlet sphere R = n/8 ... set by caller.
104void g1Setup(IbmSolver& s, int n, double R, double off) {
105 s.setRho(1.0);
106 s.setMu(1.0);
107 s.setDt(1.0);
108 for (int f = 0; f < 6; ++f)
109 s.setDomainBc(f, 1, 0.0, 0.0, 0.0);
110 s.setSolid(sphereSdf(n, 0.5 * n + off, 0.5 * n - 0.6 * off, 0.5 * n + 0.3 * off, R), false);
111 s.addScalar("c", 0.7, 1, 50, true);
112 for (int f = 0; f < 6; ++f)
113 s.setScalarBc("c", f, 2, 0.25);
114 s.setScalarWall("c", 1, 1.0, 0.0, -1);
115}
116
117void testCoarse() {
118 const int n = 32;
119 IbmSolver s(n, n, n);
120 g1Setup(s, n, 8.0, 0.37);
121 s.solveScalarSteady("c");
122 auto& st = *s.scalarField("c").cut;
123 const ScalarMG& mg = *st.mg;
124 CHECK(st.mgLevels == mg.levels() && mg.levels() == 5); // 32 -> 16 -> 8 -> 4 -> 2
125 long pinned = 0, rows = 0, badId = 0, badMM = 0, badFace = 0;
126 for (int L = 1; L < mg.levels(); ++L) {
127 const auto& c = mg.level(L);
128 const auto& f = mg.level(L - 1);
129 auto AC = host(c.AC), AX = host(c.AFX), AY = host(c.AFY), AZ = host(c.AFZ), U = host(c.unk);
130 auto PX = host(c.px);
131 const long sy = c.ext.x, sz = (long)c.ext.x * c.ext.y;
132 const double wx = 1.0 / ((double)c.cfac.x * c.cfac.x);
133 // CutcellMG's coarse face rule on the finer level's products (the recursion, checked once)
134 HV FP = L >= 2 ? host(f.px) : HV();
135 for (int z = c.g; z < c.ext.z - c.g; ++z)
136 for (int y = c.g; y < c.ext.y - c.g; ++y)
137 for (int x = c.g; x < c.ext.x - c.g; ++x) {
138 const long i = x + y * sy + z * sz;
139 ++rows;
140 if (!(U(i) > 0.5)) {
141 ++pinned;
142 if (!(AC(i) == 1.0 && AX(i) == 0.0 && AY(i) == 0.0 && AZ(i) == 0.0))
143 ++badId;
144 continue;
145 }
146 const double off = -(AX(i) + AX(i + 1) + AY(i) + AY(i + sy) + AZ(i) + AZ(i + sz));
147 const bool nonpos = AX(i) <= 0 && AX(i + 1) <= 0 && AY(i) <= 0 && AY(i + sy) <= 0 &&
148 AZ(i) <= 0 && AZ(i + sz) <= 0;
149 if (!(AC(i) > 0.0 && nonpos && AC(i) >= off * (1.0 - 1e-14)))
150 ++badMM;
151 if (!(AX(i) == -(wx * PX(i))))
152 ++badFace;
153 if (L >= 2) {
154 const long fsy = f.ext.x, fsz = (long)f.ext.x * f.ext.y;
155 const int fx = 2 * (x - c.g) + f.g, fy = 2 * (y - c.g) + f.g, fz = 2 * (z - c.g) + f.g;
156 double sxs = 0.0;
157 for (int a = 0; a < 2; ++a)
158 for (int b = 0; b < 2; ++b)
159 sxs += FP(fx + (fy + a) * fsy + (fz + b) * fsz);
160 if (!(PX(i) == sxs / 4.0))
161 ++badFace;
162 }
163 }
164 }
165 std::printf(
166 "coarse: %ld rows (%ld pinned): identity-row defects %ld, M-matrix defects %ld, "
167 "face-rule defects %ld\n",
168 rows, pinned, badId, badMM, badFace);
169 CHECK(pinned > 0 && badId == 0 && badMM == 0 && badFace == 0);
170}
171
172void testLevelRule() {
173 const int n = 32;
174 IbmSolver s(n, n, n);
175 g1Setup(s, n, 8.0, 0.37);
176 // dt D / h^2 = F -> kappa_A = 1 + 12 F; F just below / above the switch (D-WO9-3: 25 -> F = 2)
177 const double Fsw = (ScalarMG::kFullTableKappa - 1.0) / 12.0;
178 s.setDt(0.95 * Fsw / 0.7); // kappa_A = 23.8 < 25
179 s.advanceScalars();
180 const int small = s.scalarField("c").cut->mgLevels;
181 s.setDt(1.05 * Fsw / 0.7); // kappa_A = 26.2
182 s.advanceScalars();
183 const int at = s.scalarField("c").cut->mgLevels;
184 s.solveScalarSteady("c");
185 const int steady = s.scalarField("c").cut->mgLevels;
186 std::printf("level rule: dt D/h^2 = %.3f -> %d level(s), %.3f -> %d, steady -> %d\n", 0.95 * Fsw,
187 small, 1.05 * Fsw, at, steady);
188 CHECK(small == 1 && at == 5 && steady == 5);
189}
190
191double contraction(IbmSolver& s, const char* tag) {
192 auto& st = *s.scalarField("c").cut;
193 const std::size_t n = st.SAC.extent(0);
194 CCField e("e", n), t("t", n), z("z", n);
195 const std::vector<double> r = st.mg->contraction(30, e, t, z);
196 double gm = 1.0;
197 const int tail = 10;
198 for (int k = (int)r.size() - tail; k < (int)r.size(); ++k)
199 gm *= r[(std::size_t)k];
200 gm = std::pow(gm, 1.0 / tail);
201 std::printf(
202 "%s: %d levels, ||e_k||/||e_k-1||: first %.3f, k=10 %.3f, k=30 %.3f; "
203 "geometric mean of the last %d: %.3f\n",
204 tag, st.mgLevels, r[0], r[9], r.back(), tail, gm);
205 return gm;
206}
207
208// The concentric-shells scene of G2 (cell units): instance 0 a sphere Ri, instance 1 a huge box
209// minus a sphere Ro (solid outside Ro), both centred at c.
210void shellsScene(IbmSolver& s, double c, double Ri, double Ro) {
211 const std::vector<int> ni = {1, -1, -1, 3, -1, -1, 1, -1, -1, 34, 1, 2};
212 std::vector<double> nr(4 * 16, 0.0);
213 for (int k = 0; k < 4; ++k) {
214 nr[16 * k + 14] = 1.0;
215 nr[16 * k + 15] = 1.0;
216 }
217 nr[0] = Ri;
218 nr[16 + 0] = nr[16 + 1] = nr[16 + 2] = 1e5;
219 nr[32 + 0] = Ro;
220 const std::vector<int> ii = {0, -1, 3, -1};
221 std::vector<double> ir(2 * 18, 0.0);
222 for (int k = 0; k < 2; ++k) {
223 ir[18 * k + 0] = ir[18 * k + 1] = ir[18 * k + 2] = c;
224 ir[18 * k + 6] = 1.0;
225 ir[18 * k + 7] = 1.0;
226 }
227 s.setScene(ni, nr, ii, ir, false);
228 s.setSolidFromScene(false);
229}
230
231void walls(IbmSolver& s) {
232 s.setRho(1.0);
233 s.setMu(1.0);
234 s.setDt(1.0);
235 for (int f = 0; f < 6; ++f)
236 s.setDomainBc(f, 1, 0.0, 0.0, 0.0);
237}
238
239// rho on one configured problem; class 2 -> C2 (<= 0.35), 3 -> C3 (<= 0.75), always C1 (< 1).
240void guardRow(IbmSolver& s, const char* tag, int cls) {
241 s.solveScalarSteady("c");
242 char t[160];
243 std::snprintf(t, sizeof t, "%-44s %2d it", tag, s.scalarField("c").cut->iterations);
244 const double rho = contraction(s, t);
245 CHECK(rho < 1.0);
246 if (cls == 2)
247 CHECK(rho <= 0.35);
248 if (cls == 3)
249 CHECK(rho <= 0.75);
250}
251
252void testContraction() {
253 char tag[96];
254 for (int n : {32, 64, 128}) { // G1: Dirichlet sphere R = n/4, Dirichlet box
255 IbmSolver s(n, n, n);
256 g1Setup(s, n, 0.25 * n, 0.37);
257 std::snprintf(tag, sizeof tag, "C2 G1 R/h=%d (%d^3)", n / 4, n);
258 guardRow(s, tag, 2);
259 }
260 const int g2box[3] = {48, 80, 128};
261 for (int k = 0; k < 3; ++k) { // G2 on its multigrid-friendly boxes (ruling D-WO4-1)
262 const int Rih = 6 << k, n = g2box[k];
263 IbmSolver s(n, n, n);
264 walls(s);
265 shellsScene(s, 0.5 * n + 0.29, Rih, 2.5 * Rih);
266 s.addScalar("c", 0.8, 1, 50, true);
267 for (int f = 0; f < 6; ++f)
268 s.setScalarBc("c", f, 1, 0.0); // the box is solid (outside Ro): Neumann
269 s.setScalarWall("c", 0, 0.3, 0.0, 0);
270 s.setScalarWall("c", 1, 0.0, 0.0, 1);
271 std::snprintf(tag, sizeof tag, "C2 G2 Ri/h=%d (%d^3)", Rih, n);
272 guardRow(s, tag, 2);
273 }
274 for (int n : {32, 64, 128}) { // G3a, Da = 1: Robin sphere, Dirichlet box
275 IbmSolver s(n, n, n);
276 g1Setup(s, n, 0.25 * n, 0.37);
277 s.setScalarWall("c", 2, 0.0, 0.7 / (0.25 * n), -1); // k = Da D / R
278 std::snprintf(tag, sizeof tag, "C2 G3a Da=1 R/h=%d (%d^3)", n / 4, n);
279 guardRow(s, tag, 2);
280 }
281 { // Neumann sphere, Dirichlet box
282 IbmSolver s(64, 64, 64);
283 g1Setup(s, 64, 16.0, 0.37);
284 s.setScalarWall("c", 0, 0.3, 0.0, -1);
285 guardRow(s, "C2 Neumann sphere + Dirichlet box (64^3)", 2);
286 }
287 { // no solid, Dirichlet box
288 IbmSolver s(64, 64, 64);
289 walls(s);
290 s.setSolid(std::vector<double>((std::size_t)64 * 64 * 64, 100.0), false);
291 s.addScalar("c", 0.7, 1, 50, true);
292 for (int f = 0; f < 6; ++f)
293 s.setScalarBc("c", f, 2, 0.25);
294 s.setScalarSource("c", 0.1);
295 guardRow(s, "C2 no solid, Dirichlet box (64^3)", 2);
296 }
297 for (int n : {32, 48, 80, 77}) { // G5b's geometry: periodic SC array, c = 0.3, singular
298 IbmSolver s(n, n, n);
299 s.setRho(1.0);
300 s.setMu(1.0);
301 s.setDt(1.0);
302 const double R = 0.5 * n * std::cbrt(0.3 * 6.0 / M_PI);
303 s.setSolid(sphereSdf(n, 0.5 * n + 0.21, 0.5 * n - 0.13, 0.5 * n + 0.07, R), false);
304 s.addScalar("c", 0.7, 1, 50, true);
305 s.setScalarWall("c", 0, 0.3, 0.0, -1);
306 s.setScalarSource("c", 0.1);
307 // n = 77 (G5b's literal ND = 64) is odd: one level, correctness-only (C1)
308 std::snprintf(tag, sizeof tag, "%s G5b geometry n=%d (ND %.1f, singular)",
309 n == 77 ? "C1" : "C2", n, 2 * R);
310 guardRow(s, tag, n == 77 ? 1 : 2);
311 CHECK(s.scalarField("c").cut->singular);
312 }
313 for (int n : {64, 128}) { // C3: periodic box, the only sink one Dirichlet sphere R = n/4
314 IbmSolver s(n, n, n);
315 s.setRho(1.0);
316 s.setMu(1.0);
317 s.setDt(1.0);
318 s.setSolid(sphereSdf(n, 0.5 * n + 0.37, 0.5 * n - 0.22, 0.5 * n + 0.11, 0.25 * n), false);
319 s.addScalar("c", 0.7, 1, 50, true);
320 s.setScalarWall("c", 1, 1.0, 0.0, -1);
321 s.setScalarSource("c", -0.3);
322 std::snprintf(tag, sizeof tag, "C3 periodic + Dirichlet sphere R/h=%d (%d^3)", n / 4, n);
323 guardRow(s, tag, 3);
324 }
325}
326
327// ---- the advective surrogate (design Amendment A2, §6.7; WO-5c): rows u1-u6
328// ----------------------
329
330// Inner linear index of level `lv`.
331long inner(const ScalarMG::Level& lv, int x, int y, int z) {
332 return (long)(x + lv.g) + (long)(y + lv.g) * lv.ext.x + (long)(z + lv.g) * lv.ext.x * lv.ext.y;
333}
334
335// A periodic box without a solid (the cut-cell projection's all-open openness), cell units, a
336// cut-cell scalar D = 0.7 with a source; the caller sets the face velocities, then solves steady.
337void openBox(IbmSolver& s) {
338 s.setRho(1.0);
339 s.setMu(1.0);
340 s.setDt(1.0);
341 s.setSolid(std::vector<double>((std::size_t)s.nx() * s.ny() * s.nz(), 100.0), true);
342 s.addScalar("c", 0.7, 1, 50, true);
343 s.setScalarSource("c", 0.1);
344}
345
346// (u1) uniform flow along each axis, periodic box without a solid, on a grid whose table has
347// (2,2,2) and (2,2,1) levels: Qp_a = phi_a / cf_a exactly and Qm_a = 0 on every coarse level (the
348// velocity 0.75 makes every sub-face sum exact).
349void testAdvUniform() {
350 const int nn[2][3] = {{32, 32, 32}, {32, 32, 4}};
351 for (const auto& n : nn)
352 for (int ax = 0; ax < 3; ++ax) {
353 IbmSolver s(n[0], n[1], n[2]);
354 openBox(s);
355 const std::size_t nc = (std::size_t)n[0] * n[1] * n[2];
356 const char* nm[3] = {"u", "v", "w"};
357 for (int a = 0; a < 3; ++a)
358 s.setField(nm[a], std::vector<double>(nc, a == ax ? 0.75 : 0.0));
359 s.solveScalarSteady("c");
360 const auto& mg = *s.scalarField("c").cut->mg;
361 long bad = 0, cells = 0;
362 bool aniso = false;
363 for (int L = 1; L < mg.levels(); ++L) {
364 const auto& lv = mg.level(L);
365 aniso = aniso || (lv.cfac.x != lv.cfac.z);
366 const int cf[3] = {lv.cfac.x, lv.cfac.y, lv.cfac.z};
367 for (int a = 0; a < 3; ++a) {
368 const HV qp = host(lv.qp[a]), qm = host(lv.qm[a]);
369 const double want = a == ax ? 0.75 / (double)cf[a] : 0.0;
370 for (int z = 0; z < lv.inner.z; ++z)
371 for (int y = 0; y < lv.inner.y; ++y)
372 for (int x = 0; x < lv.inner.x; ++x) {
373 const long i = inner(lv, x, y, z);
374 bad += (qp(i) == want && qm(i) == 0.0) ? 0 : 1;
375 ++cells;
376 }
377 }
378 }
379 std::printf(
380 "(u1) uniform flow along %c, %dx%dx%d (%d levels%s): %ld of %ld Qp/Qm entries "
381 "differ from phi/cf_a, 0\n",
382 "xyz"[ax], n[0], n[1], n[2], mg.levels(), aniso ? ", (2,2,1) levels" : "", bad, cells);
383 CHECK(bad == 0 && cells > 0);
384 if (n[2] == 4)
385 CHECK(aniso);
386 }
387}
388
389// (u2) a divergence-free random field (node stream-function differences, periodic): on every
390// level |sum out - sum in| <= 1e-13 max Q per row (level 0 from phi), and the column sums of the
391// band operator vanish to round-off (no solid, steady, no open face: the diffusion columns cancel
392// too, so the whole column sum is the advective one).
393void testAdvDivergenceFree() {
394 const int n = 32;
395 IbmSolver s(n, n, n);
396 openBox(s);
397 std::vector<double> psi[3];
398 unsigned long long st = 0x2545F4914F6CDD1Dull;
399 auto rnd = [&]() {
400 st ^= st << 13;
401 st ^= st >> 7;
402 st ^= st << 17;
403 return (double)(st >> 11) * (1.0 / 9007199254740992.0) - 0.5;
404 };
405 for (auto& p : psi) {
406 p.resize((std::size_t)n * n * n);
407 for (double& v : p)
408 v = rnd();
409 }
410 auto P = [&](int c, int x, int y, int z) {
411 return psi[c][(std::size_t)((x + n) % n) + (std::size_t)((y + n) % n) * n +
412 (std::size_t)((z + n) % n) * n * n];
413 };
414 std::vector<double> u((std::size_t)n * n * n), v(u.size()), w(u.size());
415 for (int z = 0; z < n; ++z)
416 for (int y = 0; y < n; ++y)
417 for (int x = 0; x < n; ++x) {
418 const std::size_t k = (std::size_t)x + (std::size_t)y * n + (std::size_t)z * n * n;
419 u[k] = (P(2, x, y + 1, z) - P(2, x, y, z)) - (P(1, x, y, z + 1) - P(1, x, y, z));
420 v[k] = (P(0, x, y, z + 1) - P(0, x, y, z)) - (P(2, x + 1, y, z) - P(2, x, y, z));
421 w[k] = (P(1, x + 1, y, z) - P(1, x, y, z)) - (P(0, x, y + 1, z) - P(0, x, y, z));
422 }
423 s.setField("u", u);
424 s.setField("v", v);
425 s.setField("w", w);
426 s.solveScalarSteady("c");
427 const auto& sc = s.scalarField("c");
428 const auto& mg = *sc.cut->mg;
429 CHECK(sc.cut->advecting && sc.cut->steady);
430 bool ok = true;
431 for (int L = 0; L < mg.levels(); ++L) {
432 const auto& lv = mg.level(L);
433 const long sx = 1, sy = lv.ext.x, sz = (long)lv.ext.x * lv.ext.y;
434 HV q[6];
435 if (L == 0) { // level 0: the positive parts of phi (no solid: every face is a coupling)
436 for (int a = 0; a < 3; ++a) {
437 const HV ph = host(sc.cut->phi[a]);
438 q[a] = HV("qp", ph.extent(0));
439 q[3 + a] = HV("qm", ph.extent(0));
440 for (std::size_t k = 0; k < ph.extent(0); ++k) {
441 q[a](k) = std::fmax(ph(k), 0.0);
442 q[3 + a](k) = std::fmax(-ph(k), 0.0);
443 }
444 }
445 } else {
446 for (int a = 0; a < 3; ++a) {
447 q[a] = host(lv.qp[a]);
448 q[3 + a] = host(lv.qm[a]);
449 }
450 }
451 const HV AC = host(lv.AC), AW = host(lv.AW), AE = host(lv.AE), AS = host(lv.AS),
452 AN = host(lv.AN), AB = host(lv.AB), AT = host(lv.AT);
453 double maxQ = 0.0, maxAC = 0.0, row = 0.0, col = 0.0;
454 for (int z = 0; z < lv.inner.z; ++z)
455 for (int y = 0; y < lv.inner.y; ++y)
456 for (int x = 0; x < lv.inner.x; ++x) {
457 const long i = inner(lv, x, y, z);
458 for (int k = 0; k < 6; ++k)
459 maxQ = std::fmax(maxQ, q[k](i));
460 maxAC = std::fmax(maxAC, std::fabs(AC(i)));
461 const double out =
462 ((q[3](i) + q[0](i + sx)) + (q[4](i) + q[1](i + sy))) + (q[5](i) + q[2](i + sz));
463 const double in =
464 ((q[0](i) + q[3](i + sx)) + (q[1](i) + q[4](i + sy))) + (q[2](i) + q[5](i + sz));
465 row = std::fmax(row, std::fabs(out - in));
466 // column i: the diagonal plus each neighbour's coefficient on x_i (periodic: the
467 // neighbours of an inner cell are inner or wrapped ghosts; use the inner wrap)
468 auto I = [&](int dx, int dy, int dz) {
469 return inner(lv, (x + dx + lv.inner.x) % lv.inner.x, (y + dy + lv.inner.y) % lv.inner.y,
470 (z + dz + lv.inner.z) % lv.inner.z);
471 };
472 const double cs = AC(i) + AW(I(1, 0, 0)) + AE(I(-1, 0, 0)) + AS(I(0, 1, 0)) +
473 AN(I(0, -1, 0)) + AB(I(0, 0, 1)) + AT(I(0, 0, -1));
474 col = std::fmax(col, std::fabs(cs));
475 }
476 std::printf("(u2) level %d: max|out - in| = %.2e max Q, max|column sum| = %.2e max AC\n", L,
477 row / maxQ, col / maxAC);
478 ok = ok && row <= 1e-13 * maxQ && col <= 1e-13 * maxAC && maxQ > 0.0;
479 }
480 CHECK(ok);
481}
482
483// The C3 problem (periodic box 4R, a Dirichlet sphere R = n/4 with a source) with the projected
484// Stokes field of that geometry (20 steps under a body force: WO-5's harness), cell units, the
485// pressure at its default tolerance. (u3) checks the COLUMN sums, which are zero by construction
486// whatever the field's divergence (review finding 3); the field's divergence moves only the row
487// sums, which is why the former row check needed the pressure PCG at 1e-14.
488void c3Stokes(IbmSolver& s, int n) {
489 s.setRho(1.0);
490 s.setMu(1.0);
491 s.setDt(1.0);
492 s.setSolid(sphereSdf(n, 0.5 * n + 0.37, 0.5 * n - 0.22, 0.5 * n + 0.11, 0.25 * n), true);
493 s.setBodyForce(1e-3, 4e-4, 2e-4);
494 for (int k = 0; k < 20; ++k)
495 s.step();
496 s.addScalar("c", 0.7, 1, 50, true);
497 s.setScalarWall("c", 1, 1.0, 0.0, -1);
498 s.setScalarSource("c", -0.3);
499}
500
501// Rescale the face velocities so that the census max_cell_peclet of the next steady solve is `pe`
502// (G-adv's parametrization): one probe solve at the present field, then the field times
503// pe / measured.
504void rescalePeclet(IbmSolver& s, double pe) {
505 s.solveScalarSteady("c");
506 const double k = pe / s.scalarField("c").cut->maxCellPeclet;
507 for (const char* nm : {"u", "v", "w"}) {
508 std::vector<double> f = s.getField(nm);
509 for (double& v : f)
510 v *= k;
511 s.setField(nm, f);
512 }
513}
514
515// Seconds per V-cycle (z = M^-1 r) on the current build, r a fixed pseudo-random vector on the
516// unknowns (G-perf: the advective cycle against the symmetric one on the same block).
517double vcycleSeconds(IbmSolver& s, int reps) {
518 auto& st = *s.scalarField("c").cut;
519 const std::size_t n = st.SAC.extent(0);
520 CCField r("r", n), z("z", n);
521 const std::vector<double> unk = s.scalarGeometryField(4);
522 std::vector<double> rv(unk.size());
523 for (std::size_t k = 0; k < rv.size(); ++k)
524 rv[k] = unk[k] > 0.5 ? std::sin(0.37 * (double)k) : 0.0;
525 s.setField("c", rv);
526 Kokkos::deep_copy(r, s.scalarField("c").c);
527 st.mg->apply(z, r); // warm-up
528 Kokkos::fence();
529 const auto t0 = std::chrono::steady_clock::now();
530 for (int k = 0; k < reps; ++k)
531 st.mg->apply(z, r);
532 Kokkos::fence();
533 return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count() / reps;
534}
535
536// (u3) A2's M-matrix property on every level, by COLUMNS (review finding 3, ruling D-WOR-3): every
537// off-diagonal band <= 0, and every column sum AC(j) + sum_k band_k(j's neighbour toward j) >=
538// -1e-13 max AC. The advective couplings enter as one number per face (Q on the upwind diagonal,
539// -Q as the downwind off-diagonal), so the advection and diffusion parts have zero column sums up
540// to round-off INDEPENDENTLY of the field's divergence; the mass, wall, Dirichlet and omega_open
541// terms only add to the diagonal. A column-diagonally dominant Z-matrix is an M-matrix (by
542// columns), and the left null vector of a singular closure problem is exactly 1 on every level. The
543// C3 box is periodic on every axis: the neighbours wrap over the inner cells. Returns the defect
544// count.
545long mMatrixDefects(const ScalarMG& mg, const char* tag) {
546 long bad = 0;
547 for (int L = 0; L < mg.levels(); ++L) {
548 const auto& lv = mg.level(L);
549 const HV AC = host(lv.AC), AW = host(lv.AW), AE = host(lv.AE), AS = host(lv.AS),
550 AN = host(lv.AN), AB = host(lv.AB), AT = host(lv.AT);
551 double maxAC = 0.0;
552 for (int z = 0; z < lv.inner.z; ++z)
553 for (int y = 0; y < lv.inner.y; ++y)
554 for (int x = 0; x < lv.inner.x; ++x)
555 maxAC = std::fmax(maxAC, std::fabs(AC(inner(lv, x, y, z))));
556 double minCol = 1e300;
557 long rows = 0, positive = 0, negative = 0;
558 for (int z = 0; z < lv.inner.z; ++z)
559 for (int y = 0; y < lv.inner.y; ++y)
560 for (int x = 0; x < lv.inner.x; ++x) {
561 const long i = inner(lv, x, y, z);
562 auto I = [&](int dx, int dy, int dz) {
563 return inner(lv, (x + dx + lv.inner.x) % lv.inner.x, (y + dy + lv.inner.y) % lv.inner.y,
564 (z + dz + lv.inner.z) % lv.inner.z);
565 };
566 for (const HV* b : {&AW, &AE, &AS, &AN, &AB, &AT})
567 positive += (*b)(i) > 0.0 ? 1 : 0;
568 const double cs = AC(i) + AW(I(1, 0, 0)) + AE(I(-1, 0, 0)) + AS(I(0, 1, 0)) +
569 AN(I(0, -1, 0)) + AB(I(0, 0, 1)) + AT(I(0, 0, -1));
570 minCol = std::fmin(minCol, cs / maxAC);
571 negative += cs < -1e-13 * maxAC ? 1 : 0;
572 ++rows;
573 }
574 std::printf(
575 "(u3) %s level %d: %ld columns, min column sum = %.3e max AC, %ld below -1e-13, %ld "
576 "positive "
577 "off-diagonals\n",
578 tag, L, rows, minCol, negative, positive);
579 bad += positive + negative;
580 }
581 return bad;
582}
583
584// (u6) the C4 contraction rows (WO-5's "C3 + advection" rows, restated by A2): the C3 problem at
585// R/h 16 advecting at census Pe_h 0.1, 1, 10 — C1 (< 1) and <= 0.90 (prov.). On the Pe_h 10 row
586// also (u3) the M-matrix on every level and (u4) the level-0 advective applySurrogate == the
587// operator's band matvec with SAC as the diagonal, bitwise. G-perf: the advective V-cycle's time
588// against the symmetric one (the same problem at rest) on the same block.
589void testAdvective() {
590 const int n = 64;
591 double tRest = 0.0;
592 {
593 IbmSolver s(n, n, n);
594 c3Stokes(s, n);
595 for (const char* nm : {"u", "v", "w"})
596 s.setField(nm, std::vector<double>((std::size_t)n * n * n, 0.0));
597 s.solveScalarSteady("c");
598 CHECK(!s.scalarField("c").cut->advecting);
599 tRest = vcycleSeconds(s, 20);
600 }
601 for (const double pe : {0.1, 1.0, 10.0}) {
602 IbmSolver s(n, n, n);
603 c3Stokes(s, n);
604 rescalePeclet(s, pe);
605 const auto& st = *s.scalarField("c").cut;
606 char tag[96];
607 std::snprintf(tag, sizeof tag, "C4 C3 + advection, Pe_h=%.3g, R/h=%d (%d^3)", pe, n / 4, n);
608 s.solveScalarSteady("c");
609 std::printf(" census max_cell_peclet %.6g; %ld implicit faces of %ld carrying flux\n",
610 st.maxCellPeclet, st.numImplicitFaces, st.numFluxFaces);
611 CHECK(std::fabs(st.maxCellPeclet / pe - 1.0) <= 1e-12);
612 CHECK(st.advecting && st.numImplicitFaces == st.numFluxFaces && st.numFluxFaces > 0);
613 char t[160];
614 std::snprintf(t, sizeof t, "%-44s %2d it", tag, st.iterations);
615 const double rho = contraction(s, t);
616 CHECK(rho < 1.0);
617 CHECK(rho <= 0.90);
618 if (pe == 10.0) {
619 auto& sc = s.scalarField("c");
620 CHECK(mMatrixDefects(*st.mg, "Pe_h 10") == 0);
621 // (u4)
622 const std::size_t nn = st.SAC.extent(0);
623 CCField x("x", nn), y1("y1", nn), y2("y2", nn);
624 {
625 auto hx = Kokkos::create_mirror_view(x);
626 for (std::size_t k = 0; k < nn; ++k)
627 hx(k) = std::cos(1.3 * (double)k) * (1.0 + 0.1 * std::sin(0.01 * (double)k));
628 Kokkos::deep_copy(x, hx);
629 }
630 st.mg->applySurrogate(y1, x); // fills x's ghosts (the solver's exchange)
631 peclet::flow::applyCutcellOp(y2, peclet::flow::CCConst(x), st.SAC, sc.AW, sc.AE, sc.AS, sc.AN,
632 sc.AB, sc.AT, st.mg->level(0).ext, IbmSolver::G);
633 const HV h1 = host(y1), h2 = host(y2);
634 const auto& l0 = st.mg->level(0);
635 long diff = 0;
636 for (int z = 0; z < l0.inner.z; ++z)
637 for (int yy = 0; yy < l0.inner.y; ++yy)
638 for (int xx = 0; xx < l0.inner.x; ++xx) {
639 const long i = inner(l0, xx, yy, z);
640 diff += std::memcmp(&h1(i), &h2(i), sizeof(double)) == 0 ? 0 : 1;
641 }
642 std::printf(
643 "(u4) level-0 advective applySurrogate vs the band matvec with SAC: %ld of %d "
644 "rows not bitwise\n",
645 diff, l0.inner.x * l0.inner.y * l0.inner.z);
646 CHECK(diff == 0);
647 const double tAdv = vcycleSeconds(s, 20);
648 std::printf(
649 "G-perf: V-cycle %d^3 (%d levels): advective %.2f ms, symmetric (at rest) "
650 "%.2f ms, ratio %.2f (expected ~1.5, red flag > 2)\n",
651 n, st.mgLevels, 1e3 * tAdv, 1e3 * tRest, tAdv / tRest);
652 }
653 }
654}
655
656// (u5) Q = 0 on both domain-face planes of every non-periodic axis on every coarse level, open
657// faces included: a channel along x (inflow -x, outflow +x), no-slip y and z walls, a sphere;
658// the flow developed by step() (which captures the open-face flux), then a steady solve.
659void testAdvOpenFaces() {
660 const int nx = 64, ny = 32, nz = 32;
661 IbmSolver s(nx, ny, nz);
662 s.setRho(1.0);
663 s.setMu(1.0);
664 s.setDt(2.0);
665 s.setDomainBc(0, 2, 0.05, 0.0, 0.0); // -x inflow
666 s.setDomainBc(1, 3, 0.0, 0.0, 0.0); // +x outflow
667 for (int f = 2; f < 6; ++f)
668 s.setDomainBc(f, 1, 0.0, 0.0, 0.0);
669 std::vector<double> sdf((std::size_t)nx * ny * nz);
670 for (int z = 0; z < nz; ++z)
671 for (int y = 0; y < ny; ++y)
672 for (int x = 0; x < nx; ++x) {
673 const double dx = x + 0.5 - 27.3, dy = y + 0.5 - 15.6, dz = z + 0.5 - 16.9;
674 sdf[(std::size_t)x + (std::size_t)y * nx + (std::size_t)z * nx * ny] =
675 std::sqrt(dx * dx + dy * dy + dz * dz) - 7.4;
676 }
677 s.setSolid(sdf, true);
678 for (int k = 0; k < 10; ++k)
679 s.step();
680 s.addScalar("c", 0.4, 0, 50, true);
681 s.setScalarBc("c", 0, 2, 1.0); // dirichlet inflow
682 for (int f = 1; f < 6; ++f)
683 s.setScalarBc("c", f, 1, 0.0); // neumann
684 s.solveScalarSteady("c");
685 const auto& st = *s.scalarField("c").cut;
686 const auto& mg = *st.mg;
687 CHECK(st.advecting && st.converged);
688 long bad = 0, checked = 0;
689 for (int L = 1; L < mg.levels(); ++L) {
690 const auto& lv = mg.level(L);
691 const int ext[3] = {lv.ext.x, lv.ext.y, lv.ext.z};
692 const long sa[3] = {1, (long)lv.ext.x, (long)lv.ext.x * lv.ext.y};
693 for (int a = 0; a < 3; ++a) {
694 const HV qp = host(lv.qp[a]), qm = host(lv.qm[a]);
695 const int b = (a + 1) % 3, c = (a + 2) % 3;
696 for (const int pl : {lv.g, ext[a] - lv.g}) // the low and the high domain-face plane
697 for (int jb = lv.g; jb < ext[b] - lv.g; ++jb)
698 for (int jc = lv.g; jc < ext[c] - lv.g; ++jc) {
699 const long i = (long)pl * sa[a] + (long)jb * sa[b] + (long)jc * sa[c];
700 bad += (qp(i) == 0.0 && qm(i) == 0.0) ? 0 : 1;
701 ++checked;
702 }
703 }
704 }
705 std::printf(
706 "(u5) channel with open x faces, walls y z: %d levels, %ld domain-face entries, %ld "
707 "nonzero; steady %d iterations, census max_cell_peclet %.3g\n",
708 mg.levels(), checked, bad, st.iterations, st.maxCellPeclet);
709 CHECK(bad == 0 && checked > 0);
710}
711} // namespace
712
713int main(int argc, char** argv) {
714 Kokkos::initialize(argc, argv);
715 {
716 const bool advOnly = argc > 1 && std::strcmp(argv[argc - 1], "advective") == 0;
717 if (!advOnly) { // (`test_scalar_mg advective` runs the A2 rows alone)
718 testLevels();
719 testCoarse();
720 testLevelRule();
721 testContraction();
722 }
723 testAdvUniform();
724 testAdvDivergenceFree();
725 testAdvective();
726 testAdvOpenFaces();
727 }
728 Kokkos::finalize();
729 std::printf(failures ? "%d failure(s)\n" : "OK\n", failures);
730 return failures ? 1 : 0;
731}
static constexpr int G
Definition flow_ibm.hpp:80
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
Definition jc.py:1
contraction(A, levels, singular, nu, its=40)
void applyCutcellOp(CCField y, CCConst x, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, int g)
Kokkos::View< double *, CCMem > CCField
Solver< Staggered > IbmSolver
Kokkos::View< const double *, CCMem > CCConst
col(path, ncol)
steady(s, probe, maxit=3000, tol=1e-9)
static std::vector< double > sphereSdf()
static constexpr double AC
static constexpr double U
static bool same(double a, double b)
#define CHECK(cond)
static constexpr double AX