17#include "libmesh/boundary_info.h"
18#include "libmesh/elem.h"
19#include "libmesh/enum_elem_type.h"
20#include "libmesh/int_range.h"
21#include "libmesh/libmesh.h"
22#include "libmesh/mesh_base.h"
23#include "libmesh/node.h"
24#include "libmesh/replicated_mesh.h"
25#include "libmesh/string_to_enum.h"
32#include <unordered_set>
41constexpr unsigned int n_tri_sides = 3;
44constexpr unsigned int n_quad_sides = 4;
53std::pair<dof_id_type, dof_id_type>
54edgeKey(
const dof_id_type node_id_1,
const dof_id_type node_id_2)
56 return std::make_pair(std::min(node_id_1, node_id_2), std::max(node_id_1, node_id_2));
69edgeMidpointNode(MeshBase & mesh,
70 std::map<std::pair<dof_id_type, dof_id_type>, Node *> & midpoints,
74 const auto edge = edgeKey(node_1.id(), node_2.id());
75 const auto it = midpoints.find(edge);
76 if (it != midpoints.end())
79 Node *
const midpoint =
mesh.add_point((node_1 + node_2) / 2.0);
80 midpoints.emplace(edge, midpoint);
93std::vector<TriToQuadConverter::QuadCorners>
94subdivisionTemplate(
const unsigned int n_corners)
96 std::vector<TriToQuadConverter::QuadCorners> quads;
98 quads.push_back({k, n_corners + k, 2 * n_corners, n_corners + (k + n_corners - 1) % n_corners});
111quadSideOnEdge(
const std::array<dof_id_type, 4> & quad_node_ids,
112 const dof_id_type node_id_1,
113 const dof_id_type node_id_2)
118 const dof_id_type end_2 = quad_node_ids[(s + 1) % quad_node_ids.size()];
119 if ((end_1 == node_id_1 && end_2 == node_id_2) || (end_1 == node_id_2 && end_2 == node_id_1))
120 return cast_int<unsigned int>(s);
133removeScratchElements(MeshBase & mesh,
const subdomain_id_type scratch_subdomain_id)
135 for (
auto elem_it =
mesh.active_subdomain_elements_begin(scratch_subdomain_id);
136 elem_it !=
mesh.active_subdomain_elements_end(scratch_subdomain_id);
138 mesh.delete_elem(*elem_it);
141 mesh.prepare_for_use();
145 mesh.get_boundary_info().build_node_list_from_side_list();
155 "The TRI3 mesh to convert into QUAD4 elements.");
157 MooseEnum algorithm(
"SUBDIVISION RECOMBINE",
"RECOMBINE");
160 "The algorithm used to build the quadrilaterals. 'SUBDIVISION' splits "
161 "every triangle into three quadrilaterals. 'RECOMBINE' merges pairs "
162 "of adjacent triangles into quadrilaterals.");
167 "eta_min > 0 & eta_min <= 1",
168 "'RECOMBINE' algorithm only: the quality score eta = 1 - (2 / pi) max_k |pi / 2 - alpha_k| "
169 "of the quadrilateral, in which alpha_k are its four internal angles, that a pair of "
170 "adjacent triangles must reach to be merged. A rectangle scores 1 and a non-convex "
174 "tri_subdomain_name_suffix",
176 "'RECOMBINE' algorithm only: the triangles which could not be merged are moved out of each "
177 "subdomain into a new subdomain named after it, with an underscore and this suffix "
178 "appended. A subdomain without a name contributes its id instead.");
182 "'RECOMBINE' algorithm only: whether the triangles that could not be "
183 "merged are eliminated so that the converted mesh consists exclusively of "
188 params.
addClassDescription(
"Converts a mesh consisting of TRI3 elements into a mesh consisting "
189 "of QUAD4 elements, either by splitting every triangle into three "
190 "quadrilaterals or by merging pairs of adjacent triangles.");
197 _input(getMesh(
"input")),
198 _algorithm(getParam<
MooseEnum>(
"algorithm")),
199 _eta_min(getParam<Real>(
"eta_min")),
200 _tri_subdomain_name_suffix(getParam<SubdomainName>(
"tri_subdomain_name_suffix")),
201 _all_quad(getParam<bool>(
"all_quad"))
205 "The 'all_quad' option is only available with the 'RECOMBINE' algorithm.");
209 "The 'all_quad' option leaves no triangle for 'tri_subdomain_name_suffix' to name.");
212std::unique_ptr<MeshBase>
215 auto replicated_mesh_ptr =
dynamic_cast<ReplicatedMesh *
>(
_input.get());
216 if (!replicated_mesh_ptr)
217 paramError(
"input",
"Input is not a replicated mesh, which is required");
219 ReplicatedMesh &
mesh = *replicated_mesh_ptr;
221 for (
const auto & elem :
mesh.element_ptr_range())
222 if (elem->type() != libMesh::ElemType::TRI3)
228 " element. Only meshes consisting exclusively of TRI3 elements are supported.");
233 for (
const auto & node :
mesh.node_ptr_range())
234 if (!MooseUtils::absoluteFuzzyEqual((*node)(2), 0.0))
240 ". Only meshes in the XY plane are supported.");
253 Real max_deviation = 0.0;
254 for (
const auto k : index_range(quad_points))
256 const Point incoming = quad_points[k] - quad_points[(k + 3) % quad_points.size()];
257 const Point outgoing = quad_points[(k + 1) % quad_points.size()] - quad_points[k];
261 const Real turn = std::atan2(incoming(0) * outgoing(1) - incoming(1) * outgoing(0),
262 incoming(0) * outgoing(0) + incoming(1) * outgoing(1));
267 max_deviation = std::max(max_deviation, std::abs(
libMesh::pi / 2.0 - alpha));
270 return std::max(0.0, 1.0 - 2.0 /
libMesh::pi * max_deviation);
273std::vector<TriToQuadConverter::RecombineCandidate>
277 std::sort(candidates.begin(),
285 if (a.first_elem_id != b.first_elem_id)
286 return a.first_elem_id < b.first_elem_id;
287 return a.second_elem_id < b.second_elem_id;
290 std::vector<RecombineCandidate> selected;
291 std::unordered_set<dof_id_type> consumed;
292 for (
const auto & candidate : candidates)
295 if (candidate.eta < eta_min)
297 if (consumed.count(candidate.first_elem_id) || consumed.count(candidate.second_elem_id))
300 consumed.insert(candidate.first_elem_id);
301 consumed.insert(candidate.second_elem_id);
302 selected.push_back(candidate);
308std::vector<TriToQuadConverter::QuadCorners>
311 return subdivisionTemplate(n_tri_sides);
314std::vector<TriToQuadConverter::QuadCorners>
317 return subdivisionTemplate(n_quad_sides);
322 const unsigned int side,
323 const Elem & neighbor)
327 const unsigned int neighbor_side = neighbor.which_neighbor_am_i(&elem);
328 std::array<const Node *, 4> corners = {elem.node_ptr((side + 2) % n_tri_sides),
330 neighbor.node_ptr((neighbor_side + 2) % n_tri_sides),
331 elem.node_ptr((side + 1) % n_tri_sides)};
337 std::reverse(corners.begin(), corners.end());
339 const std::array<Point, 4> quad_points = {*corners[0], *corners[1], *corners[2], *corners[3]};
343 candidate.
first_elem_id = std::min(elem.id(), neighbor.id());
345 for (
const auto k : index_range(corners))
354 BoundaryInfo & boundary_info =
mesh.get_boundary_info();
355 const auto bdry_side_list = boundary_info.build_side_list();
361 std::map<std::pair<dof_id_type, dof_id_type>, Node *> edge_midpoints;
364 std::vector<dof_id_type> elem_ids;
365 for (
const auto & elem :
mesh.active_element_ptr_range())
366 elem_ids.push_back(elem->id());
373 std::vector<unsigned int> vertex_index;
374 std::vector<unsigned int> side_index;
375 std::vector<Node *> vertices;
376 std::vector<Node *> midpoints;
377 std::vector<Node *> points;
378 std::vector<Elem *> quads;
380 for (
const auto elem_id : elem_ids)
382 Elem *
const parent =
mesh.elem_ptr(elem_id);
383 const unsigned int n_corners = parent->n_nodes();
385 std::vector<std::vector<boundary_id_type>> elem_side_list;
387 bdry_side_list, elem_id, cast_int<unsigned short>(n_corners), elem_side_list);
392 const bool clockwise =
396 vertex_index.assign(n_corners, 0);
397 side_index.assign(n_corners, 0);
398 for (
const auto k : make_range(n_corners))
400 vertex_index[k] = clockwise ? (n_corners - k) % n_corners : k;
401 side_index[k] = clockwise ? (2 * n_corners - k - 1) % n_corners : k;
404 vertices.assign(n_corners,
nullptr);
405 for (
const auto k : index_range(vertices))
406 vertices[k] = parent->node_ptr(vertex_index[k]);
408 midpoints.assign(n_corners,
nullptr);
409 for (
const auto k : index_range(midpoints))
411 const Node & next_vertex = *vertices[(k + 1) % vertices.size()];
412 midpoints[k] = edgeMidpointNode(
mesh, edge_midpoints, *vertices[k], next_vertex);
416 Node *
const centroid =
mesh.add_point(parent->vertex_average());
421 points.insert(points.end(), vertices.begin(), vertices.end());
422 points.insert(points.end(), midpoints.begin(), midpoints.end());
423 points.push_back(centroid);
425 mooseAssert(n_corners == n_tri_sides || n_corners == n_quad_sides,
426 "Only triangles and quadrilaterals have a subdivision.");
427 const auto & quad_template = (n_corners == n_tri_sides) ? tri_subdivision : quad_subdivision;
430 for (
const auto & quad_corners : quad_template)
432 Elem *
const quad =
mesh.add_elem(Elem::build(libMesh::ElemType::QUAD4));
433 for (
const auto k : index_range(quad_corners))
434 quad->set_node(k, points[quad_corners[k]]);
435 quad->subdomain_id() = parent->subdomain_id();
438 quads.push_back(quad);
443 for (
const auto k : index_range(quads))
444 for (
const auto bid : elem_side_list[side_index[k]])
446 boundary_info.add_side(quads[k], 0, bid);
447 boundary_info.add_side(quads[(k + 1) % quads.size()], 3, bid);
450 parent->subdomain_id() = scratch_subdomain_id;
453 removeScratchElements(
mesh, scratch_subdomain_id);
459 BoundaryInfo & boundary_info =
mesh.get_boundary_info();
460 const auto bdry_side_list = boundary_info.build_side_list();
464 if (!
mesh.preparation().has_neighbor_ptrs)
465 mesh.find_neighbors();
469 std::set<std::pair<dof_id_type, dof_id_type>> boundary_edges;
470 for (
const auto & [elem_id, side, _] : bdry_side_list)
472 const Elem & elem = *
mesh.elem_ptr(elem_id);
473 boundary_edges.insert(edgeKey(elem.node_id(side), elem.node_id((side + 1) % n_tri_sides)));
476 std::vector<RecombineCandidate> candidates;
477 for (
const auto & elem :
mesh.active_element_ptr_range())
478 for (
const auto s : make_range(elem->n_sides()))
480 const Elem *
const neighbor = elem->neighbor_ptr(s);
483 if (!neighbor || neighbor->subdomain_id() != elem->subdomain_id())
486 if (neighbor->id() < elem->id())
490 if (boundary_edges.count(edgeKey(elem->node_id(s), elem->node_id((s + 1) % n_tri_sides))))
500 for (
const auto & merge : merges)
502 Elem *
const first =
mesh.elem_ptr(merge.first_elem_id);
503 Elem *
const second =
mesh.elem_ptr(merge.second_elem_id);
505 Elem *
const quad =
mesh.add_elem(Elem::build(libMesh::ElemType::QUAD4));
506 for (
const auto k : index_range(merge.quad_node_ids))
507 quad->set_node(k,
mesh.node_ptr(merge.quad_node_ids[k]));
508 quad->subdomain_id() = first->subdomain_id();
514 for (
const Elem *
const parent : {first, second})
516 std::vector<std::vector<boundary_id_type>> elem_side_list;
518 bdry_side_list, parent->id(), cast_int<unsigned short>(n_tri_sides), elem_side_list);
520 for (
const auto s : index_range(elem_side_list))
522 if (elem_side_list[s].empty())
525 const auto quad_side = quadSideOnEdge(
526 merge.quad_node_ids, parent->node_id(s), parent->node_id((s + 1) % n_tri_sides));
528 "A parent side carrying boundary ids must be a side of the merged "
529 "quadrilateral, because pairs whose shared edge carries any boundary id are "
532 for (
const auto bid : elem_side_list[s])
533 boundary_info.add_side(quad, quad_side, bid);
537 first->subdomain_id() = scratch_subdomain_id;
538 second->subdomain_id() = scratch_subdomain_id;
545 removeScratchElements(
mesh, scratch_subdomain_id);
556 const subdomain_id_type scratch_subdomain_id)
const
561 std::map<subdomain_id_type, std::vector<Elem *>> surviving_tris;
562 for (
const auto & elem :
mesh.active_element_ptr_range())
563 if (elem->type() == libMesh::ElemType::TRI3 && elem->subdomain_id() != scratch_subdomain_id)
564 surviving_tris[elem->subdomain_id()].push_back(elem);
568 const auto & subdomain_names =
mesh.get_subdomain_name_map();
569 subdomain_id_type tri_subdomain_id = scratch_subdomain_id + 1;
570 for (
const auto & [original_id, tris] : surviving_tris)
572 const auto name_it = subdomain_names.find(original_id);
573 const SubdomainName original_name =
574 (name_it == subdomain_names.end() || name_it->second.empty()) ? std::to_string(original_id)
581 "The subdomain name '",
583 "' that this suffix gives the unmerged triangles of subdomain ",
585 " already exists in the mesh.");
587 for (Elem *
const tri : tris)
588 tri->subdomain_id() = tri_subdomain_id;
589 mesh.set_subdomain_name(tri_subdomain_id, tri_subdomain_name);
registerMooseObject("MooseApp", TriToQuadConverter)
MeshGenerators are objects that can modify or add to an existing mesh.
static InputParameters validParams()
void paramError(const std::string ¶m, Args... args) const
Emits an error prefixed with the file and line number of the given param (from the input file) along ...
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.
This is a "smart" enum class intended to replace many of the shortcomings in the C++ enum type It sho...
This TriToQuadConverter object converts a mesh made of TRI3 elements into a mesh made of QUAD4 elemen...
static std::vector< RecombineCandidate > greedyMatching(std::vector< RecombineCandidate > &candidates, const Real eta_min)
Select the pairs of triangles to merge, taking the highest scoring candidates first and consuming eac...
const SubdomainName _tri_subdomain_name_suffix
Suffix appended to the name of a subdomain to name the one its surviving triangles move into.
void subdivide(ReplicatedMesh &mesh) const
Replace every element of the mesh by one quadrilateral per corner, built on its centroid and its edge...
std::unique_ptr< MeshBase > & _input
Mesh that possibly comes from another generator.
const bool _all_quad
Whether the triangles that survive recombination are eliminated.
const Real _eta_min
Score below which a pair of adjacent triangles is not recombined.
static std::vector< QuadCorners > triSubdivisionTemplate()
The three quadrilaterals that a triangle is split into, one per corner, each of them built on that co...
TriToQuadConverter(const InputParameters ¶meters)
static Real quadQuality(const std::array< Point, 4 > &quad_points)
Compute the quality score of a planar quadrilateral, eta = max(0, 1 - (2 / pi) * max_k |pi / 2 - alph...
void moveSurvivingTriangles(ReplicatedMesh &mesh, const subdomain_id_type scratch_subdomain_id) const
Move the triangles that recombination did not consume out of the subdomains they came from,...
const MooseEnum _algorithm
Algorithm used to build the quadrilaterals.
void recombine(ReplicatedMesh &mesh) const
Replace the highest scoring pairs of adjacent triangles of the mesh by quadrilaterals.
static InputParameters validParams()
std::unique_ptr< MeshBase > generate() override
Generate / modify the mesh.
static RecombineCandidate buildCandidate(const Elem &elem, const unsigned int side, const Elem &neighbor)
Build the merge candidate for the two triangles that share a side.
static std::vector< QuadCorners > quadSubdivisionTemplate()
The four quadrilaterals that a quadrilateral is split into, one per corner, each of them built on tha...
void elementBoundaryInfoCollector(const std::vector< libMesh::BoundaryInfo::BCTuple > &bdry_side_list, const dof_id_type elem_id, const unsigned short n_elem_sides, std::vector< std::vector< boundary_id_type > > &elem_side_list)
Collect the boundary information of the given element in a mesh.
void retainEEID(MeshBase &mesh, const dof_id_type &elem_id, Elem *new_elem_ptr)
Retain the extra integer of the original element in a new element.
SubdomainID getNextFreeSubdomainID(MeshBase &input_mesh)
Checks input mesh and returns max(block ID) + 1, which represents a block ID that is not currently in...
SubdomainID getSubdomainID(const SubdomainName &subdomain_name, const MeshBase &mesh)
Gets the subdomain ID associated with the given SubdomainName.
const SubdomainID INVALID_BLOCK_ID
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...
std::string enum_to_string(const T e)
auto index_range(const T &sizable)
const unsigned int invalid_uint
IntRange< T > make_range(T beg, T end)
A pair of adjacent triangles that the recombination algorithm can merge into one quadrilateral.
dof_id_type first_elem_id
Id of the lower numbered triangle of the pair.
std::array< dof_id_type, 4 > quad_node_ids
Node ids of the quadrilateral, in counter-clockwise order.
dof_id_type second_elem_id
Id of the higher numbered triangle of the pair.
Real eta
Quality score of the quadrilateral that the two triangles would form.