flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_scalar_cutcell_solve_mpi.cpp
Go to the documentation of this file.
1// Cut-cell scalar solve under MPI (doc/scalar_ibm_design.md §1.5, §5.1, §5.2, §5.4, §6; WO-3,
2// WO-4, WO-5, WO-5b, WO-5c, WO-6).
3//
4// np = 1, 2, 4 on the production ORB. Each problem is solved on the distributed solver and on a
5// single-rank reference of the same problem; the parallel contract of §5.4 (G10) is checked at
6// rtol 1e-13 (ruling Q-D of WO-4, the vardensity_mpi precedent): np = 1 in the MPI build is BITWISE
7// equal to the single-rank build (field, iterations, wall flux); np > 1: max |c - c_1| <= 1e-10
8// max |c_1|, iterations within +-1 per solve, the wall flux to the same bound; the budget identity
9// closes on every rank count. At the default rtol 1e-10 the np > 1 gap is the BiCGStab stopping
10// error (the V-cycle is decomposition-independent): `g1` is also run there and printed for
11// information only. Every solve runs the ScalarMG V-cycle of WO-4 (the transient steps have
12// dt D/h^2 > 1).
13//
14// mixed — an off-centre solid sphere, no-slip flow walls on +-x carrying scalar Dirichlet
15// values (one a per-face profile), y and z periodic: one steady solve with Dirichlet
16// walls (c = 1), then three backward-Euler steps with Robin walls and a source;
17// g1 — G1 at R/h = 16 (box 4R = 64, walls everywhere): a Dirichlet sphere c = 1, the
18// exact field R/r as the box Dirichlet profile on all six faces, steady;
19// singular — the periodic box, insulating walls with a flux and a source, steady: the
20// singular case of §5.1 (mean projection on every ScalarMG level, gauge kept);
21// g9 — G9 (WO-5): an annulus carrying a Gaussian blob by solid-body rotation, koren,
22// 50 backward-Euler steps with the explicit-implicit split of §6.3 (the small-cell
23// counts compared exactly across decompositions);
24// g9c — G9c (WO-5b): a channel with inflow / walls / outflow, a sphere and a cap cutting
25// the outflow face across the rank boundaries, the scalar advected by the full NS
26// step on the captured open-face flux, fou, 30 steps (small-cell counts exact);
27// g8 — G8 (WO-6): the Taylor-Aris closure in the mean-gradient mode (a pipe, Poiseuille
28// face velocities, insulating walls, G = e_z), steady, advecting and singular; the
29// compared output is the box-averaged flux along G (scalar_mean_flux).
30// g6 case 1 — G6 (WO-7): the conjugate composite sphere (a scene: a conjugate core inside an
31// immersed Dirichlet sphere), 20 backward-Euler steps; both fields and both bodies'
32// fluxes compared per step.
33// steady_adv — G-adv(a) (WO-5c, design Amendment A2): the periodic C3 problem (a Dirichlet sphere
34// R/h = 16 in a 64^3 box with a source) advected by a projected Stokes field
35// rescaled to census Pe_h 1, steady: the advective V-cycle. Plus its z = M^-1 r
36// for a fixed global-index r, bitwise equal to the single-rank build at np 1, 2, 4.
37//
38// Plus the level table: ScalarMG's distributed table equals VelocityMG::initMpi's (in place).
39#include <mpi.h>
40
41#include <algorithm>
42#include <cmath>
43#include <cstdio>
44#include <cstring>
45#include <functional>
46#include <Kokkos_Core.hpp>
47#include <vector>
48
49#include "flow_ibm.hpp"
50#include "peclet/core/decomp/block_decomposer.hpp"
51
53static constexpr int G = IbmSolver::G;
54
55static int failures = 0;
56#define CHECK(cond) \
57 do { \
58 if (!(cond)) { \
59 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
60 ++failures; \
61 } \
62 } while (0)
63
64static bool same(double a, double b) {
65 return std::memcmp(&a, &b, sizeof(double)) == 0;
66}
67
68static int rank_ = 0, size_ = 1;
69
70struct Block {
71 int o[3], l[3];
72};
73
74struct Run {
75 std::vector<int> iters;
76 std::vector<double> flux; // the body flux after each recorded stage
77 std::vector<double> identity;
78};
79
80using FlowSetup = std::function<void(IbmSolver&)>;
81using ScalarSetup = std::function<void(IbmSolver&, const Block&)>;
82using Solve = std::function<Run(IbmSolver&)>;
83
84static std::vector<double> sphereSdf(int n, double cx, double cy, double cz, double R) {
85 std::vector<double> s((std::size_t)n * n * n);
86 for (int z = 0; z < n; ++z)
87 for (int y = 0; y < n; ++y)
88 for (int x = 0; x < n; ++x) {
89 const double dx = x - cx, dy = y - cy, dz = z - cz;
90 s[(std::size_t)x + (std::size_t)y * n + (std::size_t)z * n * n] =
91 std::sqrt(dx * dx + dy * dy + dz * dz) - R;
92 }
93 return s;
94}
95
96static void record(IbmSolver& s, Run& r, bool transient) {
97 r.iters.push_back(s.scalarField("c").cut->iterations);
98 r.flux.push_back(s.scalarWallFlux("c")[0]);
99 auto b = s.scalarBudget("c");
100 r.identity.push_back(b.identityError / std::fabs(transient ? b.dMass : b.wallIn));
101}
102
103using Post = std::function<void(IbmSolver&, IbmSolver&, const Block&)>;
104
105static void compare(const char* tag, int n, const std::vector<double>& gsdf, FlowSetup flow,
106 ScalarSetup scalar, Solve solve, bool checkLevels = false, bool gated = true,
107 bool cutcellPressure = false, Post post = nullptr,
108 double identityBound = 1e-12) {
109 peclet::core::decomp::BlockDecomposer<3> dec =
110 peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size_), n, n, n);
111 const auto blk = dec.block(rank_);
112 Block B{{(int)blk.origin[0], (int)blk.origin[1], (int)blk.origin[2]},
113 {(int)blk.size[0], (int)blk.size[1], (int)blk.size[2]}};
114 std::vector<double> lsdf((std::size_t)B.l[0] * B.l[1] * B.l[2]);
115 for (int z = 0; z < B.l[2]; ++z)
116 for (int y = 0; y < B.l[1]; ++y)
117 for (int x = 0; x < B.l[0]; ++x)
118 lsdf[(std::size_t)x + (std::size_t)y * B.l[0] + (std::size_t)z * B.l[0] * B.l[1]] =
119 gsdf[(std::size_t)(x + B.o[0]) + (std::size_t)(y + B.o[1]) * n +
120 (std::size_t)(z + B.o[2]) * n * n];
121 IbmSolver sd(B.l[0], B.l[1], B.l[2]);
122 flow(sd);
123 sd.initMpi(n, n, n, MPI_COMM_WORLD);
124 sd.setSolid(lsdf, cutcellPressure);
125 scalar(sd, B);
126 IbmSolver sr(n, n, n);
127 flow(sr);
128 sr.setSolid(gsdf, cutcellPressure);
129 scalar(sr, Block{{0, 0, 0}, {n, n, n}});
130 if (gated) { // ruling Q-D: G10 is gated at rtol 1e-13
131 sd.setScalarTolerance("c", 1e-13);
132 sr.setScalarTolerance("c", 1e-13);
133 }
134 const Run D = solve(sd);
135 const Run R = solve(sr);
136 auto cd = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), sd.scalarField("c").c);
137 auto cr = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), sr.scalarField("c").c);
138 const long dsy = B.l[0] + 2 * G, dsz = dsy * (B.l[1] + 2 * G), rsy = n + 2 * G,
139 rsz = rsy * (n + 2 * G);
140 double maxDiff = 0.0, maxRef = 0.0;
141 long notSame = 0;
142 for (int z = 0; z < B.l[2]; ++z)
143 for (int y = 0; y < B.l[1]; ++y)
144 for (int x = 0; x < B.l[0]; ++x) {
145 const double a = cd((x + G) + (y + G) * dsy + (z + G) * dsz);
146 const double b = cr((x + B.o[0] + G) + (y + B.o[1] + G) * rsy + (z + B.o[2] + G) * rsz);
147 maxDiff = std::fmax(maxDiff, std::fabs(a - b));
148 maxRef = std::fmax(maxRef, std::fabs(b));
149 notSame += same(a, b) ? 0 : 1;
150 }
151 double g[2] = {maxDiff, maxRef}, gg[2];
152 MPI_Allreduce(g, gg, 2, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD);
153 long ns = 0;
154 MPI_Allreduce(&notSame, &ns, 1, MPI_LONG, MPI_SUM, MPI_COMM_WORLD);
155 const int lvD = sd.scalarField("c").cut->mgLevels, lvR = sr.scalarField("c").cut->mgLevels;
156 if (rank_ == 0) {
157 std::printf(
158 "[%s%s] np=%d: max|c - c_1| = %.2e (max|c_1| %.3f), %ld cells not bitwise; "
159 "mg levels np %d single %d\n",
160 tag, gated ? "" : " @ default rtol 1e-10, information only", size_, gg[0], gg[1], ns, lvD,
161 lvR);
162 std::printf(" iterations np:");
163 for (int it : D.iters)
164 std::printf(" %d", it);
165 std::printf(" single:");
166 for (int it : R.iters)
167 std::printf(" %d", it);
168 std::printf("\n wall flux np: %.15e single: %.15e\n", D.flux.back(), R.flux.back());
169 double idm = 0.0;
170 for (std::size_t k = 0; k < D.identity.size(); ++k)
171 idm = std::fmax(idm, std::fmax(std::fabs(D.identity[k]), std::fabs(R.identity[k])));
172 std::printf(" budget identity (rel) <= %.1e\n", idm);
173 }
174 if (post)
175 post(sd, sr, B);
176 if (!gated)
177 return;
178 CHECK(D.iters.size() == R.iters.size());
179 for (std::size_t k = 0; k < D.identity.size(); ++k) {
180 CHECK(std::fabs(D.identity[k]) <= identityBound);
181 CHECK(std::fabs(R.identity[k]) <= identityBound);
182 }
183 if (size_ == 1) {
184 CHECK(ns == 0);
185 for (std::size_t k = 0; k < D.iters.size(); ++k)
186 CHECK(D.iters[k] == R.iters[k]);
187 for (std::size_t k = 0; k < D.flux.size(); ++k)
188 CHECK(same(D.flux[k], R.flux[k]));
189 } else {
190 CHECK(gg[0] <= 1e-10 * gg[1]);
191 for (std::size_t k = 0; k < D.iters.size(); ++k)
192 CHECK(std::abs(D.iters[k] - R.iters[k]) <= 1);
193 for (std::size_t k = 0; k < D.flux.size(); ++k)
194 CHECK(std::fabs(D.flux[k] - R.flux[k]) <= 1e-10 * std::fabs(R.flux[k]));
195 }
196 if (checkLevels) {
197 // ScalarMG's distributed level table == VelocityMG::initMpi(dec, ., comm, inPlace)'s
198 const auto& mg = *sd.scalarField("c").cut->mg;
200 const double w[3] = {1.0, 1.0, 1.0}, hp[3] = {1.0, 1.0, 1.0};
201 vmg.setMetric(w, hp);
202 vmg.initMpi(dec, 64, MPI_COMM_WORLD);
203 bool eq = vmg.levels() == mg.levels();
204 for (int L = 0; eq && L < mg.levels(); ++L) {
205 const auto& a = vmg.level(L);
206 const auto& b = mg.level(L);
207 eq = a.inner.x == b.inner.x && a.inner.y == b.inner.y && a.inner.z == b.inner.z &&
208 a.ratio.x == b.ratio.x && a.ratio.y == b.ratio.y && a.ratio.z == b.ratio.z &&
209 a.og.x == b.og.x && a.og.y == b.og.y && a.og.z == b.og.z;
210 }
211 int all = eq ? 1 : 0, allMin = 0;
212 MPI_Allreduce(&all, &allMin, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD);
213 if (rank_ == 0)
214 std::printf(" level table: %d levels, %s VelocityMG::initMpi\n", mg.levels(),
215 allMin ? "==" : "DIFFERS from");
216 CHECK(allMin == 1);
217 }
218}
219
220int main(int argc, char** argv) {
221 MPI_Init(&argc, &argv);
222 MPI_Comm_rank(MPI_COMM_WORLD, &rank_);
223 MPI_Comm_size(MPI_COMM_WORLD, &size_);
224 Kokkos::initialize(argc, argv);
225 {
226 // ---- mixed (the WO-3 problem) ----
227 {
228 const int N = 32;
229 auto profile = [](int y, int z) { return 0.4 + 0.1 * std::sin(0.3 * y) * std::cos(0.2 * z); };
230 compare(
231 "mixed", N, sphereSdf(N, 15.3, 16.1, 14.7, 9.3),
232 [](IbmSolver& s) {
233 s.setRho(1.0);
234 s.setMu(1.0);
235 s.setDt(2.5);
236 s.setDomainBc(0, 1, 0.0, 0.0, 0.0);
237 s.setDomainBc(1, 1, 0.0, 0.0, 0.0);
238 },
239 [&](IbmSolver& s, const Block& B) {
240 s.addScalar("c", 1.2, 1, 50, true);
241 s.setScalarBc("c", 0, 2, 0.15);
242 std::vector<double> prof((std::size_t)B.l[1] * B.l[2]);
243 for (int z = 0; z < B.l[2]; ++z)
244 for (int y = 0; y < B.l[1]; ++y)
245 prof[(std::size_t)y + (std::size_t)z * B.l[1]] = profile(y + B.o[1], z + B.o[2]);
246 s.setScalarBcProfile("c", 1, prof, B.l[1], B.l[2]);
247 },
248 [](IbmSolver& s) {
249 Run r;
250 s.setScalarWall("c", 1, 1.0, 0.0, -1);
251 s.solveScalarSteady("c");
252 record(s, r, false);
253 s.setScalarWall("c", 2, 0.3, 0.7, -1);
254 s.setScalarSource("c", 0.1);
255 for (int k = 0; k < 3; ++k) {
256 s.advanceScalars();
257 record(s, r, true);
258 }
259 return r;
260 },
261 true);
262 }
263 // ---- G10 on G1 (R/h = 16, box 4R) ----
264 {
265 const int N = 64;
266 const double R = 16.0, c0[3] = {31.87, 32.21, 31.66};
267 for (const bool gated : {true, false})
268 compare(
269 "g1", N, sphereSdf(N, c0[0], c0[1], c0[2], R),
270 [](IbmSolver& s) {
271 s.setRho(1.0);
272 s.setMu(1.0);
273 s.setDt(1.0);
274 for (int f = 0; f < 6; ++f)
275 s.setDomainBc(f, 1, 0.0, 0.0, 0.0);
276 },
277 [&](IbmSolver& s, const Block& B) {
278 s.addScalar("c", 0.7, 1, 50, true);
279 for (int f = 0; f < 6; ++f) {
280 const int a = f / 2;
281 const int t1 = (a == 0) ? 1 : 0, t2 = (a == 2) ? 1 : 2;
282 const double xa = (f % 2 == 0) ? -0.5 : N - 0.5;
283 std::vector<double> prof((std::size_t)B.l[t1] * B.l[t2]);
284 for (int j2 = 0; j2 < B.l[t2]; ++j2)
285 for (int j1 = 0; j1 < B.l[t1]; ++j1) {
286 double p[3];
287 p[a] = xa;
288 p[t1] = j1 + B.o[t1];
289 p[t2] = j2 + B.o[t2];
290 const double r = std::sqrt((p[0] - c0[0]) * (p[0] - c0[0]) +
291 (p[1] - c0[1]) * (p[1] - c0[1]) +
292 (p[2] - c0[2]) * (p[2] - c0[2]));
293 prof[(std::size_t)j1 + (std::size_t)j2 * B.l[t1]] = R / r;
294 }
295 s.setScalarBc("c", f, 2, 0.0);
296 s.setScalarBcProfile("c", f, prof, B.l[t1], B.l[t2]);
297 }
298 s.setScalarWall("c", 1, 1.0, 0.0, -1);
299 },
300 [](IbmSolver& s) {
301 Run r;
302 s.solveScalarSteady("c");
303 record(s, r, false);
304 return r;
305 },
306 false, gated);
307 }
308 // ---- G10 on the singular case (periodic, insulating + flux + source, steady) ----
309 {
310 const int N = 32;
311 compare(
312 "singular", N, sphereSdf(N, 15.3, 16.1, 14.7, 9.3),
313 [](IbmSolver& s) {
314 s.setRho(1.0);
315 s.setMu(1.0);
316 s.setDt(1.0);
317 },
318 [](IbmSolver& s, const Block&) {
319 s.addScalar("c", 0.9, 1, 50, true);
320 s.setScalarWall("c", 0, 0.4, 0.0, -1);
321 s.setScalarSource("c", -0.05);
322 },
323 [](IbmSolver& s) {
324 Run r;
325 s.solveScalarSteady("c");
326 r.iters.push_back(s.scalarField("c").cut->iterations);
327 r.flux.push_back(s.scalarWallFlux("c")[0]);
328 CHECK(s.scalarField("c").cut->singular);
329 return r;
330 });
331 }
332 // ---- G10 on G9 (WO-5): the annulus, solid-body rotation, koren, 50 steps ----
333 {
334 // z-invariant annulus R_i = 6.4 < r < R_o = 16 cells in a 36^3 periodic box; the face
335 // velocities are node-potential differences of the clamped stream function, set to the
336 // wall value on both nodes of every face the projection closes (the Python G9's field), so
337 // the flux is discretely divergence-free in the predicate's openness. D = 0, bulk Courant
338 // 0.60 (Koren: 50 steps stay bounded; forward Euler + Koren is TVD only to 1/2).
339 const int N = 36;
340 const double cx = 17.78, cy = 18.13, Ri = 6.4, Ro = 16.0, Om = 0.5 / (std::sqrt(2.0) * Ro);
341 std::vector<double> gsdf((std::size_t)N * N * N);
342 for (int z = 0; z < N; ++z)
343 for (int y = 0; y < N; ++y)
344 for (int x = 0; x < N; ++x) {
345 const double r = std::hypot(x - cx, y - cy);
346 gsdf[(std::size_t)x + (std::size_t)y * N + (std::size_t)z * N * N] =
347 std::fmin(r - Ri, Ro - r);
348 }
349 auto flowG9 = [](IbmSolver& s) {
350 s.setRho(1.0);
351 s.setMu(1.0);
352 s.setDt(1.0);
353 };
354 // the global face velocities, from the projection openness of a single-rank build (a pure
355 // function of the SDF, bitwise equal across decompositions)
356 std::vector<double> vx((std::size_t)N * N * N), vy((std::size_t)N * N * N);
357 {
358 IbmSolver g(N, N, N);
359 flowG9(g);
360 g.setSolid(gsdf, true); // the projection's openness needs the cut-cell operator
361 const std::vector<double> ox = g.getOpennessProj(0), oy = g.getOpennessProj(1);
362 auto I = [&](int x, int y) {
363 return (std::size_t)((x + N) % N) + (std::size_t)((y + N) % N) * N;
364 };
365 std::vector<double> P((std::size_t)N * N);
366 for (int y = 0; y < N; ++y)
367 for (int x = 0; x < N; ++x) { // node (x - 1/2, y - 1/2)
368 const double r = std::hypot(x - 0.5 - cx, y - 0.5 - cy);
369 const double rc = std::fmin(std::fmax(r, Ri), Ro);
370 const double wall = r < 0.5 * (Ri + Ro) ? Ri : Ro;
371 const bool mark = !(ox[I(x, y)] > 0.0) || !(ox[I(x, y - 1)] > 0.0) ||
372 !(oy[I(x, y)] > 0.0) || !(oy[I(x - 1, y)] > 0.0);
373 P[I(x, y)] = -0.5 * Om * (mark ? wall * wall : rc * rc);
374 }
375 for (int z = 0; z < N; ++z)
376 for (int y = 0; y < N; ++y)
377 for (int x = 0; x < N; ++x) {
378 const std::size_t k = (std::size_t)x + (std::size_t)y * N + (std::size_t)z * N * N;
379 const double fx = P[I(x, y + 1)] - P[I(x, y)], fy = -(P[I(x + 1, y)] - P[I(x, y)]);
380 vx[k] = ox[k] > 0.0 ? fx / ox[k] : 0.0;
381 vy[k] = oy[k] > 0.0 ? fy / oy[k] : 0.0;
382 }
383 }
384 compare(
385 "g9", N, gsdf, flowG9,
386 [&](IbmSolver& s, const Block& B) {
387 s.addScalar("c", 0.0, 1, 50, true); // koren
388 const std::size_t nl = (std::size_t)B.l[0] * B.l[1] * B.l[2];
389 std::vector<double> u(nl), v(nl), w(nl, 0.0), c(nl);
390 for (int z = 0; z < B.l[2]; ++z)
391 for (int y = 0; y < B.l[1]; ++y)
392 for (int x = 0; x < B.l[0]; ++x) {
393 const std::size_t l =
394 (std::size_t)x + (std::size_t)y * B.l[0] + (std::size_t)z * B.l[0] * B.l[1];
395 const int gx = x + B.o[0], gy = y + B.o[1], gz = z + B.o[2];
396 const std::size_t k =
397 (std::size_t)gx + (std::size_t)gy * N + (std::size_t)gz * N * N;
398 u[l] = vx[k];
399 v[l] = vy[k];
400 const double dx = gx - cx - 0.7 * Ro, dy = gy - cy, sg = 0.08 * Ro;
401 c[l] = gsdf[k] > 0.0 ? std::exp(-(dx * dx + dy * dy) / (2.0 * sg * sg)) : 0.0;
402 }
403 s.setField("u", u);
404 s.setField("v", v);
405 s.setField("w", w);
406 s.setField("c", c);
407 },
408 [](IbmSolver& s) {
409 Run r;
410 for (int k = 0; k < 50; ++k) {
411 s.advanceScalars();
412 const auto& st = *s.scalarField("c").cut;
413 r.iters.push_back(st.iterations);
414 // the classification must not depend on the decomposition: compared exactly
415 r.flux.push_back((double)st.numSmall + 1e4 * (double)st.numImplicitFaces);
416 const auto b = s.scalarBudget("c");
417 r.identity.push_back(b.identityError / b.mass);
418 }
419 const auto& st = *s.scalarField("c").cut;
420 if (rank_ == 0)
421 std::printf(" [g9] C_bulk %.3f, small cells %ld, implicit faces %ld of %ld\n",
422 st.bulkCourant, st.numSmall, st.numImplicitFaces, st.numFluxFaces);
423 CHECK(st.numSmall > 0 && st.numImplicitFaces > 0);
424 return r;
425 },
426 false, true, true);
427 }
428 // ---- G10 on G9c (WO-5b): open domain faces, the scalar on flow's own projection ----
429 {
430 // A channel along z in a 32^3 box: inflow w = 1 on -z, outflow on +z, no-slip x and y walls;
431 // a sphere off the axis and a cap (a sphere centred just outside the outlet, cutting the
432 // outflow face) whose slivers straddle the rank boundaries of np = 2 and 4 (the ORB splits x
433 // first, then y), so the captured outflow plane's transverse ghost rows enter the small flags
434 // of ghost layer 1 (§6.3). fou at bulk Courant ~1.1 (small cells occur, in the outlet layer
435 // too: the cap was chosen for it by a scan of its centre and radius), D = 0.3, c0 = 0.2,
436 // Dirichlet 1 at the inlet, zero-gradient exit; 30 steps of the full NS step.
437 const int N = 32;
438 std::vector<double> gsdf((std::size_t)N * N * N);
439 for (int z = 0; z < N; ++z)
440 for (int y = 0; y < N; ++y)
441 for (int x = 0; x < N; ++x) {
442 const double a = std::sqrt((x - 13.3) * (x - 13.3) + (y - 17.1) * (y - 17.1) +
443 (z - 11.4) * (z - 11.4)) -
444 7.3;
445 const double b = std::sqrt((x - 15.6) * (x - 15.6) + (y - 16.5) * (y - 16.5) +
446 (z - 34.6) * (z - 34.6)) -
447 8.1;
448 gsdf[(std::size_t)x + (std::size_t)y * N + (std::size_t)z * N * N] = std::fmin(a, b);
449 }
450 auto flowG9c = [](IbmSolver& s) {
451 s.setRho(1.0);
452 s.setMu(1.6);
453 s.setDt(0.45);
454 for (int f = 0; f < 4; ++f)
455 s.setDomainBc(f, 1, 0.0, 0.0, 0.0);
456 s.setDomainBc(4, 2, 0.0, 0.0, 1.0); // -z inflow
457 s.setDomainBc(5, 3, 0.0, 0.0, 0.0); // +z outflow
458 };
459 int call = 0; // compare() solves the distributed run first, then the single-rank reference
460 compare(
461 "g9c", N, gsdf, flowG9c,
462 [&](IbmSolver& s, const Block& B) {
463 s.addScalar("c", 0.3, 0, 50, true); // fou
464 for (int f = 0; f < 4; ++f)
465 s.setScalarBc("c", f, 1, 0.0); // neumann
466 s.setScalarBc("c", 4, 2, 1.0); // dirichlet inflow
467 s.setScalarBc("c", 5, 1, 0.0); // zero-gradient exit
468 const std::vector<double> unk = s.scalarGeometryField(4);
469 std::vector<double> c(unk.size());
470 for (std::size_t k = 0; k < c.size(); ++k)
471 c[k] = unk[k] > 0.5 ? 0.2 : 0.0;
472 (void)B;
473 s.setField("c", c);
474 },
475 [&](IbmSolver& s) {
476 const bool ref = call++ == 1;
477 Run r;
478 long outletSmall = 0, small = 0, impl = 0;
479 for (int k = 0; k < 30; ++k) {
480 s.step();
481 const auto& st = *s.scalarField("c").cut;
482 small = std::max(small, st.numSmall);
483 impl = std::max(impl, st.numImplicitFaces);
484 r.iters.push_back(st.iterations);
485 r.flux.push_back((double)st.numSmall + 1e4 * (double)st.numImplicitFaces);
486 const auto b = s.scalarBudget("c");
487 r.identity.push_back(b.identityError / b.mass);
488 // small cells in the outlet layer (the implicit open-face rows of §1.4 are exercised)
489 auto h = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), st.small);
490 const int lx = s.nx(), ly = s.ny(), lz = s.nz();
491 const long sy = lx + 2 * G, sz = sy * (ly + 2 * G);
492 for (int y = 0; y < ly; ++y)
493 for (int x = 0; x < lx; ++x)
494 outletSmall += h((x + G) + (y + G) * sy + (lz - 1 + G) * sz) > 0.5 ? 1 : 0;
495 }
496 if (rank_ == 0)
497 std::printf(
498 " [g9c %s] small cells <= %ld (in the outlet layer, summed over the "
499 "steps, rank 0: %ld), implicit faces <= %ld\n",
500 ref ? "single" : "np", small, outletSmall, impl);
501 CHECK(small > 0 && impl > 0);
502 if (ref) // the implicit open-face rows (an outflow face of a small cell) are exercised
503 CHECK(outletSmall > 0);
504 return r;
505 },
506 false, true, true);
507 }
508 // ---- G10 on G-adv(a) (WO-5c, Amendment A2): steady advection, the advective V-cycle ----
509 {
510 // The C3 problem: a periodic 64^3 box, a Dirichlet sphere R = 16 with a source, D = 0.7;
511 // the face velocities are the projected Stokes field of a SINGLE-RANK build (20 steps under
512 // a body force: WO-5's harness), rescaled so that the census max_cell_peclet is 1, and set
513 // on every block (identical input on every decomposition).
514 const int N = 64;
515 const std::vector<double> gsdf = sphereSdf(N, 31.87, 31.28, 32.11, 16.0);
516 auto flowAdv = [](IbmSolver& s) {
517 s.setRho(1.0);
518 s.setMu(1.0);
519 s.setDt(1.0);
520 };
521 auto scalarAdv = [](IbmSolver& s) {
522 s.addScalar("c", 0.7, 1, 50, true);
523 s.setScalarWall("c", 1, 1.0, 0.0, -1);
524 s.setScalarSource("c", -0.3);
525 };
526 std::vector<double> gv[3];
527 const char* nm[3] = {"u", "v", "w"};
528 {
529 IbmSolver g(N, N, N);
530 flowAdv(g);
531 g.setSolid(gsdf, true);
532 g.setBodyForce(1e-3, 4e-4, 2e-4);
533 for (int k = 0; k < 20; ++k)
534 g.step();
535 scalarAdv(g);
536 g.solveScalarSteady("c"); // the probe: census max_cell_peclet of the unscaled field
537 const double k = 1.0 / g.scalarField("c").cut->maxCellPeclet;
538 for (int a = 0; a < 3; ++a) {
539 gv[a] = g.getField(nm[a]);
540 for (double& v : gv[a])
541 v *= k;
542 }
543 }
544 compare(
545 "steady_adv", N, gsdf, flowAdv,
546 [&](IbmSolver& s, const Block& B) {
547 const std::size_t nl = (std::size_t)B.l[0] * B.l[1] * B.l[2];
548 for (int a = 0; a < 3; ++a) {
549 std::vector<double> f(nl);
550 for (int z = 0; z < B.l[2]; ++z)
551 for (int y = 0; y < B.l[1]; ++y)
552 for (int x = 0; x < B.l[0]; ++x)
553 f[(std::size_t)x + (std::size_t)y * B.l[0] + (std::size_t)z * B.l[0] * B.l[1]] =
554 gv[a][(std::size_t)(x + B.o[0]) + (std::size_t)(y + B.o[1]) * N +
555 (std::size_t)(z + B.o[2]) * N * N];
556 s.setField(nm[a], f);
557 }
558 scalarAdv(s);
559 },
560 [](IbmSolver& s) {
561 Run r;
562 s.solveScalarSteady("c");
563 record(s, r, false);
564 // ruling D-WO5c-1: the steady identity relative to the GROSS budget (the sum of the
565 // terms' absolute values; the source is uniform, so its gross is |source_in|),
566 // bound 1e-11 (compare()'s identityBound below)
567 const auto b = s.scalarBudget("c");
568 r.identity.back() = b.identityError / (std::fabs(b.wallIn) + std::fabs(b.sourceIn) +
569 std::fabs(b.boundaryIn));
570 const auto& st = *s.scalarField("c").cut;
571 if (rank_ == 0)
572 std::printf(" [steady_adv] census max_cell_peclet %.6g, %d levels, %d iterations\n",
573 st.maxCellPeclet, st.mgLevels, st.iterations);
574 CHECK(st.advecting && st.steady && std::fabs(st.maxCellPeclet - 1.0) <= 1e-12);
575 return r;
576 },
577 false, true, true,
578 // z = M^-1 r of the advective V-cycle for a fixed global-index r: bitwise equal to the
579 // single-rank build on every decomposition (the in-place coarsening keeps children
580 // rank-local, the bands are pointwise, the colours come from global parity)
581 [&](IbmSolver& sd, IbmSolver& sr, const Block& B) {
582 auto zOf = [&](IbmSolver& s, const Block& b) {
583 const std::vector<double> unk = s.scalarGeometryField(4);
584 std::vector<double> rv(unk.size());
585 for (int z = 0; z < b.l[2]; ++z)
586 for (int y = 0; y < b.l[1]; ++y)
587 for (int x = 0; x < b.l[0]; ++x) {
588 const std::size_t l =
589 (std::size_t)x + (std::size_t)y * b.l[0] + (std::size_t)z * b.l[0] * b.l[1];
590 const double gk =
591 (double)((x + b.o[0]) + (y + b.o[1]) * N + (long)(z + b.o[2]) * N * N);
592 rv[l] = unk[l] > 0.5 ? std::sin(0.37 * gk) * (1.0 + 0.2 * std::cos(1e-3 * gk))
593 : 0.0;
594 }
595 auto& sc = s.scalarField("c");
596 s.setField("c", rv);
597 const std::size_t n = sc.c.extent(0);
598 peclet::flow::CCField r("r", n), z("z", n);
599 Kokkos::deep_copy(r, sc.c);
600 sc.cut->mg->apply(z, r);
601 return Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), z);
602 };
603 const auto zd = zOf(sd, B);
604 const auto zr = zOf(sr, Block{{0, 0, 0}, {N, N, N}});
605 const long dsy = B.l[0] + 2 * G, dsz = dsy * (B.l[1] + 2 * G), rsy = N + 2 * G,
606 rsz = rsy * (N + 2 * G);
607 long notSame = 0, cells = 0;
608 for (int z = 0; z < B.l[2]; ++z)
609 for (int y = 0; y < B.l[1]; ++y)
610 for (int x = 0; x < B.l[0]; ++x) {
611 const double a = zd((x + G) + (y + G) * dsy + (z + G) * dsz);
612 const double b =
613 zr((x + B.o[0] + G) + (y + B.o[1] + G) * rsy + (z + B.o[2] + G) * rsz);
614 notSame += same(a, b) ? 0 : 1;
615 ++cells;
616 }
617 long t[2] = {notSame, cells}, tt[2];
618 MPI_Allreduce(t, tt, 2, MPI_LONG, MPI_SUM, MPI_COMM_WORLD);
619 if (rank_ == 0)
620 std::printf(
621 " [steady_adv] advective V-cycle z = M^-1 r: %ld of %ld cells not "
622 "bitwise vs single rank\n",
623 tt[0], tt[1]);
624 CHECK(tt[0] == 0 && tt[1] == (long)N * N * N);
625 },
626 1e-11);
627 }
628 // ---- G10 on G8 (WO-6): the Taylor-Aris closure, steady, advecting, singular ----
629 {
630 // G8's pipe in a cubic box (z-invariant, so the cube is the same problem): R = 16 cells,
631 // insulating walls, x/y no-slip flow walls, z periodic; frozen Poiseuille z-face velocities
632 // at Pe = U R/D = 10 (census Pe_h 1.25) set on every block; mean gradient e_z.
633 const int N = 40;
634 const double R = 16.0, D = 0.7, U = 10.0 * D / R, cx = 19.83, cy = 20.27;
635 std::vector<double> gsdf((std::size_t)N * N * N);
636 for (int z = 0; z < N; ++z)
637 for (int y = 0; y < N; ++y)
638 for (int x = 0; x < N; ++x)
639 gsdf[(std::size_t)x + (std::size_t)y * N + (std::size_t)z * N * N] =
640 R - std::sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));
641 compare(
642 "g8", N, gsdf,
643 [](IbmSolver& s) {
644 s.setRho(1.0);
645 s.setMu(1.0);
646 s.setDt(1.0);
647 for (int f = 0; f < 4; ++f)
648 s.setDomainBc(f, 1, 0.0, 0.0, 0.0);
649 },
650 [&](IbmSolver& s, const Block& B) {
651 std::vector<double> w((std::size_t)B.l[0] * B.l[1] * B.l[2]);
652 for (int z = 0; z < B.l[2]; ++z)
653 for (int y = 0; y < B.l[1]; ++y)
654 for (int x = 0; x < B.l[0]; ++x) {
655 const double dx = x + B.o[0] - cx, dy = y + B.o[1] - cy;
656 w[(std::size_t)x + (std::size_t)y * B.l[0] + (std::size_t)z * B.l[0] * B.l[1]] =
657 std::fmax(2.0 * U * (1.0 - (dx * dx + dy * dy) / (R * R)), 0.0);
658 }
659 s.setField("w", w);
660 s.addScalar("c", D, 1, 50, true);
661 for (int f = 0; f < 4; ++f)
662 s.setScalarBc("c", f, 1, 0.0);
663 s.setScalarMeanGradient("c", {0.0, 0.0, 1.0});
664 },
665 [](IbmSolver& s) {
666 Run r;
667 s.solveScalarSteady("c");
668 r.iters.push_back(s.scalarField("c").cut->iterations);
669 // the compared "flux" is the closure's output: the box-averaged flux along G
670 r.flux.push_back(s.scalarMeanFlux("c")[2]);
671 // ruling D-WO5c-1: the steady identity relative to the GROSS budget; every net term
672 // is 0 here (insulating, no source, periodic along G), the gross is V sum |b| (b the
673 // mean-gradient terms of the right-hand side; V = 1 in cell units)
674 auto& sc = s.scalarField("c");
675 auto hb = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), sc.b);
676 const int ext[3] = {s.nx() + 2 * G, s.ny() + 2 * G, s.nz() + 2 * G};
677 double g = 0.0;
678 for (int z = G; z < ext[2] - G; ++z)
679 for (int y = G; y < ext[1] - G; ++y)
680 for (int x = G; x < ext[0] - G; ++x)
681 g += std::fabs(hb(x + (long)y * ext[0] + (long)z * ext[0] * ext[1]));
682 double gg = 0.0;
683 MPI_Allreduce(&g, &gg, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
684 r.identity.push_back(s.scalarBudget("c").identityError / gg);
685 const auto& st = *sc.cut;
686 if (rank_ == 0)
687 std::printf(
688 " [g8] census max_cell_peclet %.6g, singular %d, %d levels, %d iterations, "
689 "mean flux z %.15e\n",
690 st.maxCellPeclet, (int)st.singular, st.mgLevels, st.iterations, r.flux.back());
691 CHECK(st.advecting && st.steady && st.singular && st.meanGradOn);
692 return r;
693 },
694 false, true, true, nullptr, 1e-11);
695 }
696 // ---- G10 on G6 case 1 (WO-7): the conjugate composite sphere, 20 backward-Euler steps ----
697 // A conjugate core (instance 0: Lam_s = 10, C_s K = 1, K = 1, R_c = 0) inside an immersed
698 // Dirichlet sphere (instance 1, psi = 0), walls on every face, cell units, R = 8, Ro = 16,
699 // dt mu = 1. Both fields and both bodies' fluxes compared per step (the two-field Krylov, the
700 // coupled two-field V-cycle, the solid probes and the material halo under the decomposition).
701 {
702 const int N = 40;
703 const double R = 8.0, Ro = 16.0, c0[3] = {19.83, 20.17, 19.71};
704 const double mu = 2.6588142379399775 / (R * R); // case 1, test_scalar_cutcell_gates.g6_exact
705 std::vector<int> ni = {1, -1, -1, 3, -1, -1, 1, -1, -1, 34, 1, 2};
706 std::vector<double> nr(4 * 16, 0.0);
707 for (int k = 0; k < 4; ++k) {
708 nr[16 * k + 14] = 1.0;
709 nr[16 * k + 15] = 1.0;
710 }
711 nr[0] = R;
712 nr[16 + 0] = nr[16 + 1] = nr[16 + 2] = 1e3;
713 nr[32 + 0] = Ro;
714 std::vector<int> ii = {0, -1, 3, -1};
715 std::vector<double> ir(2 * 18, 0.0);
716 for (int k = 0; k < 2; ++k) {
717 for (int d = 0; d < 3; ++d)
718 ir[18 * k + d] = c0[d];
719 ir[18 * k + 6] = 1.0;
720 ir[18 * k + 7] = 1.0;
721 }
722 peclet::core::decomp::BlockDecomposer<3> dec =
723 peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size_), N, N, N);
724 const auto blk = dec.block(rank_);
725 const Block B{{(int)blk.origin[0], (int)blk.origin[1], (int)blk.origin[2]},
726 {(int)blk.size[0], (int)blk.size[1], (int)blk.size[2]}};
727 auto build = [&](IbmSolver& s, const Block& b, bool dist) {
728 s.setRho(1.0);
729 s.setMu(1.0);
730 s.setDt(1.0 / mu);
731 for (int f = 0; f < 6; ++f)
732 s.setDomainBc(f, 1, 0.0, 0.0, 0.0);
733 if (dist)
734 s.initMpi(N, N, N, MPI_COMM_WORLD);
735 s.setScene(ni, nr, ii, ir, false);
736 s.setSolidFromScene(false);
737 s.addScalar("c", 1.0, 1, 50, true);
738 for (int f = 0; f < 6; ++f)
739 s.setScalarBc("c", f, 1, 0.0);
740 s.setScalarWall("c", 1, 0.0, 0.0, 1);
741 s.setScalarSolid("c", 10.0, 1.0, 1.0, 0.0, 0);
742 s.setScalarTolerance("c", 1e-13);
743 // a smooth positive initial state in both fields: 1 - (r/Ro)^2
744 for (const char* nm : {"c", "c_solid"}) {
745 auto f = s.fieldView(nm);
746 auto h = Kokkos::create_mirror_view(f);
747 const long sy = b.l[0] + 2 * G, sz = sy * (b.l[1] + 2 * G);
748 for (int z = 0; z < b.l[2]; ++z)
749 for (int y = 0; y < b.l[1]; ++y)
750 for (int x = 0; x < b.l[0]; ++x) {
751 const double dx = x + b.o[0] - c0[0], dy = y + b.o[1] - c0[1],
752 dz = z + b.o[2] - c0[2];
753 h((x + G) + (y + G) * sy + (z + G) * sz) =
754 1.0 - (dx * dx + dy * dy + dz * dz) / (Ro * Ro);
755 }
756 Kokkos::deep_copy(f, h);
757 }
758 };
759 IbmSolver sd(B.l[0], B.l[1], B.l[2]);
760 build(sd, B, true);
761 IbmSolver sr(N, N, N);
762 build(sr, Block{{0, 0, 0}, {N, N, N}}, false);
763 std::vector<int> itD, itR;
764 std::vector<double> fxD, fxR, idD, idR;
765 for (int k = 0; k < 20; ++k) {
766 for (int w = 0; w < 2; ++w) {
767 IbmSolver& s = w == 0 ? sd : sr;
768 s.advanceScalars();
769 const auto fl = s.scalarWallFlux("c");
770 const auto bu = s.scalarBudget("c");
771 (w == 0 ? itD : itR).push_back(s.scalarField("c").cut->iterations);
772 (w == 0 ? fxD : fxR).push_back(fl[0]);
773 (w == 0 ? fxD : fxR).push_back(fl[1]);
774 (w == 0 ? idD : idR).push_back(bu.identityError / std::fabs(bu.dMass));
775 }
776 }
777 double gg[4] = {0, 0, 0, 0};
778 long ns = 0;
779 for (int ph = 0; ph < 2; ++ph) {
780 const char* nm = ph == 0 ? "c" : "c_solid";
781 auto cd = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), sd.fieldView(nm));
782 auto cr = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), sr.fieldView(nm));
783 const long dsy = B.l[0] + 2 * G, dsz = dsy * (B.l[1] + 2 * G), rsy = N + 2 * G,
784 rsz = rsy * (N + 2 * G);
785 double md = 0.0, mr = 0.0;
786 long nsl = 0;
787 for (int z = 0; z < B.l[2]; ++z)
788 for (int y = 0; y < B.l[1]; ++y)
789 for (int x = 0; x < B.l[0]; ++x) {
790 const double a = cd((x + G) + (y + G) * dsy + (z + G) * dsz);
791 const double b =
792 cr((x + B.o[0] + G) + (y + B.o[1] + G) * rsy + (z + B.o[2] + G) * rsz);
793 md = std::fmax(md, std::fabs(a - b));
794 mr = std::fmax(mr, std::fabs(b));
795 nsl += same(a, b) ? 0 : 1;
796 }
797 double g2[2] = {md, mr}, o2[2];
798 MPI_Allreduce(g2, o2, 2, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD);
799 gg[2 * ph] = o2[0];
800 gg[2 * ph + 1] = o2[1];
801 long o = 0;
802 MPI_Allreduce(&nsl, &o, 1, MPI_LONG, MPI_SUM, MPI_COMM_WORLD);
803 ns += o;
804 }
805 double idm = 0.0;
806 for (std::size_t k = 0; k < idD.size(); ++k)
807 idm = std::fmax(idm, std::fmax(std::fabs(idD[k]), std::fabs(idR[k])));
808 const auto& st = *sd.scalarField("c").cut;
809 if (rank_ == 0) {
810 std::printf(
811 "[g6 case 1] np=%d: max|c - c_1| = %.2e (max %.3f), max|psi_s - psi_s1| = %.2e "
812 "(max %.3f), %ld cells not bitwise; %ld solid unknowns, %d levels\n",
813 size_, gg[0], gg[1], gg[2], gg[3], ns, st.numSolidUnknowns, st.mgLevels);
814 std::printf(" iterations np:");
815 for (int it : itD)
816 std::printf(" %d", it);
817 std::printf(" single:");
818 for (int it : itR)
819 std::printf(" %d", it);
820 std::printf("\n last fluxes np: %.15e %.15e single: %.15e %.15e\n", fxD[fxD.size() - 2],
821 fxD.back(), fxR[fxR.size() - 2], fxR.back());
822 std::printf(" budget identity (rel to |d_mass|) <= %.1e\n", idm);
823 }
824 CHECK(st.conj && st.numSolidUnknowns > 0);
825 CHECK(itD.size() == itR.size());
826 CHECK(idm <= 1e-12);
827 if (size_ == 1) {
828 CHECK(ns == 0);
829 for (std::size_t k = 0; k < itD.size(); ++k)
830 CHECK(itD[k] == itR[k]);
831 for (std::size_t k = 0; k < fxD.size(); ++k)
832 CHECK(same(fxD[k], fxR[k]));
833 } else {
834 CHECK(gg[0] <= 1e-10 * gg[1]);
835 CHECK(gg[2] <= 1e-10 * gg[3]);
836 for (std::size_t k = 0; k < itD.size(); ++k)
837 CHECK(std::abs(itD[k] - itR[k]) <= 1);
838 for (std::size_t k = 0; k < fxD.size(); ++k)
839 CHECK(std::fabs(fxD[k] - fxR[k]) <= 1e-10 * std::fmax(std::fabs(fxR[k]), 1e-300));
840 }
841 }
842 }
843 Kokkos::finalize();
844 int all = 0;
845 MPI_Allreduce(&failures, &all, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
846 if (rank_ == 0)
847 std::printf(all ? "%d failure(s)\n" : "OK\n", all);
848 MPI_Finalize();
849 return all ? 1 : 0;
850}
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Host entry point: upload the inner SDF once and delegate.
ScalarField & scalarField(const std::string &name)
static constexpr int G
Definition flow_ibm.hpp:80
void setScalarTolerance(const std::string &name, double rtol)
CCField fieldView(const std::string &name)
void setMetric(const double w[3], const double hp[3])
Per-axis metric of the physical domain (doc/anisotropic_metric.md §1.1): the weights w_a = 1/h_a'^2,...
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
Kokkos::View< double *, CCMem > CCField
Solver< Staggered > IbmSolver
Definition prof.py:1
std::vector< double > flux
std::vector< int > iters
std::vector< double > identity
std::shared_ptr< ScalarCutState > cut
static constexpr int N
static std::vector< double > sphereSdf()
static constexpr double U
std::function< void(IbmSolver &)> FlowSetup
static void record(IbmSolver &s, Run &r, bool transient)
std::function< Run(IbmSolver &)> Solve
static void compare(const char *tag, int n, const std::vector< double > &gsdf, FlowSetup flow, ScalarSetup scalar, Solve solve, bool checkLevels=false, bool gated=true, bool cutcellPressure=false, Post post=nullptr, double identityBound=1e-12)
static int failures
static constexpr int G
#define CHECK(cond)
std::function< void(IbmSolver &, IbmSolver &, const Block &)> Post
std::function< void(IbmSolver &, const Block &)> ScalarSetup
static bool same(double a, double b)
static double maxDiff(const std::vector< double > &a, const std::vector< double > &b)