Path: blob/main/src/callbacks_step/time_series_dg_unstructured.jl
2055 views
# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).1# Since these FMAs can increase the performance of many numerical algorithms,2# we need to opt-in explicitly.3# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.4@muladd begin5#! format: noindent67# Elements on an `UnstructuredMesh2D` are possibly curved. Assume that each8# element is convex, i.e., all interior angles are less than 180 degrees.9# This routine computes the shortest distance from a given point to each element10# surface in the mesh. These distances then indicate possible candidate elements.11# From these candidates we (essentially) apply a ray casting strategy and identify12# the element in which the point lies by comparing the ray formed by the point to13# the nearest boundary to the rays cast by the candidate element barycenters to the14# boundary. If these rays point in the same direction, then we have identified the15# desired element location.16function get_elements_by_coordinates!(element_ids, coordinates,17mesh::UnstructuredMesh2D,18dg, cache)19if length(element_ids) != size(coordinates, 2)20throw(DimensionMismatch("storage length for element ids does not match the number of coordinates"))21end2223# Reset element ids - 0 indicates "not (yet) found"24element_ids .= 02526# Compute and save the barycentric coordinate on each element27bary_centers = zeros(eltype(mesh.corners), 2, mesh.n_elements)28calc_bary_centers!(bary_centers, dg, cache)2930# Iterate over coordinates31distances = zeros(eltype(mesh.corners), mesh.n_elements)32indices = zeros(Int, mesh.n_elements, 2)33for index in eachindex(element_ids)34# Grab the current point for which the element needs found35point = SVector(coordinates[1, index],36coordinates[2, index])3738# Compute the minimum distance between the `point` and all the element surfaces39# saved into `distances`. The point in `node_coordinates` that gives said minimum40# distance on each element is saved in `indices`41distances, indices = calc_minimum_surface_distance(point,42cache.elements.node_coordinates,43dg, mesh)4445# Get the candidate elements where the `point` might live46candidates = findall(abs.(minimum(distances) .- distances) .<47500 * eps(eltype(point)))4849# The minimal surface point is on a boundary so it plays no role which candidate50# we use to grab it. So just use the first one51surface_point = SVector(cache.elements.node_coordinates[1,52indices[candidates[1],531],54indices[candidates[1],552],56candidates[1]],57cache.elements.node_coordinates[2,58indices[candidates[1],591],60indices[candidates[1],612],62candidates[1]])6364# Compute the vector pointing from the current `point` toward the surface65P = surface_point - point6667# If the vector `P` is the zero vector then this `point` is at an element corner or68# on a surface. In this case the choice of a candidate element is ambiguous and69# we just use the first candidate. However, solutions might differ at discontinuous70# interfaces such that this choice may influence the result.71if sum(P .* P) < 500 * eps(eltype(point))72element_ids[index] = candidates[1]73continue74end7576# Loop through all the element candidates until we find a vector from the barycenter77# to the surface that points in the same direction as the current `point` vector.78# This then gives us the correct element.79for element in eachindex(candidates)80bary_center = SVector(bary_centers[1, candidates[element]],81bary_centers[2, candidates[element]])82# Vector pointing from the barycenter toward the minimal `surface_point`83B = surface_point - bary_center84if sum(P .* B) > zero(eltype(bary_center))85element_ids[index] = candidates[element]86break87end88end89end9091return element_ids92end9394# Use the available `node_coordinates` on each element to compute and save the barycenter.95# In essence, the barycenter is like an average where all the x and y node coordinates are96# summed and then we divide by the total number of degrees of freedom on the element, i.e.,97# the value of `n^2` in two spatial dimensions.98@inline function calc_bary_centers!(bary_centers, dg, cache)99n = nnodes(dg)100@views for element in eachelement(dg, cache)101bary_centers[1, element] = sum(cache.elements.node_coordinates[1, :, :,102element]) / n^2103bary_centers[2, element] = sum(cache.elements.node_coordinates[2, :, :,104element]) / n^2105end106return nothing107end108109# Compute the shortest distance from a `point` to the surface of each element110# using the available `node_coordinates`. Also return the index pair of this111# minimum surface point location. We compute and store in `min_distance`112# the squared norm to avoid computing computationally more expensive square roots.113# Note! Could be made more accurate if the `node_coordinates` were super-sampled114# and reinterpolated onto a higher polynomial degree before this computation.115function calc_minimum_surface_distance(point, node_coordinates,116dg, mesh::UnstructuredMesh2D)117n = nnodes(dg)118min_distance2 = Inf * ones(eltype(mesh.corners), length(mesh))119indices = zeros(Int, length(mesh), 2)120for k in 1:length(mesh)121# used to ensure that only boundary points are used122on_surface = MVector(false, false)123for j in 1:n124on_surface[2] = (j == 1) || (j == n)125for i in 1:n126on_surface[1] = (i == 1) || (i == n)127if !any(on_surface)128continue129end130node = SVector(node_coordinates[1, i, j, k],131node_coordinates[2, i, j, k])132distance2 = sum(abs2, node - point)133if distance2 < min_distance2[k]134min_distance2[k] = distance2135indices[k, 1] = i136indices[k, 2] = j137end138end139end140end141142return min_distance2, indices143end144145function calc_interpolating_polynomials!(interpolating_polynomials, coordinates,146element_ids,147mesh::UnstructuredMesh2D, dg::DGSEM, cache)148@unpack nodes = dg.basis149150wbary = barycentric_weights(nodes)151152# Helper array for a straight-sided quadrilateral element153corners = zeros(eltype(mesh.corners), 4, 2)154155for index in eachindex(element_ids)156# Construct point157x = SVector(ntuple(i -> coordinates[i, index], ndims(mesh)))158159# Convert to unit coordinates; procedure differs for straight-sided160# versus curvilinear elements161element = element_ids[index]162if !mesh.element_is_curved[element]163for j in 1:2, i in 1:4164# Pull the (x,y) values of the element corners from the global corners array165corners[i, j] = mesh.corners[j, mesh.element_node_ids[i, element]]166end167# Compute coordinates in reference system168unit_coordinates = invert_bilinear_interpolation(mesh, x, corners)169170# Sanity check that the computed `unit_coordinates` indeed recover the desired point `x`171x_check = straight_side_quad_map(unit_coordinates[1], unit_coordinates[2],172corners)173if !isapprox(x[1], x_check[1]) || !isapprox(x[2], x_check[2])174error("failed to compute computational coordinates for the time series point $(x), closet candidate was $(x_check)")175end176else # mesh.element_is_curved[element]177unit_coordinates = invert_transfinite_interpolation(mesh, x,178view(mesh.surface_curves,179:, element))180181# Sanity check that the computed `unit_coordinates` indeed recover the desired point `x`182x_check = transfinite_quad_map(unit_coordinates[1], unit_coordinates[2],183view(mesh.surface_curves, :, element))184if !isapprox(x[1], x_check[1]) || !isapprox(x[2], x_check[2])185error("failed to compute computational coordinates for the time series point $(x), closet candidate was $(x_check)")186end187end188189# Calculate interpolating polynomial for each dimension, making use of tensor product structure190for d in 1:ndims(mesh)191interpolating_polynomials[:, d, index] .= lagrange_interpolating_polynomials(unit_coordinates[d],192nodes,193wbary)194end195end196197return interpolating_polynomials198end199200# Use a Newton iteration to determine the computational coordinates201# (xi, eta) of an (x,y) `point` that is given in physical coordinates202# by inverting the transformation. For straight-sided elements this203# amounts to inverting a bi-linear interpolation. For curved204# elements we invert the transfinite interpolation with linear blending.205# The residual function for the Newton iteration is206# r(xi, eta) = X(xi, eta) - point207# and the Jacobian entries are computed accordingly from either208# `straight_side_quad_map_metrics` or `transfinite_quad_map_metrics`.209# We exploit the 2x2 nature of the problem and directly compute the matrix210# inverse to make things faster. The implementations below are inspired by211# an answer on Stack Overflow (https://stackoverflow.com/a/18332009) where212# the author explicitly states that their code is released to the public domain.213@inline function invert_bilinear_interpolation(mesh::UnstructuredMesh2D, point,214element_corners)215# Initial guess for the point (center of the reference element)216xi = zero(eltype(point))217eta = zero(eltype(point))218for k in 1:5 # Newton's method should converge quickly219# Compute current x and y coordinate and the Jacobian matrix220# J = (X_xi, X_eta; Y_xi, Y_eta)221x, y = straight_side_quad_map(xi, eta, element_corners)222J11, J12, J21, J22 = straight_side_quad_map_metrics(xi, eta, element_corners)223224# Compute residuals for the Newton teration for the current (x, y) coordinate225r1 = x - point[1]226r2 = y - point[2]227228# Newton update that directly applies the inverse of the 2x2 Jacobian matrix229inv_detJ = inv(J11 * J22 - J12 * J21)230231# Update with explicitly inverted Jacobian232xi = xi - inv_detJ * (J22 * r1 - J12 * r2)233eta = eta - inv_detJ * (-J21 * r1 + J11 * r2)234235# Ensure updated point is in the reference element236xi = min(max(xi, -1), 1)237eta = min(max(eta, -1), 1)238end239240return SVector(xi, eta)241end242243@inline function invert_transfinite_interpolation(mesh::UnstructuredMesh2D, point,244surface_curves::AbstractVector{<:CurvedSurface})245# Initial guess for the point (center of the reference element)246xi = zero(eltype(point))247eta = zero(eltype(point))248for k in 1:5 # Newton's method should converge quickly249# Compute current x and y coordinate and the Jacobian matrix250# J = (X_xi, X_eta; Y_xi, Y_eta)251x, y = transfinite_quad_map(xi, eta, surface_curves)252J11, J12, J21, J22 = transfinite_quad_map_metrics(xi, eta, surface_curves)253254# Compute residuals for the Newton teration for the current (x,y) coordinate255r1 = x - point[1]256r2 = y - point[2]257258# Newton update that directly applies the inverse of the 2x2 Jacobian matrix259inv_detJ = inv(J11 * J22 - J12 * J21)260261# Update with explicitly inverted Jacobian262xi = xi - inv_detJ * (J22 * r1 - J12 * r2)263eta = eta - inv_detJ * (-J21 * r1 + J11 * r2)264265# Ensure updated point is in the reference element266xi = min(max(xi, -1), 1)267eta = min(max(eta, -1), 1)268end269270return SVector(xi, eta)271end272273function record_state_at_points!(point_data, u, solution_variables,274n_solution_variables,275mesh::UnstructuredMesh2D,276equations, dg::DG, time_series_cache)277@unpack element_ids, interpolating_polynomials = time_series_cache278old_length = length(first(point_data))279new_length = old_length + n_solution_variables280281# Loop over all points/elements that should be recorded282for index in eachindex(element_ids)283# Extract data array and element id284data = point_data[index]285element_id = element_ids[index]286287# Make room for new data to be recorded288resize!(data, new_length)289data[(old_length + 1):new_length] .= zero(eltype(data))290291# Loop over all nodes to compute their contribution to the interpolated values292for j in eachnode(dg), i in eachnode(dg)293u_node = solution_variables(get_node_vars(u, equations, dg, i, j,294element_id), equations)295296for v in eachindex(u_node)297data[old_length + v] += (u_node[v]298* interpolating_polynomials[i, 1, index]299* interpolating_polynomials[j, 2, index])300end301end302end303304return nothing305end306end # @muladd307308309