DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_Reconstruction.cpp
Go to the documentation of this file.
1/**
2 * @file test_Reconstruction.cpp
3 * @brief Phase-0 regression tests for CFV Variational Reconstruction.
4 *
5 * Parameterized over [mesh+function, reconstruction_method].
6 *
7 * All iterative VR uses Jacobi iteration (SORInstead=false) so that
8 * golden values are deterministic across MPI partitionings (np=1,2,4).
9 *
10 * Error metric: L1 pointwise error at 6th-degree quadrature points,
11 * divided by domain volume (so the dimension matches the field function).
12 *
13 * Meshes:
14 * - Uniform_3x3_wall (9 quads, wall BC) -- polynomial exactness tests
15 * - IV10_10 (100 quads, periodic) + bisections -> 400, 1600 cells
16 * - IV10U_10 (322 tris, periodic) + bisections -> ~1288, ~5152 cells
17 *
18 * Golden values captured from commit c774b89 on dev/harry_refac1.
19 */
20
21#define DOCTEST_CONFIG_IMPLEMENT
22#include "doctest.h"
23
25
26#include <array>
27#include <cmath>
28#include <iostream>
29#include <iomanip>
30#include <functional>
31#include <nlohmann/json.hpp>
32#include <map>
33#include <string>
34#include <vector>
35
36using namespace DNDS;
37using namespace DNDS::Geom;
38
39static constexpr int g_dim = 2;
40static constexpr int g_nv = 1;
42
43static MPIInfo g_mpi;
44
45// ===================================================================
46// Mesh builder
47// ===================================================================
48static std::string meshPath(const std::string &name)
49{
50 std::string f(__FILE__);
51 for (int i = 0; i < 4; i++)
52 {
53 auto pos = f.rfind('/');
54 if (pos == std::string::npos)
55 pos = f.rfind('\\');
56 if (pos != std::string::npos)
57 f = f.substr(0, pos);
58 }
59 return f + "/data/mesh/" + name;
60}
61
62static ssp<UnstructuredMesh> buildMeshUpToGhost(
63 const std::string &file, bool periodic,
64 DNDS::real Lx, DNDS::real Ly, int nBisect = 0)
65{
66 auto mesh = std::make_shared<UnstructuredMesh>(g_mpi, g_dim);
68
69 if (periodic)
70 {
71 tPoint zero{0, 0, 0};
72 mesh->SetPeriodicGeometry(
73 tPoint{Lx, 0, 0}, zero, zero,
74 tPoint{0, Ly, 0}, zero, zero,
75 tPoint{0, 0, 0}, zero, zero);
76 }
77
78 reader.ReadFromCGNSSerial(meshPath(file));
79 reader.Deduplicate1to1Periodic();
80 reader.BuildCell2Cell();
81
83 pOpt.metisSeed = 42;
84 reader.MeshPartitionCell2Cell(pOpt);
85 reader.PartitionReorderToMeshCell2Cell();
86
87 mesh->RecoverNode2CellAndNode2Bnd();
88 mesh->RecoverCell2CellAndBnd2Cell();
89 mesh->BuildGhostPrimary();
90 mesh->AdjGlobal2LocalPrimary();
91
92 // Bisect nBisect times via elevation+bisection
93 for (int ib = 0; ib < nBisect; ib++)
94 {
95 auto meshO2 = std::make_shared<UnstructuredMesh>(g_mpi, g_dim);
96 meshO2->BuildO2FromO1Elevation(*mesh);
97 meshO2->RecoverNode2CellAndNode2Bnd();
98 meshO2->RecoverCell2CellAndBnd2Cell();
99 meshO2->BuildGhostPrimary();
100 meshO2->AdjGlobal2LocalPrimary();
101
102 meshO2->AdjLocal2GlobalPrimary();
103 auto meshBis = std::make_shared<UnstructuredMesh>(g_mpi, g_dim);
104 meshBis->BuildBisectO1FormO2(*meshO2);
105 meshBis->RecoverNode2CellAndNode2Bnd();
106 meshBis->RecoverCell2CellAndBnd2Cell();
107 meshBis->BuildGhostPrimary();
108 meshBis->AdjGlobal2LocalPrimary();
109 mesh = meshBis;
110 }
111
112 return mesh;
113}
114
115static ssp<UnstructuredMesh> buildMesh(
116 const std::string &file, bool periodic,
117 DNDS::real Lx, DNDS::real Ly, int nBisect = 0)
118{
119 auto mesh = buildMeshUpToGhost(file, periodic, Lx, Ly, nBisect);
120 mesh->InterpolateFace();
121 mesh->AssertOnFaces();
122 return mesh;
123}
124
125// ===================================================================
126// VR builder
127// ===================================================================
128enum class RecMethod
129{
130 GaussGreen, // explicit 2nd-order Gauss-Green gradient
131 VFV_P1_HQM, // iterative VR, maxOrder=1, HQM weights
132 VFV_P2_HQM, // iterative VR, maxOrder=2, HQM weights
133 VFV_P3_HQM, // iterative VR, maxOrder=3, HQM weights
137};
138
139static const char *recMethodName(RecMethod m)
140{
141 switch (m)
142 {
144 return "GG";
146 return "P1-HQM";
148 return "P2-HQM";
150 return "P3-HQM";
152 return "P1-Def";
154 return "P2-Def";
156 return "P3-Def";
157 }
158 return "?";
159}
160
161static int recMethodOrder(RecMethod m)
162{
163 switch (m)
164 {
166 return 1;
169 return 1;
172 return 2;
175 return 3;
176 }
177 return 1;
178}
179
180static ssp<tVR> buildVR(ssp<UnstructuredMesh> mesh, RecMethod method)
181{
182 auto vr = std::make_shared<tVR>(g_mpi, mesh);
183
184 CFV::VRSettings defaultSettings(g_dim);
185 nlohmann::ordered_json j;
186 defaultSettings.WriteIntoJson(j);
187
188 int order = recMethodOrder(method);
189 j["maxOrder"] = order;
190 j["intOrder"] = std::max(order + 2, 5);
191 j["cacheDiffBase"] = true;
192 // Force Jacobi iteration for deterministic results across MPI partitions
193 j["SORInstead"] = false;
194 j["jacobiRelax"] = 1.0;
195
196 bool isHQM = (method == RecMethod::VFV_P1_HQM ||
197 method == RecMethod::VFV_P2_HQM ||
198 method == RecMethod::VFV_P3_HQM);
199
200 if (method == RecMethod::GaussGreen)
201 {
202 j["subs2ndOrder"] = 1; // Gauss-Green
203 // weights don't matter for explicit GG
204 }
205 else if (isHQM)
206 {
207 j["subs2ndOrder"] = 0; // full VFV
208 j["functionalSettings"]["dirWeightScheme"] = "HQM_OPT";
209 j["functionalSettings"]["geomWeightScheme"] = "HQM_SD";
210 j["functionalSettings"]["geomWeightPower"] = 0.5;
211 j["functionalSettings"]["geomWeightBias"] = 1.0;
212 }
213 else
214 {
215 j["subs2ndOrder"] = 0; // full VFV
216 j["functionalSettings"]["dirWeightScheme"] = "Factorial";
217 j["functionalSettings"]["geomWeightScheme"] = "GWNone";
218 }
219
220 vr->parseSettings(j);
221 if (mesh->isPeriodic)
222 vr->SetPeriodicTransformations(); // identity for scalar
223 vr->ConstructMetrics();
224 vr->ConstructBaseAndWeight();
225 vr->ConstructRecCoeff();
226 return vr;
227}
228
229// ===================================================================
230// Boundary callback: Dirichlet (returns exact value for wall tests)
231// ===================================================================
232using ScalarFunc = std::function<DNDS::real(const tPoint &)>;
233
234static tVR::TFBoundary<g_nv> makeDirichletBC(const ScalarFunc &f)
235{
236 return [f](const auto &, const auto &, DNDS::index, DNDS::index, int,
237 const tPoint &, const tPoint &pPhy, t_index)
238 {
239 return Eigen::Vector<DNDS::real, g_nv>{f(pPhy)};
240 };
241}
242
243static tVR::TFBoundary<g_nv> g_zeroBC =
244 [](const auto &, const auto &, DNDS::index, DNDS::index, int,
245 const tPoint &, const tPoint &, t_index)
246{ return Eigen::Vector<DNDS::real, g_nv>::Zero(); };
247
248// ===================================================================
249// Core: run reconstruction and measure L1 error at 6th-degree quadrature
250// points, normalized by domain volume.
251// Returns the error and optionally prints iteration progress.
252// ===================================================================
253static DNDS::real runTest(
254 ssp<tVR> vr,
255 RecMethod method,
256 const ScalarFunc &exactFunc,
257 const tVR::TFBoundary<g_nv> &bc,
258 int maxIters,
259 DNDS::real convTol, // convergence threshold on increment; 0 = no early stop
260 bool printProgress)
261{
262 auto mesh = vr->mesh;
263
264 // --- Allocate arrays ---
266 vr->BuildUDof(u, 1);
267
268 // --- Set cell-averaged DOFs via quadrature ---
269 for (DNDS::index iCell = 0; iCell < mesh->NumCell(); iCell++)
270 {
271 auto qCell = vr->GetCellQuad(iCell);
272 Eigen::Vector<DNDS::real, g_nv> uc;
273 uc.setZero();
274 qCell.IntegrationSimple(
275 uc,
276 [&](auto &vInc, int iG)
277 {
278 vInc(0) = exactFunc(vr->GetCellQuadraturePPhys(iCell, iG)) *
279 vr->GetCellJacobiDet(iCell, iG);
280 });
281 u[iCell] = uc / vr->GetCellVol(iCell);
282 }
283 u.trans.startPersistentPull();
284 u.trans.waitPersistentPull();
285
286 // --- Reconstruct ---
287 if (method == RecMethod::GaussGreen)
288 {
289 // Explicit Gauss-Green: produces gradient, not polynomial coefficients
291 vr->BuildUGrad(uGrad, 1);
292 vr->DoReconstruction2ndGrad<g_nv>(uGrad, u, bc, 1 /*GG method*/);
293 uGrad.trans.startPersistentPull();
294 uGrad.trans.waitPersistentPull();
295
296 // Measure L1 error: u_rec(x) = u_mean + grad^T * (x - x_bary)
297 static const auto Seq012 = Eigen::seq(Eigen::fix<0>, Eigen::fix<g_dim - 1>);
298 DNDS::real errLocal = 0.0;
299 for (DNDS::index iCell = 0; iCell < mesh->NumCell(); iCell++)
300 {
301 auto qCell = vr->GetCellQuad(iCell);
302 DNDS::real errCell = 0.0;
303 qCell.IntegrationSimple(
304 errCell,
305 [&](DNDS::real &vInc, int iG)
306 {
307 tPoint pPhy = vr->GetCellQuadraturePPhys(iCell, iG);
308 DNDS::real uRecVal = u[iCell](0) +
309 (uGrad[iCell].transpose() *
310 (pPhy - vr->GetCellBary(iCell))(Seq012))(0);
311 DNDS::real uExact = exactFunc(pPhy);
312 vInc = std::abs(uRecVal - uExact) * vr->GetCellJacobiDet(iCell, iG);
313 });
314 errLocal += errCell;
315 }
316 DNDS::real errGlobal = 0.0;
317 MPI::Allreduce(&errLocal, &errGlobal, 1, DNDS_MPI_REAL, MPI_SUM, g_mpi.comm);
318 return errGlobal / vr->GetGlobalVol();
319 }
320 else
321 {
322 // Iterative VFV reconstruction
323 CFV::tURec<g_nv> uRec, uRecNew;
324 vr->BuildURec(uRec, 1);
325 vr->BuildURec(uRecNew, 1);
326
327 DNDS::real lastInc = veryLargeReal;
328 for (int iter = 0; iter < maxIters; iter++)
329 {
330 vr->DoReconstructionIter<g_nv>(
331 uRec, uRecNew, u, bc, /*putIntoNew=*/true);
332
333 // Compute increment norm
334 DNDS::real incLocal = 0.0;
335 for (DNDS::index iCell = 0; iCell < mesh->NumCell(); iCell++)
336 incLocal += (uRecNew[iCell] - uRec[iCell]).array().square().sum();
337 DNDS::real incGlobal = 0.0;
338 MPI::Allreduce(&incLocal, &incGlobal, 1, DNDS_MPI_REAL, MPI_SUM, g_mpi.comm);
339 incGlobal = std::sqrt(incGlobal / mesh->NumCellGlobal());
340
341 std::swap(uRec, uRecNew);
342 uRec.trans.startPersistentPull();
343 uRec.trans.waitPersistentPull();
344
345 lastInc = incGlobal;
346 if (printProgress && g_mpi.rank == 0 && (iter < 5 || (iter + 1) % 20 == 0))
347 std::cout << " iter " << iter + 1 << " inc = "
348 << std::scientific << incGlobal << std::endl;
349
350 if (convTol > 0 && incGlobal < convTol)
351 {
352 if (printProgress && g_mpi.rank == 0)
353 std::cout << " converged at iter " << iter + 1 << std::endl;
354 break;
355 }
356 }
357
358 // Measure L1 error at cell quadrature points (using VR's intOrder, which >= 6)
359 DNDS::real errLocal = 0.0;
360 for (DNDS::index iCell = 0; iCell < mesh->NumCell(); iCell++)
361 {
362 auto qCell = vr->GetCellQuad(iCell);
363 DNDS::real errCell = 0.0;
364 qCell.IntegrationSimple(
365 errCell,
366 [&](DNDS::real &vInc, int iG)
367 {
368 Eigen::VectorXd baseVal =
369 vr->GetIntPointDiffBaseValue(
370 iCell, -1, -1, iG, std::array<int, 1>{0}, 1) *
371 uRec[iCell];
372 DNDS::real uRecVal = baseVal(0) + u[iCell](0);
373 DNDS::real uExact = exactFunc(vr->GetCellQuadraturePPhys(iCell, iG));
374 vInc = std::abs(uRecVal - uExact) * vr->GetCellJacobiDet(iCell, iG);
375 });
376 errLocal += errCell;
377 }
378 DNDS::real errGlobal = 0.0;
379 MPI::Allreduce(&errLocal, &errGlobal, 1, DNDS_MPI_REAL, MPI_SUM, g_mpi.comm);
380 return errGlobal / vr->GetGlobalVol();
381 }
382}
383
384// ===================================================================
385// Prebuilt test data
386// ===================================================================
387
388// --- Wall mesh (polynomial exactness) ---
389static ssp<UnstructuredMesh> g_wall_mesh;
390
391// --- Periodic meshes for convergence (IV10 quad, IV10U tri) ---
392static ssp<UnstructuredMesh> g_iv10[3]; // bisect 0,1,2
393static ssp<UnstructuredMesh> g_iv10u[3]; // bisect 0,1,2
394
395// --- VR objects keyed by (mesh_ptr, method) ---
396// We build them on demand in the test runner to avoid combinatorial explosion
397// at startup. But for the wall mesh we prebuild a few.
398
399int main(int argc, char **argv)
400{
401 MPI_Init(&argc, &argv);
402 g_mpi.setWorld();
403
404 if (g_mpi.rank == 0)
405 std::cout << "=== Building meshes ===" << std::endl;
406
407 g_wall_mesh = buildMesh("Uniform_3x3_wall.cgns", false, 0, 0, 0);
408
409 for (int ib = 0; ib < 3; ib++)
410 {
411 if (g_mpi.rank == 0)
412 std::cout << " IV10_10 bisect=" << ib << std::endl;
413 g_iv10[ib] = buildMesh("IV10_10.cgns", true, 10, 10, ib);
414 }
415 for (int ib = 0; ib < 3; ib++)
416 {
417 if (g_mpi.rank == 0)
418 std::cout << " IV10U_10 bisect=" << ib << std::endl;
419 g_iv10u[ib] = buildMesh("IV10U_10.cgns", true, 10, 10, ib);
420 }
421
422 if (g_mpi.rank == 0)
423 std::cout << "=== Meshes built ===" << std::endl;
424
425 doctest::Context ctx;
426 ctx.applyCommandLine(argc, argv);
427 int res = ctx.run();
428
429 g_wall_mesh.reset();
430 for (auto &m : g_iv10)
431 m.reset();
432 for (auto &m : g_iv10u)
433 m.reset();
434 MPI_Finalize();
435 return res;
436}
437
438// ===================================================================
439// Test functions (periodic, L=10)
440// ===================================================================
441static const DNDS::real g_L = 10.0;
442static const DNDS::real g_k = 2.0 * pi / g_L;
443
444static ScalarFunc sinCos = [](const tPoint &p)
445{ return std::sin(g_k * p[0]) * std::cos(g_k * p[1]); };
446
447static ScalarFunc cosPlusCos = [](const tPoint &p)
448{ return std::cos(g_k * p[0]) + std::cos(g_k * p[1]); };
449
450// ===================================================================
451// POLYNOMIAL EXACTNESS on wall mesh (Uniform_3x3_wall, [-1,2]^2)
452// ===================================================================
453
454#define POLY_TEST(testName, method, polyFunc, polyDeg) \
455 TEST_CASE("Wall/" testName "/" #method) \
456 { \
457 ScalarFunc f = polyFunc; \
458 auto vr = buildVR(g_wall_mesh, RecMethod::method); \
459 auto bc = makeDirichletBC(f); \
460 DNDS::real err = runTest(vr, RecMethod::method, f, bc, 100, 1e-15, false); \
461 if (g_mpi.rank == 0) \
462 std::cout << "[Wall/" testName "/" #method "] err = " \
463 << std::scientific << err << std::endl; \
464 if (polyDeg == 0) \
465 CHECK(err < 1e-12); \
466 else \
467 CHECK(err < 10.0); /* golden-value regression checked separately */ \
468 }
469
470// Constant (degree 0): exact for all methods
471POLY_TEST("const", GaussGreen, [](const tPoint &)
472 { return 1.0; }, 0)
473POLY_TEST("const", VFV_P1_HQM, [](const tPoint &)
474 { return 1.0; }, 0)
475POLY_TEST("const", VFV_P2_HQM, [](const tPoint &)
476 { return 1.0; }, 0)
477POLY_TEST("const", VFV_P3_HQM, [](const tPoint &)
478 { return 1.0; }, 0)
479POLY_TEST("const", VFV_P1_Default, [](const tPoint &)
480 { return 1.0; }, 0)
481
482// Linear (degree 1): exact for GG and p>=1
483POLY_TEST("linear", GaussGreen, [](const tPoint &p)
484 { return p[0] + 2 * p[1]; }, 1)
485POLY_TEST("linear", VFV_P1_HQM, [](const tPoint &p)
486 { return p[0] + 2 * p[1]; }, 1)
487POLY_TEST("linear", VFV_P2_HQM, [](const tPoint &p)
488 { return p[0] + 2 * p[1]; }, 1)
489POLY_TEST("linear", VFV_P3_HQM, [](const tPoint &p)
490 { return p[0] + 2 * p[1]; }, 1)
491POLY_TEST("linear", VFV_P1_Default, [](const tPoint &p)
492 { return p[0] + 2 * p[1]; }, 1)
493
494// Quadratic (degree 2): exact for p>=2
495POLY_TEST("quad", VFV_P2_HQM, [](const tPoint &p)
496 { return p[0] * p[0] + p[1] * p[1]; }, 2)
497POLY_TEST("quad", VFV_P3_HQM, [](const tPoint &p)
498 { return p[0] * p[0] + p[1] * p[1]; }, 2)
499POLY_TEST("quad", VFV_P2_Default, [](const tPoint &p)
500 { return p[0] * p[0] + p[1] * p[1]; }, 2)
501
502// Cubic (degree 3): exact for p>=3
503POLY_TEST("cubic", VFV_P3_HQM, [](const tPoint &p)
504 { return p[0] * p[0] * p[0] + p[0] * p[1] * p[1]; }, 3)
505POLY_TEST("cubic", VFV_P3_Default, [](const tPoint &p)
506 { return p[0] * p[0] * p[0] + p[0] * p[1] * p[1]; }, 3)
507
508#undef POLY_TEST
509
510// ===================================================================
511// PERIODIC SMOOTH FUNCTIONS on IV10 (quad) and IV10U (tri) meshes
512// with convergence series (bisect 0,1,2).
513// Golden values -- to be filled after first acquisition run.
514// ===================================================================
515
516struct PeriodicTestCase
517{
518 const char *meshName; // "IV10" or "IV10U"
519 ssp<UnstructuredMesh> *meshArray; // pointer to g_iv10 or g_iv10u
520 RecMethod method;
521 ScalarFunc func;
522 const char *funcName;
523 int maxIters;
524 DNDS::real convTol;
525 DNDS::real golden[3]; // golden L1/vol for bisect 0,1,2 (0 = not yet acquired)
526 bool checkConvergence; // whether to also check the iteration converges
527};
528
529// Golden values captured from commit c774b89 on dev/harry_refac1 (np=1).
530// Jacobi iteration ensures determinism across all np values.
531static PeriodicTestCase g_periodicTests[] = {
532 // IV10 (quad) + sin*cos
533 {"IV10", g_iv10, RecMethod::GaussGreen, sinCos, "sincos", 1, 0, {1.5599270188e-02, 3.4233789068e-03, 7.8943623278e-04}, false},
534 {"IV10", g_iv10, RecMethod::VFV_P1_HQM, sinCos, "sincos", 200, 1e-14, {4.6604402914e-02, 9.2961629825e-03, 1.5347312301e-03}, true},
535 {"IV10", g_iv10, RecMethod::VFV_P2_HQM, sinCos, "sincos", 200, 1e-14, {3.0528143687e-03, 2.3057099673e-04, 2.4367733525e-05}, true},
536 {"IV10", g_iv10, RecMethod::VFV_P3_HQM, sinCos, "sincos", 200, 1e-14, {1.9105219870e-03, 4.6701352192e-05, 1.4890868814e-06}, false},
537 {"IV10", g_iv10, RecMethod::VFV_P1_Default, sinCos, "sincos", 200, 1e-14, {4.6604402914e-02, 9.2961629825e-03, 1.5347312301e-03}, false},
538 {"IV10", g_iv10, RecMethod::VFV_P3_Default, sinCos, "sincos", 200, 1e-14, {1.8840503911e-03, 2.4995731251e-05, 8.3670061853e-07}, false},
539
540 // IV10U (tri) + sin*cos
541 {"IV10U", g_iv10u, RecMethod::GaussGreen, sinCos, "sincos", 1, 0, {1.3027440876e-02, 3.8074751503e-03, 1.0853979656e-03}, false},
542 {"IV10U", g_iv10u, RecMethod::VFV_P1_HQM, sinCos, "sincos", 200, 1e-14, {1.2040354507e-02, 2.2707678072e-03, 4.6833420900e-04}, false},
543 {"IV10U", g_iv10u, RecMethod::VFV_P2_HQM, sinCos, "sincos", 200, 1e-14, {5.5617445849e-04, 5.9964034731e-05, 6.4337896956e-06}, false},
544 {"IV10U", g_iv10u, RecMethod::VFV_P3_HQM, sinCos, "sincos", 200, 1e-14, {1.5878119293e-04, 9.8836538778e-06, 4.3407055649e-07}, false},
545
546 // IV10 (quad) + cos+cos
547 {"IV10", g_iv10, RecMethod::VFV_P3_HQM, cosPlusCos, "cos+cos", 200, 1e-14, {5.5612138851e-04, 1.6082305163e-05, 6.6188225747e-07}, false},
548};
549
550static const int g_nPeriodicTests = sizeof(g_periodicTests) / sizeof(g_periodicTests[0]);
551
552TEST_CASE("Periodic reconstruction convergence series")
553{
554 for (int ti = 0; ti < g_nPeriodicTests; ti++)
555 {
556 auto &tc = g_periodicTests[ti];
557 std::string label = std::string(tc.meshName) + "/" + tc.funcName +
558 "/" + recMethodName(tc.method);
559
560 SUBCASE(label.c_str())
561 {
562 for (int ib = 0; ib < 3; ib++)
563 {
564 CAPTURE(ib);
565 auto mesh = tc.meshArray[ib];
566 auto vr = buildVR(mesh, tc.method);
567 bool print = (ib == 0 && tc.checkConvergence);
568 DNDS::real err = runTest(vr, tc.method, tc.func,
569 g_zeroBC, tc.maxIters, tc.convTol, print);
570
571 if (g_mpi.rank == 0)
572 std::cout << "[" << label << " bis=" << ib
573 << "] err = " << std::scientific
574 << std::setprecision(10) << err << std::endl;
575
576 if (tc.golden[ib] != 0.0)
577 CHECK(err == doctest::Approx(tc.golden[ib]).epsilon(1e-6));
578 else
579 CHECK(err >= 0.0); // acquisition: just check non-negative
580 }
581 }
582 }
583}
584
585// ===================================================================
586// Convergence check: selected VFV cases should converge
587// ===================================================================
588
589TEST_CASE("VFV P2 HQM converges on IV10 base mesh")
590{
591 auto vr = buildVR(g_iv10[0], RecMethod::VFV_P2_HQM);
592
594 vr->BuildUDof(u, 1);
595 CFV::tURec<g_nv> uRec, uRecNew;
596 vr->BuildURec(uRec, 1);
597 vr->BuildURec(uRecNew, 1);
598
599 for (DNDS::index iCell = 0; iCell < vr->mesh->NumCell(); iCell++)
600 {
601 auto qCell = vr->GetCellQuad(iCell);
602 Eigen::Vector<DNDS::real, g_nv> uc;
603 uc.setZero();
604 qCell.IntegrationSimple(
605 uc,
606 [&](auto &vInc, int iG)
607 {
608 vInc(0) = sinCos(vr->GetCellQuadraturePPhys(iCell, iG)) *
609 vr->GetCellJacobiDet(iCell, iG);
610 });
611 u[iCell] = uc / vr->GetCellVol(iCell);
612 }
613 u.trans.startPersistentPull();
614 u.trans.waitPersistentPull();
615
616 DNDS::real lastInc = veryLargeReal;
617 int convergedAt = 0;
618 for (int iter = 0; iter < 200; iter++)
619 {
620 vr->DoReconstructionIter<g_nv>(uRec, uRecNew, u, g_zeroBC, true);
621
622 DNDS::real incLocal = 0.0;
623 for (DNDS::index iCell = 0; iCell < vr->mesh->NumCell(); iCell++)
624 incLocal += (uRecNew[iCell] - uRec[iCell]).array().square().sum();
625 DNDS::real incGlobal = 0.0;
626 MPI::Allreduce(&incLocal, &incGlobal, 1, DNDS_MPI_REAL, MPI_SUM, g_mpi.comm);
627 incGlobal = std::sqrt(incGlobal / vr->mesh->NumCellGlobal());
628
629 std::swap(uRec, uRecNew);
630 uRec.trans.startPersistentPull();
631 uRec.trans.waitPersistentPull();
632
633 lastInc = incGlobal;
634 if (incGlobal < 1e-14)
635 {
636 convergedAt = iter + 1;
637 break;
638 }
639 }
640
641 if (g_mpi.rank == 0)
642 std::cout << "[Convergence P2-HQM IV10] " << convergedAt
643 << " iters, final inc = " << std::scientific << lastInc << std::endl;
644
645 CHECK(convergedAt > 0);
646 CHECK(convergedAt < 200);
647}
648
649// ===================================================================
650// DEBUG: Compare InterpolateFace (DSL) vs InterpolateFaceLegacy
651// ===================================================================
652
653TEST_CASE("DEBUG compare InterpolateFace vs Legacy face2cell")
654{
655 // Build a periodic mesh up to ghost primary, then run both methods
656 auto meshA = buildMeshUpToGhost("IV10_10.cgns", true, 10, 10, 0);
657 auto meshB = buildMeshUpToGhost("IV10_10.cgns", true, 10, 10, 0);
658
659 meshA->InterpolateFace();
660 meshA->AssertOnFaces();
661 meshB->InterpolateFaceLegacy();
662 meshB->AssertOnFaces();
663
664 DNDS::index nFaceA = meshA->NumFace();
665 DNDS::index nFaceB = meshB->NumFace();
666 DNDS::index nFaceProcA = meshA->NumFaceProc();
667 DNDS::index nFaceProcB = meshB->NumFaceProc();
668
669 if (g_mpi.rank == 0)
670 std::cout << "[DBG] nFaceOwned: DSL=" << nFaceA << " Legacy=" << nFaceB
671 << " nFaceProc: DSL=" << nFaceProcA << " Legacy=" << nFaceProcB << std::endl;
672
673 // For each local cell, compare the face data seen through cell2face
674 DNDS::index nLocalCells = meshA->cell2cell.father->Size();
675 DNDS::index nDiffF2C = 0;
676 DNDS::index nDiffF2N = 0;
677 DNDS::index nDiffZone = 0;
678 DNDS::index nSwappedF2C = 0;
679
680 for (DNDS::index iCell = 0; iCell < nLocalCells; iCell++)
681 {
682 DNDS::rowsize nFacesA = meshA->cell2face.RowSize(iCell);
683 DNDS::rowsize nFacesB = meshB->cell2face.RowSize(iCell);
684 if (nFacesA != nFacesB)
685 {
686 if (g_mpi.rank == 0)
687 std::cout << "[DBG] Cell " << iCell << " nFaces differ: " << nFacesA << " vs " << nFacesB << std::endl;
688 continue;
689 }
690
691 for (DNDS::rowsize ic2f = 0; ic2f < nFacesA; ic2f++)
692 {
693 DNDS::index iFaceA = meshA->cell2face(iCell, ic2f);
694 DNDS::index iFaceB = meshB->cell2face(iCell, ic2f);
695
696 // Get face2cell for both
697 DNDS::index f2cA0 = meshA->face2cell(iFaceA, 0);
698 DNDS::index f2cA1 = meshA->face2cell(iFaceA, 1);
699 DNDS::index f2cB0 = meshB->face2cell(iFaceB, 0);
700 DNDS::index f2cB1 = meshB->face2cell(iFaceB, 1);
701
702 // Convert to global cell indices for comparison
703 DNDS::index gA0 = (f2cA0 != DNDS::UnInitIndex) ? meshA->cell2node.trans.pLGhostMapping->operator()(-1, f2cA0) : DNDS::UnInitIndex;
704 DNDS::index gA1 = (f2cA1 != DNDS::UnInitIndex) ? meshA->cell2node.trans.pLGhostMapping->operator()(-1, f2cA1) : DNDS::UnInitIndex;
705 DNDS::index gB0 = (f2cB0 != DNDS::UnInitIndex) ? meshB->cell2node.trans.pLGhostMapping->operator()(-1, f2cB0) : DNDS::UnInitIndex;
706 DNDS::index gB1 = (f2cB1 != DNDS::UnInitIndex) ? meshB->cell2node.trans.pLGhostMapping->operator()(-1, f2cB1) : DNDS::UnInitIndex;
707
708 bool same = (gA0 == gB0 && gA1 == gB1);
709 bool swapped = (!same && gA0 == gB1 && gA1 == gB0);
710
711 if (!same)
712 {
713 nDiffF2C++;
714 if (swapped)
715 nSwappedF2C++;
716 if (nDiffF2C <= 5)
717 std::cout << "[DBG rank=" << g_mpi.rank << "] Cell " << iCell
718 << " ic2f=" << ic2f
719 << " f2c DSL=(" << gA0 << "," << gA1 << ")"
720 << " Legacy=(" << gB0 << "," << gB1 << ")"
721 << (swapped ? " SWAPPED" : " DIFF")
722 << std::endl;
723 }
724
725 // Compare face2node ordered
726 auto f2nA = meshA->face2node[iFaceA];
727 auto f2nB = meshB->face2node[iFaceB];
728 if (f2nA.size() == f2nB.size())
729 {
730 std::vector<DNDS::index> gnAOrd(f2nA.size()), gnBOrd(f2nB.size());
731 for (int k = 0; k < (int)f2nA.size(); k++)
732 gnAOrd[k] = meshA->coords.trans.pLGhostMapping->operator()(-1, f2nA[k]);
733 for (int k = 0; k < (int)f2nB.size(); k++)
734 gnBOrd[k] = meshB->coords.trans.pLGhostMapping->operator()(-1, f2nB[k]);
735 if (gnAOrd != gnBOrd)
736 {
737 nDiffF2N++;
738 if (nDiffF2N <= 5)
739 {
740 std::cout << "[DBG rank=" << g_mpi.rank << "] Cell " << iCell
741 << " ic2f=" << ic2f
742 << " f2n DSL=(";
743 for (auto v : gnAOrd)
744 std::cout << v << " ";
745 std::cout << ") Legacy=(";
746 for (auto v : gnBOrd)
747 std::cout << v << " ";
748 std::cout << ")" << std::endl;
749 }
750 }
751 }
752
753 // Compare zone
754 auto zoneA = meshA->faceElemInfo(iFaceA, 0).zone;
755 auto zoneB = meshB->faceElemInfo(iFaceB, 0).zone;
756 if (zoneA != zoneB)
757 {
758 nDiffZone++;
759 if (nDiffZone <= 5)
760 {
761 std::vector<DNDS::index> gnAF, gnBF;
762 for (int k = 0; k < (int)f2nA.size(); k++)
763 gnAF.push_back(meshA->coords.trans.pLGhostMapping->operator()(-1, f2nA[k]));
764 for (int k = 0; k < (int)f2nB.size(); k++)
765 gnBF.push_back(meshB->coords.trans.pLGhostMapping->operator()(-1, f2nB[k]));
766 std::cout << "[DBG rank=" << g_mpi.rank << "] Cell " << iCell
767 << " ic2f=" << ic2f
768 << " zone DSL=" << zoneA << " Legacy=" << zoneB
769 << " f2c DSL=(" << gA0 << "," << gA1 << ")"
770 << " f2n DSL=(";
771 for (auto v : gnAF)
772 std::cout << v << " ";
773 std::cout << ") Legacy=(";
774 for (auto v : gnBF)
775 std::cout << v << " ";
776 std::cout << ")" << std::endl;
777 }
778 }
779 }
780 }
781
782 DNDS::index totalDiffF2C = 0, totalSwapped = 0, totalDiffF2N = 0, totalDiffZone = 0;
783 MPI::Allreduce(&nDiffF2C, &totalDiffF2C, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
784 MPI::Allreduce(&nSwappedF2C, &totalSwapped, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
785 MPI::Allreduce(&nDiffF2N, &totalDiffF2N, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
786 MPI::Allreduce(&nDiffZone, &totalDiffZone, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
787
788 if (g_mpi.rank == 0)
789 {
790 std::cout << "[DBG] Total face2cell differences: " << totalDiffF2C
791 << " (swapped: " << totalSwapped << ")" << std::endl;
792 std::cout << "[DBG] Total face2node order differences: " << totalDiffF2N << std::endl;
793 std::cout << "[DBG] Total zone differences: " << totalDiffZone << std::endl;
794 }
795
796 CHECK(totalDiffF2C == 0);
797 CHECK(totalDiffF2N == 0);
798 CHECK(totalDiffZone == 0);
799}
800
801// ===================================================================
802// LIMITER PROCEDURE TESTS
803//
804// After reconstruction, apply DoCalculateSmoothIndicator + DoLimiterWBAP_C
805// and measure the post-limiter L1 error. For scalar fields the
806// eigenvalue transform (FM/FMI) is identity.
807//
808// Golden value sentinel: 0.0 means "not yet acquired -- just print and
809// check non-negative".
810// ===================================================================
811
813
814/// Identity eigenvalue transform for nVarsFixed==1 (scalar).
815static tVR::tLimitBatch<g_nv> identityFM(
816 const Eigen::Vector<DNDS::real, g_nv> &,
817 const Eigen::Vector<DNDS::real, g_nv> &,
818 const tPoint &,
819 const Eigen::Ref<tVR::tLimitBatch<g_nv>> &data)
820{
821 return data;
822}
823
824/// Run reconstruction, then limiter, then measure L1 error.
825/// @param limiterKind 0 = WBAP_C, 1 = WBAP_3
826static DNDS::real runLimitedTest(
827 ssp<tVR> vr,
828 RecMethod method,
829 const ScalarFunc &exactFunc,
830 const tVR::TFBoundary<g_nv> &bc,
831 int maxIters,
832 DNDS::real convTol,
833 int limiterKind,
834 bool ifAll,
835 bool printProgress)
836{
837 auto mesh = vr->mesh;
838
839 // --- Allocate arrays ---
841 vr->BuildUDof(u, 1);
842
843 // --- Set cell-averaged DOFs via quadrature ---
844 for (DNDS::index iCell = 0; iCell < mesh->NumCell(); iCell++)
845 {
846 auto qCell = vr->GetCellQuad(iCell);
847 Eigen::Vector<DNDS::real, g_nv> uc;
848 uc.setZero();
849 qCell.IntegrationSimple(
850 uc,
851 [&](auto &vInc, int iG)
852 {
853 vInc(0) = exactFunc(vr->GetCellQuadraturePPhys(iCell, iG)) *
854 vr->GetCellJacobiDet(iCell, iG);
855 });
856 u[iCell] = uc / vr->GetCellVol(iCell);
857 }
858 u.trans.startPersistentPull();
859 u.trans.waitPersistentPull();
860
861 // --- Reconstruct (iterative VFV) ---
862 CFV::tURec<g_nv> uRec, uRecNew, uRecBuf;
863 vr->BuildURec(uRec, 1);
864 vr->BuildURec(uRecNew, 1);
865 vr->BuildURec(uRecBuf, 1);
866
867 for (int iter = 0; iter < maxIters; iter++)
868 {
869 vr->DoReconstructionIter<g_nv>(uRec, uRecNew, u, bc, true);
870 std::swap(uRec, uRecNew);
871 uRec.trans.startPersistentPull();
872 uRec.trans.waitPersistentPull();
873
874 DNDS::real incLocal = 0.0;
875 for (DNDS::index iCell = 0; iCell < mesh->NumCell(); iCell++)
876 incLocal += (uRecNew[iCell] - uRec[iCell]).array().square().sum();
877 DNDS::real incGlobal = 0.0;
878 MPI::Allreduce(&incLocal, &incGlobal, 1, DNDS_MPI_REAL, MPI_SUM, g_mpi.comm);
879 incGlobal = std::sqrt(incGlobal / mesh->NumCellGlobal());
880 if (convTol > 0 && incGlobal < convTol)
881 break;
882 }
883
884 // --- Smooth indicator ---
886 vr->BuildScalar(si);
887 vr->DoCalculateSmoothIndicator<g_nv, 1>(si, uRec, u, std::array<int, 1>{0});
888 si.trans.startPersistentPull();
889 si.trans.waitPersistentPull();
890
891 // --- Limiter ---
892 tVR::tFMEig<g_nv> fm = identityFM;
893 tVR::tFMEig<g_nv> fmi = identityFM;
894
895 if (limiterKind == 0)
896 vr->DoLimiterWBAP_C<g_nv>(u, uRec, uRecNew, uRecBuf, si, ifAll, fm, fmi, /*putIntoNew=*/true);
897 else
898 vr->DoLimiterWBAP_3<g_nv>(u, uRec, uRecNew, uRecBuf, si, ifAll, fm, fmi, /*putIntoNew=*/true);
899
900 // uRecNew now holds the limited reconstruction
901 auto &uRecLim = uRecNew;
902
903 // --- Measure L1 error ---
904 DNDS::real errLocal = 0.0;
905 for (DNDS::index iCell = 0; iCell < mesh->NumCell(); iCell++)
906 {
907 auto qCell = vr->GetCellQuad(iCell);
908 DNDS::real errCell = 0.0;
909 qCell.IntegrationSimple(
910 errCell,
911 [&](DNDS::real &vInc, int iG)
912 {
913 Eigen::VectorXd baseVal =
914 vr->GetIntPointDiffBaseValue(
915 iCell, -1, -1, iG, std::array<int, 1>{0}, 1) *
916 uRecLim[iCell];
917 DNDS::real uRecVal = baseVal(0) + u[iCell](0);
918 DNDS::real uExact = exactFunc(vr->GetCellQuadraturePPhys(iCell, iG));
919 vInc = std::abs(uRecVal - uExact) * vr->GetCellJacobiDet(iCell, iG);
920 });
921 errLocal += errCell;
922 }
923 DNDS::real errGlobal = 0.0;
924 MPI::Allreduce(&errLocal, &errGlobal, 1, DNDS_MPI_REAL, MPI_SUM, g_mpi.comm);
925 return errGlobal / vr->GetGlobalVol();
926}
927
929{
930 const char *meshName;
934 const char *funcName;
935 int limiterKind; // 0 = WBAP_C, 1 = WBAP_3
936 const char *limName;
937 bool ifAll; // limiter applied to all cells (no smooth-indicator skip)
938 DNDS::real golden[3]; // golden L1/vol for bisect 0,1,2
939 // 0.0 = not yet acquired (just print, CHECK >= 0)
940};
941
942static LimiterTestCase g_limiterTests[] = {
943 // IV10 (quad), P2-HQM, sincos, WBAP_C (ifAll=true so every cell is limited)
944 {"IV10", g_iv10, RecMethod::VFV_P2_HQM, sinCos, "sincos", 0, "CWBAP", true, {6.6975633577e-02, 2.2647765241e-02, 9.2028443979e-03}},
945
946 // IV10 (quad), P3-HQM, sincos, WBAP_C (ifAll=true)
947 {"IV10", g_iv10, RecMethod::VFV_P3_HQM, sinCos, "sincos", 0, "CWBAP", true, {7.1333971937e-02, 2.6226392939e-02, 1.1194383457e-02}},
948
949 // IV10U (tri), P2-HQM, sincos, WBAP_C (ifAll=true)
950 {"IV10U", g_iv10u, RecMethod::VFV_P2_HQM, sinCos, "sincos", 0, "CWBAP", true, {3.5176301510e-02, 1.4925026476e-02, 6.9002847378e-03}},
951
952 // IV10 (quad), P2-HQM, sincos, WBAP_3 (ifAll=true)
953 {"IV10", g_iv10, RecMethod::VFV_P2_HQM, sinCos, "sincos", 1, "3WBAP", true, {6.6488044323e-02, 2.2593150005e-02, 9.1963848358e-03}},
954
955 // IV10 (quad), P3-HQM, sincos, WBAP_3 (ifAll=true)
956 {"IV10", g_iv10, RecMethod::VFV_P3_HQM, sinCos, "sincos", 1, "3WBAP", true, {7.1372867657e-02, 2.6697894435e-02, 1.1593632890e-02}},
957};
958
959static const int g_nLimiterTests = sizeof(g_limiterTests) / sizeof(g_limiterTests[0]);
960
961TEST_CASE("Limiter procedure: reconstruction + smooth indicator + WBAP limiter")
962{
963 for (int ti = 0; ti < g_nLimiterTests; ti++)
964 {
965 auto &tc = g_limiterTests[ti];
966 std::string label = std::string(tc.meshName) + "/" + tc.funcName +
967 "/" + recMethodName(tc.method) + "/" + tc.limName;
968
969 SUBCASE(label.c_str())
970 {
971 for (int ib = 0; ib < 3; ib++)
972 {
973 CAPTURE(ib);
974 auto mesh = tc.meshArray[ib];
975 auto vr = buildVR(mesh, tc.method);
976 DNDS::real err = runLimitedTest(
977 vr, tc.method, tc.func, g_zeroBC,
978 200, 1e-14, tc.limiterKind, tc.ifAll, false);
979
980 if (g_mpi.rank == 0)
981 std::cout << "[" << label << " bis=" << ib
982 << "] limited err = " << std::scientific
983 << std::setprecision(10) << err << std::endl;
984
985 if (tc.golden[ib] != 0.0)
986 CHECK(err == doctest::Approx(tc.golden[ib]).epsilon(1e-6));
987 else
988 CHECK(err >= 0.0);
989 }
990 }
991 }
992}
Eigen::Matrix< real, 3, 3 > m
Primary solver state container: an ArrayEigenMatrix pair with MPI-collective vector-space operations.
Definition ArrayDOF.hpp:176
The VR class that provides any information needed in high-order CFV.
int main()
Definition dummy.cpp:3
int32_t t_index
Definition Geometric.hpp:6
Eigen::Vector3d tPoint
Definition Geometric.hpp:9
the host side operators are provided as implemented
const MPI_Datatype DNDS_MPI_INDEX
MPI datatype matching index (= MPI_INT64_T).
Definition MPI.hpp:106
DNDS_CONSTANT const index UnInitIndex
Sentinel "not initialised" index value (= INT64_MIN).
Definition Defines.hpp:181
int32_t rowsize
Row-width / per-row element-count type (signed 32-bit).
Definition Defines.hpp:114
DNDS_CONSTANT const real pi
π in double precision (matches DNDS_E_PI macro).
Definition Defines.hpp:204
int64_t index
Global row / DOF index type (signed 64-bit; handles multi-billion-cell meshes).
Definition Defines.hpp:112
std::shared_ptr< T > ssp
Shortened alias for std::shared_ptr used pervasively in DNDSR.
Definition Defines.hpp:143
double real
Canonical floating-point scalar used throughout DNDSR (double precision).
Definition Defines.hpp:110
DNDS_CONSTANT const real veryLargeReal
Catch-all upper bound ("practically infinity") for physical scalars.
Definition Defines.hpp:195
const MPI_Datatype DNDS_MPI_REAL
MPI datatype matching real (= MPI_REAL8).
Definition MPI.hpp:108
TTrans trans
Ghost-communication engine bound to father and son.
A means to translate nlohmann json into c++ primitive data types and back; and stores then during com...
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:231
int rank
This rank's 0-based index within comm (-1 until initialised).
Definition MPI.hpp:235
MPI_Comm comm
The underlying MPI communicator handle.
Definition MPI.hpp:233
void setWorld()
Initialise the object to MPI_COMM_WORLD. Requires MPI_Init to have run.
Definition MPI.hpp:258
ssp< UnstructuredMesh > * meshArray
Eigen::Matrix< real, 5, 1 > v
real err
CHECK(result.size()==3)
DistributedHex3D mesh
double order
Definition test_ODE.cpp:257
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")
std::function< DNDS::real(const tPoint &)> ScalarFunc
#define POLY_TEST(testName, method, polyFunc, polyDeg)
CFV::VariationalReconstruction< g_dim > tVR
const tPoint VFV_P3_HQM
const tPoint const tPoint const tPoint & p
const tPoint const tPoint GaussGreen