Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
trixi-framework
GitHub Repository: trixi-framework/Trixi.jl
Path: blob/main/src/callbacks_step/time_series_dg_unstructured.jl
2055 views
1
# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
2
# Since these FMAs can increase the performance of many numerical algorithms,
3
# we need to opt-in explicitly.
4
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
5
@muladd begin
6
#! format: noindent
7
8
# Elements on an `UnstructuredMesh2D` are possibly curved. Assume that each
9
# element is convex, i.e., all interior angles are less than 180 degrees.
10
# This routine computes the shortest distance from a given point to each element
11
# surface in the mesh. These distances then indicate possible candidate elements.
12
# From these candidates we (essentially) apply a ray casting strategy and identify
13
# the element in which the point lies by comparing the ray formed by the point to
14
# the nearest boundary to the rays cast by the candidate element barycenters to the
15
# boundary. If these rays point in the same direction, then we have identified the
16
# desired element location.
17
function get_elements_by_coordinates!(element_ids, coordinates,
18
mesh::UnstructuredMesh2D,
19
dg, cache)
20
if length(element_ids) != size(coordinates, 2)
21
throw(DimensionMismatch("storage length for element ids does not match the number of coordinates"))
22
end
23
24
# Reset element ids - 0 indicates "not (yet) found"
25
element_ids .= 0
26
27
# Compute and save the barycentric coordinate on each element
28
bary_centers = zeros(eltype(mesh.corners), 2, mesh.n_elements)
29
calc_bary_centers!(bary_centers, dg, cache)
30
31
# Iterate over coordinates
32
distances = zeros(eltype(mesh.corners), mesh.n_elements)
33
indices = zeros(Int, mesh.n_elements, 2)
34
for index in eachindex(element_ids)
35
# Grab the current point for which the element needs found
36
point = SVector(coordinates[1, index],
37
coordinates[2, index])
38
39
# Compute the minimum distance between the `point` and all the element surfaces
40
# saved into `distances`. The point in `node_coordinates` that gives said minimum
41
# distance on each element is saved in `indices`
42
distances, indices = calc_minimum_surface_distance(point,
43
cache.elements.node_coordinates,
44
dg, mesh)
45
46
# Get the candidate elements where the `point` might live
47
candidates = findall(abs.(minimum(distances) .- distances) .<
48
500 * eps(eltype(point)))
49
50
# The minimal surface point is on a boundary so it plays no role which candidate
51
# we use to grab it. So just use the first one
52
surface_point = SVector(cache.elements.node_coordinates[1,
53
indices[candidates[1],
54
1],
55
indices[candidates[1],
56
2],
57
candidates[1]],
58
cache.elements.node_coordinates[2,
59
indices[candidates[1],
60
1],
61
indices[candidates[1],
62
2],
63
candidates[1]])
64
65
# Compute the vector pointing from the current `point` toward the surface
66
P = surface_point - point
67
68
# If the vector `P` is the zero vector then this `point` is at an element corner or
69
# on a surface. In this case the choice of a candidate element is ambiguous and
70
# we just use the first candidate. However, solutions might differ at discontinuous
71
# interfaces such that this choice may influence the result.
72
if sum(P .* P) < 500 * eps(eltype(point))
73
element_ids[index] = candidates[1]
74
continue
75
end
76
77
# Loop through all the element candidates until we find a vector from the barycenter
78
# to the surface that points in the same direction as the current `point` vector.
79
# This then gives us the correct element.
80
for element in eachindex(candidates)
81
bary_center = SVector(bary_centers[1, candidates[element]],
82
bary_centers[2, candidates[element]])
83
# Vector pointing from the barycenter toward the minimal `surface_point`
84
B = surface_point - bary_center
85
if sum(P .* B) > zero(eltype(bary_center))
86
element_ids[index] = candidates[element]
87
break
88
end
89
end
90
end
91
92
return element_ids
93
end
94
95
# Use the available `node_coordinates` on each element to compute and save the barycenter.
96
# In essence, the barycenter is like an average where all the x and y node coordinates are
97
# summed and then we divide by the total number of degrees of freedom on the element, i.e.,
98
# the value of `n^2` in two spatial dimensions.
99
@inline function calc_bary_centers!(bary_centers, dg, cache)
100
n = nnodes(dg)
101
@views for element in eachelement(dg, cache)
102
bary_centers[1, element] = sum(cache.elements.node_coordinates[1, :, :,
103
element]) / n^2
104
bary_centers[2, element] = sum(cache.elements.node_coordinates[2, :, :,
105
element]) / n^2
106
end
107
return nothing
108
end
109
110
# Compute the shortest distance from a `point` to the surface of each element
111
# using the available `node_coordinates`. Also return the index pair of this
112
# minimum surface point location. We compute and store in `min_distance`
113
# the squared norm to avoid computing computationally more expensive square roots.
114
# Note! Could be made more accurate if the `node_coordinates` were super-sampled
115
# and reinterpolated onto a higher polynomial degree before this computation.
116
function calc_minimum_surface_distance(point, node_coordinates,
117
dg, mesh::UnstructuredMesh2D)
118
n = nnodes(dg)
119
min_distance2 = Inf * ones(eltype(mesh.corners), length(mesh))
120
indices = zeros(Int, length(mesh), 2)
121
for k in 1:length(mesh)
122
# used to ensure that only boundary points are used
123
on_surface = MVector(false, false)
124
for j in 1:n
125
on_surface[2] = (j == 1) || (j == n)
126
for i in 1:n
127
on_surface[1] = (i == 1) || (i == n)
128
if !any(on_surface)
129
continue
130
end
131
node = SVector(node_coordinates[1, i, j, k],
132
node_coordinates[2, i, j, k])
133
distance2 = sum(abs2, node - point)
134
if distance2 < min_distance2[k]
135
min_distance2[k] = distance2
136
indices[k, 1] = i
137
indices[k, 2] = j
138
end
139
end
140
end
141
end
142
143
return min_distance2, indices
144
end
145
146
function calc_interpolating_polynomials!(interpolating_polynomials, coordinates,
147
element_ids,
148
mesh::UnstructuredMesh2D, dg::DGSEM, cache)
149
@unpack nodes = dg.basis
150
151
wbary = barycentric_weights(nodes)
152
153
# Helper array for a straight-sided quadrilateral element
154
corners = zeros(eltype(mesh.corners), 4, 2)
155
156
for index in eachindex(element_ids)
157
# Construct point
158
x = SVector(ntuple(i -> coordinates[i, index], ndims(mesh)))
159
160
# Convert to unit coordinates; procedure differs for straight-sided
161
# versus curvilinear elements
162
element = element_ids[index]
163
if !mesh.element_is_curved[element]
164
for j in 1:2, i in 1:4
165
# Pull the (x,y) values of the element corners from the global corners array
166
corners[i, j] = mesh.corners[j, mesh.element_node_ids[i, element]]
167
end
168
# Compute coordinates in reference system
169
unit_coordinates = invert_bilinear_interpolation(mesh, x, corners)
170
171
# Sanity check that the computed `unit_coordinates` indeed recover the desired point `x`
172
x_check = straight_side_quad_map(unit_coordinates[1], unit_coordinates[2],
173
corners)
174
if !isapprox(x[1], x_check[1]) || !isapprox(x[2], x_check[2])
175
error("failed to compute computational coordinates for the time series point $(x), closet candidate was $(x_check)")
176
end
177
else # mesh.element_is_curved[element]
178
unit_coordinates = invert_transfinite_interpolation(mesh, x,
179
view(mesh.surface_curves,
180
:, element))
181
182
# Sanity check that the computed `unit_coordinates` indeed recover the desired point `x`
183
x_check = transfinite_quad_map(unit_coordinates[1], unit_coordinates[2],
184
view(mesh.surface_curves, :, element))
185
if !isapprox(x[1], x_check[1]) || !isapprox(x[2], x_check[2])
186
error("failed to compute computational coordinates for the time series point $(x), closet candidate was $(x_check)")
187
end
188
end
189
190
# Calculate interpolating polynomial for each dimension, making use of tensor product structure
191
for d in 1:ndims(mesh)
192
interpolating_polynomials[:, d, index] .= lagrange_interpolating_polynomials(unit_coordinates[d],
193
nodes,
194
wbary)
195
end
196
end
197
198
return interpolating_polynomials
199
end
200
201
# Use a Newton iteration to determine the computational coordinates
202
# (xi, eta) of an (x,y) `point` that is given in physical coordinates
203
# by inverting the transformation. For straight-sided elements this
204
# amounts to inverting a bi-linear interpolation. For curved
205
# elements we invert the transfinite interpolation with linear blending.
206
# The residual function for the Newton iteration is
207
# r(xi, eta) = X(xi, eta) - point
208
# and the Jacobian entries are computed accordingly from either
209
# `straight_side_quad_map_metrics` or `transfinite_quad_map_metrics`.
210
# We exploit the 2x2 nature of the problem and directly compute the matrix
211
# inverse to make things faster. The implementations below are inspired by
212
# an answer on Stack Overflow (https://stackoverflow.com/a/18332009) where
213
# the author explicitly states that their code is released to the public domain.
214
@inline function invert_bilinear_interpolation(mesh::UnstructuredMesh2D, point,
215
element_corners)
216
# Initial guess for the point (center of the reference element)
217
xi = zero(eltype(point))
218
eta = zero(eltype(point))
219
for k in 1:5 # Newton's method should converge quickly
220
# Compute current x and y coordinate and the Jacobian matrix
221
# J = (X_xi, X_eta; Y_xi, Y_eta)
222
x, y = straight_side_quad_map(xi, eta, element_corners)
223
J11, J12, J21, J22 = straight_side_quad_map_metrics(xi, eta, element_corners)
224
225
# Compute residuals for the Newton teration for the current (x, y) coordinate
226
r1 = x - point[1]
227
r2 = y - point[2]
228
229
# Newton update that directly applies the inverse of the 2x2 Jacobian matrix
230
inv_detJ = inv(J11 * J22 - J12 * J21)
231
232
# Update with explicitly inverted Jacobian
233
xi = xi - inv_detJ * (J22 * r1 - J12 * r2)
234
eta = eta - inv_detJ * (-J21 * r1 + J11 * r2)
235
236
# Ensure updated point is in the reference element
237
xi = min(max(xi, -1), 1)
238
eta = min(max(eta, -1), 1)
239
end
240
241
return SVector(xi, eta)
242
end
243
244
@inline function invert_transfinite_interpolation(mesh::UnstructuredMesh2D, point,
245
surface_curves::AbstractVector{<:CurvedSurface})
246
# Initial guess for the point (center of the reference element)
247
xi = zero(eltype(point))
248
eta = zero(eltype(point))
249
for k in 1:5 # Newton's method should converge quickly
250
# Compute current x and y coordinate and the Jacobian matrix
251
# J = (X_xi, X_eta; Y_xi, Y_eta)
252
x, y = transfinite_quad_map(xi, eta, surface_curves)
253
J11, J12, J21, J22 = transfinite_quad_map_metrics(xi, eta, surface_curves)
254
255
# Compute residuals for the Newton teration for the current (x,y) coordinate
256
r1 = x - point[1]
257
r2 = y - point[2]
258
259
# Newton update that directly applies the inverse of the 2x2 Jacobian matrix
260
inv_detJ = inv(J11 * J22 - J12 * J21)
261
262
# Update with explicitly inverted Jacobian
263
xi = xi - inv_detJ * (J22 * r1 - J12 * r2)
264
eta = eta - inv_detJ * (-J21 * r1 + J11 * r2)
265
266
# Ensure updated point is in the reference element
267
xi = min(max(xi, -1), 1)
268
eta = min(max(eta, -1), 1)
269
end
270
271
return SVector(xi, eta)
272
end
273
274
function record_state_at_points!(point_data, u, solution_variables,
275
n_solution_variables,
276
mesh::UnstructuredMesh2D,
277
equations, dg::DG, time_series_cache)
278
@unpack element_ids, interpolating_polynomials = time_series_cache
279
old_length = length(first(point_data))
280
new_length = old_length + n_solution_variables
281
282
# Loop over all points/elements that should be recorded
283
for index in eachindex(element_ids)
284
# Extract data array and element id
285
data = point_data[index]
286
element_id = element_ids[index]
287
288
# Make room for new data to be recorded
289
resize!(data, new_length)
290
data[(old_length + 1):new_length] .= zero(eltype(data))
291
292
# Loop over all nodes to compute their contribution to the interpolated values
293
for j in eachnode(dg), i in eachnode(dg)
294
u_node = solution_variables(get_node_vars(u, equations, dg, i, j,
295
element_id), equations)
296
297
for v in eachindex(u_node)
298
data[old_length + v] += (u_node[v]
299
* interpolating_polynomials[i, 1, index]
300
* interpolating_polynomials[j, 2, index])
301
end
302
end
303
end
304
305
return nothing
306
end
307
end # @muladd
308
309