Skip to content

Commit 5c58762

Browse files
DanielDoehringjlchanranocha
authored
Thermally perfect gas as an AbstractEquationOfState (#3079)
* start thermally perfect with other EoS * cont * test and elixir * adaps * coverage * fmt * fix * bowshock * tols * hll * Update examples/p4est_2d_dgsem/elixir_euler_therm_perf_cylinder_bowshock_mach6.jl * adapt structure * fix examples * store gamma * rm copy paste * rm * update * typo * Update src/equations/equation_of_state_thermally_perfect_gas.jl Co-authored-by: Jesse Chan <1156048+jlchan@users.noreply.github.com> * Update src/equations/equation_of_state_thermally_perfect_gas.jl Co-authored-by: Jesse Chan <1156048+jlchan@users.noreply.github.com> * Update src/equations/equation_of_state_thermally_perfect_gas.jl Co-authored-by: Jesse Chan <1156048+jlchan@users.noreply.github.com> * move * default values for air * move * elaborate * unit tests * FIX * test cylidner * docstrings, warn * add `Base.similar` * revert wawrn * note * Update examples/p4est_2d_dgsem/elixir_euler_therm_perf_cylinder_bowshock_mach6.jl * Update test/test_unit.jl Co-authored-by: Jesse Chan <1156048+jlchan@users.noreply.github.com> * news, mention calorically imperfect * Update NEWS.md * Update test/test_unit.jl * Apply suggestion from @jlchan format * fmt * bb * type test * more * change tol --------- Co-authored-by: Jesse Chan <1156048+jlchan@users.noreply.github.com> Co-authored-by: Hendrik Ranocha <ranocha@users.noreply.github.com>
1 parent 1a70af5 commit 5c58762

10 files changed

Lines changed: 810 additions & 4 deletions

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ for human readability.
3030
The new function `temperature_given_Vp` computes temperature given the specific volume `V` and pressure `p` using Newton's method,
3131
analogous to the existing `temperature` function for `AbstractEquationOfState` which takes in `V` and specific internal energy `e_internal` ([#3093]).
3232
- Added support to apply the positivity-preserving limiter after coarsening and refinement steps in AMR via the keyword argument `limiter!` in `AMRCallback` ([#2396]).
33+
- To model a calorically imperfect but thermally perfect gas, a new equation of state `ThermallyPerfectGas9PolyFit` has been added.
34+
This is a concrete implementation for `AbstractThermallyPerfectGas` that uses a 9th order polynomial fit to the NASA polynomials for specific heat capacities, as described in the corresponding [NASA Technical Publication](https://ntrs.nasa.gov/citations/20020085330).
35+
This EOS allows for temperature-dependent specific heat capacities (`c_p(T)`, `c_v(T)`) and ratio of specific heats (`\gamma(T)`), while obeying the ideal gas law to relate pressure, density, and temperature ([#3079]).
36+
This equation of state needs to be supplied to `NonIdealCompressibleEulerEquations`.
3337

3438
#### Changed
3539
- For performance, `LaplaceDiffusionEntropyVariables` parabolic fluxes for `CompressibleEulerEquations1D`, `CompressibleEulerEquations2D`, and `CompressibleEulerEquations3D` now use explicit Jacobian formulas from Barth 1999 instead of AD ([#3028]). Other equation types continue to use an automatic differentiation fallback.
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
using Trixi
2+
using OrdinaryDiffEqSSPRK
3+
4+
###############################################################################
5+
# Geometry & boundary conditions
6+
7+
# Mapping to create a "close-up" mesh around the second quadrant of a cylinder,
8+
# implemented by Georgii Oblapenko. If you use this in your own work, please cite:
9+
#
10+
# - G. Oblapenko and A. Tarnovskiy (2024)
11+
# Reproducibility Repository for the paper:
12+
# Entropy-stable fluxes for high-order Discontinuous Galerkin simulations of high-enthalpy flows.
13+
# [DOI: 10.5281/zenodo.13981615](https://doi.org/10.5281/zenodo.13981615)
14+
# [GitHub](https://github.com/knstmrd/paper_ec_trixi_chem)
15+
#
16+
# as well as the corresponding paper:
17+
# - G. Oblapenko and M. Torrilhon (2025)
18+
# Entropy-conservative high-order methods for high-enthalpy gas flows.
19+
# Computers & Fluids, 2025.
20+
# [DOI: 10.1016/j.compfluid.2025.106640](https://doi.org/10.1016/j.compfluid.2025.106640)
21+
#
22+
# The mapping produces the following geometry & shock (indicated by the asterisks `* `):
23+
# ____x_neg____
24+
# | |
25+
# | |
26+
# | |
27+
# | * |
28+
# | * y
29+
# | Inflow * _
30+
# | state * p
31+
# x * o
32+
# _ * s
33+
# n * |
34+
# e * |
35+
# g Shock .
36+
# | * .
37+
# | * . <- x_pos
38+
# | * .
39+
# | * . (Cylinder)
40+
# |_______y_neg_______.
41+
function mapping_cylinder_shock_fitted(xi_, eta_,
42+
cylinder_radius, spline_points)
43+
shock_shape = [
44+
(spline_points[1], 0.0), # Shock position on the stagnation line (`y_neg`, y = 0)
45+
(spline_points[2], spline_points[2]), # Shock position at -45° angle
46+
(0.0, spline_points[3]) # Shock position at outflow (`y_pos`, x = x_max)
47+
] # 3 points that define the geometry of the mesh which follows the shape of the shock (known a-priori)
48+
R = [sqrt(shock_shape[i][1]^2 + shock_shape[i][2]^2) for i in 1:3] # 3 radii
49+
50+
# Construct spline with form R[1] + c2 * eta_01^2 + c3 * eta_01^3,
51+
# chosen such that derivative w.r.t eta_01 is 0 at eta_01 = 0 such that
52+
# we have symmetry along the stagnation line (`y_neg`, y = 0).
53+
#
54+
# A single cubic spline doesn't fit the shock perfectly,
55+
# but is the simplest curve that does a reasonable job and it also can be easily computed analytically.
56+
# The choice of points on the stagnation line and outflow region is somewhat self-evident
57+
# (capture the minimum and maximum extent of the shock stand-off),
58+
# and the point at the 45 degree angle seemed the most logical to add
59+
# since it only requires one additional value (and not two),
60+
# simplifies the math a bit, and the angle lies exactly in between the other angles.
61+
spline_matrix = [1.0 1.0; 0.25 0.125]
62+
spline_RHS = [R[3] - R[1], R[2] - R[1]]
63+
spline_coeffs = spline_matrix \ spline_RHS # c2, c3
64+
65+
eta_01 = (eta_ + 1) / 2 # Transform `eta_` in [-1, 1] to `eta_01` in [0, 1]
66+
# "Flip" `xi_` in [-1, 1] to `xi_01` in [0, 1] since
67+
# shock positions where originally for first quadrant, here we use second quadrant
68+
xi_01 = (-xi_ + 1) / 2
69+
70+
R_outer = R[1] + spline_coeffs[1] * eta_01^2 + spline_coeffs[2] * eta_01^3
71+
72+
angle = -π / 4 + eta_ * π / 4 # Angle runs from -90° to 0°
73+
r = (cylinder_radius + xi_01 * (R_outer - cylinder_radius))
74+
75+
return SVector(round(r * sin(angle); digits = 8), round(r * cos(angle); digits = 8))
76+
end
77+
78+
@inline function initial_condition_mach6_flow(x, t,
79+
equations::NonIdealCompressibleEulerEquations2D)
80+
RealT = eltype(x)
81+
eos = equations.equation_of_state
82+
83+
# Freestream conditions at 40 km altitude
84+
rho = 0.00385101 # [kg/m^3]
85+
V = inv(rho) # [m^3/kg]
86+
87+
p = 277.522 # [Pa]
88+
89+
# invert for temperature given p, V
90+
T = temperature_given_Vp(V, p, eos; initial_T = 251.05, # [K]
91+
tol = 100 * eps(RealT), maxiter = 100)
92+
93+
a = Trixi.speed_of_sound(V, T, eos)
94+
M = 6.0 # [1]
95+
v1 = M * a # [m/s]
96+
v2 = 0.0 # [m/s]
97+
98+
return thermo2cons(SVector(V, v1, v2, T), equations)
99+
end
100+
101+
@inline function boundary_condition_supersonic_inflow(u_inner,
102+
normal_direction::AbstractVector,
103+
x, t,
104+
surface_flux_function,
105+
equations)
106+
u_boundary = initial_condition_mach6_flow(x, t, equations)
107+
return flux(u_boundary, normal_direction, equations)
108+
end
109+
110+
# For physical significance of boundary conditions, see sketch at `mapping_cylinder_shock_fitted`
111+
boundary_conditions = (; x_neg = boundary_condition_supersonic_inflow, # Supersonic inflow
112+
y_neg = boundary_condition_slip_wall, # Induce symmetry by slip wall
113+
y_pos = boundary_condition_do_nothing, # Free outflow
114+
x_pos = boundary_condition_slip_wall) # Cylinder
115+
116+
###############################################################################
117+
# Equations, mesh and solver
118+
119+
# The default values correspond to air with temperature bounds/intervals [200.0, 1000.0, 6000.0], see
120+
# https://ntrs.nasa.gov/api/citations/20020085330/downloads/20020085330.pdf
121+
eos = ThermallyPerfectGas9PolyFit()
122+
123+
equations = NonIdealCompressibleEulerEquations2D(eos)
124+
125+
# Reduce tolerance to speed things up (otherwise, `eos_newton_maxiter` would need to be increased)
126+
Trixi.eos_newton_tol(eos::ThermallyPerfectGas9PolyFit) = 1e-5
127+
128+
polydeg = 3
129+
basis = LobattoLegendreBasis(polydeg)
130+
131+
surface_flux = flux_lax_friedrichs
132+
volume_flux = flux_terashima_etal
133+
134+
shock_indicator = IndicatorHennemannGassner(equations, basis,
135+
alpha_max = 1.0,
136+
alpha_min = 0.001,
137+
alpha_smooth = true,
138+
variable = density_pressure)
139+
volume_integral = VolumeIntegralShockCapturingHG(shock_indicator;
140+
volume_flux_dg = volume_flux,
141+
volume_flux_fv = surface_flux)
142+
143+
solver = DGSEM(polydeg = polydeg, surface_flux = surface_flux,
144+
volume_integral = volume_integral)
145+
146+
trees_per_dimension = (20, 16)
147+
148+
cylinder_radius = 0.5
149+
# Follow from a-priori known shock shape, originally for first qaudrant,
150+
# here transformed to second quadrant, see `mapping_cylinder_shock_fitted`.
151+
spline_points = 0.6 .* [1.32, 1.05, 2.25]
152+
cylinder_mapping = (xi, eta) -> mapping_cylinder_shock_fitted(xi, eta,
153+
cylinder_radius,
154+
spline_points)
155+
156+
mesh = P4estMesh(trees_per_dimension,
157+
polydeg = polydeg,
158+
mapping = cylinder_mapping,
159+
periodicity = false)
160+
161+
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition_mach6_flow, solver;
162+
boundary_conditions = boundary_conditions)
163+
164+
###############################################################################
165+
# Semidiscretization & callbacks
166+
167+
tspan = (0.0, 1e-3)
168+
ode = semidiscretize(semi, tspan)
169+
170+
summary_callback = SummaryCallback()
171+
172+
analysis_callback = AnalysisCallback(semi, interval = 5000)
173+
alive_callback = AliveCallback(alive_interval = 200)
174+
175+
# Add `:gamma` to `extra_node_variables` tuple ...
176+
extra_node_variables = (:gamma,)
177+
178+
# ... and specify the function `get_node_variable` for this symbol,
179+
# with first argument matching the symbol (turned into a type via `Val`) for dispatching.
180+
function Trixi.get_node_variable(::Val{:gamma}, u, mesh, equations, dg, cache)
181+
n_nodes = nnodes(dg)
182+
n_elements = nelements(dg, cache)
183+
# By definition, the variable must be provided at every node of every element!
184+
# Otherwise, the `SaveSolutionCallback` will crash.
185+
gamma_array = zeros(eltype(cache.elements),
186+
n_nodes, n_nodes, # equivalent: `ntuple(_ -> n_nodes, ndims(mesh))...,`
187+
n_elements)
188+
189+
eos = equations.equation_of_state
190+
191+
# We can accelerate the computation by thread-parallelizing the loop over elements
192+
# by using the `@threaded` macro.
193+
Trixi.@threaded for element in eachelement(dg, cache)
194+
for j in eachnode(dg), i in eachnode(dg)
195+
u_node = get_node_vars(u, equations, dg, i, j, element)
196+
197+
# Get temperature
198+
u_node_thermo = cons2thermo(u_node, equations)
199+
T = u_node_thermo[4]
200+
201+
gamma_array[i, j, element] = Trixi.gamma(T, eos)
202+
end
203+
end
204+
205+
return gamma_array
206+
end
207+
208+
save_solution = SaveSolutionCallback(interval = 5000,
209+
solution_variables = cons2thermo,
210+
extra_node_variables = extra_node_variables)
211+
212+
amr_controller = ControllerThreeLevel(semi, shock_indicator;
213+
base_level = 0,
214+
med_level = 1, med_threshold = 0.175,
215+
max_level = 2, max_threshold = 0.35)
216+
217+
amr_callback = AMRCallback(semi, amr_controller,
218+
interval = 25,
219+
adapt_initial_condition = true,
220+
adapt_initial_condition_only_refine = true)
221+
222+
callbacks = CallbackSet(summary_callback,
223+
analysis_callback, alive_callback,
224+
save_solution, amr_callback)
225+
226+
###############################################################################
227+
# Run the simulation
228+
229+
sol = solve(ode, SSPRK43(; thread = Trixi.Threaded());
230+
dt = 1e-7, abstol = 1e-4, reltol = 1e-4,
231+
ode_default_options()..., callback = callbacks);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using OrdinaryDiffEqLowStorageRK
2+
using Trixi
3+
4+
###############################################################################
5+
# semidiscretization of the compressible Euler equations
6+
7+
#=
8+
Data taken from https://ntrs.nasa.gov/api/citations/20020085330/downloads/20020085330.pdf page 276/284
9+
10+
Air Mole%:N2 78.084,O2 20.9476,Ar .9365,CO2 .0319.Gordon,1982.Reac
11+
2 g 9/95 N 1.5617O .41959AR.00937C .00032 .00000 0 28.9651159 -125.530
12+
200.000 1000.0007 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0 0.0 8649.264
13+
1.009950160D+04-1.968275610D+02 5.009155110D+00-5.761013730D-03 1.066859930D-05
14+
-7.940297970D-09 2.185231910D-12 -1.767967310D+02-3.921504225D+00
15+
1000.000 6000.0007 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0 0.0 8649.264
16+
2.415214430D+05-1.257874600D+03 5.144558670D+00-2.138541790D-04 7.065227840D-08
17+
-1.071483490D-11 6.577800150D-16 6.462263190D+03-8.147411905D+00
18+
19+
and page 2/10
20+
Reference temperature: 298.15 K
21+
Reference pressure: 1 bar = 100000 Pa
22+
=#
23+
24+
M = 0.0289651159 # [kg/mol]
25+
R_universal = 8.31446261815324 # [J/(mol K)]
26+
R_specific = R_universal / M # [J/(kg K)]
27+
28+
temp_bounds = SVector(200.0, 1000.0, 6000.0) # [K]
29+
30+
a_cold = [1.009950160e+04; -1.968275610e+02; 5.009155110e+00; -5.761013730e-03;
31+
1.066859930e-05; -7.940297970e-09; 2.185231910e-12; -1.767967310e+02;
32+
-3.921504225e+00]
33+
a_hot = [2.415214430e+05; -1.257874600e+03; 5.144558670e+00; -2.138541790e-04;
34+
7.065227840e-08; -1.071483490e-11; 6.577800150e-16; 6.462263190e+03;
35+
-8.147411905e+00]
36+
a_ = hcat(a_cold, a_hot)
37+
a = Trixi.SMatrix{9, 2}(a_)
38+
39+
eos = ThermallyPerfectGas9PolyFit(R_specific = R_specific,
40+
temperature_bounds = temp_bounds,
41+
coefficients = a,
42+
p_ref = 100000.0, T_ref = 298.15)
43+
44+
equations = NonIdealCompressibleEulerEquations1D(eos)
45+
46+
# The default amplitude and frequency k are consistent with initial_condition_density_wave
47+
# for CompressibleEulerEquations1D. Note that this initial condition may not define admissible
48+
# solution states for all non-ideal equations of state!
49+
function Trixi.initial_condition_density_wave(x, t,
50+
equations::NonIdealCompressibleEulerEquations1D;
51+
amplitude = 0.98, k = 2)
52+
RealT = eltype(x)
53+
eos = equations.equation_of_state
54+
55+
v1 = convert(RealT, 0.1) # [m/s]
56+
rho = 1.225 + convert(RealT, amplitude) * sinpi(k * (x[1] - v1 * t)) # [kg/m^3]
57+
p = 101325 # [Pa]
58+
59+
V = inv(rho)
60+
61+
# invert for temperature given p, V
62+
T = temperature_given_Vp(V, p, eos; initial_T = 298.15, # [K]
63+
tol = 100 * eps(RealT), maxiter = 100)
64+
65+
return thermo2cons(SVector(V, v1, T), equations)
66+
end
67+
initial_condition = initial_condition_density_wave
68+
69+
solver = DGSEM(polydeg = 3, surface_flux = flux_hll)
70+
71+
coordinates_min = -1.0 # [m]
72+
coordinates_max = 1.0 # [m]
73+
mesh = TreeMesh(coordinates_min, coordinates_max,
74+
initial_refinement_level = 4,
75+
n_cells_max = 30_000, periodicity = true)
76+
77+
semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver;
78+
boundary_conditions = boundary_condition_periodic)
79+
80+
###############################################################################
81+
# ODE solvers, callbacks etc.
82+
83+
tspan = (0.0, 0.1) # [s]
84+
ode = semidiscretize(semi, tspan)
85+
86+
summary_callback = SummaryCallback()
87+
88+
analysis_interval = 2000
89+
analysis_callback = AnalysisCallback(semi, interval = analysis_interval)
90+
91+
alive_callback = AliveCallback(analysis_interval = analysis_interval)
92+
93+
stepsize_callback = StepsizeCallback(cfl = 1.2)
94+
95+
callbacks = CallbackSet(summary_callback,
96+
analysis_callback, alive_callback,
97+
stepsize_callback)
98+
99+
###############################################################################
100+
# run the simulation
101+
102+
sol = solve(ode, CarpenterKennedy2N54(williamson_condition = false);
103+
dt = stepsize_callback(ode), # solve needs some value here but it will be overwritten by the stepsize_callback
104+
ode_default_options()..., callback = callbacks);

src/Trixi.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,8 @@ export AcousticPerturbationEquations2D,
224224
PassiveTracerEquations
225225

226226
export NonIdealCompressibleEulerEquations1D, NonIdealCompressibleEulerEquations2D
227-
export IdealGas, VanDerWaals, PengRobinson, HelmholtzIdealGas
227+
export IdealGas, ThermallyPerfectGas9PolyFit,
228+
VanDerWaals, PengRobinson, HelmholtzIdealGas
228229

229230
export LinearDiffusionEquation1D, LinearDiffusionEquation2D,
230231
LaplaceDiffusion1D, LaplaceDiffusion2D, LaplaceDiffusion3D,

0 commit comments

Comments
 (0)