flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_phase_change_mpi.cpp
Go to the documentation of this file.
1// flow — VoF Part II rungs P0/P1 (WO-P01), multi-rank: phase change on a decomposition that CUTS
2// the interface.
3//
4// What can only break here:
5//
6// 1. THE PER-CELL INTERFACE DATA AT DEPTH 1 AND 2. `mdot`, the PLIC polygon area and the unit
7// normal are computed on INNER cells only and then halo-exchanged, because two consumers read
8// them outside the inner region: the clip-and-redistribute pass (a deficit may be pushed
9// ACROSS a rank boundary, so the receiving rank must recompute the donor's whole allocation)
10// and the divergence-source gather (the donor may be up to two cells outside). Both are
11// written as GATHERS with a fixed summation order — never an atomic scatter — so the result
12// is bitwise, not "at the reduction floor".
13//
14// 2. THE NON-PERIODIC GHOST. On a wall/inflow/outflow face the halo's periodic wrap would import
15// the far side's interface as a phantom donor. `pcZeroDomainGhosts` kills that, per rank-OWNED
16// face; the P1 configuration below has walls on +-x and a decomposition that cuts x.
17//
18// 3. THE PER-CELL DIRICHLET MASK of the energy scalar, which is rebuilt from the colour on every
19// rank and must agree across a boundary.
20//
21// Gates: P0a (planar regression, 1000 steps) BITWISE against the full-grid single-rank run, and P1
22// (the Stefan problem with the thermal mass flux) at the reduction floor — the energy solve's
23// red-black Gauss-Seidel is the only thing in the loop whose result depends on the decomposition,
24// and it does so at the level of its own unconverged residual.
25#include <mpi.h>
26
27#include <cmath>
28#include <cstdio>
29#include <cstdlib>
30#include <cstring>
31#include <Kokkos_Core.hpp>
32#include <vector>
33
34#include "flow_ibm.hpp"
35#include "peclet/core/common/types.hpp"
36#include "peclet/core/decomp/block_decomposer.hpp"
37
39
40static constexpr int NX = 64, NY = 4, NZ = 4;
41
42static int failures = 0;
43#define CHECK(cond) \
44 do { \
45 if (!(cond)) { \
46 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
47 ++failures; \
48 } \
49 } while (0)
50
51static std::size_t gidx(int x, int y, int z) {
52 return (std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY;
53}
54
55template <class Fn>
56static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
57 std::vector<double> v((std::size_t)lnx * lny * lnz);
58 for (int z = 0; z < lnz; ++z)
59 for (int y = 0; y < lny; ++y)
60 for (int x = 0; x < lnx; ++x)
61 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
62 f(x + ox, y + oy, z + oz);
63 return v;
64}
65static std::vector<double> sliceOf(const std::vector<double>& g, int ox, int oy, int oz, int lnx,
66 int lny, int lnz) {
67 return blockOf([&](int x, int y, int z) { return g[gidx(x, y, z)]; }, ox, oy, oz, lnx, lny, lnz);
68}
69
70// Bitwise comparison of a distributed field against the global reference, reduced over ranks.
71static double blockDiff(const std::vector<double>& local, const std::vector<double>& global, int ox,
72 int oy, int oz, int lnx, int lny, int lnz) {
73 double m = 0.0;
74 for (int z = 0; z < lnz; ++z)
75 for (int y = 0; y < lny; ++y)
76 for (int x = 0; x < lnx; ++x)
77 m = std::fmax(
78 m, std::fabs(local[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] -
79 global[gidx(x + ox, y + oy, z + oz)]));
80 double g = 0.0;
82 return g;
83}
84
85static double stefanLambda(double St) {
86 const double rhs = St / std::sqrt(M_PI);
87 double a = 1e-8, b = 5.0;
88 for (int i = 0; i < 200; ++i) {
89 const double m = 0.5 * (a + b);
90 ((m * std::exp(m * m) * std::erf(m) - rhs) > 0 ? b : a) = m;
91 }
92 return 0.5 * (a + b);
93}
94
95// WO-P3c: run every scene below with a non-default interfacial-AREA geometry
96// (`set_phase_change_area`) when `PECLET_P3C_AREA` is set, so the planar rungs can be re-taken on
97// the cascade area without a second binary. Inert (and byte-identical) when the variable is unset.
98template <class S>
99void applyAreaModeEnv(S& s) {
100 if (const char* e = std::getenv("PECLET_P3C_AREA"))
101 s.setPhaseChangeArea(std::atoi(e));
102}
103
104// WO-P3f: the same hook for this work order's two OPTIONS, so the planar rungs and the MPI
105// decomposition gate can be re-taken with them on without a second binary. Both are inert (and
106// byte-identical) when the variables are unset. `PECLET_P3F_CARRY=1` turns on the
107// enthalpy-conserving Dirichlet overwrite (a fixed-order gather over face neighbours: the piece
108// with a real neighbour dependency, hence the one the decomposition gate is about);
109// `PECLET_P3F_KAPPA=<k>` prescribes the curvature the one-sided fits correct their sample
110// distances with (a purely per-cell function of the existing stencil, no reduction).
111template <class S>
112void applyP3fEnv(S& s) {
113 if (const char* e = std::getenv("PECLET_P3F_CARRY"))
114 s.setPhaseChangeCarryConserve(e[0] != '0');
115 if (const char* e = std::getenv("PECLET_P3F_KAPPA"))
116 s.setPhaseChangeFitCurvature(std::atof(e));
117 // WO-P3g: the second-order interfacial energy operator through the SAME hook, applied to both
118 // the reference and the distributed solver. `PECLET_P3G_ORDER=2` is the package; the three
119 // singles are the ablation. Inert (byte-identical) when unset.
120 if (const char* e = std::getenv("PECLET_P3G_ORDER"))
121 s.setPhaseChangeEnergyOrder(std::atoi(e));
122 if (const char* e = std::getenv("PECLET_P3G_OPMDOT"))
123 s.setPhaseChangeMdotOperator(e[0] != '0');
124 if (const char* e = std::getenv("PECLET_P3G_GFMORDER"))
125 s.setPhaseChangeGfmOrder(std::atoi(e));
126 if (const char* e = std::getenv("PECLET_P3G_CURVDIST"))
127 s.setPhaseChangeCurvatureDistance(e[0] != '0');
128}
129
130int main(int argc, char** argv) {
131 std::setvbuf(stdout, nullptr, _IONBF, 0);
132 MPI_Init(&argc, &argv);
133 Kokkos::initialize(argc, argv);
134 {
135 int rank = 0, size = 1;
138 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
139 auto blk = dec.block(rank);
140 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
141 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
142 bool cutx = false;
143 for (const auto& sz : dec.sizes())
144 if ((int)sz[0] != NX)
145 cutx = true;
146 if (rank == 0)
147 std::printf("VOF PHASE CHANGE MPI np=%d grid %dx%dx%d block %dx%dx%d x cut: %s\n", size,
148 NX, NY, NZ, lnx, lny, lnz, cutx ? "yes" : "no");
149 if (size > 1)
150 CHECK(cutx); // the interface is normal to x: the decomposition MUST cut it
151
152 // ================================================================ P0a, bitwise
153 {
154 const double x0 = 32.25, mdot = 0.02, dt = 1.0;
155 auto colour = [&](int x, int, int) { return std::fmin(1.0, std::fmax(0.0, x0 - x)); };
156 std::vector<double> refC;
157 { // the full-grid single-rank run, on every rank (identical arithmetic, no reduction)
158 IbmSolver ref(NX, NY, NZ);
159 ref.setRho(1.0);
160 ref.setMu(0.01);
161 ref.setDt(dt);
162 ref.enableVof();
163 ref.setVof(blockOf(colour, 0, 0, 0, NX, NY, NZ));
164 ref.enablePhaseChange(1.0, 1.0, 1.0);
165 applyAreaModeEnv(ref);
166 applyP3fEnv(ref);
167 ref.setMassFluxUniform(mdot);
168 for (int k = 0; k < 1000; ++k)
169 ref.applyPhaseChange(dt);
170 refC = ref.getVof();
171 }
173 sd.initMpi(dec, MPI_COMM_WORLD);
174 sd.setRho(1.0);
175 sd.setMu(0.01);
176 sd.setDt(dt);
177 sd.enableVof();
178 sd.setVof(blockOf(colour, ox, oy, oz, lnx, lny, lnz));
179 sd.enablePhaseChange(1.0, 1.0, 1.0);
183 for (int k = 0; k < 1000; ++k)
185 const double d = blockDiff(sd.getVof(), refC, ox, oy, oz, lnx, lny, lnz);
186 double refSum = 0.0;
187 for (double v : refC)
188 refSum += v;
189 if (rank == 0)
190 std::printf(
191 " P0a 1000 steps: max |C_dist - C_ref| = %.3e (bitwise expected); x_ref = "
192 "%.15g, exact %.15g\n",
193 d, refSum / (NY * NZ), x0 - mdot * dt * 1000);
194 CHECK(d == 0.0);
195 }
196
197 // ================================================================ P1 Stefan, walls on +-x
198 {
199 const double St = 1.0, alpha = 1.0, x0p = 0.10, xep = 0.25, Fo = 0.5;
200 const double lam = stefanLambda(St);
201 const double t0 = (x0p / (2 * lam)) * (x0p / (2 * lam)) / alpha;
202 const double te = (xep / (2 * lam)) * (xep / (2 * lam)) / alpha;
203 const double D = alpha * NX * NX;
204 const int ns = (int)std::llround((te - t0) / (Fo / D));
205 const double dt = (te - t0) / ns;
206 const double xg = x0p * NX;
207 auto colour = [&](int x, int, int) { return std::fmin(1.0, std::fmax(0.0, (x + 1) - xg)); };
208 auto temp = [&](int x, int, int) {
209 const double xp = (x + 0.5) / NX;
210 return xp < x0p ? 1.0 - std::erf(lam * xp / x0p) / std::erf(lam) : 0.0;
211 };
212 auto configure = [&](IbmSolver& s, int nx, int ny, int nz, int Ox, int Oy, int Oz) {
213 s.setRho(1.0);
214 s.setMu(1e-3);
215 s.setDt(dt);
216 s.setDomainBc(0, 1, 0, 0, 0);
217 s.setDomainBc(1, 1, 0, 0, 0);
218 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 1.0));
219 s.enableVof();
220 s.setVof(blockOf(colour, Ox, Oy, Oz, nx, ny, nz));
221 s.addScalar("T", D, 1, 60);
222 s.setScalarBc("T", 0, 2, 1.0);
223 s.setScalarBc("T", 1, 2, 0.0);
224 s.setField("T", blockOf(temp, Ox, Oy, Oz, nx, ny, nz));
225 s.enablePhaseChange(1.0, 1.0, 1.0);
227 applyP3fEnv(s);
228 s.setPhaseChangeThermal("T", 0.0, D, D, 0.0);
229 };
230 std::vector<double> refC;
231 {
232 IbmSolver ref(NX, NY, NZ);
233 configure(ref, NX, NY, NZ, 0, 0, 0);
234 for (int k = 0; k < ns; ++k) {
235 ref.applyPhaseChange(dt);
236 ref.advanceScalars();
237 }
238 refC = ref.getVof();
239 }
241 sd.initMpi(dec, MPI_COMM_WORLD);
242 configure(sd, lnx, lny, lnz, ox, oy, oz);
243 for (int k = 0; k < ns; ++k) {
246 }
247 const double d = blockDiff(sd.getVof(), refC, ox, oy, oz, lnx, lny, lnz);
248 double refSum = 0.0;
249 for (double v : refC)
250 refSum += v;
251 const double xExact = 2 * lam * std::sqrt(alpha * te) * NX;
252 if (rank == 0)
253 std::printf(
254 " P1 Stefan %d steps: max |C_dist - C_ref| = %.3e (RB-GS reduction floor); layer_ref "
255 "= %.5f, exact %.5f (%+.4f %%)\n",
256 ns, d, NX - refSum / (NY * NZ), xExact,
257 100.0 * ((NX - refSum / (NY * NZ)) - xExact) / xExact);
258 CHECK(d < 1e-9);
259 }
260
261 // ================================================================ P2 sucking interface
262 // (WO-P23) Everything rung P2/P3 adds, distributed and on a decomposition that cuts the
263 // interface: the plane-anchored (ghost-fluid) Dirichlet rows (whose theta reads the NEIGHBOUR
264 // cell's plane normal and centre distance, i.e. depth-1 data that has to be exchanged and
265 // domain-zeroed), the quadratic one-sided fit, the per-phase k(C) / rho c_p(C) operator, and
266 // the CONSISTENT rho c_p T transport, which rides the colour advector's own g = 3 block and
267 // therefore its own halo. Coupled steps, so the pressure solve is in the loop too.
268 {
269 const char* xe = std::getenv("PECLET_P23_XEP");
270 const double ratio = 10.0, ja = 1.0, alpha_l = 1.0, x0p = 0.10, Fo = 0.5;
271 const double xep = xe ? std::atof(xe) : 0.14;
272 const double cfl = 0.2, rr = 1.0 / ratio;
273 double b = ja / std::sqrt(M_PI);
274 for (int i = 0; i < 300; ++i) {
275 const double g = b * rr;
276 b = ja * std::exp(-g * g) / (std::sqrt(M_PI) * std::erfc(g));
277 }
278 const double t0 = (x0p / (2 * b)) * (x0p / (2 * b)) / alpha_l;
279 const double te = (xep / (2 * b)) * (xep / (2 * b)) / alpha_l;
280 const double al = alpha_l * NX * NX, rho_l = 1.0, rho_v = rr, cpl = 1.0;
281 const double k_l = al * rho_l * cpl, k_v = k_l / ratio, dT = 1.0;
282 const double h_lv = rho_l * cpl * dT / (rho_v * ja);
283 const double X0 = x0p * NX;
284 auto colour = [&](int x, int, int) { return std::fmin(1.0, std::fmax(0.0, (x + 1) - X0)); };
285 auto temp = [&](int x, int, int) {
286 const double xp = (x + 0.5) / NX;
287 if (!(xp > x0p))
288 return 0.0;
289 const double sv = xp / (2 * std::sqrt(alpha_l * t0)) - b * (1.0 - rr);
290 return dT - dT * std::erfc(sv) / std::erfc(b * rr);
291 };
292 auto farT = [&](double tt) {
293 const double sv = 1.0 / (2 * std::sqrt(alpha_l * tt)) - b * (1.0 - rr);
294 return dT - dT * std::erfc(sv) / std::erfc(b * rr);
295 };
296 auto configure = [&](IbmSolver& s, int nx, int ny, int nz, int Ox, int Oy, int Oz) {
297 s.setRho(rho_l);
298 s.setMu(1e-3);
299 s.setDomainBc(0, 1, 0, 0, 0);
300 s.setDomainBc(1, 3, 0, 0, 0);
301 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 1.0));
302 s.enableVof();
303 s.setVof(blockOf(colour, Ox, Oy, Oz, nx, ny, nz));
305 {rho_v, rho_l - rho_v});
306 s.setPressureFcg(true, 4000, 1e-10);
307 s.addScalar("T", al, 1, 60);
308 s.setScalarBc("T", 0, 2, 0.0);
309 s.setScalarBc("T", 1, 2, farT(t0));
310 s.setField("T", blockOf(temp, Ox, Oy, Oz, nx, ny, nz));
313 applyP3fEnv(s);
314 s.setPhaseChangeThermal("T", 0.0, k_v, k_l, 0.0);
315 // Probe switches (findings only): PECLET_P23_OFF is a subset of "pqe" to disable —
316 // p = the plane-anchored Dirichlet, q = the quadratic fit, e = the consistent rho c_p T.
317 const char* off = std::getenv("PECLET_P23_OFF");
318 const bool offP = off && std::strchr(off, 'p');
319 const bool offQ = off && std::strchr(off, 'q');
320 const bool offE = off && std::strchr(off, 'e');
323 if (!offE)
325 };
326 auto march = [&](IbmSolver& s) {
327 double tc = t0;
328 int ns = 0;
329 while (tc < te) {
330 const double Xd = b * std::sqrt(alpha_l / tc) * NX;
331 double dt = std::fmin(cfl / std::fmax(Xd * (1.0 - rr), 1e-30), Fo / al);
332 dt = std::fmin(dt, te - tc);
333 s.setDt(dt);
334 s.setScalarBc("T", 1, 2, farT(tc + dt));
335 s.step();
336 tc += dt;
337 ++ns;
338 }
339 return ns;
340 };
341 std::vector<double> refC, refT;
342 int ns = 0;
343 {
344 IbmSolver ref(NX, NY, NZ);
345 configure(ref, NX, NY, NZ, 0, 0, 0);
346 ns = march(ref);
347 refC = ref.getVof();
348 refT = ref.getField("T");
349 }
351 sd.initMpi(dec, MPI_COMM_WORLD);
352 configure(sd, lnx, lny, lnz, ox, oy, oz);
353 march(sd);
354 const double dC = blockDiff(sd.getVof(), refC, ox, oy, oz, lnx, lny, lnz);
355 const double dT_ = blockDiff(sd.getField("T"), refT, ox, oy, oz, lnx, lny, lnz);
356 double refSum = 0.0;
357 for (double v : refC)
358 refSum += v;
359 double locSum = 0.0;
360 for (double v : sd.getVof())
361 locSum += v;
362 double gSum = 0.0;
364 const double layerRef = NX - refSum / (NY * NZ), layerDist = NX - gSum / (NY * NZ);
365 const double xExact = 2 * b * std::sqrt(alpha_l * te) * NX;
366 if (rank == 0)
367 std::printf(
368 " P2 sucking %d coupled steps (plane-anchored Dirichlet + quadratic fit + consistent "
369 "rho c_p T): layer_dist = %.9f vs layer_ref = %.9f (rel %.3e); pointwise max "
370 "|C_dist - C_ref| = %.3e, |T_dist - T_ref| = %.3e; exact %.5f (%+.4f %%)\n",
371 ns, layerDist, layerRef, std::fabs(layerDist - layerRef) / layerRef, dC, dT_, xExact,
372 100.0 * (layerRef - xExact) / xExact);
373 // THE GATE IS THE PHYSICAL, DECOMPOSITION-INDEPENDENT QUANTITY, and the reason is measured.
374 // This COUPLED scene is ROUND-OFF SENSITIVE, and it is NOT an MPI defect: at np = 1, where
375 // the distributed and reference solvers differ only in the arithmetic path `initMpi` selects,
376 // the pointwise colour difference is 0.0 at 1 step, 3.3e-16 at 3, 2.4e-4 at 12 and then
377 // PLATEAUS (it does not keep growing). It appears exactly when the interface crosses a cell
378 // boundary and the two runs cross it at round-off-different moments. Bisected with
379 // PECLET_P23_OFF at 12 steps: all three WO-P23 options off gives 4.1e-14, and ANY ONE of them
380 // on gives 1.2e-4 … 2.8e-4 — the sharper interfacial treatment is what turns a crossing into
381 // a discrete event (the classification threshold `pcIsInterfacial` switches a cell's whole
382 // energy row). The INTEGRAL of the colour — the interface position, which is the physics —
383 // moves by only 5e-5 … 1.4e-4 relative, and that is what is gated. P0a and P1 above are
384 // BITWISE, so the exchange, the gather-based deposit and the redistribution are all exact;
385 // this is a property of the coupled step, not of the distribution.
386 CHECK(std::fabs(layerDist - layerRef) / layerRef < 5e-4); // measured 8.5e-5 / 5.2e-5 /
387 // 1.4e-4 at np 1 / 2 / 4
388 CHECK(dC < 1e-2);
389 CHECK(dT_ < 1e-2);
390 }
391 }
392 int fail = failures;
394 Kokkos::finalize();
395 MPI_Finalize();
396 return fail ? 1 : 0;
397}
void setPhaseChangeGfmOrder(int order)
WO-P3g item 2: the order of the one-sided (ghost-fluid) Dirichlet row.
void setMassFluxUniform(double v)
Prescribe a UNIFORM mass flux (P0), in mass per area per time.
void setDomainBc(int face, int type, double vx, double vy, double vz)
void setPhaseChangeFitCurvature(double kappa)
WO-P3f INSTRUMENT: prescribe the interface curvature kappa = div(n) the one-sided fits use to correct...
void setPhaseChangeMdotOperator(bool on)
WO-P3g item 1: take mdot from the energy operator's own interfacial flux instead of the one-sided lea...
std::vector< double > getField(const std::string &name)
void addScalar(const std::string &name, double D, int scheme, int iters, bool cutcell=false)
std::vector< double > getVof()
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
void setVof(const std::vector< double > &c)
void setScalarBc(const std::string &name, int face, int type, double value)
void setPhaseChangeThermal(const std::string &tname, double Tsat, double kg, double kl, double Rint)
P1: compute mdot each step from the registered scalar tname by the one-sided pure-cell weighted least...
void setPhaseChangePlaneDirichlet(bool on)
The PLANE-ANCHORED (ghost-fluid) Dirichlet condition, ON by default.
void setPhaseChangeQuadraticFit(bool on)
The QUADRATIC one-sided gradient fit (T - T_G = G phi + Q phi^2) instead of the linear one.
void setPressureGeometry(const std::vector< double > &sdfInner)
void applyPhaseChange(double dtPhysArg)
Kinematic entry point (the P0a/P1 driver): build mdot/A_G/n from the current colour and temperature,...
void setPhaseChangeCarryConserve(bool on)
WO-P3f: make the per-cell Dirichlet overwrite ENTHALPY-CONSERVING.
void setField(const std::string &name, const std::vector< double > &v)
void enablePhaseChange(double rhoG, double rhoL, double hlv)
Turn on phase change.
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setPhaseChangeCurvatureDistance(bool on)
WO-P3g item 3: measure the GFM row's theta and the one-sided fits' sample distances to the CURVED int...
void setPhaseChangeEnergyOrder(int order)
WO-P3g — the SECOND-ORDER interfacial energy operator, as one package.
void setPhaseChangeEnergy(double rcpGas, double rcpLiquid)
Turn on the CONSISTENT energy transport (VOF_PLAN §9 item 6) for the scalar set_phase_change_thermal ...
void setRho(double r)
Fluid density, in the caller's units.
void setPressureFcg(bool on, int maxit, double rtol)
void setPhaseChangeArea(int mode)
WO-P3c — which geometry the interfacial AREA comes from.
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
static void configure(IbmSolver &s)
static double X0()
static std::vector< double > colour(int m)
static constexpr int NY
static std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
static double blockDiff(const std::vector< double > &local, const std::vector< double > &global, int ox, int oy, int oz, int lnx, int lny, int lnz)
static int failures
static std::size_t gidx(int x, int y, int z)
void applyP3fEnv(S &s)
static double stefanLambda(double St)
static std::vector< double > sliceOf(const std::vector< double > &g, int ox, int oy, int oz, int lnx, int lny, int lnz)
#define CHECK(cond)
static constexpr int NX
void applyAreaModeEnv(S &s)
static constexpr int NZ