https://mooseframework.inl.gov
Loading...
Searching...
No Matches
BoundaryLayerUtils.C
Go to the documentation of this file.
1//* This file is part of the MOOSE framework
2//* https://mooseframework.inl.gov
3//*
4//* All rights reserved, see COPYRIGHT for full restrictions
5//* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6//*
7//* Licensed under LGPL 2.1, please see LICENSE for details
8//* https://www.gnu.org/licenses/lgpl-2.1.html
9
10#include "BoundaryLayerUtils.h"
12#include "MooseMeshUtils.h"
13#include "CastUniquePointer.h"
14#include "GeometryUtils.h"
15
16#include "libmesh/elem.h"
17#include "libmesh/boundary_info.h"
18#include "libmesh/edge_edge3.h"
19#include "libmesh/fe_base.h"
20#include "libmesh/int_range.h"
21#include "libmesh/mesh_serializer.h"
22#include "libmesh/mesh_triangle_holes.h"
23#include "libmesh/poly2tri_triangulator.h"
24#include "libmesh/unstructured_mesh.h"
25
26namespace BoundaryLayerUtils
27{
28
29std::unique_ptr<MeshBase>
31 MeshBase & input_mesh,
32 const std::vector<BoundaryName> & boundary_names,
33 unsigned int num_layers,
34 Real thickness,
35 Real layer_bias,
36 bool outward,
37 const MooseEnum & tri_elem_type,
38 SubdomainID output_subdomain_id,
39 const SubdomainName & output_subdomain_name)
40{
41 // Extract seed boundary polyline points (vertices + optional midpoints) from input_mesh.
42 std::set<std::size_t> bdry_id_set;
43 if (!boundary_names.empty())
44 {
45 auto ids =
46 MooseMeshUtils::getBoundaryIDs(input_mesh, boundary_names, /*generate unknown*/ false);
47 bdry_id_set.insert(ids.begin(), ids.end());
48 }
49 TriangulatorInterface::MeshedHole bdry_mh(input_mesh, bdry_id_set);
50 std::vector<Point> cur_pts;
51 std::vector<Point> cur_mids;
52 // Preserve every input boundary node (including colinear interior nodes on straight edges) so
53 // the ring's innermost polyline can be stitched back to the input mesh's boundary exactly when
54 // keep_input is requested by a downstream caller.
56 cur_pts,
57 cur_mids,
58 /*skip_node_reduction=*/true);
59
60 // Geometric progression of incremental thicknesses. layer_thicknesses[i] is the offset
61 // distance from polyline i-1 to polyline i (for i >= 1); layer_thicknesses[0] is unused.
62 std::vector<Real> layer_thicknesses(num_layers + 1);
63 const Real unit_thickness =
64 (layer_bias == 1.0)
65 ? (thickness / num_layers)
66 : (thickness / (std::pow(layer_bias, num_layers) - 1.0) * (layer_bias - 1.0));
67 layer_thicknesses[0] = 0.0;
68 for (auto i : make_range(std::vector<Real>::size_type(1), layer_thicknesses.size()))
69 layer_thicknesses[i] = unit_thickness * std::pow(layer_bias, i - 1);
70
71 // Generate the N+1 polyline meshes. Polylines are indexed so that polyline 0 is geometrically
72 // innermost (smallest) and polyline N is outermost (largest). Iteration order depends on
73 // direction: for outward, build polyline 0 first (= input boundary); for inward, build polyline
74 // N first.
75 std::vector<std::unique_ptr<MeshBase>> polylines(num_layers + 1);
76 for (auto layer_i : make_range(num_layers + 1))
77 {
78 const unsigned int layer_index = outward ? layer_i : (num_layers - layer_i);
79
80 // Build the polyline mesh for storage as polylines[layer_index].
81 auto ply = std::make_unique<ReplicatedMesh>(mg.comm());
83 cur_pts,
84 cur_mids,
85 /*loop=*/true,
86 BoundaryName(),
87 BoundaryName(),
88 std::vector<unsigned int>({1}));
89 polylines[layer_index] = std::move(ply);
90
91 if (layer_i + 1 < num_layers + 1)
92 {
93 // Build a sacrificial polyline mesh to feed generateOffsetPolyline (it triangulates the
94 // input mesh in place to compute side normals; we discard it afterwards).
95 auto ply_for_offset = std::make_unique<ReplicatedMesh>(mg.comm());
97 cur_pts,
98 cur_mids,
99 /*loop=*/true,
100 BoundaryName(),
101 BoundaryName(),
102 std::vector<unsigned int>({1}));
103 std::unique_ptr<UnstructuredMesh> ply_for_offset_u =
104 dynamic_pointer_cast<UnstructuredMesh>(std::move(ply_for_offset));
105
106 std::vector<Point> next_combined = generateOffsetPolyline(
107 &mg, ply_for_offset_u, cur_pts, cur_mids, outward, layer_thicknesses[layer_i + 1]);
108 if (cur_mids.empty())
109 cur_pts = std::move(next_combined);
110 else
111 {
112 const auto n_vert = cur_pts.size();
113 cur_pts.assign(next_combined.begin(), next_combined.begin() + n_vert);
114 cur_mids.assign(next_combined.begin() + n_vert, next_combined.end());
115 }
116 }
117 }
118
119 // Triangulate each annulus (between polylines[i] and polylines[i+1]). Stitch each annulus to
120 // the accumulating ring (except the first one).
121 std::unique_ptr<MeshBase> ring;
122 for (auto i : make_range(num_layers))
123 {
125 xyd_opts.refine_bdy = false;
126 xyd_opts.verify_holes = false;
127 xyd_opts.stitch_holes = {i > 0};
128 xyd_opts.refine_holes = {false};
129 xyd_opts.tri_elem_type = std::string(tri_elem_type);
130 if (output_subdomain_id != 0)
131 {
132 xyd_opts.has_output_subdomain_id = true;
133 xyd_opts.output_subdomain_id = output_subdomain_id;
134 }
135 if (output_subdomain_name.size())
136 {
137 xyd_opts.has_output_subdomain_name = true;
138 xyd_opts.output_subdomain_name = output_subdomain_name;
139 }
140
141 std::vector<std::unique_ptr<MeshBase>> holes;
142 holes.reserve(1);
143 if (i == 0)
144 holes.push_back(std::move(polylines[0]));
145 else
146 holes.push_back(std::move(ring));
147
149 mg, std::move(polylines[i + 1]), std::move(holes), xyd_opts);
150 }
151 // We now have 2 * num_layers boundaries
152 // Let's only keep the innermost (1) and outermost (2 * num_layers) boundaries, and remove all
153 // intermediate ring bcids
154 std::vector<BoundaryID> bids_to_delete;
155 for (const auto b : make_range(2 * num_layers))
156 {
157 if (b == 1 || b == (num_layers - 1) * 2)
158 continue;
159 bids_to_delete.push_back(b);
160 }
161 auto & bi = ring->get_boundary_info();
162 for (auto b : bids_to_delete)
163 bi.remove_id(b);
164
165 return ring;
166}
167
168std::vector<Point>
170 std::unique_ptr<libMesh::UnstructuredMesh> & ply_mesh_u,
171 std::vector<Point> & points,
172 std::vector<Point> & mid_points,
173 const bool outward,
174 const Real thickness)
175{
176 // If the input points are empty, we will extract them from the input 1D mesh
177 if (points.empty())
178 {
179 mooseAssert(mid_points.empty(),
180 "If the input points are empty, the input mid_points must be also empty.");
181
182 TriangulatorInterface::MeshedHole bdry_mh(*ply_mesh_u);
183 collectExteriorVertexPointsFromMesh(bdry_mh, points, mid_points);
184 }
185 // Generate a very simple triangulation mesh so that we can get the outward normal vectors
186 libMesh::Poly2TriTriangulator poly2tri(*ply_mesh_u);
187 poly2tri.triangulation_type() = libMesh::TriangulatorInterface::PSLG;
188
190 poly2tri.set_refine_boundary_allowed(false);
191 poly2tri.set_verify_hole_boundaries(false);
192 poly2tri.desired_area() = 0;
193 poly2tri.minimum_angle() = 0; // Not yet supported
194 poly2tri.smooth_after_generating() = false;
195 if (mid_points.size())
196 poly2tri.elem_type() = libMesh::ElemType::TRI6;
197 poly2tri.triangulate();
198
199 // We need to serialize the mesh for next steps
200 libMesh::MeshSerializer serial(*ply_mesh_u);
201 // The mesh now only contains one side set that corresponds to the outer boundary with an ID of 0
202 auto bdry_list(ply_mesh_u->get_boundary_info().build_side_list());
203
204 // For each vertex, the shifting direction to form the offset is defined by the normal vectors of
205 // the two sides that contain the vertex.
206 // We gather the normals for each node on the boundary here, which are all vertices because of
207 // the pre-selection of nodes.
208 std::map<dof_id_type, std::vector<Point>> node_normal_map;
209 std::map<dof_id_type, Point> mid_node_normal_map;
210 for (const auto & bside : bdry_list)
211 {
212 const auto & side = ply_mesh_u->elem_ptr(std::get<0>(bside))->side_ptr(std::get<1>(bside));
213 // For linear elements, the side normal is constant and can be obtained straightforwardly;
214 // For quadratic elements, we need the normal at the shared vertices
215 const Point side_normal_0 =
216 mid_points.size()
217 ? getKeyNormal(ply_mesh_u->elem_ptr(std::get<0>(bside)), std::get<1>(bside), 0)
218 : ply_mesh_u->elem_ptr(std::get<0>(bside))
219 ->side_vertex_average_normal(std::get<1>(bside));
220 const Point side_normal_1 =
221 mid_points.size()
222 ? getKeyNormal(ply_mesh_u->elem_ptr(std::get<0>(bside)), std::get<1>(bside), 1)
223 : side_normal_0;
224
225 if (node_normal_map.count(side->node_ptr(0)->id()))
226 node_normal_map[side->node_ptr(0)->id()].push_back(side_normal_0);
227 else
228 node_normal_map[side->node_ptr(0)->id()] = {side_normal_0};
229 if (node_normal_map.count(side->node_ptr(1)->id()))
230 node_normal_map[side->node_ptr(1)->id()].push_back(side_normal_1);
231 else
232 node_normal_map[side->node_ptr(1)->id()] = {side_normal_1};
233
234 if (mid_points.size())
235 {
236 const Point mid_node_normal =
237 getKeyNormal(ply_mesh_u->elem_ptr(std::get<0>(bside)), std::get<1>(bside), 2);
238 mid_node_normal_map
239 [ply_mesh_u->elem_ptr(std::get<0>(bside))->node_ptr(std::get<1>(bside) + 3)->id()] =
240 mid_node_normal;
241 }
242 }
243
244 std::vector<Point> mod_reduced_pts_list(points);
245 for (const auto & [node_id, normal_vecs] : node_normal_map)
246 {
247 mooseAssert(normal_vecs.size() == 2,
248 "Each vertex should be connected to exactly two sides in a polygon.");
249
250 const Point original_pt = *(ply_mesh_u->node_ptr(node_id));
251 // Form an average normal at the vertex from the two connected sides' normals
252 const Point move_dir =
253 (normal_vecs.front() + normal_vecs.back()).unit() * (outward ? 1.0 : -1.0);
254 // Consider four points of interest to determine the moving distance
255 // 1. the vertex point
256 // 2. point along normal_vecs.front() from the vertex with a distance thickness
257 // 3. point along normal_vecs.back() from the vertex with a distance thickness
258 // 4. point along move_dir from the vertex with a distance mov_dist
259 // The four points form a kite shape with its symmetry axis along 1-4 direction
260 // Angle 1-2-4 and angle 1-3-4 are right angles
261 // Angle 2-1-3 (i.e., theta) can be calculated using the interior product of
262 // v1 = normal_vecs.front() and v2 = normal_vecs.back():
263 // cos(theta) = (v1 . v2) / (|v1| * |v2|)
264 // Because of the right angles, the distance between 1 and 4 can be calculated as:
265 // mov_dist = thickness / cos(theta/2)
266 // and cos(theta/2) = sqrt((1 + cos(theta)) / 2)
267 const Real mov_dist =
268 thickness / std::sqrt((1.0 + (normal_vecs.front() * normal_vecs.back()) /
269 (normal_vecs.front().norm() * normal_vecs.back().norm())) /
270 2.0);
271 mooseAssert(std::count(points.begin(), points.end(), original_pt) == 1,
272 "The original point should be found exactly once in the reduced points list.");
273 mod_reduced_pts_list[std::distance(points.begin(),
274 std::find(points.begin(), points.end(), original_pt))] =
275 original_pt + move_dir * mov_dist;
276 }
277
278 // To ensure no overlapping, we need to check set of four points
279 // p1 and p2 should be a pair of points before and after shifting
280 // p3 and p4 should be a pair of adjacent shifted points that are neither not p2
281 for (const auto & i_node_1 : make_range(mod_reduced_pts_list.size()))
282 {
283 const Point & p1 = points[i_node_1];
284 const Point & p2 = mod_reduced_pts_list[i_node_1];
285 for (const auto & i_node_2 : make_range(mod_reduced_pts_list.size()))
286 {
287 if (i_node_2 == i_node_1 || (i_node_2 + 1) % mod_reduced_pts_list.size() == i_node_1)
288 continue;
289 const Point & p3 = mod_reduced_pts_list[i_node_2];
290 const Point & p4 = mod_reduced_pts_list[(i_node_2 + 1) % mod_reduced_pts_list.size()];
291 if (thickness > 0)
292 if (geom_utils::segmentsIntersect(p1, p2, p3, p4))
293 mg->paramError(
294 "thickness",
295 "The thickness is so large that the mesh is tangled because the offset nodes "
296 "are no longer in the same order when following the original boundary. Please "
297 "reduce the thickness value.");
298 }
299 }
300
301 std::vector<Point> mid_mod_reduced_pts_list(mid_points.size());
302 for (const auto & [node_id, normal_vec] : mid_node_normal_map)
303 {
304 const Point original_pt = *(ply_mesh_u->node_ptr(node_id));
305 const Point move_dir = normal_vec.unit() * (outward ? 1.0 : -1.0);
306 mid_mod_reduced_pts_list[std::distance(
307 mid_points.begin(), std::find(mid_points.begin(), mid_points.end(), original_pt))] =
308 original_pt + move_dir * thickness;
309 }
310
311 // combine mod_reduced_pts_list and mid_mod_reduced_pts_list to get the final list of points for
312 // the layer mesh
313 std::vector<Point> layer_pts_list(mod_reduced_pts_list);
314 layer_pts_list.insert(
315 layer_pts_list.end(), mid_mod_reduced_pts_list.begin(), mid_mod_reduced_pts_list.end());
316
317 return layer_pts_list;
318}
319
320void
322 std::vector<Point> & points,
323 std::vector<Point> & mid_points,
324 const bool skip_node_reduction)
325{
326 for (const auto i : make_range(bdry_mh.n_points()))
327 {
328 if (skip_node_reduction || !geom_utils::arePointsColinear(
329 bdry_mh.point((i - 1 + bdry_mh.n_points()) % bdry_mh.n_points()),
330 bdry_mh.point(i),
331 bdry_mh.point((i + 1) % bdry_mh.n_points())))
332 {
333 points.push_back(bdry_mh.point(i));
334 if (bdry_mh.n_midpoints() == 1 && skip_node_reduction)
335 mid_points.push_back(bdry_mh.midpoint(0, i));
336 }
337 }
338}
339
340Point
341getKeyNormal(const Elem * elem, const unsigned int s, const unsigned int node_index)
342{
343 const std::unique_ptr<const Elem> face = elem->build_side_ptr(s);
344 mooseAssert(face->type() == ElemType::EDGE3,
345 "Only elements with EDGE3 sides are supported in this function.");
346 mooseAssert(node_index < 3,
347 "The node index for an EDGE3 side should be 0, 1, or 2 (for the two vertices and the "
348 "midpoint).");
349 std::unique_ptr<libMesh::FEBase> fe(
350 libMesh::FEBase::build(2, libMesh::FEType(elem->default_order())));
351 const std::vector<Point> & normals = fe->get_normals();
352 std::vector<Point> ref_pts = {face->reference_elem()->point(node_index)};
353 fe->reinit(elem, s, TOLERANCE, &ref_pts);
354 return normals[0];
355}
356}
subdomain_id_type SubdomainID
MeshGenerators are objects that can modify or add to an existing mesh.
void paramError(const std::string &param, Args... args) const
Emits an error prefixed with the file and line number of the given param (from the input file) along ...
Definition MooseBase.h:457
This is a "smart" enum class intended to replace many of the shortcomings in the C++ enum type It sho...
Definition MooseEnum.h:55
std::unique_ptr< FEGenericBase< Real > > build(const unsigned int dim, const FEType &fet)
const Parallel::Communicator & comm() const
virtual void set_refine_boundary_allowed(bool refine_bdy_allowed) override
virtual void triangulate() override
virtual Point point(const unsigned int n) const override
virtual Point midpoint(const unsigned int m, const unsigned int n) const override
virtual unsigned int n_points() const override
virtual unsigned int n_midpoints() const override
TriangulationType & triangulation_type()
void set_verify_hole_boundaries(bool v)
void set_interpolate_boundary_points(int n_points)
std::unique_ptr< MeshBase > buildBoundaryLayerRing(MeshGenerator &mg, MeshBase &input_mesh, const std::vector< BoundaryName > &boundary_names, unsigned int num_layers, Real thickness, Real layer_bias, bool outward, const MooseEnum &tri_elem_type, SubdomainID output_subdomain_id, const SubdomainName &output_subdomain_name)
Builds a conformal boundary-layer ring of triangulated annuli along a boundary of an input 2D mesh (o...
Point getKeyNormal(const Elem *elem, const unsigned int s, const unsigned int node_index)
Extracts the normal vector of the EDGE3 side of a quadratic element at a given node index.
void collectExteriorVertexPointsFromMesh(libMesh::TriangulatorInterface::MeshedHole &bdry_mh, std::vector< Point > &points, std::vector< Point > &mid_points, const bool skip_node_reduction=false)
Collects key vertex points (and optional midpoints) from a meshed hole, optionally discarding colinea...
std::vector< Point > generateOffsetPolyline(MeshGenerator *mg, std::unique_ptr< libMesh::UnstructuredMesh > &ply_mesh_u, std::vector< Point > &points, std::vector< Point > &mid_points, const bool outward, const Real thickness)
Generates a list of points offset from the input boundary polyline by a specified thickness in either...
std::unique_ptr< MeshBase > triangulateWithDelaunay(MeshGenerator &mg, std::unique_ptr< MeshBase > boundary_mesh, std::vector< std::unique_ptr< MeshBase > > hole_meshes, const XYDelaunayOptions &xyd_opts)
Performs a 2D Delaunay triangulation (via libMesh::Poly2TriTriangulator) inside a closed boundary mes...
void buildPolyLineMesh(MeshBase &mesh, const std::vector< Point > &points, const bool loop, const BoundaryName &start_boundary, const BoundaryName &end_boundary, const std::vector< unsigned int > &nums_edges_between_points)
Generates meshes from edges connecting a list of points.
std::vector< BoundaryID > getBoundaryIDs(const libMesh::MeshBase &mesh, const std::vector< BoundaryName > &boundary_name, bool generate_unknown, const std::set< BoundaryID > &mesh_boundary_ids)
Gets the boundary IDs with their names.
bool segmentsIntersect(const Point &p1, const Point &p2, const Point &p3, const Point &p4)
Check if the line segment p1-p2 intersects with line segment p3-p4 (only working in 2D (x-y plane)).
bool arePointsColinear(const Point &p1, const Point &p2, const Point &p3)
Check if three points are colinear.
MooseUnits pow(const MooseUnits &, int)
Definition Units.C:537
Bundle of inputs for triangulateWithDelaunay.
const unsigned int n_vert