DNDSR 0.1.0.dev1+gcd065ad
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_GasThermo.cpp
Go to the documentation of this file.
1/**
2 * @file test_GasThermo.cpp
3 * @brief Unit tests for ideal-gas thermodynamics and eigenvector routines in Gas.hpp.
4 *
5 * Tests cover:
6 * - IdealGasThermal: pressure, speed-of-sound, enthalpy from conservative
7 * - Conservative2Primitive / Primitive2Conservative round-trip (2D and 3D)
8 * - PrimitiveGetP0T0: stagnation quantities
9 * - EulerGasRightEigenVector / LeftEigenVector: L*R = I orthogonality (3D)
10 * - IdealGas_EulerGas{Right,Left}EigenVector convenience wrappers
11 * - IdealGasUIncrement: velocity/pressure increments from conservative increments
12 * - GasInviscidFlux / GasInviscidFlux_XY: inviscid flux computation
13 * - GradientCons2Prim_IdealGas: gradient transformation
14 * - GetRoeAverage: Roe-averaged state properties
15 * - IdealGasGetCompressionRatioPressure: positivity limiter
16 *
17 * All functions are pure (no MPI, no mesh). Serial doctest.
18 */
19
20#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
21#include "doctest.h"
22
23#include "Euler/Gas.hpp"
24
25#include <Eigen/Dense>
26#include <cmath>
27#include <iostream>
28
29using namespace DNDS;
30using namespace DNDS::Euler::Gas;
31
32// Standard air at rest: rho=1.0, p=1/gamma (so a=1), gamma=1.4
33static constexpr real g_gamma = 1.4;
34
35// Build a 3D conservative state from primitive [rho, u, v, w, p]
36static Eigen::Vector<real, 5> primToCons3D(real rho, real u, real v, real w, real p)
37{
38 Eigen::Vector<real, 5> U;
39 real E = p / (g_gamma - 1.0) + 0.5 * rho * (u * u + v * v + w * w);
40 U << rho, rho * u, rho * v, rho * w, E;
41 return U;
42}
43
44// Build a 2D conservative state from primitive [rho, u, v, p]
45[[maybe_unused]] static Eigen::Vector<real, 4> primToCons2D(real rho, real u, real v, real p)
46{
47 Eigen::Vector<real, 4> U;
48 real E = p / (g_gamma - 1.0) + 0.5 * rho * (u * u + v * v);
49 U << rho, rho * u, rho * v, E;
50 return U;
51}
52
53// ===================================================================
54// IdealGasThermal
55// ===================================================================
56
57TEST_CASE("IdealGasThermal: standard quiescent air")
58{
59 // rho=1.0, p=1.0/1.4, v=0 => a^2 = gamma*p/rho = 1.0
60 real rho = 1.0, p_expected = 1.0 / g_gamma;
61 real E = p_expected / (g_gamma - 1.0); // rho*E = p/(gamma-1)
62 real vSqr = 0.0;
63
64 real p, asqr, H;
65 IdealGasThermal(E, rho, vSqr, g_gamma, p, asqr, H);
66
67 CHECK(p == doctest::Approx(p_expected).epsilon(1e-14));
68 CHECK(asqr == doctest::Approx(1.0).epsilon(1e-14));
69 CHECK(H == doctest::Approx((E + p) / rho).epsilon(1e-14));
70}
71
72TEST_CASE("IdealGasThermal: Mach 2 flow")
73{
74 real rho = 1.0, u = 2.0, p_expected = 1.0;
75 real vSqr = u * u;
76 real E = p_expected / (g_gamma - 1.0) + rho * 0.5 * vSqr;
77
78 real p, asqr, H;
79 IdealGasThermal(E, rho, vSqr, g_gamma, p, asqr, H);
80
81 CHECK(p == doctest::Approx(p_expected).epsilon(1e-14));
82 CHECK(asqr == doctest::Approx(g_gamma * p_expected / rho).epsilon(1e-14));
83 real H_expected = (E + p_expected) / rho;
84 CHECK(H == doctest::Approx(H_expected).epsilon(1e-14));
85}
86
87// ===================================================================
88// Conservative <-> Primitive round-trip
89// ===================================================================
90
91TEST_CASE("Cons2Prim and Prim2Cons round-trip: 3D")
92{
93 Eigen::Vector<real, 5> prim0;
94 prim0 << 1.225, 100.0, 50.0, -30.0, 101325.0; // air at ~Mach 0.3
95
96 Eigen::Vector<real, 5> U, prim1;
97 IdealGasThermalPrimitive2Conservative<3>(prim0, U, g_gamma);
98 IdealGasThermalConservative2Primitive<3>(U, prim1, g_gamma);
99
100 for (int i = 0; i < 5; i++)
101 {
102 CAPTURE(i);
103 CHECK(prim1(i) == doctest::Approx(prim0(i)).epsilon(1e-12));
104 }
105}
106
107TEST_CASE("Cons2Prim and Prim2Cons round-trip: 2D")
108{
109 Eigen::Vector<real, 4> prim0;
110 prim0 << 0.5, 300.0, -100.0, 50000.0;
111
112 Eigen::Vector<real, 4> U, prim1;
113 IdealGasThermalPrimitive2Conservative<2>(prim0, U, g_gamma);
114 IdealGasThermalConservative2Primitive<2>(U, prim1, g_gamma);
115
116 for (int i = 0; i < 4; i++)
117 {
118 CAPTURE(i);
119 CHECK(prim1(i) == doctest::Approx(prim0(i)).epsilon(1e-12));
120 }
121}
122
123TEST_CASE("Prim2Cons: known state verification")
124{
125 // rho=2, u=3, v=0, w=0, p=10, gamma=1.4
126 // E = p/(gamma-1) + 0.5*rho*vSqr = 10/0.4 + 0.5*2*9 = 25 + 9 = 34
127 Eigen::Vector<real, 5> prim;
128 prim << 2.0, 3.0, 0.0, 0.0, 10.0;
129 Eigen::Vector<real, 5> U;
130 IdealGasThermalPrimitive2Conservative<3>(prim, U, g_gamma);
131
132 CHECK(U(0) == doctest::Approx(2.0)); // rho
133 CHECK(U(1) == doctest::Approx(6.0)); // rho*u
134 CHECK(U(2) == doctest::Approx(0.0)); // rho*v
135 CHECK(U(3) == doctest::Approx(0.0)); // rho*w
136 CHECK(U(4) == doctest::Approx(34.0)); // rho*E
137}
138
139// ===================================================================
140// PrimitiveGetP0T0: stagnation quantities
141// ===================================================================
142
143TEST_CASE("PrimitiveGetP0T0: quiescent gas")
144{
145 // At rest: p0 = p, T0 = T
146 Eigen::Vector<real, 5> prim;
147 prim << 1.0, 0.0, 0.0, 0.0, 100000.0;
148 real Rgas = 287.0;
149
150 auto [p0, T0] = IdealGasThermalPrimitiveGetP0T0<3>(prim, g_gamma, Rgas);
151 real T = prim(4) / (prim(0) * Rgas);
152
153 CHECK(p0 == doctest::Approx(100000.0).epsilon(1e-10));
154 CHECK(T0 == doctest::Approx(T).epsilon(1e-10));
155}
156
157TEST_CASE("PrimitiveGetP0T0: p0 > p for moving gas")
158{
159 Eigen::Vector<real, 5> prim;
160 prim << 1.225, 300.0, 0.0, 0.0, 101325.0;
161 real Rgas = 287.0;
162
163 auto [p0, T0] = IdealGasThermalPrimitiveGetP0T0<3>(prim, g_gamma, Rgas);
164 real T = prim(4) / (prim(0) * Rgas);
165
166 CHECK(p0 > prim(4));
167 CHECK(T0 > T);
168}
169
170// ===================================================================
171// Eigenvectors: L * R = I
172// ===================================================================
173
174TEST_CASE("EulerGas eigenvectors: L*R = I for 3D")
175{
176 Eigen::Vector3d velo(1.5, -0.3, 0.7);
177 real Vsqr = velo.squaredNorm();
178 // Compute p, a, H from a state
179 real rho = 1.0, p = 1.0;
180 real E = p / (g_gamma - 1.0) + 0.5 * rho * Vsqr;
181 real a = std::sqrt(g_gamma * p / rho);
182 real H = (E + p) / rho;
183
184 Eigen::Matrix<real, 5, 5> R, L;
185 R.setZero();
186 L.setZero();
187 EulerGasRightEigenVector<3>(velo, Vsqr, H, a, R);
188 EulerGasLeftEigenVector<3>(velo, Vsqr, H, a, g_gamma, L);
189
190 Eigen::Matrix<real, 5, 5> LR = L * R;
191 Eigen::Matrix<real, 5, 5> I = Eigen::Matrix<real, 5, 5>::Identity();
192
193 for (int i = 0; i < 5; i++)
194 for (int j = 0; j < 5; j++)
195 {
196 CAPTURE(i);
197 CAPTURE(j);
198 CHECK(LR(i, j) == doctest::Approx(I(i, j)).epsilon(1e-12));
199 }
200}
201
202TEST_CASE("EulerGas eigenvectors: L*R = I for non-trivial velocity")
203{
204 Eigen::Vector3d velo(300.0, -150.0, 75.0);
205 real Vsqr = velo.squaredNorm();
206 real rho = 1.225, p = 101325.0;
207 real E = p / (g_gamma - 1.0) + 0.5 * rho * Vsqr;
208 real a = std::sqrt(g_gamma * p / rho);
209 real H = (E + p) / rho;
210
211 Eigen::Matrix<real, 5, 5> R, L;
212 R.setZero();
213 L.setZero();
214 EulerGasRightEigenVector<3>(velo, Vsqr, H, a, R);
215 EulerGasLeftEigenVector<3>(velo, Vsqr, H, a, g_gamma, L);
216
217 Eigen::Matrix<real, 5, 5> LR = L * R;
218 Eigen::Matrix<real, 5, 5> I = Eigen::Matrix<real, 5, 5>::Identity();
219
220 real maxErr = (LR - I).cwiseAbs().maxCoeff();
221 CHECK(maxErr < 1e-10);
222}
223
224TEST_CASE("IdealGas convenience eigenvector wrappers produce L*R=I")
225{
226 auto U = primToCons3D(1.225, 100.0, -50.0, 25.0, 101325.0);
227
228 auto R = IdealGas_EulerGasRightEigenVector<3>(U, g_gamma);
229 auto L = IdealGas_EulerGasLeftEigenVector<3>(U, g_gamma);
230
231 auto LR = L * R;
232 real maxErr = (LR - Eigen::Matrix<real, 5, 5>::Identity()).cwiseAbs().maxCoeff();
233 CHECK(maxErr < 1e-10);
234}
235
236// ===================================================================
237// GasInviscidFlux: x-direction flux
238// ===================================================================
239
240TEST_CASE("GasInviscidFlux: x-direction, quiescent gas")
241{
242 // At rest: flux = [0, p, 0, 0, 0]
243 auto U = primToCons3D(1.0, 0.0, 0.0, 0.0, 1.0);
244 Eigen::Vector3d velo(0, 0, 0), vg(0, 0, 0);
245 real p = 1.0;
246
247 Eigen::Vector<real, 5> F;
248 GasInviscidFlux<3>(U, velo, vg, p, F);
249
250 CHECK(F(0) == doctest::Approx(0.0).epsilon(1e-14));
251 CHECK(F(1) == doctest::Approx(p).epsilon(1e-14)); // momentum flux = p
252 CHECK(F(2) == doctest::Approx(0.0).epsilon(1e-14));
253 CHECK(F(3) == doctest::Approx(0.0).epsilon(1e-14));
254 CHECK(F(4) == doctest::Approx(0.0).epsilon(1e-14));
255}
256
257TEST_CASE("GasInviscidFlux: x-direction, moving gas")
258{
259 real rho = 2.0, u = 3.0, p = 10.0;
260 auto U = primToCons3D(rho, u, 0.0, 0.0, p);
261 Eigen::Vector3d velo(u, 0, 0), vg(0, 0, 0);
262
263 Eigen::Vector<real, 5> F;
264 GasInviscidFlux<3>(U, velo, vg, p, F);
265
266 // F = [rho*u, rho*u^2+p, 0, 0, (E+p)*u]
267 real E = U(4);
268 CHECK(F(0) == doctest::Approx(rho * u).epsilon(1e-12));
269 CHECK(F(1) == doctest::Approx(rho * u * u + p).epsilon(1e-12));
270 CHECK(F(2) == doctest::Approx(0.0).epsilon(1e-12));
271 CHECK(F(3) == doctest::Approx(0.0).epsilon(1e-12));
272 CHECK(F(4) == doctest::Approx((E + p) * u).epsilon(1e-12));
273}
274
275// ===================================================================
276// GasInviscidFlux_XY: normal-direction flux
277// ===================================================================
278
279TEST_CASE("GasInviscidFlux_XY: n=(1,0,0) equals GasInviscidFlux")
280{
281 auto U = primToCons3D(1.225, 100.0, -50.0, 25.0, 101325.0);
282 Eigen::Vector3d velo = U.segment<3>(1) / U(0);
283 Eigen::Vector3d vg(0, 0, 0);
285 {
286 real asqr, H;
287 IdealGasThermal(U(4), U(0), velo.squaredNorm(), g_gamma, p_val, asqr, H);
288 }
289
290 Eigen::Vector<real, 5> Fx, Fn;
292 Eigen::Vector3d nx(1, 0, 0);
294
295 for (int i = 0; i < 5; i++)
296 {
297 CAPTURE(i);
298 CHECK(Fn(i) == doctest::Approx(Fx(i)).epsilon(1e-10));
299 }
300}
301
302// ===================================================================
303// IdealGasUIncrement
304// ===================================================================
305
306TEST_CASE("IdealGasUIncrement: zero increment gives zero output")
307{
308 auto U = primToCons3D(1.0, 100.0, -50.0, 25.0, 101325.0);
309 Eigen::Vector<real, 5> dU = Eigen::Vector<real, 5>::Zero();
310 Eigen::Vector3d velo = U.segment<3>(1) / U(0);
311 Eigen::Vector3d dVelo;
312 real dp;
313
314 IdealGasUIncrement<3>(U, dU, velo, g_gamma, dVelo, dp);
315
316 CHECK(dVelo.norm() == doctest::Approx(0.0).epsilon(1e-14));
317 CHECK(dp == doctest::Approx(0.0).epsilon(1e-10));
318}
319
320TEST_CASE("IdealGasUIncrement: finite-difference verification")
321{
322 // IdealGasUIncrement is a linearization (exact for infinitesimal dU).
323 // Use a very small perturbation to reduce second-order error.
324 auto U0 = primToCons3D(1.225, 100.0, -50.0, 25.0, 101325.0);
325 Eigen::Vector3d velo0 = U0.segment<3>(1) / U0(0);
326
327 real eps = 1e-7;
328 Eigen::Vector<real, 5> prim0;
329 IdealGasThermalConservative2Primitive<3>(U0, prim0, g_gamma);
330
331 Eigen::Vector<real, 5> prim1;
332 prim1 << prim0(0) + 0.01 * eps, prim0(1) + 0.5 * eps,
333 prim0(2) - 0.3 * eps, prim0(3) + 0.1 * eps,
334 prim0(4) + 100.0 * eps;
335 Eigen::Vector<real, 5> U1;
336 IdealGasThermalPrimitive2Conservative<3>(prim1, U1, g_gamma);
337
338 Eigen::Vector<real, 5> dU = U1 - U0;
339 Eigen::Vector3d dVelo;
340 real dp;
341 IdealGasUIncrement<3>(U0, dU, velo0, g_gamma, dVelo, dp);
342
343 Eigen::Vector3d dVeloExpected = prim1.segment<3>(1) - prim0.segment<3>(1);
344 real dpExpected = prim1(4) - prim0(4);
345
346 for (int i = 0; i < 3; i++)
347 {
348 CAPTURE(i);
349 CHECK(dVelo(i) == doctest::Approx(dVeloExpected(i)).epsilon(1e-4));
350 }
351 CHECK(dp == doctest::Approx(dpExpected).epsilon(1e-3));
352}
353
354// ===================================================================
355// GetRoeAverage
356// ===================================================================
357
358TEST_CASE("GetRoeAverage: identical states give same state")
359{
360 auto U = primToCons3D(1.225, 100.0, -50.0, 25.0, 101325.0);
361
362 Eigen::Vector3d veloRoe;
363 real vsqrRoe, aRoe, asqrRoe, HRoe;
364 Eigen::Vector<real, 5> URoe;
365
366 GetRoeAverage<3>(U, U, g_gamma, veloRoe, vsqrRoe, aRoe, asqrRoe, HRoe, URoe);
367
368 Eigen::Vector3d velo = U.segment<3>(1) / U(0);
369 real p_val, asqr_val, H_val;
370 IdealGasThermal(U(4), U(0), velo.squaredNorm(), g_gamma, p_val, asqr_val, H_val);
371
372 for (int i = 0; i < 3; i++)
373 {
374 CAPTURE(i);
375 CHECK(veloRoe(i) == doctest::Approx(velo(i)).epsilon(1e-10));
376 }
377 CHECK(HRoe == doctest::Approx(H_val).epsilon(1e-10));
378 CHECK(aRoe == doctest::Approx(std::sqrt(asqr_val)).epsilon(1e-10));
379}
380
381TEST_CASE("GetRoeAverage: density is geometric mean")
382{
383 auto UL = primToCons3D(1.0, 100.0, 0.0, 0.0, 100000.0);
384 auto UR = primToCons3D(4.0, 100.0, 0.0, 0.0, 100000.0);
385
386 Eigen::Vector3d veloRoe;
387 real vsqrRoe, aRoe, asqrRoe, HRoe;
388 Eigen::Vector<real, 5> URoe;
389
390 GetRoeAverage<3>(UL, UR, g_gamma, veloRoe, vsqrRoe, aRoe, asqrRoe, HRoe, URoe);
391
392 // Roe-averaged rho = sqrt(rhoL * rhoR) = sqrt(1*4) = 2
393 CHECK(URoe(0) == doctest::Approx(2.0).epsilon(1e-10));
394}
395
396// ===================================================================
397// GradientCons2Prim_IdealGas
398// ===================================================================
399
400TEST_CASE("GradientCons2Prim: zero gradient produces zero")
401{
402 auto U = primToCons3D(1.225, 100.0, -50.0, 25.0, 101325.0);
403 Eigen::Matrix<real, 3, 5> GradU = Eigen::Matrix<real, 3, 5>::Zero();
404 Eigen::Matrix<real, 3, 5> GradPrim;
405
406 GradientCons2Prim_IdealGas<3>(U, GradU, GradPrim, g_gamma);
407
408 CHECK(GradPrim.cwiseAbs().maxCoeff() < 1e-12);
409}
410
411TEST_CASE("GradientCons2Prim: finite-difference verification")
412{
413 // Base state
414 Eigen::Vector<real, 5> prim0;
415 prim0 << 1.225, 100.0, -50.0, 25.0, 101325.0;
416 Eigen::Vector<real, 5> U0;
417 IdealGasThermalPrimitive2Conservative<3>(prim0, U0, g_gamma);
418
419 // Perturbed state (small perturbation in x-direction)
420 real dx = 1e-6;
421 Eigen::Vector<real, 5> prim1;
422 prim1 << 1.225 + 0.01 * dx, 100.0 + 0.5 * dx, -50.0 - 0.3 * dx, 25.0 + 0.1 * dx, 101325.0 + 100.0 * dx;
423 Eigen::Vector<real, 5> U1;
424 IdealGasThermalPrimitive2Conservative<3>(prim1, U1, g_gamma);
425
426 // Conservative gradient: dU/dx in x-direction only
427 Eigen::Matrix<real, 3, 5> GradU = Eigen::Matrix<real, 3, 5>::Zero();
428 GradU.row(0) = (U1 - U0).transpose() / dx;
429
430 Eigen::Matrix<real, 3, 5> GradPrim;
431 GradientCons2Prim_IdealGas<3>(U0, GradU, GradPrim, g_gamma);
432
433 // Expected primitive gradient
434 Eigen::RowVector<real, 5> dPrimdx_expected = (prim1 - prim0).transpose() / dx;
435
436 for (int j = 0; j < 5; j++)
437 {
438 CAPTURE(j);
439 CHECK(GradPrim(0, j) == doctest::Approx(dPrimdx_expected(j)).epsilon(1e-4));
440 }
441}
442
443// ===================================================================
444// IdealGasGetCompressionRatioPressure
445// ===================================================================
446
447TEST_CASE("CompressionRatio: zero increment gives alpha=0 (no compression needed)")
448{
449 // Zero increment means nothing to limit; the quadratic solver
450 // has c1=c2=0, yielding alpha=0 by convention.
451 auto U = primToCons3D(1.225, 100.0, -50.0, 25.0, 101325.0);
452 Eigen::Vector<real, 5> dU = Eigen::Vector<real, 5>::Zero();
453
454 real alpha = IdealGasGetCompressionRatioPressure<3, 0, 5>(U, dU, 0.0);
455 CHECK(alpha == doctest::Approx(0.0).epsilon(1e-14));
456}
457
458TEST_CASE("CompressionRatio: alpha in [0,1]")
459{
460 auto U = primToCons3D(1.0, 0.0, 0.0, 0.0, 1.0);
461 // Large decrement that would make pressure negative
462 Eigen::Vector<real, 5> dU;
463 dU << 0, 0, 0, 0, -100.0; // massive energy reduction
464
465 real alpha = IdealGasGetCompressionRatioPressure<3, 0, 5>(U, dU, 0.0);
466 CHECK(alpha >= 0.0);
467 CHECK(alpha <= 1.0);
468 // With alpha, the state should maintain non-negative internal energy
469 auto Unew = U + alpha * dU;
470 real vSqr = (Unew.segment<3>(1) / Unew(0)).squaredNorm();
471 real eInternal = Unew(4) - 0.5 * Unew(0) * vSqr;
472 CHECK(eInternal >= -1e-10);
473}
474
475// ===================================================================
476// ViscousFlux_IdealGas: uniform flow produces zero viscous flux
477// ===================================================================
478
479TEST_CASE("ViscousFlux: zero gradient produces zero flux")
480{
481 auto U = primToCons3D(1.225, 100.0, -50.0, 25.0, 101325.0);
482 Eigen::Matrix<real, 3, 5> GradPrim = Eigen::Matrix<real, 3, 5>::Zero();
483 Eigen::Vector3d norm(1, 0, 0);
484 real mu = 1.8e-5, k = 0.025, Cp = 1005.0;
485
486 Eigen::Vector<real, 5> Flux;
487 ViscousFlux_IdealGas<3>(U, GradPrim, norm, false, g_gamma, mu, 0.0, false, k, Cp, Flux);
488
489 for (int i = 0; i < 5; i++)
490 {
491 CAPTURE(i);
492 CHECK(Flux(i) == doctest::Approx(0.0).epsilon(1e-10));
493 }
494}
Ideal-gas Riemann solvers, flux functions, and thermodynamic utilities for the compressible Euler / N...
void IdealGasThermal(real E, real rho, real vSqr, real gamma, real &p, real &asqr, real &H)
Thin wrapper delegating to IdealGas::IdealGasThermal.
Definition Gas.hpp:190
the host side operators are provided as implemented
double real
Canonical floating-point scalar used throughout DNDSR (double precision).
Definition Defines.hpp:105
Eigen::Matrix< real, 5, 1 > v
CHECK(alpha==doctest::Approx(0.0).epsilon(1e-14))
real alpha
Eigen::Vector3d nx(1, 0, 0)
GasInviscidFlux_XY< 3 >(U, velo, vg, nx, p_val, Fn)
Eigen::Vector3d vg(0, 0, 0)
Eigen::Vector3d velo
TEST_CASE("EulerGas eigenvectors: L*R = I for 3D")
real p_val
Eigen::Vector< real, 5 > dU
GasInviscidFlux< 3 >(U, velo, vg, p_val, Fx)
Eigen::Vector< real, 5 > Fx
Eigen::Vector< real, 5 > Fn