https://mooseframework.inl.gov
Loading...
Searching...
No Matches
XYFrontalDelaunayGenerator.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
11
12#include "GeometryUtils.h"
13
14#include "libmesh/boundary_info.h"
15#include "libmesh/elem.h"
16#include "libmesh/enum_to_string.h"
17#include "libmesh/int_range.h"
18#include "libmesh/mesh_base.h"
19#include "libmesh/node.h"
20#include "libmesh/replicated_mesh.h"
21#include "libmesh/unstructured_mesh.h"
22#include "libmesh/utility.h"
23
24// C++ includes
25#include <algorithm>
26#include <array>
27#include <cmath>
28#include <complex>
29#include <limits>
30
32
33// The mesh generator sources are compiled as one unity translation unit, which puts this file scope
34// and that of every sibling generator together, so everything here carries the name of this one
35namespace
36{
38constexpr unsigned int n_frontal_tri_sides = 3;
39
41Point
42frontalToPoint(const XYIncrementalDelaunay::Point2D & point)
43{
44 return Point(point.x, point.y, 0.0);
45}
46
48bool
49frontalPointLess(const Point & first, const Point & second)
50{
51 return (first(0) != second(0)) ? first(0) < second(0) : first(1) < second(1);
52}
53
62void
63frontalCanonicalizeLoop(std::vector<Point> & loop)
64{
65 if (geom_utils::signedArea2D(loop) < 0.0)
66 std::reverse(loop.begin(), loop.end());
67
68 std::rotate(
69 loop.begin(), std::min_element(loop.begin(), loop.end(), frontalPointLess), loop.end());
70}
71
81Real
82frontalCircumradius(const Point & first, const Point & second, const Point & third)
83{
84 std::array<Point, 3> corners = {first, second, third};
85 std::sort(corners.begin(), corners.end(), frontalPointLess);
86
87 const Real twice_area = (corners[1](0) - corners[0](0)) * (corners[2](1) - corners[0](1)) -
88 (corners[1](1) - corners[0](1)) * (corners[2](0) - corners[0](0));
89 if (twice_area == 0.0)
90 return std::numeric_limits<Real>::max();
91
92 std::array<Real, 3> sides = {(corners[1] - corners[0]).norm(),
93 (corners[2] - corners[1]).norm(),
94 (corners[0] - corners[2]).norm()};
95 std::sort(sides.begin(), sides.end());
96
97 return sides[0] * sides[1] * sides[2] / (2.0 * std::abs(twice_area));
98}
99
113Point
114frontalLinfCorner(const Point & start,
115 const Point & end,
116 const Point & normal,
117 const Real size,
118 const std::pair<Point, Point> & frame)
119{
120 const Point & u = frame.first;
121 const Point & v = frame.second;
122
123 const Real start_u = start * u;
124 const Real start_v = start * v;
125 const Real end_u = end * u;
126 const Real end_v = end * v;
127
128 // Neither square reaches the other end once the edge is longer than twice the target size in a
129 // frame coordinate, so the target grows to whatever that edge needs, as it does for the L2 metric
130 const Real reach =
131 std::max({size, 0.5 * std::abs(end_u - start_u), 0.5 * std::abs(end_v - start_v)});
132
133 const Real low_u = std::max(start_u, end_u) - reach;
134 const Real high_u = std::min(start_u, end_u) + reach;
135 const Real low_v = std::max(start_v, end_v) - reach;
136 const Real high_v = std::min(start_v, end_v) + reach;
137
138 // A corner sits at the target distance from the end that is larger in u and from the end that is
139 // larger in v, so which pair of corners qualifies turns on whether that is the same end
140 const bool same_end_larger = ((end_u >= start_u) == (end_v >= start_v));
141 const Point first = low_u * u + (same_end_larger ? high_v : low_v) * v;
142 const Point second = high_u * u + (same_end_larger ? low_v : high_v) * v;
143
144 return ((first - start) * normal > (second - start) * normal) ? first : second;
145}
146
157std::vector<bool>
158frontalInsideTriangles(const XYIncrementalDelaunay & delaunay,
159 const std::vector<XYIncrementalDelaunay::Triangle> & triangles,
160 const std::size_t seed_start,
161 const std::size_t seed_end)
162{
163 // The triangles are counter-clockwise, so the one that holds the segment in the order that puts
164 // the domain on its left is the one on the domain side of it
165 std::size_t seed = XYIncrementalDelaunay::invalid_index;
166 for (const auto t : index_range(triangles))
167 for (const auto k : make_range(n_frontal_tri_sides))
168 if (triangles[t].vertices[k] == seed_start &&
169 triangles[t].vertices[(k + 1) % n_frontal_tri_sides] == seed_end)
170 seed = t;
171
173 mooseError("The segment from vertex ",
174 seed_start,
175 " to vertex ",
176 seed_end,
177 " of the outer boundary is not an edge of the triangulation, so the triangles that "
178 "lie in the domain cannot be found. A point placed exactly on that segment divides "
179 "it in two, which removes it.");
180
181 std::vector<bool> inside(triangles.size(), false);
182 inside[seed] = true;
183
184 std::vector<std::size_t> pending{seed};
185
186 while (!pending.empty())
187 {
188 const auto current = pending.back();
189 pending.pop_back();
190
191 const auto & triangle = triangles[current];
192 for (const auto i : make_range(n_frontal_tri_sides))
193 {
194 const auto neighbor = triangle.neighbors[i];
195 if (neighbor == XYIncrementalDelaunay::invalid_index || inside[neighbor])
196 continue;
197 if (delaunay.isConstrainedSegment(triangle.vertices[(i + 1) % n_frontal_tri_sides],
198 triangle.vertices[(i + 2) % n_frontal_tri_sides]))
199 continue;
200
201 inside[neighbor] = true;
202 pending.push_back(neighbor);
203 }
204 }
205
206 return inside;
207}
208}
209
212{
215
216 MooseEnum metric("L2 LINF", "LINF");
217 MooseEnum orientation("BOUNDARY CROSS_FIELD", "CROSS_FIELD");
218
219 params.addParam<std::vector<Point>>(
220 "interior_points",
221 {},
222 "Interior node locations. Any point outside the surface will not be meshed.");
223
224 params.addParam<MooseEnum>("metric",
225 metric,
226 "The norm the target size is measured in when a point is placed ahead "
227 "of the front. 'L2' places points that make equilateral triangles. "
228 "'LINF' places points that make right isosceles triangles in the "
229 "local frame, the shape that recombines into good quadrilaterals.");
230 params.addParam<MooseEnum>("orientation",
231 orientation,
232 "Where the local frame the 'LINF' metric measures in comes from. "
233 "'CROSS_FIELD' solves for a cross field over the domain. 'BOUNDARY' "
234 "takes the frame of the nearest boundary segment, which needs no "
235 "solve. This parameter has no effect when metric is 'L2'.");
236
237 params.addParamNamesToGroup("interior_points", "Mandatory mesh interior nodes");
238 params.addParamNamesToGroup("metric orientation", "Frontal advance");
239
240 params.addClassDescription(
241 "Triangulates meshes within boundaries defined by input meshes by advancing a front, which "
242 "places points at a target size ahead of the triangles that are still too large.");
243
244 return params;
245}
246
248 : SurfaceDelaunayGeneratorBase(parameters),
249 _bdy_ptr(getMesh("boundary")),
250 _hole_ptrs(getMeshes("holes")),
251 _add_nodes_per_boundary_segment(getParam<unsigned int>("add_nodes_per_boundary_segment")),
252 _refine_bdy(getParam<bool>("refine_boundary")),
253 _stitch_holes(getParam<std::vector<bool>>("stitch_holes")),
254 _refine_holes(getParam<std::vector<bool>>("refine_holes")),
255 _desired_area(getParam<Real>("desired_area")),
256 _desired_area_func(getParam<std::string>("desired_area_func")),
257 _interior_points(getParam<std::vector<Point>>("interior_points")),
258 _metric(getParam<MooseEnum>("metric")),
259 _orientation(getParam<MooseEnum>("orientation")),
260 _background_mean_area(0.0),
261 _boundary_cell(0.0),
262 _grid_cell(0.0)
263{
266
267 // The frame the orientation selects only enters the LINF metric, so it does nothing under L2
268 if (_metric == "L2" && isParamSetByUser("orientation"))
269 paramError("orientation", "This parameter only applies to the 'LINF' metric.");
270}
271
272std::unique_ptr<MeshBase>
274 const MeshBase & boundary_mesh,
275 const std::vector<std::unique_ptr<MeshBase>> & holes,
277{
278 MeshTriangulationUtils::XYDelaunayOptions background_opts = opts;
279
280 // The background mesh only has to cover the domain, so it carries none of the naming and none of
281 // the stitching the output mesh gets
282 background_opts.stitch_holes.clear();
283 background_opts.hole_boundaries.clear();
284 background_opts.has_output_subdomain_name = false;
285 background_opts.has_output_boundary = false;
286
287 background_opts.desired_area = _background_area_factor * opts.desired_area;
288 if (!opts.desired_area_func.empty())
289 background_opts.desired_area_func =
290 std::to_string(_background_area_factor) + "*(" + opts.desired_area_func + ")";
291
292 std::vector<std::unique_ptr<MeshBase>> hole_clones;
293 for (const auto & hole : holes)
294 hole_clones.push_back(hole->clone());
295
297 *this, boundary_mesh.clone(), std::move(hole_clones), background_opts);
298}
299
300std::map<dof_id_type, Real>
302{
303 std::map<dof_id_type, std::complex<Real>> directions;
304
305 for (const auto & elem : mesh.element_ptr_range())
306 for (const auto side : elem->side_index_range())
307 {
308 if (elem->neighbor_ptr(side))
309 continue;
310
311 const Node & start = elem->node_ref(side);
312 const Node & end = elem->node_ref((side + 1) % elem->n_sides());
313
314 // Accumulating exp(4 i theta) rather than theta averages the directions modulo pi / 2, the
315 // symmetry a cross carries, so that the two sides of a right angle agree with each other
316 const std::complex<Real> direction =
317 std::polar(1.0, 4.0 * std::atan2(end(1) - start(1), end(0) - start(0)));
318 directions[start.id()] += direction;
319 directions[end.id()] += direction;
320 }
321
322 std::map<dof_id_type, Real> angles;
323 for (const auto & [node_id, direction] : directions)
324 angles[node_id] = std::arg(direction) / 4.0;
325
326 return angles;
327}
328
329void
330XYFrontalDelaunayGenerator::appendLoop(const std::vector<Point> & loop,
331 const bool refine,
332 const unsigned int extra_nodes,
333 const boundary_id_type bcid,
334 std::vector<Point> & points,
335 std::vector<XYIncrementalDelaunay::Segment> & segments)
336{
337 const std::size_t loop_start = points.size();
338
339 for (const auto i : index_range(loop))
340 {
341 const Point & start = loop[i];
342 const Point & end = loop[(i + 1) % loop.size()];
343 _boundary_segments.emplace_back(start, end);
344
345 unsigned int pieces = extra_nodes + 1;
346 if (refine)
347 {
348 // Splitting a segment the front would otherwise have to advance along is the only chance to
349 // refine it, because the front never moves a constrained segment
350 const Real size = targetSize(targetArea(0.5 * (start + end)));
351 pieces = std::max(pieces, static_cast<unsigned int>(std::ceil((end - start).norm() / size)));
352 }
353
354 points.push_back(start);
355 for (const auto piece : make_range(1u, pieces))
356 points.push_back(start + (Real(piece) / pieces) * (end - start));
357 }
358
359 for (const auto vertex : make_range(loop_start, points.size()))
360 {
361 const std::size_t next = (vertex + 1 < points.size()) ? vertex + 1 : loop_start;
362 segments.emplace_back(vertex, next);
364 }
365}
366
367Real
369{
370 if (_area_function)
371 {
372 const Real area = (*_area_function)(point);
373 if (area <= 0.0)
374 paramError("desired_area_func",
375 "The desired area must be positive everywhere in the meshed domain, but it is ",
376 area,
377 " at ",
378 point,
379 ".");
380 return area;
381 }
382
383 if (_desired_area > 0.0)
384 return _desired_area;
385
386 // With no area limit of its own the advance targets the background triangulation, which the
387 // automatic area function or the spacing of the boundary points sized
388 const Elem * const background_elem = (*_background_locator)(point);
389
390 return background_elem ? background_elem->volume() : _background_mean_area;
391}
392
393Real
395{
396 // The equilateral triangle of side h has area sqrt(3) h^2 / 4, the right isosceles triangle of
397 // legs h has area h^2 / 2
398 return (_metric == "L2") ? std::sqrt(4.0 * area / std::sqrt(3.0)) : std::sqrt(2.0 * area);
399}
400
401Real
403{
404 // The equilateral triangle of side h has circumradius h / sqrt(3), the right isosceles triangle
405 // of legs h has half of its hypotenuse
406 return (_metric == "L2") ? size / std::sqrt(3.0) : size / std::sqrt(2.0);
407}
408
409std::pair<long, long>
410XYFrontalDelaunayGenerator::gridKey(const Point & point, const Real cell) const
411{
412 return {static_cast<long>(std::floor(point(0) / cell)),
413 static_cast<long>(std::floor(point(1) / cell))};
414}
415
416void
418{
419 mooseAssert(!_boundary_segments.empty(),
420 "The boundary was seeded before the grid over it is built.");
421
422 Real min_x = std::numeric_limits<Real>::max();
423 Real max_x = std::numeric_limits<Real>::lowest();
424 Real min_y = min_x;
425 Real max_y = max_x;
426 for (const auto & [start, end] : _boundary_segments)
427 for (const auto & corner : {start, end})
428 {
429 min_x = std::min(min_x, corner(0));
430 max_x = std::max(max_x, corner(0));
431 min_y = std::min(min_y, corner(1));
432 max_y = std::max(max_y, corner(1));
433 }
434
435 // Buckets that hold about one segment each: coarser ones leave a list to walk in every bucket the
436 // search reaches, finer ones leave the search reaching over more buckets to cover the same
437 // distance. They are never finer than the vertex grid, which is as fine as the mesh itself gets
438 const Real extent = (max_x - min_x) * (max_y - min_y);
439 _boundary_cell = std::max(_grid_cell, std::sqrt(extent / _boundary_segments.size()));
440
441 for (const auto segment : index_range(_boundary_segments))
442 {
443 const auto & [start, end] = _boundary_segments[segment];
444
445 // Walking the segment in steps of half a bucket puts consecutive steps in the same bucket or in
446 // neighboring ones, so every bucket the segment passes through neighbors one it is recorded in,
447 // which is the reach the search below adds to the distance it has covered
448 const auto steps =
449 std::max(std::size_t(1),
450 static_cast<std::size_t>(std::ceil(2.0 * (end - start).norm() / _boundary_cell)));
451 for (const auto step : make_range(steps + 1))
452 {
453 auto & bucket = _boundary_segment_grid[gridKey(start + (Real(step) / steps) * (end - start),
455 // The steps of a straight segment reach a bucket in one run, so the last entry is the only
456 // one that can already be this segment
457 if (bucket.empty() || bucket.back() != segment)
458 bucket.push_back(segment);
459 }
460 }
461}
462
463std::pair<Point, Point>
465{
466 if (_cross_field)
467 return _cross_field->crossFrame(point);
468
469 mooseAssert(_boundary_cell > 0.0,
470 "The grid over the boundary segments is built before any frame is taken from them.");
471
472 const auto [center_i, center_j] = gridKey(point, _boundary_cell);
473
474 // The nearest segment is the one of the lowest index at the smallest distance, whichever buckets
475 // it is found in, so that the frame follows from the geometry and not from the order of the
476 // search. The distances are compared squared, which orders them the same way
477 Real nearest = std::numeric_limits<Real>::max();
478 std::size_t nearest_segment = std::numeric_limits<std::size_t>::max();
479 const auto search = [&](const long i, const long j)
480 {
481 const auto bucket = _boundary_segment_grid.find({i, j});
482 if (bucket == _boundary_segment_grid.end())
483 return;
484
485 for (const auto segment : bucket->second)
486 {
487 const auto & [start, end] = _boundary_segments[segment];
488 const Real distance = geom_utils::pointSegmentDistanceSq(point, start, end);
489 if (distance < nearest || (distance == nearest && segment < nearest_segment))
490 {
491 nearest = distance;
492 nearest_segment = segment;
493 }
494 }
495 };
496
497 // The buckets are searched a ring at a time, until the nearest segment found is closer than the
498 // rings still to come can reach. A segment that has not been searched yet lies in a bucket more
499 // than one ring out, and so no nearer than the ring before the one just searched
500 for (long span = 0;; ++span)
501 {
502 if (span == 0)
503 search(center_i, center_j);
504 else
505 {
506 for (long i = center_i - span; i <= center_i + span; ++i)
507 {
508 search(i, center_j - span);
509 search(i, center_j + span);
510 }
511 for (long j = center_j - span + 1; j <= center_j + span - 1; ++j)
512 {
513 search(center_i - span, j);
514 search(center_i + span, j);
515 }
516 }
517
518 const Real covered = std::max(0L, span - 1) * _boundary_cell;
519 if (nearest < covered * covered)
520 break;
521 }
522
523 const auto & [start, end] = _boundary_segments[nearest_segment];
524 const Point tangent = (end - start).unit();
525
526 return {tangent, Point(-tangent(1), tangent(0), 0.0)};
527}
528
529Real
531 const Point & second,
532 const std::pair<Point, Point> & frame) const
533{
534 const Point offset = first - second;
535 if (_metric == "L2")
536 return offset.norm();
537
538 return std::max(std::abs(offset * frame.first), std::abs(offset * frame.second));
539}
540
541bool
543{
544 if (!_outer_outline->contains(point))
545 return false;
546
547 for (const auto & outline : _hole_outlines)
548 if (outline->contains(point))
549 return false;
550
551 return true;
552}
553
554void
555XYFrontalDelaunayGenerator::addToGrid(const std::size_t vertex, const Point & point)
556{
557 _vertex_grid[gridKey(point, _grid_cell)].push_back(vertex);
558}
559
560bool
562 const Point & point,
563 const Real distance,
564 const std::pair<Point, Point> & frame) const
565{
566 // A LINF ball of the given radius reaches sqrt(2) times as far as an L2 one of the same radius,
567 // so that is how far the buckets have to be searched for the vertices the metric then judges
568 const Real reach = (_metric == "L2") ? distance : distance * std::sqrt(2.0);
569 const long span = static_cast<long>(std::ceil(reach / _grid_cell));
570 const auto [center_i, center_j] = gridKey(point, _grid_cell);
571
572 // The buckets are sized on the smallest triangle the advance is asked for, so where the target is
573 // coarser the reach spans many of them and all but a few are empty. Walking each row of the
574 // search from the first bucket at or past its start leaves the empty ones unvisited, rather than
575 // looked up one by one
576 for (long i = center_i - span; i <= center_i + span; ++i)
577 for (auto bucket = _vertex_grid.lower_bound({i, center_j - span});
578 bucket != _vertex_grid.end() && bucket->first.first == i &&
579 bucket->first.second <= center_j + span;
580 ++bucket)
581 for (const auto vertex : bucket->second)
582 if (metricDistance(point, frontalToPoint(delaunay.point(vertex)), frame) < distance)
583 return true;
584
585 return false;
586}
587
588bool
590 const FrontEdge & edge,
591 Point & point) const
592{
593 const Point start = frontalToPoint(delaunay.point(edge.start));
594 const Point end = frontalToPoint(delaunay.point(edge.end));
595 const Point midpoint = 0.5 * (start + end);
596
597 const Real size = targetSize(targetArea(midpoint));
598
599 // The triangle the front advances into is on the left of the edge, so the left normal points to
600 // the side the new point goes on
601 const Point along = end - start;
602 const Real length = along.norm();
603 const Point normal(-along(1) / length, along(0) / length, 0.0);
604
605 // The frame only enters the LINF metric, so an L2 advance never asks for the cross field
606 const std::pair<Point, Point> world_frame(Point(1.0, 0.0, 0.0), Point(0.0, 1.0, 0.0));
607 const std::pair<Point, Point> frame = (_metric == "L2") ? world_frame : localFrame(midpoint);
608
609 if (_metric == "L2")
610 {
611 // The apex at the target distance from both ends of the edge, or of the right isosceles
612 // triangle on the edge once that edge is longer than sqrt(2) times the target size
613 const Real half_length = 0.5 * length;
614 point = midpoint + std::sqrt(std::max(size * size - half_length * half_length,
615 half_length * half_length)) *
616 normal;
617 }
618 else
619 point = frontalLinfCorner(start, end, normal, size, frame);
620
621 if (!insideDomain(point))
622 return false;
623
624 // A point too close to a vertex that is already there would make a sliver; the front edge is
625 // instead left to connect to that vertex, which the triangulation has already done
626 return !hasVertexWithin(delaunay, point, _rejection_factor * size, frame);
627}
628
629std::vector<XYFrontalDelaunayGenerator::FrontEdge>
631 const XYIncrementalDelaunay & delaunay,
632 const std::vector<XYIncrementalDelaunay::Triangle> & triangles,
633 const std::vector<bool> & inside) const
634{
635 // The circumradius answers for both the size and the shape of a triangle: it grows with the
636 // triangle and it runs away as the triangle flattens
637 std::vector<Real> excess(triangles.size(), 0.0);
638 for (const auto t : index_range(triangles))
639 {
640 if (!inside[t])
641 continue;
642
643 const Point first = frontalToPoint(delaunay.point(triangles[t].vertices[0]));
644 const Point second = frontalToPoint(delaunay.point(triangles[t].vertices[1]));
645 const Point third = frontalToPoint(delaunay.point(triangles[t].vertices[2]));
646 const Point centroid = (first + second + third) / 3.0;
647
648 excess[t] = frontalCircumradius(first, second, third) /
650 }
651
652 std::vector<FrontEdge> front;
653 for (const auto t : index_range(triangles))
654 {
655 if (!inside[t] || excess[t] <= _size_tolerance)
656 continue;
657
658 const auto & triangle = triangles[t];
659 for (const auto i : make_range(n_frontal_tri_sides))
660 {
661 // An edge between two triangles that both miss the target is behind the front, not on it
662 const auto neighbor = triangle.neighbors[i];
663 if (neighbor != XYIncrementalDelaunay::invalid_index && inside[neighbor] &&
664 excess[neighbor] > _size_tolerance)
665 continue;
666
667 front.push_back({triangle.vertices[(i + 1) % n_frontal_tri_sides],
668 triangle.vertices[(i + 2) % n_frontal_tri_sides],
669 excess[t]});
670 }
671 }
672
673 // Ties are broken by the vertices of the edge so that the same points are placed, in the same
674 // order, from one run to the next
675 std::sort(front.begin(),
676 front.end(),
677 [](const FrontEdge & a, const FrontEdge & b)
678 {
679 if (a.excess > b.excess)
680 return true;
681 if (b.excess > a.excess)
682 return false;
683
684 return XYIncrementalDelaunay::makeSegment(a.start, a.end) <
685 XYIncrementalDelaunay::makeSegment(b.start, b.end);
686 });
687
688 return front;
689}
690
691void
693 const std::size_t vertex)
694{
695 // The vertex the insertion placed is the newest one, so it is the larger id of every segment it
696 // is an end of, and the two halves are the only constrained segments it is an end of at all
697 std::vector<std::size_t> ends;
698 for (const auto & [first, second] : delaunay.constrainedSegments())
699 if (second == vertex)
700 ends.push_back(first);
701
702 mooseAssert(ends.size() == 2,
703 "A split replaces the segment the new vertex landed on by the two halves that vertex "
704 "divides it into");
705
706 const auto split = XYIncrementalDelaunay::makeSegment(ends.front(), ends.back());
707 const auto recorded = _segment_boundary_ids.find(split);
708 // A constrained segment carrying no boundary id belongs to no input boundary, which
709 // buildTriangleMesh() reports; leaving the halves without one as well keeps that report
710 if (recorded == _segment_boundary_ids.end())
711 return;
712
713 const boundary_id_type bcid = recorded->second;
714 _segment_boundary_ids.erase(recorded);
715 _segment_boundary_ids[XYIncrementalDelaunay::makeSegment(ends.front(), vertex)] = bcid;
717}
718
719void
721{
722 // The front is walked over and over rather than rebuilt after every point, which is what keeps
723 // the cost of the advance down. A pass that places nothing has nothing left to place: every
724 // point is at least the rejection distance from every other, so only finitely many fit
725 bool placed_any = true;
726 while (placed_any)
727 {
728 placed_any = false;
729
730 const auto triangles = delaunay.getTriangles();
731 // The outer boundary was seeded first and counter-clockwise, so the domain is on the left of
732 // the segment from vertex 0 to vertex 1
733 const auto inside = frontalInsideTriangles(delaunay, triangles, 0, 1);
734
735 for (const auto & edge : collectFront(delaunay, triangles, inside))
736 {
737 Point point;
738 if (!placePoint(delaunay, edge, point))
739 continue;
740
741 const auto vertices_before = delaunay.numPoints();
742 const auto segments_before = delaunay.constrainedSegments().size();
743 const auto vertex = delaunay.insertPoint({point(0), point(1)});
744 if (delaunay.numPoints() == vertices_before)
745 continue;
746
747 // A point that lands exactly on a constrained segment splits it, which leaves the boundary id
748 // of that segment recorded against a segment the triangulation no longer has
749 if (delaunay.constrainedSegments().size() != segments_before)
750 recordSplitBoundaryIds(delaunay, vertex);
751
752 addToGrid(vertex, point);
753 placed_any = true;
754 }
755 }
756}
757
758std::unique_ptr<MeshBase>
760{
761 const auto triangles = delaunay.getTriangles();
762 // The outer boundary was seeded first and counter-clockwise, so the domain is on the left of the
763 // segment from vertex 0 to vertex 1
764 const auto inside = frontalInsideTriangles(delaunay, triangles, 0, 1);
765
766 auto mesh = buildReplicatedMesh(2);
767
768 // Adding the nodes in vertex order rather than as the triangles reach them keeps the node
769 // numbering of the output tied to the order the points were placed in
770 std::set<std::size_t> used_vertices;
771 for (const auto t : index_range(triangles))
772 if (inside[t])
773 used_vertices.insert(triangles[t].vertices.begin(), triangles[t].vertices.end());
774
775 std::map<std::size_t, Node *> nodes;
776 for (const auto vertex : used_vertices)
777 nodes[vertex] = mesh->add_point(frontalToPoint(delaunay.point(vertex)));
778
779 auto & boundary_info = mesh->get_boundary_info();
780 for (const auto t : index_range(triangles))
781 {
782 if (!inside[t])
783 continue;
784
785 const auto & triangle = triangles[t];
786 Elem * const elem = mesh->add_elem(Elem::build(libMesh::ElemType::TRI3));
787 for (const auto k : make_range(n_frontal_tri_sides))
788 elem->set_node(k, libmesh_map_find(nodes, triangle.vertices[k]));
789 elem->subdomain_id() = 0;
790
791 for (const auto side : make_range(n_frontal_tri_sides))
792 {
793 // Side s of a triangle runs from vertex s to vertex s + 1, which is the edge the
794 // triangulation holds opposite vertex s + 2
795 const auto neighbor = triangle.neighbors[(side + 2) % n_frontal_tri_sides];
796 if (neighbor != XYIncrementalDelaunay::invalid_index && inside[neighbor])
797 continue;
798
799 const auto first = triangle.vertices[side];
800 const auto second = triangle.vertices[(side + 1) % n_frontal_tri_sides];
801 const auto bcid =
803 if (bcid == _segment_boundary_ids.end())
804 mooseError("A side of the triangulation lies on the boundary of the domain without lying "
805 "on any of the input boundaries, which happens when a point of "
806 "'interior_points' falls on the outer boundary or on a hole boundary.");
807
808 boundary_info.add_side(elem, side, bcid->second);
809 }
810 }
811
812 mesh->prepare_for_use();
813
814 return mesh;
815}
816
817std::unique_ptr<MeshBase>
819{
820 std::unique_ptr<MeshBase> boundary_mesh = std::move(_bdy_ptr);
821
822 std::vector<std::unique_ptr<MeshBase>> hole_meshes(_hole_ptrs.size());
823 for (const auto hole_i : index_range(_hole_ptrs))
824 hole_meshes[hole_i] = std::move(*_hole_ptrs[hole_i]);
825
826 // The advance places one point at a time against the whole triangulation, which no process holds
827 // a part of
828 if (!boundary_mesh->is_replicated())
829 mooseError("XYFrontalDelaunayGenerator is not implemented for distributed meshes");
830
831 // The outer boundary is only known once the mesh knows which of its sides face outward
832 if (!boundary_mesh->is_prepared())
833 boundary_mesh->prepare_for_use();
834
835 for (const auto & elem : boundary_mesh->element_ptr_range())
836 if (elem->default_order() != libMesh::FIRST)
837 paramError("boundary",
838 "Element ",
839 elem->id(),
840 " is a ",
842 " element. Only first order boundary elements are supported, because this mesh "
843 "generator produces TRI3 elements.");
844
847
848 _outer_outline = std::make_unique<libMesh::TriangulatorInterface::MeshedHole>(
849 *boundary_mesh, MeshTriangulationUtils::outerBoundaryIds(*this, *boundary_mesh, opts));
850
851 std::vector<bool> holes_with_midpoints(hole_meshes.size());
852 _hole_outlines.reserve(hole_meshes.size());
853 for (const auto hole_i : index_range(hole_meshes))
854 {
855 if (!hole_meshes[hole_i]->is_prepared())
856 hole_meshes[hole_i]->prepare_for_use();
857
858 _hole_outlines.push_back(
859 std::make_unique<libMesh::TriangulatorInterface::MeshedHole>(*hole_meshes[hole_i]));
860 holes_with_midpoints[hole_i] = _hole_outlines.back()->n_midpoints();
861
862 if (holes_with_midpoints[hole_i] && hole_i < _stitch_holes.size() && _stitch_holes[hole_i])
863 paramError("stitch_holes",
864 "Cannot stitch a quadratic element hole to the first order triangles this mesh "
865 "generator produces. Please reduce the order of the hole inputs.");
866 }
867
868 _background_mesh = buildBackgroundMesh(*boundary_mesh, hole_meshes, opts);
869 _background_mesh->prepare_for_use();
870 _background_locator = _background_mesh->sub_point_locator();
871 _background_locator->enable_out_of_mesh_mode();
872
873 Real background_area = 0.0;
874 for (const auto & elem : _background_mesh->element_ptr_range())
875 background_area += elem->volume();
876 _background_mean_area = background_area / _background_mesh->n_elem();
877
878 if (!_desired_area_func.empty())
880
881 // The buckets of the rejection rule are sized on the smallest triangle the advance is asked for,
882 // so that a search of the buckets around a point covers the distance the rule needs
883 _grid_cell = std::numeric_limits<Real>::max();
884 for (const auto & elem : _background_mesh->element_ptr_range())
885 _grid_cell = std::min(_grid_cell, targetSize(targetArea(elem->vertex_average())));
886
887 if (_metric == "LINF" && _orientation == "CROSS_FIELD")
888 {
889 _cross_field = std::make_unique<XYCrossFieldSolver>(*_background_mesh,
891 _cross_field->solve();
892 }
893
894 // The outer boundary is walked counter-clockwise so that the domain is on the left of its first
895 // segment, which is where the triangles in the domain are then found from
896 std::vector<Point> outer_loop;
897 for (const auto i : make_range(_outer_outline->n_points()))
898 outer_loop.push_back(_outer_outline->point(i));
899 frontalCanonicalizeLoop(outer_loop);
900
901 std::vector<Point> points;
902 std::vector<XYIncrementalDelaunay::Segment> segments;
903 appendLoop(outer_loop, _refine_bdy, _add_nodes_per_boundary_segment, 0, points, segments);
904
905 for (const auto hole_i : index_range(_hole_outlines))
906 {
907 std::vector<Point> hole_loop;
908 for (const auto i : make_range(_hole_outlines[hole_i]->n_points()))
909 hole_loop.push_back(_hole_outlines[hole_i]->point(i));
910 frontalCanonicalizeLoop(hole_loop);
911
912 const bool refine = (hole_i >= _refine_holes.size() || _refine_holes[hole_i]);
913 appendLoop(hole_loop, refine, 0, static_cast<boundary_id_type>(hole_i + 1), points, segments);
914 }
915
916 // The frame of the nearest boundary segment is only asked for once every segment has been seeded,
917 // and only when no cross field answers for it
918 if (_metric == "LINF" && !_cross_field)
920
921 // A point on a boundary would split the segment it lies on, which would leave the output mesh
922 // with a side that belongs to no input boundary
923 for (const auto & interior_point : _interior_points)
924 if (insideDomain(interior_point))
925 points.push_back(interior_point);
926
927 std::vector<XYIncrementalDelaunay::Point2D> plane_points;
928 plane_points.reserve(points.size());
929 for (const auto & point : points)
930 plane_points.push_back({point(0), point(1)});
931
932 XYIncrementalDelaunay delaunay;
933 delaunay.initialize(plane_points, segments);
934
935 for (const auto vertex : make_range(delaunay.numPoints()))
936 addToGrid(vertex, frontalToPoint(delaunay.point(vertex)));
937
938 advanceFront(delaunay);
939
940 auto mesh = buildTriangleMesh(delaunay);
941
943 *this, dynamic_cast<UnstructuredMesh &>(*mesh), hole_meshes, holes_with_midpoints, opts);
944
945 return mesh;
946}
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
Definition MooseError.h:311
for(PetscInt i=0;i< nvars;++i)
char ** sides
if(!dmm->_nl) SETERRQ(PETSC_COMM_WORLD
registerMooseObject("MooseApp", XYFrontalDelaunayGenerator)
void ErrorVector unsigned int
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system.
void addParamNamesToGroup(const std::string &space_delim_names, const std::string group_name)
This method takes a space delimited list of parameter names and adds them to the specified group name...
void addParam(const std::string &name, const S &value, const std::string &doc_string)
These methods add an optional parameter and a documentation string to the InputParameters object.
void addClassDescription(const std::string &doc_string)
This method adds a description of the class that will be displayed in the input file syntax dump.
std::unique_ptr< ReplicatedMesh > buildReplicatedMesh(unsigned int dim=libMesh::invalid_uint)
Build a replicated 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
bool isParamSetByUser(const std::string &name) const
Test if the supplied parameter is set by a user, as opposed to not set or set to default.
Definition MooseBase.h:205
This is a "smart" enum class intended to replace many of the shortcomings in the C++ enum type It sho...
Definition MooseEnum.h:55
Base class for Delaunay mesh generators applied to a surface.
void checkInteriorPoints(const std::vector< Point > &interior_points) const
Errors if a point was given twice as an interior point, which the triangulation cannot honor.
void fillDelaunayOptions(MeshTriangulationUtils::XYDelaunayOptions &opts) const
Fills the triangulation options that follow from the parameters boundaryAndHolesParams() adds,...
static InputParameters boundaryAndHolesParams()
The parameters that select the outer boundary to triangulate within and the holes to leave out of the...
void checkBoundaryAndHolesParams(const std::vector< std::unique_ptr< MeshBase > * > &hole_ptrs) const
Errors if the parameters boundaryAndHolesParams() adds contradict each other or the holes they refer ...
Generates a triangulation in the XY plane by advancing a front, based on an input mesh defining the o...
static constexpr Real _size_tolerance
How far the circumradius of a triangle may exceed the target before the advance refines it.
const std::vector< bool > _refine_holes
Whether to allow automatically refining each hole boundary.
std::map< XYIncrementalDelaunay::Segment, boundary_id_type > _segment_boundary_ids
Boundary id of each seed segment, keyed on its two vertices with the smaller id first.
const std::string _desired_area_func
Desired triangle area as a (fparser-compatible) function of x,y.
std::unique_ptr< MeshBase > buildTriangleMesh(const XYIncrementalDelaunay &delaunay)
static constexpr Real _rejection_factor
How close to an existing vertex, as a fraction of the target size, a new point may not come.
Real _boundary_cell
Side of the buckets the boundary segments are sorted into so that the frame search stays local.
std::pair< Point, Point > localFrame(const Point &point) const
void recordSplitBoundaryIds(const XYIncrementalDelaunay &delaunay, std::size_t vertex)
Moves the boundary id recorded for the constrained segment an insertion split onto the two halves tha...
const std::vector< std::unique_ptr< MeshBase > * > _hole_ptrs
Holds pointers to the pointers to input meshes defining holes.
Real _grid_cell
Side of the buckets the vertices are sorted into so that the rejection rule stays local.
void appendLoop(const std::vector< Point > &loop, bool refine, unsigned int extra_nodes, boundary_id_type bcid, std::vector< Point > &points, std::vector< XYIncrementalDelaunay::Segment > &segments)
Adds one closed loop of the input boundary to the points the triangulation is seeded with and to the ...
static std::map< dof_id_type, Real > boundaryTangentAngles(const MeshBase &mesh)
void advanceFront(XYIncrementalDelaunay &delaunay)
Advances the front over the whole domain, inserting points into the triangulation.
std::vector< FrontEdge > collectFront(const XYIncrementalDelaunay &delaunay, const std::vector< XYIncrementalDelaunay::Triangle > &triangles, const std::vector< bool > &inside) const
bool hasVertexWithin(const XYIncrementalDelaunay &delaunay, const Point &point, Real distance, const std::pair< Point, Point > &frame) const
std::unique_ptr< MeshBase > buildBackgroundMesh(const MeshBase &boundary_mesh, const std::vector< std::unique_ptr< MeshBase > > &holes, const MeshTriangulationUtils::XYDelaunayOptions &opts)
Triangulates the domain with the existing Delaunay triangulator at _background_area_factor times the ...
Real metricDistance(const Point &first, const Point &second, const std::pair< Point, Point > &frame) const
std::map< std::pair< long, long >, std::vector< std::size_t > > _vertex_grid
Vertex ids of the triangulation, bucketed by position.
const MooseEnum _metric
Norm the target size is measured in when a point is placed ahead of the front.
const Real _desired_area
Desired (maximum) triangle area.
std::map< std::pair< long, long >, std::vector< std::size_t > > _boundary_segment_grid
Indices into _boundary_segments, bucketed by the cells each of those segments passes through.
XYFrontalDelaunayGenerator(const InputParameters &parameters)
std::vector< std::unique_ptr< libMesh::TriangulatorInterface::MeshedHole > > _hole_outlines
Outlines of the holes, which the advance stays outside.
Real targetArea(const Point &point) const
std::unique_ptr< MeshBase > _background_mesh
Coarse triangulation of the domain the cross field is solved on.
std::unique_ptr< libMesh::ParsedFunction< Real > > _area_function
Desired area as a function of position, built only when 'desired_area_func' is set.
std::unique_ptr< MeshBase > & _bdy_ptr
Input mesh defining the boundary to triangulate within.
std::unique_ptr< libMesh::TriangulatorInterface::MeshedHole > _outer_outline
Outline of the outer boundary, which the advance stays inside.
std::pair< long, long > gridKey(const Point &point, Real cell) const
void addToGrid(std::size_t vertex, const Point &point)
Records a vertex in the grid the rejection rule searches.
static constexpr Real _background_area_factor
How much coarser in area the background triangulation is than the mesh being generated.
void buildBoundarySegmentGrid()
Sorts the boundary segments into the buckets the BOUNDARY frame searches, so that a search only has t...
Real _background_mean_area
Mean area of the background elements, the target where the background locator finds nothing.
const std::vector< Point > _interior_points
Desired interior node locations.
bool placePoint(const XYIncrementalDelaunay &delaunay, const FrontEdge &edge, Point &point) const
Computes where the advance would place a point ahead of a front edge and applies the rejection rule t...
std::vector< std::pair< Point, Point > > _boundary_segments
Segments of the outer boundary and of the holes, whose tangents give the BOUNDARY frame.
const std::vector< bool > _stitch_holes
Whether to stitch to the mesh defining each hole.
std::unique_ptr< XYCrossFieldSolver > _cross_field
Cross field over the domain, built only when the LINF metric asks for the CROSS_FIELD frame.
std::unique_ptr< libMesh::PointLocatorBase > _background_locator
Locates the background element whose area is the target where no area limit was given.
const MooseEnum _orientation
Where the local frame the LINF metric measures in comes from.
bool insideDomain(const Point &point) const
const unsigned int _add_nodes_per_boundary_segment
How many more nodes to add in each outer boundary segment.
const bool _refine_bdy
Whether to allow automatically refining the outer boundary.
std::unique_ptr< MeshBase > generate() override
Generate / modify the mesh.
Constrained Delaunay triangulation of a set of points in the plane, built one point at a time.
const std::set< Segment > & constrainedSegments() const
void initialize(const std::vector< Point2D > &points, const std::vector< Segment > &segments)
Triangulates points and recovers every entry of segments as an edge of the result.
std::size_t numPoints() const
std::size_t insertPoint(const Point2D &p)
Inserts a point, restoring the constrained Delaunay property around it.
bool isConstrainedSegment(std::size_t v0, std::size_t v1) const
static Segment makeSegment(std::size_t v0, std::size_t v1)
const Point2D & point(std::size_t id) const
static constexpr std::size_t invalid_index
Sentinel for a vertex, triangle or neighbor that does not exist.
std::vector< Triangle > getTriangles() const
MeshBase & mesh
void finalizeTriangulation(MeshGenerator &mg, UnstructuredMesh &mesh, std::vector< std::unique_ptr< MeshBase > > &holes, const std::vector< bool > &holes_with_midpoints, const XYDelaunayOptions &opts)
Performs the subdomain and boundary naming, the boundary id remapping (outer boundary to 0 and hole i...
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...
std::set< std::size_t > outerBoundaryIds(MeshGenerator &mg, MeshBase &boundary_mesh, const XYDelaunayOptions &opts)
Resolves the outer-boundary selection of the options into the set of ids that define it: the ids of '...
Real pointSegmentDistanceSq(const Point &point, const Point &a, const Point &b)
Compute the squared distance from a point to a 3-D line segment.
libMesh::Real signedArea2D(const libMesh::Point &pt1, const libMesh::Point &pt2, const libMesh::Point &pt3)
Twice the signed area of a triangle in the xy plane, which is positive when its corners are ordered c...
auto norm(const T &a)
std::string enum_to_string(const T e)
auto index_range(const T &sizable)
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
IntRange< T > make_range(T beg, T end)
Bundle of inputs for triangulateWithDelaunay.
An edge of the front, which separates the triangles that meet the target size from the triangles that...
std::size_t start
Vertex at the start of the edge, which has the triangle that misses the target on its left.
std::size_t end
Vertex at the end of the edge.
A point of the triangulation, held as plain coordinates.
Real distance(const Point &p)