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