rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
HydroStep_test.cpp
Go to the documentation of this file.
1
19
22
23#include <gtest/gtest.h>
24
25#include <algorithm>
26#include <cmath>
27#include <stdexcept>
28#include <vector>
29
30namespace
31{
32
36 using rizer::NodeState;
37
38 constexpr double kGamma = 1.4;
39 constexpr double kMolecularWeight = 28.96; // [kg/kmol]
40 constexpr double kPressure = 101325.0; // [Pa]
41 constexpr double kTemperature = 300.0; // [K]
42 constexpr double kGasConstant = 8314.462618153242; // [J/kmol/K]
43
49 class RecordingThermo : public rizer::ThermoAccessor
50 {
51 public:
52 RecordingThermo()
53 : m_backing(kGamma, {kMolecularWeight}, /*electron_index=*/-1)
54 {
55 }
56
57 void pressures(double r, double t, double rho, double Tg, double Te,
58 const double *Y, double &p_heavy,
59 double &p_electron) const override
60 {
61 radii.push_back(r);
62 m_backing.pressures(r, t, rho, Tg, Te, Y, p_heavy, p_electron);
63 }
64
65 double soundSpeed(double r, double t, double rho, double Tg, double Te,
66 const double *Y) const override
67 {
68 return m_backing.soundSpeed(r, t, rho, Tg, Te, Y);
69 }
70
71 double heavyEnergy(double Tg, const double *Y) const override
72 {
73 return m_backing.heavyEnergy(Tg, Y);
74 }
75
76 double electronCv() const override { return m_backing.electronCv(); }
77
78 double densityFromPressure(double pressure, double Tg, double Te,
79 const double *Y) const override
80 {
81 return m_backing.densityFromPressure(pressure, Tg, Te, Y);
82 }
83
84 double heavyTemperature(double rho, const double *Y,
85 double heavy_internal_energy_target,
86 double Tg_seed) const override
87 {
88 return m_backing.heavyTemperature(rho, Y, heavy_internal_energy_target,
89 Tg_seed);
90 }
91
92 mutable std::vector<double> radii;
93
94 private:
95 GammaLawThermo m_backing;
96 };
97
101 struct Grid
102 {
103 std::vector<double> r_node;
104 std::vector<double> r_face;
105 std::vector<double> V_cell;
106 };
107
114 Grid makeGrid(std::size_t n_points, double r_max, double r_min = 0.0,
115 double stretch = 1.0)
116 {
117 Grid grid;
118 grid.r_node.resize(n_points);
119 for (std::size_t i = 0; i < n_points; i++)
120 {
121 const double x =
122 static_cast<double>(i) / static_cast<double>(n_points - 1);
123 grid.r_node[i] = r_min + (r_max - r_min) * std::pow(x, stretch);
124 }
125 grid.r_face.resize(n_points + 1);
126 grid.r_face[0] = (r_min == 0.0)
127 ? 0.0
128 : grid.r_node[0] -
129 0.5 * (grid.r_node[1] - grid.r_node[0]);
130 for (std::size_t i = 1; i < n_points; i++)
131 {
132 grid.r_face[i] = 0.5 * (grid.r_node[i - 1] + grid.r_node[i]);
133 }
134 grid.r_face[n_points] =
135 grid.r_node[n_points - 1] +
136 0.5 * (grid.r_node[n_points - 1] - grid.r_node[n_points - 2]);
137 grid.V_cell.resize(n_points);
138 for (std::size_t i = 0; i < n_points; i++)
139 {
140 grid.V_cell[i] = 0.5 * (grid.r_face[i + 1] * grid.r_face[i + 1] -
141 grid.r_face[i] * grid.r_face[i]);
142 }
143 return grid;
144 }
145
147 HydroStepState makeState(std::size_t n_points)
148 {
149 HydroStepState state;
150 state.n_species = 1;
151 const double rho = kPressure * kMolecularWeight / (kGasConstant * kTemperature);
152 state.rho.assign(n_points, rho);
153 state.u_r.assign(n_points, 0.0);
154 state.Tg.assign(n_points, kTemperature);
155 state.Te.assign(n_points, kTemperature);
156 state.Y.assign(n_points, 1.0);
157 return state;
158 }
159
162 HydroStepConfig makeConfig(const Grid &grid, const rizer::ThermoAccessor &thermo)
163 {
164 HydroStepConfig cfg;
165 cfg.r_node = grid.r_node;
166 cfg.r_face = grid.r_face;
167 cfg.V_cell = grid.V_cell;
168 cfg.cylindrical_source = true;
169 cfg.p_amb = kPressure;
170 cfg.T_amb = kTemperature;
171 cfg.Y_amb = {1.0};
172 cfg.dt_total = 1.0e-12;
173 cfg.n_subcycles = 1;
174 cfg.electron_index = -1;
175 cfg.thermo = &thermo;
176 return cfg;
177 }
178
179 // ------------------------------------------------------------------------------ //
180 // Ghost placement //
181 // ------------------------------------------------------------------------------ //
182
185 TEST(HydroStepGhostRadius, OuterGhostSitsAtItsOwnCellNode)
186 {
187 const Grid grid = makeGrid(21, 4.0e-3);
188 RecordingThermo thermo;
189 HydroStepConfig cfg = makeConfig(grid, thermo);
190
191 std::vector<double> offered;
192 cfg.ghost_state = [&](double r_ghost, double, bool is_inner,
193 const NodeState &interior) {
194 EXPECT_FALSE(is_inner) << "a full disk must not consult the axis face";
195 offered.push_back(r_ghost);
196 return interior;
197 };
198
199 HydroStepState state = makeState(21);
200 rizer::hydroStep(cfg, state);
201
202 const double expected = 2.0 * grid.r_node[20] - grid.r_node[19];
203 ASSERT_FALSE(offered.empty());
204 EXPECT_DOUBLE_EQ(offered.front(), expected);
205 // Half a spacing away from the face, which is what it must NOT be.
206 EXPECT_GT(std::abs(offered.front() - grid.r_face[21]), 1.0e-5);
207 }
208
211 TEST(HydroStepGhostRadius, BothGhostsAreOfferedOnAnAnnulus)
212 {
213 const Grid grid = makeGrid(21, 4.0e-3, /*r_min=*/1.0e-3);
214 RecordingThermo thermo;
215 HydroStepConfig cfg = makeConfig(grid, thermo);
216
217 double inner = 0.0;
218 double outer = 0.0;
219 cfg.ghost_state = [&](double r_ghost, double, bool is_inner,
220 const NodeState &interior) {
221 (is_inner ? inner : outer) = r_ghost;
222 return interior;
223 };
224
225 HydroStepState state = makeState(21);
226 rizer::hydroStep(cfg, state);
227
228 EXPECT_DOUBLE_EQ(inner, 2.0 * grid.r_node[0] - grid.r_node[1]);
229 EXPECT_DOUBLE_EQ(outer, 2.0 * grid.r_node[20] - grid.r_node[19]);
230 }
231
234 TEST(HydroStepGhostRadius, EachEndUsesItsOwnSpacing)
235 {
236 const Grid grid = makeGrid(21, 4.0e-3, /*r_min=*/1.0e-3, /*stretch=*/2.5);
237 const double inner_spacing = grid.r_node[1] - grid.r_node[0];
238 const double outer_spacing = grid.r_node[20] - grid.r_node[19];
239 // The premise of the test: the mesh really is lopsided.
240 ASSERT_GT(outer_spacing / inner_spacing, 5.0);
241
242 RecordingThermo thermo;
243 HydroStepConfig cfg = makeConfig(grid, thermo);
244 double inner = 0.0;
245 double outer = 0.0;
246 cfg.ghost_state = [&](double r_ghost, double, bool is_inner,
247 const NodeState &interior) {
248 (is_inner ? inner : outer) = r_ghost;
249 return interior;
250 };
251
252 HydroStepState state = makeState(21);
253 rizer::hydroStep(cfg, state);
254
255 EXPECT_DOUBLE_EQ(inner, grid.r_node[0] - inner_spacing);
256 EXPECT_DOUBLE_EQ(outer, grid.r_node[20] + outer_spacing);
257 }
258
263 TEST(HydroStepGhostRadius, ThermoSeesTheSameGhostRadiiAsTheCallback)
264 {
265 const Grid grid = makeGrid(21, 4.0e-3, /*r_min=*/1.0e-3, /*stretch=*/2.5);
266 RecordingThermo thermo;
267 HydroStepConfig cfg = makeConfig(grid, thermo);
268 double inner = 0.0;
269 double outer = 0.0;
270 cfg.ghost_state = [&](double r_ghost, double, bool is_inner,
271 const NodeState &interior) {
272 (is_inner ? inner : outer) = r_ghost;
273 return interior;
274 };
275
276 HydroStepState state = makeState(21);
277 rizer::hydroStep(cfg, state);
278
279 ASSERT_FALSE(thermo.radii.empty());
280 const auto bounds = std::minmax_element(thermo.radii.begin(), thermo.radii.end());
281 EXPECT_DOUBLE_EQ(*bounds.first, inner);
282 EXPECT_DOUBLE_EQ(*bounds.second, outer);
283 }
284
287 TEST(HydroStepGhostRadius, TheAxisGhostStaysOnTheAxis)
288 {
289 const Grid grid = makeGrid(21, 4.0e-3);
290 RecordingThermo thermo;
291 HydroStepConfig cfg = makeConfig(grid, thermo);
292 cfg.outer_bc_mode = rizer::OuterBc::AmbientGhost;
293
294 HydroStepState state = makeState(21);
295 rizer::hydroStep(cfg, state);
296
297 ASSERT_FALSE(thermo.radii.empty());
298 const double smallest =
299 *std::min_element(thermo.radii.begin(), thermo.radii.end());
300 // Exactly the axis, not merely non-negative: a ghost placed one spacing
301 // below zero would be a negative radius, but one placed at the first
302 // face would be positive and still wrong.
303 EXPECT_DOUBLE_EQ(smallest, 0.0);
304 }
305
306 // ------------------------------------------------------------------------------ //
307 // Input guards //
308 // ------------------------------------------------------------------------------ //
309
315 TEST(HydroStepGuards, RefusesASingleNode)
316 {
317 Grid grid;
318 grid.r_node = {1.0e-3};
319 grid.r_face = {0.5e-3, 1.5e-3};
320 grid.V_cell = {1.0e-6};
321 RecordingThermo thermo;
322 HydroStepConfig cfg = makeConfig(grid, thermo);
323 HydroStepState state = makeState(1);
324 EXPECT_THROW(rizer::hydroStep(cfg, state), std::invalid_argument);
325 }
326
329 TEST(HydroStepGuards, RefusesANullThermo)
330 {
331 const Grid grid = makeGrid(21, 4.0e-3);
332 RecordingThermo thermo;
333 HydroStepConfig cfg = makeConfig(grid, thermo);
334 cfg.thermo = nullptr;
335 HydroStepState state = makeState(21);
336 EXPECT_THROW(rizer::hydroStep(cfg, state), std::invalid_argument);
337 }
338
340 TEST(HydroStepGuards, RefusesACflOutsideTheUnitInterval)
341 {
342 const Grid grid = makeGrid(21, 4.0e-3);
343 RecordingThermo thermo;
344 HydroStepState state = makeState(21);
345 for (const double cfl : {0.0, -0.5, 1.5})
346 {
347 HydroStepConfig cfg = makeConfig(grid, thermo);
348 cfg.n_subcycles = 0; // so the CFL bound is actually derived
349 cfg.cfl = cfl;
350 EXPECT_THROW(rizer::hydroStep(cfg, state), std::invalid_argument)
351 << "cfl = " << cfl;
352 }
353 }
354
357 TEST(HydroStepGuards, RefusesAnAnnulusWithNoGhostCallback)
358 {
359 const Grid grid = makeGrid(21, 4.0e-3, /*r_min=*/1.0e-3);
360 RecordingThermo thermo;
361 HydroStepConfig cfg = makeConfig(grid, thermo);
362 HydroStepState state = makeState(21);
363 EXPECT_THROW(rizer::hydroStep(cfg, state), std::invalid_argument);
364 }
365
368 TEST(HydroStepGuards, RefusesAGhostWhoseCompositionDoesNotSumToOne)
369 {
370 const Grid grid = makeGrid(21, 4.0e-3);
371 RecordingThermo thermo;
372 HydroStepConfig cfg = makeConfig(grid, thermo);
373 cfg.ghost_state = [](double, double, bool, const NodeState &interior) {
374 NodeState ghost = interior;
375 ghost.Y[0] = 0.5;
376 return ghost;
377 };
378 HydroStepState state = makeState(21);
379 EXPECT_THROW(rizer::hydroStep(cfg, state), std::invalid_argument);
380 }
381
384 TEST(HydroStepGuards, RefusesANonFiniteGhost)
385 {
386 const Grid grid = makeGrid(21, 4.0e-3);
387 RecordingThermo thermo;
388 HydroStepConfig cfg = makeConfig(grid, thermo);
389 cfg.ghost_state = [](double, double, bool, const NodeState &interior) {
390 NodeState ghost = interior;
391 ghost.u_r = std::nan("");
392 return ghost;
393 };
394 HydroStepState state = makeState(21);
395 EXPECT_THROW(rizer::hydroStep(cfg, state), std::invalid_argument);
396 }
397
399 TEST(HydroStepGuards, AZeroLengthCallAdvancesNothing)
400 {
401 const Grid grid = makeGrid(21, 4.0e-3);
402 RecordingThermo thermo;
403 HydroStepConfig cfg = makeConfig(grid, thermo);
404 cfg.dt_total = 0.0;
405 cfg.n_subcycles = 0;
406 HydroStepState state = makeState(21);
407 const HydroStepState before = state;
408 const rizer::HydroStepResult result = rizer::hydroStep(cfg, state);
409 EXPECT_EQ(result.n_subcycles_used, 0u);
410 EXPECT_EQ(state.rho, before.rho);
411 EXPECT_EQ(state.u_r, before.u_r);
412 EXPECT_EQ(state.Tg, before.Tg);
413 }
414
415 // ------------------------------------------------------------------------------ //
416 // Well-balancedness //
417 // ------------------------------------------------------------------------------ //
418
422 TEST(HydroStepWellBalanced, AStateAtRestStaysAtRestOnAStretchedMesh)
423 {
424 const Grid grid = makeGrid(41, 4.0e-3, /*r_min=*/0.0, /*stretch=*/2.0);
425 RecordingThermo thermo;
426 HydroStepConfig cfg = makeConfig(grid, thermo);
427 cfg.outer_bc_mode = rizer::OuterBc::AmbientGhost;
428 cfg.dt_total = 1.0e-9;
429 cfg.n_subcycles = 0;
430
431 HydroStepState state = makeState(41);
432 rizer::hydroStep(cfg, state);
433
434 for (std::size_t i = 0; i < state.u_r.size(); i++)
435 {
436 EXPECT_NEAR(state.u_r[i], 0.0, 1.0e-14) << "node " << i;
437 }
438 }
439
440} // namespace
Calorically perfect ThermoAccessor: one constant gamma for the heavy pool, ElectronGamma for the elec...
Standalone explicit hydrodynamic sub-stepper for the 1D radial two-temperature plasma channel: the "A...
Calorically perfect ideal-gas accessor: one constant for the heavy pool, ElectronGamma for the elect...
Equation-of-state and caloric accessor injected into hydroStep().
@ AmbientGhost
Fix rho, u_r, p, T and Y_s to ambient.
HydroStepResult hydroStep(const HydroStepConfig &cfg, HydroStepState &state)
Advance state over one Strang half-step of cfg.dt_total.
Geometry, boundary treatment and stepping controls for one call.
Definition HydroStep.h:94
Diagnostics from one call, enough to assert NUMERICS.md section "Positivity under a strong rarefactio...
Definition HydroStep.h:179
std::size_t n_subcycles_used
M actually taken [-].
Definition HydroStep.h:180
Per-node primitive state, advanced in place.
Definition HydroStep.h:166
Primitive state at one radial node, as the ghost-state callback sees it and returns it.