xref: /honee/problems/bc_freestream.c (revision 9bafb13780ba412b8d5d5cd4dbcf231b2fa1babc)
1 // Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and other CEED contributors.
2 // All Rights Reserved. See the top-level LICENSE and NOTICE files for details.
3 //
4 // SPDX-License-Identifier: BSD-2-Clause
5 //
6 // This file is part of CEED:  http://github.com/ceed
7 
8 /// @file
9 /// Utility functions for setting up Freestream boundary condition
10 
11 #include "../qfunctions/bc_freestream.h"
12 
13 #include <ceed.h>
14 #include <petscdm.h>
15 
16 #include "../navierstokes.h"
17 #include "../qfunctions/newtonian_types.h"
18 
19 static const char *const RiemannSolverTypes[] = {"HLL", "HLLC", "RiemannSolverTypes", "RIEMANN_", NULL};
20 
21 static PetscErrorCode RiemannSolverUnitTests(NewtonianIdealGasContext gas, CeedScalar rtol);
22 
23 PetscErrorCode FreestreamBCSetup(ProblemData problem, DM dm, void *ctx, NewtonianIdealGasContext newtonian_ig_ctx, const StatePrimitive *reference) {
24   User                 user = *(User *)ctx;
25   MPI_Comm             comm = user->comm;
26   Ceed                 ceed = user->ceed;
27   FreestreamContext    freestream_ctx;
28   CeedQFunctionContext freestream_context;
29   RiemannFluxType      riemann = RIEMANN_HLLC;
30   PetscScalar          meter   = user->units->meter;
31   PetscScalar          second  = user->units->second;
32   PetscScalar          Kelvin  = user->units->Kelvin;
33   PetscScalar          Pascal  = user->units->Pascal;
34 
35   PetscFunctionBeginUser;
36   // Freestream inherits reference state. We re-dimensionalize so the defaults
37   // in -help will be visible in SI units.
38   StatePrimitive Y_inf = {.pressure = reference->pressure / Pascal, .velocity = {0}, .temperature = reference->temperature / Kelvin};
39   for (int i = 0; i < 3; i++) Y_inf.velocity[i] = reference->velocity[i] * second / meter;
40 
41   PetscOptionsBegin(comm, NULL, "Options for Freestream boundary condition", NULL);
42   PetscCall(PetscOptionsEnum("-freestream_riemann", "Riemann solver to use in freestream boundary condition", NULL, RiemannSolverTypes,
43                              (PetscEnum)riemann, (PetscEnum *)&riemann, NULL));
44   PetscCall(PetscOptionsScalar("-freestream_pressure", "Pressure at freestream condition", NULL, Y_inf.pressure, &Y_inf.pressure, NULL));
45   PetscInt narray = 3;
46   PetscCall(PetscOptionsScalarArray("-freestream_velocity", "Velocity at freestream condition", NULL, Y_inf.velocity, &narray, NULL));
47   PetscCall(PetscOptionsScalar("-freestream_temperature", "Temperature at freestream condition", NULL, Y_inf.temperature, &Y_inf.temperature, NULL));
48   PetscOptionsEnd();
49 
50   switch (user->phys->state_var) {
51     case STATEVAR_CONSERVATIVE:
52       switch (riemann) {
53         case RIEMANN_HLL:
54           problem->apply_freestream.qfunction              = Freestream_Conserv_HLL;
55           problem->apply_freestream.qfunction_loc          = Freestream_Conserv_HLL_loc;
56           problem->apply_freestream_jacobian.qfunction     = Freestream_Jacobian_Conserv_HLL;
57           problem->apply_freestream_jacobian.qfunction_loc = Freestream_Jacobian_Conserv_HLL_loc;
58           break;
59         case RIEMANN_HLLC:
60           problem->apply_freestream.qfunction              = Freestream_Conserv_HLLC;
61           problem->apply_freestream.qfunction_loc          = Freestream_Conserv_HLLC_loc;
62           problem->apply_freestream_jacobian.qfunction     = Freestream_Jacobian_Conserv_HLLC;
63           problem->apply_freestream_jacobian.qfunction_loc = Freestream_Jacobian_Conserv_HLLC_loc;
64           break;
65       }
66       break;
67     case STATEVAR_PRIMITIVE:
68       switch (riemann) {
69         case RIEMANN_HLL:
70           problem->apply_freestream.qfunction              = Freestream_Prim_HLL;
71           problem->apply_freestream.qfunction_loc          = Freestream_Prim_HLL_loc;
72           problem->apply_freestream_jacobian.qfunction     = Freestream_Jacobian_Prim_HLL;
73           problem->apply_freestream_jacobian.qfunction_loc = Freestream_Jacobian_Prim_HLL_loc;
74           break;
75         case RIEMANN_HLLC:
76           problem->apply_freestream.qfunction              = Freestream_Prim_HLLC;
77           problem->apply_freestream.qfunction_loc          = Freestream_Prim_HLLC_loc;
78           problem->apply_freestream_jacobian.qfunction     = Freestream_Jacobian_Prim_HLLC;
79           problem->apply_freestream_jacobian.qfunction_loc = Freestream_Jacobian_Prim_HLLC_loc;
80           break;
81       }
82       break;
83     case STATEVAR_ENTROPY:
84       switch (riemann) {
85         case RIEMANN_HLL:
86           problem->apply_freestream.qfunction              = Freestream_Entropy_HLL;
87           problem->apply_freestream.qfunction_loc          = Freestream_Entropy_HLL_loc;
88           problem->apply_freestream_jacobian.qfunction     = Freestream_Jacobian_Entropy_HLL;
89           problem->apply_freestream_jacobian.qfunction_loc = Freestream_Jacobian_Entropy_HLL_loc;
90           break;
91         case RIEMANN_HLLC:
92           problem->apply_freestream.qfunction              = Freestream_Entropy_HLLC;
93           problem->apply_freestream.qfunction_loc          = Freestream_Entropy_HLLC_loc;
94           problem->apply_freestream_jacobian.qfunction     = Freestream_Jacobian_Entropy_HLLC;
95           problem->apply_freestream_jacobian.qfunction_loc = Freestream_Jacobian_Entropy_HLLC_loc;
96           break;
97       }
98       break;
99   }
100 
101   Y_inf.pressure *= Pascal;
102   for (int i = 0; i < 3; i++) Y_inf.velocity[i] *= meter / second;
103   Y_inf.temperature *= Kelvin;
104 
105   State S_infty = StateFromPrimitive(newtonian_ig_ctx, Y_inf);
106 
107   // -- Set freestream_ctx struct values
108   PetscCall(PetscCalloc1(1, &freestream_ctx));
109   freestream_ctx->newtonian_ctx = *newtonian_ig_ctx;
110   freestream_ctx->S_infty       = S_infty;
111 
112   PetscCallCeed(ceed, CeedQFunctionContextCreate(user->ceed, &freestream_context));
113   PetscCallCeed(ceed, CeedQFunctionContextSetData(freestream_context, CEED_MEM_HOST, CEED_USE_POINTER, sizeof(*freestream_ctx), freestream_ctx));
114   PetscCallCeed(ceed, CeedQFunctionContextSetDataDestroy(freestream_context, CEED_MEM_HOST, FreeContextPetsc));
115   problem->apply_freestream.qfunction_context = freestream_context;
116   PetscCallCeed(ceed, CeedQFunctionContextReferenceCopy(freestream_context, &problem->apply_freestream_jacobian.qfunction_context));
117 
118   {
119     PetscBool run_unit_tests = PETSC_FALSE;
120 
121     PetscCall(PetscOptionsGetBool(NULL, NULL, "-riemann_solver_unit_tests", &run_unit_tests, NULL));
122     if (run_unit_tests) PetscCall(RiemannSolverUnitTests(newtonian_ig_ctx, 5e-7));
123   }
124   PetscFunctionReturn(PETSC_SUCCESS);
125 }
126 
127 static const char *const OutflowTypes[] = {"RIEMANN", "PRESSURE", "OutflowType", "OUTFLOW_", NULL};
128 typedef enum {
129   OUTFLOW_RIEMANN,
130   OUTFLOW_PRESSURE,
131 } OutflowType;
132 
133 PetscErrorCode OutflowBCSetup(ProblemData problem, DM dm, void *ctx, NewtonianIdealGasContext newtonian_ig_ctx, const StatePrimitive *reference) {
134   User                 user = *(User *)ctx;
135   Ceed                 ceed = user->ceed;
136   OutflowContext       outflow_ctx;
137   OutflowType          outflow_type = OUTFLOW_RIEMANN;
138   CeedQFunctionContext outflow_context;
139   const PetscScalar    Kelvin = user->units->Kelvin;
140   const PetscScalar    Pascal = user->units->Pascal;
141 
142   PetscFunctionBeginUser;
143   CeedScalar pressure    = reference->pressure / Pascal;
144   CeedScalar temperature = reference->temperature / Kelvin;
145   CeedScalar recirc = 1, softplus_velocity = 1e-2;
146   PetscOptionsBegin(user->comm, NULL, "Options for Outflow boundary condition", NULL);
147   PetscCall(
148       PetscOptionsEnum("-outflow_type", "Type of outflow condition", NULL, OutflowTypes, (PetscEnum)outflow_type, (PetscEnum *)&outflow_type, NULL));
149   PetscCall(PetscOptionsScalar("-outflow_pressure", "Pressure at outflow condition", NULL, pressure, &pressure, NULL));
150   if (outflow_type == OUTFLOW_RIEMANN) {
151     PetscCall(PetscOptionsScalar("-outflow_temperature", "Temperature at outflow condition", NULL, temperature, &temperature, NULL));
152     PetscCall(
153         PetscOptionsReal("-outflow_recirc", "Fraction of recirculation to allow in exterior velocity state [0,1]", NULL, recirc, &recirc, NULL));
154     PetscCall(PetscOptionsReal("-outflow_softplus_velocity", "Characteristic velocity of softplus regularization", NULL, softplus_velocity,
155                                &softplus_velocity, NULL));
156   }
157   PetscOptionsEnd();
158   pressure *= Pascal;
159   temperature *= Kelvin;
160 
161   switch (outflow_type) {
162     case OUTFLOW_RIEMANN:
163       switch (user->phys->state_var) {
164         case STATEVAR_CONSERVATIVE:
165           problem->apply_outflow.qfunction              = RiemannOutflow_Conserv;
166           problem->apply_outflow.qfunction_loc          = RiemannOutflow_Conserv_loc;
167           problem->apply_outflow_jacobian.qfunction     = RiemannOutflow_Jacobian_Conserv;
168           problem->apply_outflow_jacobian.qfunction_loc = RiemannOutflow_Jacobian_Conserv_loc;
169           break;
170         case STATEVAR_PRIMITIVE:
171           problem->apply_outflow.qfunction              = RiemannOutflow_Prim;
172           problem->apply_outflow.qfunction_loc          = RiemannOutflow_Prim_loc;
173           problem->apply_outflow_jacobian.qfunction     = RiemannOutflow_Jacobian_Prim;
174           problem->apply_outflow_jacobian.qfunction_loc = RiemannOutflow_Jacobian_Prim_loc;
175           break;
176         case STATEVAR_ENTROPY:
177           problem->apply_outflow.qfunction              = RiemannOutflow_Entropy;
178           problem->apply_outflow.qfunction_loc          = RiemannOutflow_Entropy_loc;
179           problem->apply_outflow_jacobian.qfunction     = RiemannOutflow_Jacobian_Entropy;
180           problem->apply_outflow_jacobian.qfunction_loc = RiemannOutflow_Jacobian_Entropy_loc;
181           break;
182       }
183       break;
184     case OUTFLOW_PRESSURE:
185       switch (user->phys->state_var) {
186         case STATEVAR_CONSERVATIVE:
187           problem->apply_outflow.qfunction              = PressureOutflow_Conserv;
188           problem->apply_outflow.qfunction_loc          = PressureOutflow_Conserv_loc;
189           problem->apply_outflow_jacobian.qfunction     = PressureOutflow_Jacobian_Conserv;
190           problem->apply_outflow_jacobian.qfunction_loc = PressureOutflow_Jacobian_Conserv_loc;
191           break;
192         case STATEVAR_PRIMITIVE:
193           problem->apply_outflow.qfunction              = PressureOutflow_Prim;
194           problem->apply_outflow.qfunction_loc          = PressureOutflow_Prim_loc;
195           problem->apply_outflow_jacobian.qfunction     = PressureOutflow_Jacobian_Prim;
196           problem->apply_outflow_jacobian.qfunction_loc = PressureOutflow_Jacobian_Prim_loc;
197           break;
198         case STATEVAR_ENTROPY:
199           problem->apply_outflow.qfunction              = PressureOutflow_Entropy;
200           problem->apply_outflow.qfunction_loc          = PressureOutflow_Entropy_loc;
201           problem->apply_outflow_jacobian.qfunction     = PressureOutflow_Jacobian_Entropy;
202           problem->apply_outflow_jacobian.qfunction_loc = PressureOutflow_Jacobian_Entropy_loc;
203           break;
204       }
205       break;
206   }
207   PetscCall(PetscCalloc1(1, &outflow_ctx));
208   outflow_ctx->gas               = *newtonian_ig_ctx;
209   outflow_ctx->recirc            = recirc;
210   outflow_ctx->softplus_velocity = softplus_velocity;
211   outflow_ctx->pressure          = pressure;
212   outflow_ctx->temperature       = temperature;
213 
214   PetscCallCeed(ceed, CeedQFunctionContextCreate(user->ceed, &outflow_context));
215   PetscCallCeed(ceed, CeedQFunctionContextSetData(outflow_context, CEED_MEM_HOST, CEED_USE_POINTER, sizeof(*outflow_ctx), outflow_ctx));
216   PetscCallCeed(ceed, CeedQFunctionContextSetDataDestroy(outflow_context, CEED_MEM_HOST, FreeContextPetsc));
217   problem->apply_outflow.qfunction_context = outflow_context;
218   PetscCallCeed(ceed, CeedQFunctionContextReferenceCopy(outflow_context, &problem->apply_outflow_jacobian.qfunction_context));
219   PetscFunctionReturn(PETSC_SUCCESS);
220 }
221 
222 // @brief Calculate relative error, (A - B) / S
223 // If S < threshold, then set S=1
224 static inline CeedScalar RelativeError(CeedScalar S, CeedScalar A, CeedScalar B, CeedScalar threshold) {
225   return (A - B) / (fabs(S) > threshold ? S : 1);
226 }
227 
228 // @brief Check errors of a State vector and print if above tolerance
229 static PetscErrorCode CheckQWithTolerance(const CeedScalar Q_s[5], const CeedScalar Q_a[5], const CeedScalar Q_b[5], const char *name,
230                                           PetscReal rtol_0, PetscReal rtol_u, PetscReal rtol_4) {
231   CeedScalar relative_error[5];  // relative error
232   CeedScalar divisor_threshold = 10 * CEED_EPSILON;
233 
234   PetscFunctionBeginUser;
235   relative_error[0] = RelativeError(Q_s[0], Q_a[0], Q_b[0], divisor_threshold);
236   relative_error[4] = RelativeError(Q_s[4], Q_a[4], Q_b[4], divisor_threshold);
237 
238   CeedScalar u_magnitude = sqrt(Square(Q_s[1]) + Square(Q_s[2]) + Square(Q_s[3]));
239   for (int i = 1; i < 4; i++) {
240     relative_error[i] = RelativeError(u_magnitude, Q_a[i], Q_b[i], divisor_threshold);
241   }
242 
243   if (fabs(relative_error[0]) >= rtol_0) {
244     printf("%s[0] error %g (expected %.10e, got %.10e)\n", name, relative_error[0], Q_s[0], Q_a[0]);
245   }
246   for (int i = 1; i < 4; i++) {
247     if (fabs(relative_error[i]) >= rtol_u) {
248       printf("%s[%d] error %g (expected %.10e, got %.10e)\n", name, i, relative_error[i], Q_s[i], Q_a[i]);
249     }
250   }
251   if (fabs(relative_error[4]) >= rtol_4) {
252     printf("%s[4] error %g (expected %.10e, got %.10e)\n", name, relative_error[4], Q_s[4], Q_a[4]);
253   }
254   PetscFunctionReturn(PETSC_SUCCESS);
255 }
256 
257 // @brief Verify RiemannFlux_HLL_fwd function against finite-difference approximation
258 static PetscErrorCode TestRiemannHLL_fwd(NewtonianIdealGasContext gas, CeedScalar rtol_0, CeedScalar rtol_u, CeedScalar rtol_4) {
259   CeedScalar       eps = 4e-7;  // Finite difference step
260   char             buf[128];
261   const CeedScalar T           = 200;
262   const CeedScalar rho         = 1.2;
263   const CeedScalar p           = (HeatCapacityRatio(gas) - 1) * rho * gas->cv * T;
264   const CeedScalar u_base      = 40;
265   const CeedScalar u[3]        = {u_base, u_base * 1.1, u_base * 1.2};
266   const CeedScalar Y0_left[5]  = {p, u[0], u[1], u[2], T};
267   const CeedScalar Y0_right[5] = {1.2 * p, 1.2 * u[0], 1.2 * u[1], 1.2 * u[2], 1.2 * T};
268   CeedScalar       normal[3]   = {1, 2, 3};
269 
270   PetscFunctionBeginUser;
271   State left0  = StateFromY(gas, Y0_left);
272   State right0 = StateFromY(gas, Y0_right);
273   ScaleN(normal, 1 / sqrt(Dot3(normal, normal)), 3);
274 
275   for (int i = 0; i < 10; i++) {
276     CeedScalar dFlux[5] = {0.}, dFlux_fd[5] = {0.};
277     {  // Calculate dFlux using *_fwd function
278       CeedScalar dY_right[5] = {0};
279       CeedScalar dY_left[5]  = {0};
280 
281       if (i < 5) {
282         dY_left[i] = Y0_left[i];
283       } else {
284         dY_right[i % 5] = Y0_right[i % 5];
285       }
286       State dleft0  = StateFromY_fwd(gas, left0, dY_left);
287       State dright0 = StateFromY_fwd(gas, right0, dY_right);
288 
289       StateConservative dFlux_state = RiemannFlux_HLL_fwd(gas, left0, dleft0, right0, dright0, normal);
290       UnpackState_U(dFlux_state, dFlux);
291     }
292 
293     {  // Calculate dFlux_fd via finite difference approximation
294       CeedScalar Y1_left[5]  = {Y0_left[0], Y0_left[1], Y0_left[2], Y0_left[3], Y0_left[4]};
295       CeedScalar Y1_right[5] = {Y0_right[0], Y0_right[1], Y0_right[2], Y0_right[3], Y0_right[4]};
296       CeedScalar Flux0[5], Flux1[5];
297 
298       if (i < 5) {
299         Y1_left[i] *= 1 + eps;
300       } else {
301         Y1_right[i % 5] *= 1 + eps;
302       }
303       State left1  = StateFromY(gas, Y1_left);
304       State right1 = StateFromY(gas, Y1_right);
305 
306       StateConservative Flux0_state = RiemannFlux_HLL(gas, left0, right0, normal);
307       StateConservative Flux1_state = RiemannFlux_HLL(gas, left1, right1, normal);
308       UnpackState_U(Flux0_state, Flux0);
309       UnpackState_U(Flux1_state, Flux1);
310       for (int j = 0; j < 5; j++) dFlux_fd[j] = (Flux1[j] - Flux0[j]) / eps;
311     }
312 
313     snprintf(buf, sizeof buf, "RiemannFlux_HLL i=%d: Flux", i);
314     PetscCall(CheckQWithTolerance(dFlux_fd, dFlux, dFlux_fd, buf, rtol_0, rtol_u, rtol_4));
315   }
316   PetscFunctionReturn(PETSC_SUCCESS);
317 }
318 
319 // @brief Verify RiemannFlux_HLLC_fwd function against finite-difference approximation
320 static PetscErrorCode TestRiemannHLLC_fwd(NewtonianIdealGasContext gas, CeedScalar rtol_0, CeedScalar rtol_u, CeedScalar rtol_4) {
321   CeedScalar       eps = 4e-7;  // Finite difference step
322   char             buf[128];
323   const CeedScalar T           = 200;
324   const CeedScalar rho         = 1.2;
325   const CeedScalar p           = (HeatCapacityRatio(gas) - 1) * rho * gas->cv * T;
326   const CeedScalar u_base      = 40;
327   const CeedScalar u[3]        = {u_base, u_base * 1.1, u_base * 1.2};
328   const CeedScalar Y0_left[5]  = {p, u[0], u[1], u[2], T};
329   const CeedScalar Y0_right[5] = {1.2 * p, 1.2 * u[0], 1.2 * u[1], 1.2 * u[2], 1.2 * T};
330   CeedScalar       normal[3]   = {1, 2, 3};
331 
332   PetscFunctionBeginUser;
333   State left0  = StateFromY(gas, Y0_left);
334   State right0 = StateFromY(gas, Y0_right);
335   ScaleN(normal, 1 / sqrt(Dot3(normal, normal)), 3);
336 
337   for (int i = 0; i < 10; i++) {
338     CeedScalar dFlux[5] = {0.}, dFlux_fd[5] = {0.};
339     {  // Calculate dFlux using *_fwd function
340       CeedScalar dY_right[5] = {0};
341       CeedScalar dY_left[5]  = {0};
342 
343       if (i < 5) {
344         dY_left[i] = Y0_left[i];
345       } else {
346         dY_right[i % 5] = Y0_right[i % 5];
347       }
348       State dleft0  = StateFromY_fwd(gas, left0, dY_left);
349       State dright0 = StateFromY_fwd(gas, right0, dY_right);
350 
351       StateConservative dFlux_state = RiemannFlux_HLLC_fwd(gas, left0, dleft0, right0, dright0, normal);
352       UnpackState_U(dFlux_state, dFlux);
353     }
354 
355     {  // Calculate dFlux_fd via finite difference approximation
356       CeedScalar Y1_left[5]  = {Y0_left[0], Y0_left[1], Y0_left[2], Y0_left[3], Y0_left[4]};
357       CeedScalar Y1_right[5] = {Y0_right[0], Y0_right[1], Y0_right[2], Y0_right[3], Y0_right[4]};
358       CeedScalar Flux0[5], Flux1[5];
359 
360       if (i < 5) {
361         Y1_left[i] *= 1 + eps;
362       } else {
363         Y1_right[i % 5] *= 1 + eps;
364       }
365       State left1  = StateFromY(gas, Y1_left);
366       State right1 = StateFromY(gas, Y1_right);
367 
368       StateConservative Flux0_state = RiemannFlux_HLLC(gas, left0, right0, normal);
369       StateConservative Flux1_state = RiemannFlux_HLLC(gas, left1, right1, normal);
370       UnpackState_U(Flux0_state, Flux0);
371       UnpackState_U(Flux1_state, Flux1);
372       for (int j = 0; j < 5; j++) dFlux_fd[j] = (Flux1[j] - Flux0[j]) / eps;
373     }
374 
375     snprintf(buf, sizeof buf, "RiemannFlux_HLLC i=%d: Flux", i);
376     PetscCall(CheckQWithTolerance(dFlux_fd, dFlux, dFlux_fd, buf, rtol_0, rtol_u, rtol_4));
377   }
378   PetscFunctionReturn(PETSC_SUCCESS);
379 }
380 
381 // @brief Verify ComputeHLLSpeeds_Roe_fwd function against finite-difference approximation
382 static PetscErrorCode TestComputeHLLSpeeds_Roe_fwd(NewtonianIdealGasContext gas, CeedScalar rtol) {
383   CeedScalar       eps = 4e-7;  // Finite difference step
384   char             buf[128];
385   const CeedScalar T           = 200;
386   const CeedScalar rho         = 1.2;
387   const CeedScalar p           = (HeatCapacityRatio(gas) - 1) * rho * gas->cv * T;
388   const CeedScalar u_base      = 40;
389   const CeedScalar u[3]        = {u_base, u_base * 1.1, u_base * 1.2};
390   const CeedScalar Y0_left[5]  = {p, u[0], u[1], u[2], T};
391   const CeedScalar Y0_right[5] = {1.2 * p, 1.2 * u[0], 1.2 * u[1], 1.2 * u[2], 1.2 * T};
392   CeedScalar       normal[3]   = {1, 2, 3};
393 
394   PetscFunctionBeginUser;
395   State left0  = StateFromY(gas, Y0_left);
396   State right0 = StateFromY(gas, Y0_right);
397   ScaleN(normal, 1 / sqrt(Dot3(normal, normal)), 3);
398   CeedScalar u_left0  = Dot3(left0.Y.velocity, normal);
399   CeedScalar u_right0 = Dot3(right0.Y.velocity, normal);
400 
401   for (int i = 0; i < 10; i++) {
402     CeedScalar ds_left, ds_right, ds_left_fd, ds_right_fd;
403     {  // Calculate ds_{left,right} using *_fwd function
404       CeedScalar dY_right[5] = {0};
405       CeedScalar dY_left[5]  = {0};
406 
407       if (i < 5) {
408         dY_left[i] = Y0_left[i];
409       } else {
410         dY_right[i % 5] = Y0_right[i % 5];
411       }
412       State      dleft0   = StateFromY_fwd(gas, left0, dY_left);
413       State      dright0  = StateFromY_fwd(gas, right0, dY_right);
414       CeedScalar du_left  = Dot3(dleft0.Y.velocity, normal);
415       CeedScalar du_right = Dot3(dright0.Y.velocity, normal);
416 
417       CeedScalar s_left, s_right;  // Throw away
418       ComputeHLLSpeeds_Roe_fwd(gas, left0, dleft0, u_left0, du_left, right0, dright0, u_right0, du_right, &s_left, &ds_left, &s_right, &ds_right);
419     }
420 
421     {  // Calculate ds_{left,right}_fd via finite difference approximation
422       CeedScalar Y1_left[5]  = {Y0_left[0], Y0_left[1], Y0_left[2], Y0_left[3], Y0_left[4]};
423       CeedScalar Y1_right[5] = {Y0_right[0], Y0_right[1], Y0_right[2], Y0_right[3], Y0_right[4]};
424 
425       if (i < 5) {
426         Y1_left[i] *= 1 + eps;
427       } else {
428         Y1_right[i % 5] *= 1 + eps;
429       }
430       State      left1    = StateFromY(gas, Y1_left);
431       State      right1   = StateFromY(gas, Y1_right);
432       CeedScalar u_left1  = Dot3(left1.Y.velocity, normal);
433       CeedScalar u_right1 = Dot3(right1.Y.velocity, normal);
434 
435       CeedScalar s_left0, s_right0, s_left1, s_right1;
436       ComputeHLLSpeeds_Roe(gas, left0, u_left0, right0, u_right0, &s_left0, &s_right0);
437       ComputeHLLSpeeds_Roe(gas, left1, u_left1, right1, u_right1, &s_left1, &s_right1);
438       ds_left_fd  = (s_left1 - s_left0) / eps;
439       ds_right_fd = (s_right1 - s_right0) / eps;
440     }
441 
442     snprintf(buf, sizeof buf, "ComputeHLLSpeeds_Roe i=%d:", i);
443     {
444       CeedScalar divisor_threshold = 10 * CEED_EPSILON;
445       CeedScalar ds_left_err, ds_right_err;
446 
447       ds_left_err  = RelativeError(ds_left_fd, ds_left, ds_left_fd, divisor_threshold);
448       ds_right_err = RelativeError(ds_right_fd, ds_right, ds_right_fd, divisor_threshold);
449       if (fabs(ds_left_err) >= rtol) printf("%s ds_left error %g (expected %.10e, got %.10e)\n", buf, ds_left_err, ds_left_fd, ds_left);
450       if (fabs(ds_right_err) >= rtol) printf("%s ds_right error %g (expected %.10e, got %.10e)\n", buf, ds_right_err, ds_right_fd, ds_right);
451     }
452   }
453   PetscFunctionReturn(PETSC_SUCCESS);
454 }
455 
456 // @brief Verify TotalSpecificEnthalpy_fwd function against finite-difference approximation
457 static PetscErrorCode TestTotalSpecificEnthalpy_fwd(NewtonianIdealGasContext gas, CeedScalar rtol) {
458   CeedScalar       eps = 4e-7;  // Finite difference step
459   char             buf[128];
460   const CeedScalar T      = 200;
461   const CeedScalar rho    = 1.2;
462   const CeedScalar p      = (HeatCapacityRatio(gas) - 1) * rho * gas->cv * T;
463   const CeedScalar u_base = 40;
464   const CeedScalar u[3]   = {u_base, u_base * 1.1, u_base * 1.2};
465   const CeedScalar Y0[5]  = {p, u[0], u[1], u[2], T};
466 
467   PetscFunctionBeginUser;
468   State state0 = StateFromY(gas, Y0);
469 
470   for (int i = 0; i < 5; i++) {
471     CeedScalar dH, dH_fd;
472     {  // Calculate dH using *_fwd function
473       CeedScalar dY[5] = {0};
474 
475       dY[i]         = Y0[i];
476       State dstate0 = StateFromY_fwd(gas, state0, dY);
477       dH            = TotalSpecificEnthalpy_fwd(gas, state0, dstate0);
478     }
479 
480     {  // Calculate dH_fd via finite difference approximation
481       CeedScalar H0, H1;
482       CeedScalar Y1[5] = {Y0[0], Y0[1], Y0[2], Y0[3], Y0[4]};
483       Y1[i] *= 1 + eps;
484       State state1 = StateFromY(gas, Y1);
485 
486       H0    = TotalSpecificEnthalpy(gas, state0);
487       H1    = TotalSpecificEnthalpy(gas, state1);
488       dH_fd = (H1 - H0) / eps;
489     }
490 
491     snprintf(buf, sizeof buf, "TotalSpecificEnthalpy i=%d:", i);
492     {
493       CeedScalar divisor_threshold = 10 * CEED_EPSILON;
494       CeedScalar dH_err;
495 
496       dH_err = RelativeError(dH_fd, dH, dH_fd, divisor_threshold);
497       if (fabs(dH_err) >= rtol) printf("%s dH error %g (expected %.10e, got %.10e)\n", buf, dH_err, dH_fd, dH);
498     }
499   }
500   PetscFunctionReturn(PETSC_SUCCESS);
501 }
502 
503 // @brief Verify RoeSetup_fwd function against finite-difference approximation
504 static PetscErrorCode TestRowSetup_fwd(NewtonianIdealGasContext gas, CeedScalar rtol) {
505   CeedScalar       eps = 4e-7;  // Finite difference step
506   char             buf[128];
507   const CeedScalar rho0[2] = {1.2, 1.4};
508 
509   PetscFunctionBeginUser;
510   for (int i = 0; i < 2; i++) {
511     RoeWeights dR, dR_fd;
512     {  // Calculate using *_fwd function
513       CeedScalar drho[5] = {0};
514 
515       drho[i] = rho0[i];
516       dR      = RoeSetup_fwd(rho0[0], rho0[1], drho[0], drho[1]);
517     }
518 
519     {  // Calculate via finite difference approximation
520       RoeWeights R0, R1;
521       CeedScalar rho1[5] = {rho0[0], rho0[1]};
522       rho1[i] *= 1 + eps;
523 
524       R0          = RoeSetup(rho0[0], rho0[1]);
525       R1          = RoeSetup(rho1[0], rho1[1]);
526       dR_fd.left  = (R1.left - R0.left) / eps;
527       dR_fd.right = (R1.right - R0.right) / eps;
528     }
529 
530     snprintf(buf, sizeof buf, "RoeSetup i=%d:", i);
531     {
532       CeedScalar divisor_threshold = 10 * CEED_EPSILON;
533       RoeWeights dR_err;
534 
535       dR_err.left  = RelativeError(dR_fd.left, dR.left, dR_fd.left, divisor_threshold);
536       dR_err.right = RelativeError(dR_fd.right, dR.right, dR_fd.right, divisor_threshold);
537       if (fabs(dR_err.left) >= rtol) printf("%s dR.left error %g (expected %.10e, got %.10e)\n", buf, dR_err.left, dR_fd.left, dR.left);
538       if (fabs(dR_err.right) >= rtol) printf("%s dR.right error %g (expected %.10e, got %.10e)\n", buf, dR_err.right, dR_fd.right, dR.right);
539     }
540   }
541   PetscFunctionReturn(PETSC_SUCCESS);
542 }
543 
544 // @brief Test Riemann solver related `*_fwd` functions via finite-difference approximation
545 static PetscErrorCode RiemannSolverUnitTests(NewtonianIdealGasContext gas, CeedScalar rtol) {
546   PetscFunctionBeginUser;
547   PetscCall(TestRiemannHLL_fwd(gas, rtol, rtol, rtol));
548   PetscCall(TestRiemannHLLC_fwd(gas, rtol, rtol, rtol));
549   PetscCall(TestComputeHLLSpeeds_Roe_fwd(gas, rtol));
550   PetscCall(TestTotalSpecificEnthalpy_fwd(gas, rtol));
551   PetscCall(TestRowSetup_fwd(gas, rtol));
552   PetscFunctionReturn(PETSC_SUCCESS);
553 }
554