Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
trixi-framework
GitHub Repository: trixi-framework/Trixi.jl
Path: blob/main/src/solvers/dgmulti/shock_capturing.jl
5591 views
1
# Since `@muladd` can fuse multiply-add operations and thus improve performance in
2
# the flux differencing loops, we opt-in explicitly.
3
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
4
@muladd begin
5
#! format: noindent
6
7
# by default, return an empty tuple for volume integral caches
8
function create_cache(mesh::DGMultiMesh{NDIMS}, equations,
9
volume_integral::VolumeIntegralShockCapturingHGType,
10
dg::DGMultiFluxDiff{<:GaussSBP}, RealT, uEltype) where {NDIMS}
11
(; volume_integral_default, volume_integral_blend_high_order) = volume_integral
12
@assert volume_integral_default isa VolumeIntegralFluxDifferencing "DGMulti is currently only compatible with `VolumeIntegralFluxDifferencing` as `volume_integral_default`"
13
@assert volume_integral_blend_high_order isa VolumeIntegralFluxDifferencing "DGMulti is currently only compatible with `VolumeIntegralFluxDifferencing` as `volume_integral_blend_high_order`"
14
# `volume_integral_blend_low_order` limited to finite-volume on Gauss-node subcells
15
16
element_to_element_connectivity = build_element_to_element_connectivity(mesh, dg)
17
18
# create sparse hybridized operators for low order scheme
19
Qrst, E = StartUpDG.sparse_low_order_SBP_operators(dg.basis)
20
Brst = map(n -> Diagonal(n .* dg.basis.wf), dg.basis.nrstJ)
21
sparse_SBP_operators = map((Q, B) -> 0.5f0 * [Q-Q' E'*B; -B*E zeros(size(B))],
22
Qrst, Brst)
23
24
# Find the joint sparsity pattern of the entire matrix. We store the sparsity pattern as
25
# an adjoint for faster iteration through the rows.
26
sparsity_pattern = sum(map(A -> abs.(A)', sparse_SBP_operators)) .>
27
100 * eps()
28
29
return (; sparse_SBP_operators, sparsity_pattern,
30
element_to_element_connectivity)
31
end
32
33
# this method is used when the indicator is constructed as for shock-capturing volume integrals
34
function create_cache(::Type{IndicatorHennemannGassner}, equations::AbstractEquations,
35
basis::DGMultiBasis{NDIMS}) where {NDIMS}
36
uEltype = real(basis)
37
alpha = Vector{uEltype}()
38
alpha_tmp = similar(alpha)
39
40
MVec_nodes = MVector{nnodes(basis), uEltype}
41
indicator_threaded = MVec_nodes[MVec_nodes(undef) for _ in 1:Threads.maxthreadid()]
42
MVec_modes = MVector{nmodes(basis.N, basis.element_type), uEltype}
43
modal_threaded = MVec_modes[MVec_modes(undef) for _ in 1:Threads.maxthreadid()]
44
45
inverse_vandermonde = calc_inverse_vandermonde(basis)
46
47
return (; alpha, alpha_tmp, indicator_threaded, modal_threaded, inverse_vandermonde)
48
end
49
50
# calculates the inverse of the Vandermonde matrix for shock capturing purposes.
51
# This version is for tensor product elements (Line, Quad, Hex)
52
function calc_inverse_vandermonde(basis::DGMultiBasis{NDIMS, <:Union{Line, Quad, Hex}}) where {NDIMS}
53
# initialize inverse Vandermonde matrices at Gauss-Legendre nodes
54
(; N) = basis
55
lobatto_node_coordinates_1D, _ = StartUpDG.gauss_lobatto_quad(0, 0, N)
56
VDM_1D = StartUpDG.vandermonde(Line(), N, lobatto_node_coordinates_1D)
57
inverse_vandermonde = SimpleKronecker(NDIMS, inv(VDM_1D))
58
59
return inverse_vandermonde
60
end
61
62
function (indicator_hg::IndicatorHennemannGassner)(u, mesh::DGMultiMesh,
63
equations,
64
dg::DGMulti{NDIMS,
65
<:Union{Line, Quad, Hex}},
66
cache;
67
kwargs...) where {NDIMS}
68
(; alpha_max, alpha_min, alpha_smooth, variable) = indicator_hg
69
(; alpha, alpha_tmp, indicator_threaded, modal_threaded, inverse_vandermonde) = indicator_hg.cache
70
71
resize!(alpha, nelements(mesh, dg))
72
if alpha_smooth
73
resize!(alpha_tmp, nelements(mesh, dg))
74
end
75
76
# magic parameters
77
threshold = 0.5f0 * 10^(-1.8 * (dg.basis.N + 1)^0.25)
78
parameter_s = log((1 - 0.0001) / 0.0001)
79
80
@threaded for element in eachelement(mesh, dg)
81
indicator = indicator_threaded[Threads.threadid()]
82
modal_ = modal_threaded[Threads.threadid()]
83
84
# Calculate indicator variable at interpolation (Lobatto) nodes.
85
# TODO: calculate indicator variables at Gauss nodes or using `cache.entropy_projected_u_values`
86
for i in eachnode(dg)
87
indicator[i] = variable(u[i, element], equations)
88
end
89
90
# multiply by invVDM::SimpleKronecker
91
LinearAlgebra.mul!(modal_, inverse_vandermonde, indicator)
92
93
# Create Returns functors to return the constructor args (e.g., Base.OneTo(dg.basis.N)) no matter what
94
# Returns(Base.OneTo(dg.basis.N)) equiv to _ -> Base.OneTo(dg.basis.N), with possibly fewer allocs
95
return_N_plus_one = Returns(dg.basis.N + 1)
96
return_to_N_minus_one = Returns(Base.OneTo(dg.basis.N - 1))
97
return_to_N = Returns(Base.OneTo(dg.basis.N))
98
99
# As of Julia 1.9, Base.ReshapedArray does not produce allocations when setting values.
100
# Thus, Base.ReshapedArray should be used if you are setting values in the array.
101
# `reshape` is fine if you are only accessing values.
102
# Here, we reshape modal coefficients to expose the tensor product structure.
103
104
modal = Base.ReshapedArray(modal_, ntuple(return_N_plus_one, NDIMS), ())
105
106
# Calculate total energies for all modes, all modes minus the highest mode, and
107
# all modes without the two highest modes
108
total_energy = sum(abs2, modal)
109
clip_1_ranges = ntuple(return_to_N, NDIMS)
110
clip_2_ranges = ntuple(return_to_N_minus_one, NDIMS)
111
# These splattings do not seem to allocate as of Julia 1.9.0?
112
total_energy_clip1 = sum(abs2, view(modal, clip_1_ranges...))
113
total_energy_clip2 = sum(abs2, view(modal, clip_2_ranges...))
114
115
# Calculate energy in higher modes
116
if !(iszero(total_energy))
117
energy_frac_1 = (total_energy - total_energy_clip1) / total_energy
118
else
119
energy_frac_1 = zero(total_energy)
120
end
121
if !(iszero(total_energy_clip1))
122
energy_frac_2 = (total_energy_clip1 - total_energy_clip2) /
123
total_energy_clip1
124
else
125
energy_frac_2 = zero(total_energy_clip1)
126
end
127
energy = max(energy_frac_1, energy_frac_2)
128
129
alpha_element = 1 / (1 + exp(-parameter_s / threshold * (energy - threshold)))
130
131
# Take care of the case close to pure DG
132
if alpha_element < alpha_min
133
alpha_element = zero(alpha_element)
134
end
135
136
# Take care of the case close to pure FV
137
if alpha_element > 1 - alpha_min
138
alpha_element = one(alpha_element)
139
end
140
141
# Clip the maximum amount of FV allowed
142
alpha[element] = min(alpha_max, alpha_element)
143
end
144
145
# smooth element indices after they're all computed
146
if alpha_smooth
147
apply_smoothing!(mesh, alpha, alpha_tmp, dg, cache)
148
end
149
150
return alpha
151
end
152
153
# Diffuse alpha values by setting each alpha to at least 50% of neighboring elements' alpha
154
function apply_smoothing!(mesh::DGMultiMesh, alpha, alpha_tmp, dg::DGMulti, cache)
155
156
# Copy alpha values such that smoothing is independent of the element access order
157
alpha_tmp .= alpha
158
159
# smooth alpha with its neighboring value
160
@threaded for element in eachelement(mesh, dg)
161
for face in Base.OneTo(StartUpDG.num_faces(dg.basis.element_type))
162
neighboring_element = cache.element_to_element_connectivity[face, element]
163
alpha_neighbor = alpha_tmp[neighboring_element]
164
alpha[element] = max(alpha[element], 0.5f0 * alpha_neighbor)
165
end
166
end
167
168
return nothing
169
end
170
171
function calc_volume_integral!(du, u, mesh::DGMultiMesh,
172
have_nonconservative_terms, equations,
173
volume_integral::VolumeIntegralShockCapturingHGType,
174
dg::DGMultiFluxDiff, cache)
175
(; indicator, volume_integral_default,
176
volume_integral_blend_high_order, volume_integral_blend_low_order) = volume_integral
177
178
# Calculate blending factors α: u = u_DG * (1 - α) + u_FV * α
179
alpha = @trixi_timeit timer() "blending factors" indicator(u, mesh, equations, dg,
180
cache)
181
182
# For `Float64`, this gives 1.8189894035458565e-12
183
# For `Float32`, this gives 1.1920929f-5
184
RealT = eltype(alpha)
185
atol = max(100 * eps(RealT), eps(RealT)^convert(RealT, 0.75f0))
186
187
@threaded for element in eachelement(mesh, dg)
188
alpha_element = alpha[element]
189
# Clip blending factor for values close to zero (-> pure DG)
190
dg_only = isapprox(alpha_element, 0, atol = atol)
191
192
if dg_only
193
volume_integral_kernel!(du, u, element, mesh,
194
have_nonconservative_terms, equations,
195
volume_integral_default,
196
dg, cache)
197
else
198
# Calculate DG volume integral contribution
199
volume_integral_kernel!(du, u, element, mesh,
200
have_nonconservative_terms, equations,
201
volume_integral_blend_high_order,
202
dg, cache, 1 - alpha_element)
203
204
# Calculate "FV" low order volume integral contribution
205
volume_integral_kernel!(du, u, element, mesh,
206
have_nonconservative_terms, equations,
207
volume_integral_blend_low_order,
208
dg, cache, alpha_element)
209
end
210
end
211
212
return nothing
213
end
214
215
function get_sparse_operator_entries(i, j, mesh::DGMultiMesh{1}, cache)
216
return SVector(cache.sparse_SBP_operators[1][i, j])
217
end
218
219
function get_sparse_operator_entries(i, j, mesh::DGMultiMesh{2}, cache)
220
Qr, Qs = cache.sparse_SBP_operators
221
return SVector(Qr[i, j], Qs[i, j])
222
end
223
224
function get_sparse_operator_entries(i, j, mesh::DGMultiMesh{3}, cache)
225
Qr, Qs, Qt = cache.sparse_SBP_operators
226
return SVector(Qr[i, j], Qs[i, j], Qt[i, j])
227
end
228
229
function get_contravariant_matrix(element, mesh::DGMultiMesh{1}, cache)
230
return SMatrix{1, 1}(cache.dxidxhatj[1, 1][1, element])
231
end
232
233
function get_contravariant_matrix(element, mesh::DGMultiMesh{2, <:Affine}, cache)
234
(; dxidxhatj) = cache
235
return SMatrix{2, 2}(dxidxhatj[1, 1][1, element], dxidxhatj[2, 1][1, element],
236
dxidxhatj[1, 2][1, element], dxidxhatj[2, 2][1, element])
237
end
238
239
function get_contravariant_matrix(element, mesh::DGMultiMesh{3, <:Affine}, cache)
240
(; dxidxhatj) = cache
241
return SMatrix{3, 3}(dxidxhatj[1, 1][1, element], dxidxhatj[2, 1][1, element],
242
dxidxhatj[3, 1][1, element],
243
dxidxhatj[1, 2][1, element], dxidxhatj[2, 2][1, element],
244
dxidxhatj[3, 2][1, element],
245
dxidxhatj[1, 3][1, element], dxidxhatj[2, 3][1, element],
246
dxidxhatj[3, 3][1, element])
247
end
248
249
function get_contravariant_matrix(i, element, mesh::DGMultiMesh{2}, cache)
250
(; dxidxhatj) = cache
251
return SMatrix{2, 2}(dxidxhatj[1, 1][i, element], dxidxhatj[2, 1][i, element],
252
dxidxhatj[1, 2][i, element], dxidxhatj[2, 2][i, element])
253
end
254
255
function get_contravariant_matrix(i, element, mesh::DGMultiMesh{3}, cache)
256
(; dxidxhatj) = cache
257
return SMatrix{3, 3}(dxidxhatj[1, 1][i, element], dxidxhatj[2, 1][i, element],
258
dxidxhatj[3, 1][i, element],
259
dxidxhatj[1, 2][i, element], dxidxhatj[2, 2][i, element],
260
dxidxhatj[3, 2][i, element],
261
dxidxhatj[1, 3][i, element], dxidxhatj[2, 3][i, element],
262
dxidxhatj[3, 3][i, element])
263
end
264
265
function get_avg_contravariant_matrix(i, j, element, mesh::DGMultiMesh, cache)
266
return 0.5f0 * (get_contravariant_matrix(i, element, mesh, cache) +
267
get_contravariant_matrix(j, element, mesh, cache))
268
end
269
270
# On affine meshes, the geometric matrix is constant over the element, so we compute it
271
# once and reuse it for all node pairs (i, j). The compiler is expected to hoist this
272
# out of the inner loop after inlining.
273
@inline function get_low_order_geometric_matrix(i, j, element,
274
mesh::DGMultiMesh{NDIMS, <:Affine},
275
cache) where {NDIMS}
276
return get_contravariant_matrix(element, mesh, cache)
277
end
278
279
# On non-affine meshes, we use the average of the geometric matrices at nodes i and j
280
# for provably entropy-stable de-aliasing of the geometric terms.
281
@inline function get_low_order_geometric_matrix(i, j, element,
282
mesh::DGMultiMesh,
283
cache)
284
return get_avg_contravariant_matrix(i, j, element, mesh, cache)
285
end
286
287
# Calculates the volume integral corresponding to an algebraic low order method.
288
# This is used, for example, in shock capturing.
289
function volume_integral_kernel!(du, u, element, mesh::DGMultiMesh,
290
have_nonconservative_terms::False, equations,
291
volume_integral::VolumeIntegralPureLGLFiniteVolume,
292
dg::DGMultiFluxDiff{<:GaussSBP}, cache,
293
alpha = true)
294
(; volume_flux_fv) = volume_integral
295
296
# accumulates output from flux differencing
297
rhs_local = cache.rhs_local_threaded[Threads.threadid()]
298
fill!(rhs_local, zero(eltype(rhs_local)))
299
300
u_local = view(cache.entropy_projected_u_values, :, element)
301
302
(; sparsity_pattern) = cache
303
A_base, row_ids, rows, _ = sparse_operator_data(sparsity_pattern)
304
for i in row_ids
305
u_i = u_local[i]
306
du_i = zero(u_i)
307
for id in nzrange(A_base, i)
308
j = rows[id]
309
u_j = u_local[j]
310
311
# compute (Q_1[i,j], Q_2[i,j], ...) where Q_i = ∑_j dxidxhatj * Q̂_j
312
geometric_matrix = get_low_order_geometric_matrix(i, j, element,
313
mesh, cache)
314
reference_operator_entries = get_sparse_operator_entries(i, j, mesh, cache)
315
normal_direction_ij = geometric_matrix * reference_operator_entries
316
317
# note that we do not need to normalize `normal_direction_ij` since
318
# it is typically normalized within the flux computation.
319
f_ij = volume_flux_fv(u_i, u_j, normal_direction_ij, equations)
320
du_i = du_i + 2 * f_ij
321
end
322
rhs_local[i] = du_i
323
end
324
325
# TODO: factor this out to avoid calling it twice during calc_volume_integral!
326
return project_rhs_to_gauss_nodes!(du, rhs_local, element, mesh, dg, cache, alpha)
327
end
328
end # @muladd
329
330