DNDSR 0.1.0.dev1+gcd065ad
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_EulerEvaluator.cpp
Go to the documentation of this file.
1/**
2 * @file test_EulerEvaluator.cpp
3 * @brief Integration test for the EulerEvaluator pipeline: fdt -> frhs -> fsolve.
4 *
5 * Exercises the full evaluator pipeline on three configurations:
6 * 1. Isentropic Vortex (NS, 2D periodic, inviscid)
7 * 2. NACA0012 (NS_SA, 2D external, viscous + SA turbulence)
8 * 3. 3D Box (NS_3D, 3D periodic, inviscid)
9 *
10 * For each case the test:
11 * - Reads the JSON config and mesh
12 * - Initializes the evaluator and DOF arrays
13 * - Runs one Newton-like iteration: EvaluateDt -> EvaluateRHS -> LUSGSMatrixInit
14 * -> LUSGSForward -> LUSGSBackward
15 * - Measures the L1 RHS norm (golden regression value)
16 *
17 * This is an MPI test registered at np=1, 2, 4.
18 * Golden value sentinel: 1e300 means not yet acquired.
19 */
20
21#define DOCTEST_CONFIG_IMPLEMENT
22#include "doctest.h"
23
24#include "Euler/EulerSolver.hpp"
25
26#include <fstream>
27#include <iostream>
28#include <iomanip>
29#include <filesystem>
30
31using namespace DNDS;
32using namespace DNDS::Euler;
33
34static MPIInfo g_mpi;
35static constexpr real GOLDEN_NOT_ACQUIRED = 1e300;
36
37// ===================================================================
38// Helper: resolve path relative to project root
39// ===================================================================
40static std::string projectRoot()
41{
42 std::string f(__FILE__);
43 for (int i = 0; i < 4; i++) // test/cpp/Euler/file -> root
44 {
45 auto pos = f.rfind('/');
46 if (pos == std::string::npos)
47 pos = f.rfind('\\');
48 if (pos != std::string::npos)
49 f = f.substr(0, pos);
50 }
51 return f;
52}
53
54// ===================================================================
55// Helper: write a temp JSON config file with mesh path fixup
56// ===================================================================
57/// Write a temp config by: (1) writing defaults via ConfigureFromJson(read=false),
58/// (2) reading the case JSON and merge-patching, (3) applying test overrides.
59/// This ensures all required config fields exist (filled with defaults).
60template <EulerModel model>
61static std::string writeTempConfig(const std::string &caseJsonPath, const std::string &tag,
62 const nlohmann::ordered_json &overrides = {})
63{
64 auto tmpDir = std::filesystem::temp_directory_path() / "dndsr_euler_test";
65 std::filesystem::create_directories(tmpDir);
66 std::string defaultPath = (tmpDir / (tag + "_default.json")).string();
67 std::string cfgPath = (tmpDir / (tag + ".json")).string();
68
69 // Step 1: Write defaults
70 {
71 EulerSolver<model> solverTmp(g_mpi);
72 solverTmp.ConfigureFromJson(defaultPath, false); // writes all defaults
73 }
74 MPI::Barrier(g_mpi.comm);
75
76 // Step 2: Read defaults, merge case JSON, merge overrides
77 auto fDefault = std::ifstream(defaultPath);
78 DNDS_assert(fDefault);
79 auto j = nlohmann::ordered_json::parse(fDefault, nullptr, true, true);
80 fDefault.close();
81
82 auto fCase = std::ifstream(caseJsonPath);
83 DNDS_assert_info(fCase, ("config file not found: " + caseJsonPath).c_str());
84 auto jCase = nlohmann::ordered_json::parse(fCase, nullptr, true, true);
85 fCase.close();
86
87 j.merge_patch(jCase);
88 j.merge_patch(overrides);
89
90 // Fix mesh path to be absolute
91 std::string meshRel = j["dataIOControl"]["meshFile"];
92 std::filesystem::path meshAbs = std::filesystem::weakly_canonical(
93 std::filesystem::path(caseJsonPath).parent_path() / meshRel);
94 j["dataIOControl"]["meshFile"] = meshAbs.string();
95
96 // Disable all output
97 j["outputControl"]["nDataOutC"] = 1000000;
98 j["outputControl"]["nDataOut"] = 1000000;
99 j["outputControl"]["nDataOutCInternal"] = 1000000;
100 j["outputControl"]["nDataOutInternal"] = 1000000;
101 j["outputControl"]["dataOutAtInit"] = false;
102 j["timeMarchControl"]["nTimeStep"] = 1;
103
104 // Write final config
105 if (g_mpi.rank == 0)
106 {
107 auto fOut = std::ofstream(cfgPath);
108 DNDS_assert(fOut);
109 fOut << std::setw(4) << j;
110 fOut.close();
111 }
112 MPI::Barrier(g_mpi.comm);
113
114 return cfgPath;
115}
116
117// ===================================================================
118// Template: one Newton-like step for a given EulerModel
119// Returns: {rhsL1Norm_density, rhsL1Norm_energy, incrementL2Norm}
120// ===================================================================
121template <EulerModel model>
122std::array<real, 3> runOneNewtonStep(const std::string &cfgPath)
123{
124 using TSolver = EulerSolver<model>;
125 using TEval = EulerEvaluator<model>;
126 constexpr int nVarsFixed = TEval::nVarsFixed;
127 constexpr int gDim = TEval::gDim;
128 int nVars = getNVars(model);
129
130 // --- Setup ---
131 TSolver solver(g_mpi);
132 solver.ConfigureFromJson(cfgPath, true);
133 solver.ReadMeshAndInitialize();
134
135 auto &eval = solver.getEval();
136 auto &u = solver.getU();
137 auto &uRec = solver.getURec();
138 auto &uRecNew = solver.getURecNew();
139 auto vfv = solver.getVFV();
140 auto mesh = solver.getMesh();
141 auto &config = solver.getConfiguration();
142 auto &JSource = solver.getJSource();
143 auto &betaPP = solver.getBetaPP();
144
145 // Build working arrays
146 ArrayDOFV<nVarsFixed> rhs, xinc, xincNew;
147 ArrayDOFV<1> dTau, alphaPP_tmp;
149
150 vfv->BuildUDof(rhs, nVars);
151 vfv->BuildUDof(xinc, nVars);
152 vfv->BuildUDof(xincNew, nVars);
153 vfv->BuildUDof(dTau, 1);
154 vfv->BuildUDof(alphaPP_tmp, 1);
155 alphaPP_tmp.setConstant(1.0);
156
158 config.eulerSettings.useScalarJacobian ? 0 : 1,
159 nVars, u);
160
161 // --- Initialize DOF (not done by ReadMeshAndInitialize) ---
162 eval.InitializeUDOF(u);
163 u.trans.startPersistentPull();
164 u.trans.waitPersistentPull();
165
166 // --- STEP 1: fdt (compute local pseudo-timestep) ---
167 real CFL = config.implicitCFLControl.CFL;
168 real curDtMin = 1e100;
169
170 eval.FixUMaxFilter(u);
171 u.trans.startPersistentPull();
172 u.trans.waitPersistentPull();
173
174 eval.EvaluateDt(dTau, u, uRec, CFL, curDtMin, 1e100,
175 config.implicitCFLControl.useLocalDt, 0.0);
176
177 // --- STEP 2: skip reconstruction (first-order, uRec stays zero) ---
178 // For a proper high-order test we'd need the boundary callback from
179 // frhsOuter, which requires significant setup. A first-order RHS
180 // evaluation is a valid regression test of the evaluator pipeline.
181
182 // --- STEP 3: frhs (evaluate spatial RHS, first-order) ---
183 eval.EvaluateRHS(rhs, JSource, u, uRec, uRec,
184 betaPP, alphaPP_tmp, false, 0.0);
185 rhs.trans.startPersistentPull();
186 rhs.trans.waitPersistentPull();
187
188 // --- Measure RHS L1 norm (volume-weighted) ---
189 Eigen::VectorXd resNorm(nVars);
190 eval.EvaluateNorm(resNorm, rhs, 1, true);
191
192 if (g_mpi.rank == 0)
193 {
194 std::cout << " RHS L1 norms:";
195 for (int i = 0; i < nVars; i++)
196 std::cout << " " << std::scientific << std::setprecision(10) << resNorm(i);
197 std::cout << std::endl;
198 }
199
200 // --- STEP 4: fsolve (one Jacobi iteration -- MPI-deterministic) ---
201 // Jacobi pattern: Forward(uInc, uIncNew); Backward(uInc, uIncNew); pull uIncNew; swap.
202 // Since uInc starts at zero, this is a block-diagonal solve: xincNew = D^{-1} * rhs.
203 real dt = 1e100; // pseudo-steady
204 real alphaDiag = 1.0;
205
206 eval.LUSGSMatrixInit(JD, JSource, dTau, dt, alphaDiag, u, uRec, 0, 0.0);
207
208 xinc.setConstant(0.0);
209 xincNew.setConstant(0.0);
210 eval.UpdateLUSGSForward(alphaDiag, 0.0, rhs, u, xinc, JD, xincNew);
211 eval.UpdateLUSGSBackward(alphaDiag, 0.0, rhs, u, xinc, JD, xincNew);
212 xincNew.trans.startPersistentPull();
213 xincNew.trans.waitPersistentPull();
214 xinc = xincNew;
215
216 // --- Measure increment L2 norm ---
217 Eigen::VectorXd incNorm(nVars);
218 eval.EvaluateNorm(incNorm, xinc, 2, false);
219 real incL2 = incNorm.norm();
220
221 if (g_mpi.rank == 0)
222 std::cout << " Inc L2 norm: " << std::scientific << std::setprecision(10) << incL2 << std::endl;
223
224 return {resNorm(0), resNorm(nVars > 4 ? 4 : nVars - 1), incL2};
225}
226
227// ===================================================================
228// TEST 1: Isentropic Vortex (NS, 2D, inviscid)
229// ===================================================================
230
231TEST_CASE("EulerEvaluator pipeline: IV (NS, P1, 2D)")
232{
233 std::string root = projectRoot();
234 std::string cfgPath = writeTempConfig<NS>(
235 root + "/cases/euler/euler_config_IV.json", "iv_test",
236 {{"vfvSettings", {{"maxOrder", 1}, {"intOrder", 3}}},
237 {"dataIOControl", {{"meshDirectBisect", 0},
238 {"meshFile", root + "/data/mesh/IV10_10.cgns"}}}});
239
240 if (g_mpi.rank == 0)
241 std::cout << "=== IV (NS, P1) ===" << std::endl;
242
243 auto [rhsDensity, rhsEnergy, incL2] = runOneNewtonStep<NS>(cfgPath);
244
245 CHECK(std::isfinite(rhsDensity));
246 CHECK(std::isfinite(rhsEnergy));
247 CHECK(std::isfinite(incL2));
248 CHECK(rhsDensity > 0);
249
250 // Golden values (Jacobi iteration is MPI-deterministic)
251 real goldenRhsDensity = 1.0524823780e+01;
252 real goldenIncL2 = 3.1557730986e+00;
253
254 if (g_mpi.rank == 0)
255 std::cout << " golden candidates: rhsDensity=" << std::scientific
256 << std::setprecision(10) << rhsDensity
257 << " incL2=" << incL2 << std::endl;
258
259 CHECK(rhsDensity == doctest::Approx(goldenRhsDensity).epsilon(1e-6));
260 CHECK(incL2 == doctest::Approx(goldenIncL2).epsilon(1e-6));
261}
262
263// ===================================================================
264// TEST 2: NACA0012 (NS_SA, 2D, viscous + SA)
265// ===================================================================
266
267TEST_CASE("EulerEvaluator pipeline: NACA0012 (NS_SA, P1)")
268{
269 std::string root = projectRoot();
270 std::string cfgPath = writeTempConfig<NS_SA>(
271 root + "/cases/eulerSA/eulerSA_config.json", "naca_test",
272 {{"vfvSettings", {{"maxOrder", 1}, {"intOrder", 3}}},
273 {"restartState", {{"iStep", 0}, {"iStepInternal", 0}}},
274 {"dataIOControl", {{"meshFile", root + "/data/mesh/NACA0012_H2.cgns"}}}});
275
276 if (g_mpi.rank == 0)
277 std::cout << "=== NACA0012 (NS_SA, P1) ===" << std::endl;
278
279 auto [rhsDensity, rhsEnergy, incL2] = runOneNewtonStep<NS_SA>(cfgPath);
280
281 CHECK(std::isfinite(rhsDensity));
282 CHECK(std::isfinite(rhsEnergy));
283 CHECK(std::isfinite(incL2));
284 CHECK(rhsDensity > 0);
285
286 real goldenRhsDensity = 2.4001929638e-01;
287 real goldenIncL2 = 9.7941775117e+01;
288
289 if (g_mpi.rank == 0)
290 std::cout << " golden candidates: rhsDensity=" << std::scientific
291 << std::setprecision(10) << rhsDensity
292 << " incL2=" << incL2 << std::endl;
293
294 CHECK(rhsDensity == doctest::Approx(goldenRhsDensity).epsilon(1e-6));
295 CHECK(incL2 == doctest::Approx(goldenIncL2).epsilon(1e-6));
296}
297
298// ===================================================================
299// TEST 3: 3D Box (NS_3D, periodic, inviscid)
300// ===================================================================
301
302TEST_CASE("EulerEvaluator pipeline: Box (NS_3D, P1)")
303{
304 std::string root = projectRoot();
305 std::string cfgPath = writeTempConfig<NS_3D>(
306 root + "/cases/euler3D/euler3D_config_Box.json", "box3d_test",
307 {{"vfvSettings", {{"maxOrder", 1}, {"intOrder", 3}, {"cacheDiffBase", false}}},
308 {"dataIOControl", {{"meshFile", root + "/data/mesh/Uniform32_3D_Periodic.cgns"}}}});
309
310 if (g_mpi.rank == 0)
311 std::cout << "=== Box (NS_3D, P1) ===" << std::endl;
312
313 auto [rhsDensity, rhsEnergy, incL2] = runOneNewtonStep<NS_3D>(cfgPath);
314
315 CHECK(std::isfinite(rhsDensity));
316 CHECK(std::isfinite(rhsEnergy));
317 CHECK(std::isfinite(incL2));
318 CHECK(rhsDensity > 0);
319
320 real goldenRhsDensity = 5.8855408029e-01;
321 real goldenIncL2 = 3.4532977412e+01;
322
323 if (g_mpi.rank == 0)
324 std::cout << " golden candidates: rhsDensity=" << std::scientific
325 << std::setprecision(10) << rhsDensity
326 << " incL2=" << incL2 << std::endl;
327
328 CHECK(rhsDensity == doctest::Approx(goldenRhsDensity).epsilon(1e-6));
329 CHECK(incL2 == doctest::Approx(goldenIncL2).epsilon(1e-6));
330}
331
332// ===================================================================
333// main with MPI
334// ===================================================================
335int main(int argc, char **argv)
336{
337 MPI_Init(&argc, &argv);
338 g_mpi.setWorld();
339
340 doctest::Context ctx;
341 ctx.applyCommandLine(argc, argv);
342 int res = ctx.run();
343
344 MPI_Finalize();
345 return res;
346}
#define DNDS_assert_info(expr, info)
Debug-only assertion with an extra std::string info message.
Definition Errors.hpp:113
#define DNDS_assert(expr)
Debug-only assertion (compiled out when DNDS_NDEBUG is defined). Prints the expression + file/line + ...
Definition Errors.hpp:108
Top-level solver orchestrator for compressible Navier-Stokes / Euler simulations.
MPI-parallel distributed array of per-cell Degrees-of-Freedom (conservative variable vectors).
Definition Euler.hpp:93
void setConstant(real R)
Set all DOF entries to a uniform scalar value.
Definition Euler.hpp:103
Core finite-volume evaluator for compressible Navier-Stokes / Euler equations.
Top-level solver orchestrator for compressible Navier-Stokes / Euler equations.
Per-cell diagonal or block-diagonal Jacobian storage for implicit time stepping.
void SetModeAndInit(int mode, int nVarsC, ArrayDOFV< nVarsFixed > &mock)
Select the storage mode and allocate arrays matching mock's layout.
int main()
Definition dummy.cpp:3
the host side operators are provided as implemented
double real
Canonical floating-point scalar used throughout DNDSR (double precision).
Definition Defines.hpp:105
TTrans trans
Ghost-communication engine bound to father and son.
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:215
int rank
This rank's 0-based index within comm (-1 until initialised).
Definition MPI.hpp:219
MPI_Comm comm
The underlying MPI communicator handle.
Definition MPI.hpp:217
void setWorld()
Initialise the object to MPI_COMM_WORLD. Requires MPI_Init to have run.
Definition MPI.hpp:242
std::array< real, 3 > runOneNewtonStep(const std::string &cfgPath)
real goldenIncL2
std::string cfgPath
CHECK(std::isfinite(rhsDensity))
real goldenRhsDensity
auto res
Definition test_ODE.cpp:196
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")