flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_curvature_mpi.cpp
Go to the documentation of this file.
1// flow — multi-rank validation of the VoF curvature cascade (rung V3, WO-O).
2//
3// The cascade (`src/vof/curvature.hpp` + `curvature_field.hpp`) is a PURE LOCAL STENCIL on the
4// colour field's g = 3 block: three height-function tiers over a 3x3x7 patch, then a
5// PLIC-volumetric paraboloid fit over a 5^3 stencil of MYC planes — reach exactly +/-3 in every
6// direction, and **not one reduction anywhere in it**. So unlike every other coupled VoF gate in
7// this directory, this one is required to be **BITWISE** identical at np 2 and 4, not merely
8// identical to the reduction-order floor: there is no allreduce whose summation order could move.
9// A failure here is a real defect, never a floor.
10//
11// What it can catch, in order of likelihood:
12// 1. a stencil that reaches past g = 3. The whole rung was designed to fit exactly inside the
13// colour halo; one cell too far and the answer becomes decomposition-dependent, silently and
14// only in the ghost-adjacent cells (the symptom WO-E's finding 2 named).
15// 2. the non-periodic colour ghost fill. `vof::clampFill` takes the value at the GLOBALLY
16// clamped index precisely so it is decomposition-independent; sequential zero-gradient axis
17// passes over the block are NOT. The height function integrates a 7-cell column, so a
18// curvature at the third inner cell from a wall reads that fill directly. `walls-z` cuts the
19// walled axis to exercise it.
20// 3. the branch census. It is a per-rank local count; the sum over ranks must equal the
21// single-rank census exactly, which is a much stronger statement than the field comparison
22// alone (a cell that changes branch but happens to land on a similar value would pass the
23// field check at 1e-12 and fail this one).
24//
25// Two configurations on a 16x16x32 grid — the aligned ORB cuts the long z axis at np = 2 and 4 —
26// with the sphere deliberately placed so its interface straddles a rank boundary, and a resolution
27// (R = 4 cells, D/dx = 8) at which the PLIC-volumetric fallback provably fires on a large fraction
28// of the interfacial cells. A test in which only the height-function branch runs would not gate the
29// fallback's stencil reach at all.
30#include <mpi.h>
31
32#include <cmath>
33#include <cstdio>
34#include <cstring>
35#include <Kokkos_Core.hpp>
36#include <vector>
37
38#include "flow_ibm.hpp"
39#include "peclet/core/common/types.hpp"
40#include "peclet/core/decomp/block_decomposer.hpp"
41
43
44static constexpr int NX = 16, NY = 16, NZ = 32;
45static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
46
47struct Config {
48 const char* name;
49 bool walls; // walls +-z (exercises the clamped, non-periodic colour ghost) vs fully periodic
50};
51
52// Sharp colour at a GLOBAL cell index — a pure function of global indices, so a block gets
53// bit-identical data to the corresponding part of the single-rank run. Fractions by 8^3 midpoint
54// subsampling: exactness is irrelevant here (both runs see the SAME field), coverage of the
55// cascade's branches is not.
56static double colourAt(const Config& c, int x, int y, int z) {
57 const double cx = NX / 2.0 + 0.137, cy = NY / 2.0 - 0.311;
58 // periodic: sphere centred on the z rank boundary at np = 2. walls: on the quarter plane, so the
59 // np = 4 boundary at z = NZ/4 cuts it too and the +-z walls stay far from the interface.
60 const double cz = c.walls ? NZ / 4.0 + 0.241 : NZ / 2.0 + 0.241;
61 const double R = 4.0;
62 const int NS = 8;
63 int in = 0;
64 for (int k = 0; k < NS; ++k)
65 for (int j = 0; j < NS; ++j)
66 for (int i = 0; i < NS; ++i) {
67 const double dx = x + (i + 0.5) / NS - cx, dy = y + (j + 0.5) / NS - cy,
68 dz = z + (k + 0.5) / NS - cz;
69 if (dx * dx + dy * dy + dz * dz < R * R)
70 ++in;
71 }
72 return static_cast<double>(in) / (NS * NS * NS);
73}
74
75template <class Fn>
76static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
77 std::vector<double> v((std::size_t)lnx * lny * lnz);
78 for (int z = 0; z < lnz; ++z)
79 for (int y = 0; y < lny; ++y)
80 for (int x = 0; x < lnx; ++x)
81 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
82 f(x + ox, y + oy, z + oz);
83 return v;
84}
85
86static void configure(IbmSolver& s, const Config& c, int ox, int oy, int oz, int lnx, int lny,
87 int lnz) {
88 s.setRho(1.0);
89 s.setMu(0.01);
90 s.setDt(1.0);
91 if (c.walls) {
92 s.setDomainBc(4, 1, 0, 0, 0);
93 s.setDomainBc(5, 1, 0, 0, 0); // walls +-z (the CUT axis)
94 }
95 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
96 s.setVof(blockOf([&](int x, int y, int z) { return colourAt(c, x, y, z); }, ox, oy, oz, lnx, lny,
97 lnz));
98}
99
100// Gather per-rank inner blocks (x-fastest) into the global field on rank 0.
101static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
102 int lnx, int lny, int lnz, int rank, int size) {
103 std::vector<double> global;
104 if (rank == 0)
105 global.assign(GCELLS, 0.0);
106 for (int r = 0; r < size; ++r) {
107 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
108 if (r == 0) {
109 if (rank == 0)
110 for (int z = 0; z < lnz; ++z)
111 for (int y = 0; y < lny; ++y)
112 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
113 (std::size_t)(z + oz) * NX * NY],
114 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
115 (std::size_t)lnx * sizeof(double));
116 continue;
117 }
118 if (rank == r) {
119 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
120 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
121 } else if (rank == 0) {
123 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
124 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
126 for (int z = 0; z < meta[5]; ++z)
127 for (int y = 0; y < meta[4]; ++y)
128 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
129 (std::size_t)(z + meta[2]) * NX * NY],
130 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
131 (std::size_t)meta[3] * sizeof(double));
132 }
133 }
134 return global;
135}
136
137static long countBitDiff(const std::vector<double>& a, const std::vector<double>& b, double& mx) {
138 long n = 0;
139 mx = 0.0;
140 for (std::size_t i = 0; i < b.size(); ++i)
141 if (a[i] != b[i]) {
142 ++n;
143 mx = std::fmax(mx, std::fabs(a[i] - b[i]));
144 }
145 return n;
146}
147
148int main(int argc, char** argv) {
149 MPI_Init(&argc, &argv);
150 Kokkos::initialize(argc, argv);
151 int fail = 0;
152 {
153 int rank = 0, size = 1;
156
157 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
158 auto blk = dec.block(rank);
159 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
160 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
161 const int gn[3] = {NX, NY, NZ};
162 bool cut[3] = {false, false, false};
163 for (const auto& sz : dec.sizes())
164 for (int a = 0; a < 3; ++a)
165 if ((int)sz[a] != gn[a])
166 cut[a] = true;
167 if (rank == 0)
168 std::printf("VOF CURVATURE MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n",
169 size, NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "",
170 cut[2] ? "z" : "");
171
172 const Config configs[] = {{"sphere-per", false}, {"sphere-walls-z", true}};
173
174 for (const Config& c : configs) {
175 // The interface must straddle a rank boundary for the test to mean anything.
176 if (size > 1 && !cut[2]) {
177 if (rank == 0)
178 std::printf(" [%-14s np=%d] FAIL — the decomposition does NOT cut z\n", c.name, size);
179 fail = 1;
180 continue;
181 }
182
183 // --- distributed ---
185 sd.initMpi(dec, MPI_COMM_WORLD);
186 configure(sd, c, ox, oy, oz, lnx, lny, lnz);
188 const auto stLoc = sd.vofCurvatureStats();
189 long locCounts[7] = {stLoc.interfacial, stLoc.hf, stLoc.hfMixed, stLoc.hfFit,
190 stLoc.pv, stLoc.pvReduced, stLoc.noEstimate};
191 long sumCounts[7] = {0, 0, 0, 0, 0, 0, 0};
193
194 const std::vector<double> gk =
195 gatherGlobal(sd.getVofCurvature(), ox, oy, oz, lnx, lny, lnz, rank, size);
196 const std::vector<double> gb =
197 gatherGlobal(sd.getVofCurvatureBranch(), ox, oy, oz, lnx, lny, lnz, rank, size);
198
199 // --- single-rank full-grid reference on rank 0 ---
200 if (rank == 0) {
201 IbmSolver ref(NX, NY, NZ);
202 configure(ref, c, 0, 0, 0, NX, NY, NZ);
203 ref.computeVofCurvature();
204 const auto st = ref.vofCurvatureStats();
205 const long refCounts[7] = {st.interfacial, st.hf, st.hfMixed, st.hfFit,
206 st.pv, st.pvReduced, st.noEstimate};
207 double dk = 0.0, db = 0.0;
208 const long nk = countBitDiff(gk, ref.getVofCurvature(), dk);
209 const long nb = countBitDiff(gb, ref.getVofCurvatureBranch(), db);
210
211 bool countsOk = true;
212 for (int i = 0; i < 7; ++i)
213 countsOk = countsOk && (sumCounts[i] == refCounts[i]);
214
215 std::printf(
216 " [%-14s np=%d] interfacial %ld | HF %ld HFdir %ld HFfit %ld PV %ld PVred %ld"
217 " none %ld\n",
218 c.name, size, refCounts[0], refCounts[1], refCounts[2], refCounts[3], refCounts[4],
219 refCounts[5], refCounts[6]);
220 std::printf(
221 " kappa: %ld/%ld cells differ (max |d| %.3e) branch: %ld differ"
222 " census %s\n",
223 nk, (long)GCELLS, dk, nb, countsOk ? "MATCHES" : "DIFFERS");
224
225 // Bitwise, with no tolerance: the cascade contains no reduction.
226 if (nk != 0 || nb != 0 || !countsOk) {
227 std::printf(" FAIL — the curvature cascade is decomposition-DEPENDENT\n");
228 fail = 1;
229 }
230 // The fallback must actually be exercised, or the test gates only the HF stencil.
231 if (refCounts[4] + refCounts[5] == 0) {
232 std::printf(
233 " FAIL — the PLIC-volumetric fallback never fired; this configuration "
234 "no longer gates its stencil reach\n");
235 fail = 1;
236 }
237 if (refCounts[6] != 0) {
238 std::printf(" FAIL — %ld interfacial cells got NO curvature estimate\n",
239 refCounts[6]);
240 fail = 1;
241 }
242 }
244 }
245 if (rank == 0)
246 std::printf("%s\n", fail ? "FAILED" : "PASSED");
247 }
248 Kokkos::finalize();
249 MPI_Finalize();
250 return fail;
251}
std::vector< double > getVofCurvature()
The interface curvature kappa = 2H in the caller's units, i.e.
void setDomainBc(int face, int type, double vx, double vy, double vz)
std::vector< double > getVofCurvatureBranch()
void setVof(const std::vector< double > &c)
void setPressureGeometry(const std::vector< double > &sdfInner)
vof::VofCurvature::Stats vofCurvatureStats() const
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setRho(double r)
Fluid density, in the caller's units.
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
const char * name
static constexpr int NY
static std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
static std::vector< double > gatherGlobal(const std::vector< double > &local, int ox, int oy, int oz, int lnx, int lny, int lnz, int rank, int size)
static long countBitDiff(const std::vector< double > &a, const std::vector< double > &b, double &mx)
static double colourAt(const Config &c, int x, int y, int z)
static constexpr int NX
static constexpr std::size_t GCELLS
static constexpr int NZ
static void configure(IbmSolver &s, const Config &c, int ox, int oy, int oz, int lnx, int lny, int lnz)