https://mooseframework.inl.gov
Loading...
Searching...
No Matches
CoarsenSurfaceMeshAlongSidesetGenerator.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#include "CastUniquePointer.h"
12#include "MooseMeshUtils.h"
13
14#include "libmesh/elem.h"
15
17
20{
22
24 "Coarsens a 2D-element (TRI3/QUAD4) surface mesh along a sideset by collapsing alternate "
25 "boundary nodes. The sideset may be internal: elements on both sides of it are coarsened "
26 "and the sideset itself is preserved. Apply the generator multiple times for additional "
27 "coarsening.");
28 params.addRequiredParam<MeshGeneratorName>("input", "Input mesh to coarsen");
29 params.addParam<std::vector<BoundaryName>>("boundaries",
30 "The sideset(s) to coarsen the mesh along");
31 params.addParam<std::vector<BoundaryName>>(
32 "exclude_boundaries",
33 "Coarsen the mesh along all of its sidesets except these. Mutually exclusive with "
34 "'boundaries'");
35 params.addRangeCheckedParam<Real>(
36 "max_normal_deviation",
37 "max_normal_deviation >= 0 & max_normal_deviation <= 180",
38 "Maximum angle, in degrees, between the normals of the two elements merged together. "
39 "Merges exceeding it are skipped, which preserves features/corners");
40 params.addRangeCheckedParam<Real>(
41 "max_merged_side_length",
42 "max_merged_side_length > 0",
43 "Maximum length of the side created along the sideset by merging two elements. "
44 "Merges exceeding it are skipped");
45 params.addRangeCheckedParam<Real>(
46 "max_merged_element_area",
47 "max_merged_element_area > 0",
48 "Maximum area of an element created by merging two elements. Merges exceeding it are "
49 "skipped");
50 params.addParam<bool>(
51 "coarsen_more_than_two_elements",
52 false,
53 "Whether to coarsen iteratively in a single invocation so that more than two elements can "
54 "be merged together. The amount of coarsening is then bounded by the merge criteria");
55 params.addParam<bool>(
56 "verbose",
57 false,
58 "Whether to make the mesh generator output details of its actions on the console");
59 return params;
60}
61
63 const InputParameters & parameters)
64 : MeshGenerator(parameters),
65 _input(getMesh("input")),
66 _boundaries(isParamValid("boundaries") ? getParam<std::vector<BoundaryName>>("boundaries")
67 : std::vector<BoundaryName>{}),
68 _exclude_boundaries(isParamValid("exclude_boundaries")
69 ? getParam<std::vector<BoundaryName>>("exclude_boundaries")
70 : std::vector<BoundaryName>{}),
71 _has_max_normal_deviation(isParamValid("max_normal_deviation")),
72 _max_normal_deviation(_has_max_normal_deviation ? getParam<Real>("max_normal_deviation") : 0),
73 _has_max_side_length(isParamValid("max_merged_side_length")),
74 _max_merged_side_length(_has_max_side_length ? getParam<Real>("max_merged_side_length") : 0),
75 _has_max_element_area(isParamValid("max_merged_element_area")),
76 _max_merged_element_area(_has_max_element_area ? getParam<Real>("max_merged_element_area") : 0),
77 _coarsen_more_than_two_elements(getParam<bool>("coarsen_more_than_two_elements")),
78 _verbose(getParam<bool>("verbose"))
79{
80 if (_boundaries.empty() == _exclude_boundaries.empty())
81 paramError("boundaries",
82 "Exactly one of 'boundaries' and 'exclude_boundaries' must be provided");
83}
84
85namespace
86{
90Point
91newellNormal(const std::vector<Point> & pts)
92{
93 Point n(0, 0, 0);
94 for (const auto i : index_range(pts))
95 {
96 const Point & current = pts[i];
97 const Point & next = pts[(i + 1) % pts.size()];
98 n(0) += (current(1) - next(1)) * (current(2) + next(2));
99 n(1) += (current(2) - next(2)) * (current(0) + next(0));
100 n(2) += (current(0) - next(0)) * (current(1) + next(1));
101 }
102 return n;
103}
104}
105
106std::unique_ptr<MeshBase>
108{
109 std::unique_ptr<MeshBase> mesh = std::move(_input);
110
111 if (!mesh->is_serial())
112 paramError("input", "Input mesh must not be distributed");
113 if (mesh->mesh_dimension() != 2)
114 paramError("input",
115 "Only meshes of 2D elements (TRI3/QUAD4) are supported, but the input mesh "
116 "dimension is " +
117 std::to_string(mesh->mesh_dimension()));
118
119 if (!mesh->is_prepared())
120 mesh->complete_preparation();
121
122 const auto & boundary_info = mesh->get_boundary_info();
123
124 // Resolve the sideset names to the set of ids to coarsen along, either directly or by excluding
125 // the requested sidesets from all the mesh sidesets
126 std::set<boundary_id_type> boundary_id_set;
127 if (!_boundaries.empty())
128 {
129 const auto boundary_ids = MooseMeshUtils::getBoundaryIDs(*mesh, _boundaries, false);
130 boundary_id_set.insert(boundary_ids.begin(), boundary_ids.end());
131 }
132 else
133 {
134 boundary_id_set = boundary_info.get_side_boundary_ids();
135 const auto exclude_ids = MooseMeshUtils::getBoundaryIDs(*mesh, _exclude_boundaries, false);
136 for (const auto id : exclude_ids)
137 boundary_id_set.erase(id);
138 }
139
140 // Verify at least one side exists for the requested sidesets
141 bool found_side = false;
142 for (const auto & t : boundary_info.build_side_list())
143 if (boundary_id_set.count(std::get<2>(t)))
144 {
145 found_side = true;
146 break;
147 }
148 if (!found_side)
149 paramError("boundaries", "No sides were found for the requested sideset(s)");
150
151 // Run the coarsening pass once, or repeatedly until no collapse remains, so that more than two
152 // elements may be merged together
153 const auto n_elem_before = mesh->n_elem();
154 unsigned int collapsed = 0;
155 do
156 collapsed = coarsenAlongSidesets(mesh, boundary_id_set);
157 while (_coarsen_more_than_two_elements && collapsed > 0);
158
159 _console << name() << ": merged " << (n_elem_before - mesh->n_elem())
160 << " element(s) together along the sideset(s)." << std::endl;
161
162 return dynamic_pointer_cast<MeshBase>(mesh);
163}
164
165unsigned int
167 std::unique_ptr<MeshBase> & mesh, const std::set<boundary_id_type> & boundary_id_set)
168{
169 const auto & boundary_info = mesh->get_boundary_info();
170
171 // Gather the unique boundary edges (an internal sideset lists each edge twice, once per side)
172 // and from them the boundary-node adjacency along the sideset curve(s)
173 std::map<dof_id_type, std::set<dof_id_type>> boundary_node_neighbors;
174 for (const auto & [elem_id, side, bid] : boundary_info.build_side_list())
175 {
176 if (!boundary_id_set.count(bid))
177 continue;
178 const Elem * elem = mesh->elem_ptr(elem_id);
179 const auto edge = elem->build_side_ptr(side);
180 const auto n0 = edge->node_id(0);
181 const auto n1 = edge->node_id(1);
182 boundary_node_neighbors[n0].insert(n1);
183 boundary_node_neighbors[n1].insert(n0);
184 }
185
186 // Map every node to the ids of the elements referencing it. We use ids (not pointers) so that
187 // entries pointing at elements deleted earlier in the pass can be safely skipped.
188 std::map<dof_id_type, std::vector<dof_id_type>> node_to_elems;
189 for (const auto & elem : mesh->active_element_ptr_range())
190 for (const auto & node : elem->node_ref_range())
191 node_to_elems[node.id()].push_back(elem->id());
192
193 // Greedy independent set: collapse a boundary node onto a neighbor, lock that node and both of
194 // its boundary neighbors so the kept collapses do not interact (this yields the alternating,
195 // ~2x coarsening pattern).
196 std::set<dof_id_type> locked;
197 std::vector<Elem *> elems_to_delete;
198 unsigned int num_collapsed = 0;
199
200 for (const auto & [node_id, neighbors] : boundary_node_neighbors)
201 {
202 // Only collapse nodes interior to a sideset polyline (skip endpoints and junctions)
203 if (neighbors.size() != 2 || locked.count(node_id))
204 continue;
205
206 const Node * b_node = mesh->node_ptr(node_id);
207
208 // Try to collapse onto each boundary neighbor, preferring the unlocked one with the lower id
209 for (const auto target_id : neighbors)
210 {
211 if (locked.count(target_id))
212 continue;
213
214 const Point a_point = *mesh->node_ptr(target_id);
215
216 // The collapse merges the two sideset edges (target,node) and (node,other) into the single
217 // edge (target,other), regardless of which way we collapse
218 dof_id_type other_id = DofObject::invalid_id;
219 for (const auto n : neighbors)
220 if (n != target_id)
221 other_id = n;
222
223 // Criterion: maximum length of the side created along the sideset
225 (a_point - *mesh->node_ptr(other_id)).norm() > _max_merged_side_length)
226 continue;
227
228 // Validate the collapse and classify each incident element as kept (re-pointed) or
229 // degenerate (to be deleted). A degenerate element must be a TRI3; a re-pointed element
230 // must not become degenerate or flip its normal.
231 struct ElemInfo
232 {
233 Elem * elem;
234 bool degenerate;
235 std::vector<dof_id_type> ids;
236 std::vector<Point> orig_pts;
237 std::vector<Point> new_pts;
238 };
239 bool valid = true;
240 std::vector<ElemInfo> infos;
241 for (const auto incident_id : node_to_elems[node_id])
242 {
243 Elem * elem = mesh->elem_ptr(incident_id);
244 if (!elem) // deleted earlier in this pass
245 continue;
246 if (elem->type() != TRI3 && elem->type() != QUAD4)
247 {
248 valid = false;
249 break;
250 }
251
252 // Does the element already contain the target node? If so the collapse degenerates it.
253 ElemInfo info{elem, false, {}, {}, {}};
254 for (const auto & node : elem->node_ref_range())
255 {
256 info.ids.push_back(node.id());
257 info.orig_pts.push_back(node);
258 if (node.id() == node_id)
259 info.new_pts.push_back(a_point);
260 else
261 {
262 if (node.id() == target_id)
263 info.degenerate = true;
264 info.new_pts.push_back(node);
265 }
266 }
267
268 if (info.degenerate)
269 {
270 // We only delete triangles. A degenerating quad would require a quad->tri conversion
271 // (and boundary bookkeeping of its other sides), so we decline the collapse instead.
272 if (elem->type() != TRI3)
273 {
274 valid = false;
275 break;
276 }
277 }
278 else
279 {
280 // Re-pointed element: reject the collapse if it would invert or nearly flatten it
281 const Point orig_n = newellNormal(info.orig_pts);
282 const Point new_n = newellNormal(info.new_pts);
283 if (new_n * orig_n <= libMesh::TOLERANCE * orig_n.norm_sq())
284 {
285 valid = false;
286 break;
287 }
288 // Criterion: maximum area of the merged element
289 if (_has_max_element_area && 0.5 * new_n.norm() > _max_merged_element_area)
290 {
291 valid = false;
292 break;
293 }
294 }
295 infos.push_back(info);
296 }
297
298 // Criterion: maximum normal deviation between the two elements being merged. Each deleted
299 // triangle (target,node,apex) merges with the re-pointed element sharing its apex node.
300 if (valid && _has_max_normal_deviation)
301 {
302 const Real cos_threshold = std::cos(_max_normal_deviation * libMesh::pi / 180.0);
303 for (const auto & degen_only : infos)
304 {
305 if (!degen_only.degenerate)
306 continue;
307 dof_id_type apex_id = DofObject::invalid_id;
308 for (const auto id : degen_only.ids)
309 if (id != target_id && id != node_id)
310 apex_id = id;
311 for (const auto & remaining : infos)
312 {
313 if (remaining.degenerate ||
314 std::find(remaining.ids.begin(), remaining.ids.end(), apex_id) ==
315 remaining.ids.end())
316 continue;
317 const Point normal_degenerate = newellNormal(degen_only.orig_pts);
318 const Point normal_remaining = newellNormal(remaining.orig_pts);
319 const Real denom = normal_degenerate.norm() * normal_remaining.norm();
320 if (denom > 0 && (normal_degenerate * normal_remaining) / denom < cos_threshold)
321 valid = false;
322 }
323 }
324 }
325
326 // A clean collapse removes exactly one triangle per side of the sideset
327 std::set<Elem *> degenerate_set;
328 for (const auto & info : infos)
329 if (info.degenerate)
330 degenerate_set.insert(info.elem);
331 if (!valid || degenerate_set.empty())
332 continue;
333
334 // Commit: re-point every incident element from the collapsed node to the target node, and
335 // mark the degenerate triangles for deletion.
336 for (const auto incident_id : node_to_elems[node_id])
337 {
338 Elem * elem = mesh->elem_ptr(incident_id);
339 if (!elem || degenerate_set.count(elem))
340 continue;
341 elem->set_node(elem->get_node_index(b_node), mesh->node_ptr(target_id));
342 }
343 for (auto elem : degenerate_set)
344 elems_to_delete.push_back(elem);
345
346 locked.insert(node_id);
347 for (const auto neighbor : neighbors)
348 locked.insert(neighbor);
349 num_collapsed++;
350 break;
351 }
352 }
353
354 for (auto elem : elems_to_delete)
355 mesh->delete_elem(elem);
356
357 if (_verbose)
358 _console << name() << ": collapsed " << num_collapsed << " boundary node(s), deleted "
359 << elems_to_delete.size() << " element(s)." << std::endl;
360
361 // deleting nodes possibly changes neighbors, nodesets and element sets
362 mesh->unset_has_neighbor_ptrs();
363 mesh->unset_has_cached_elem_data();
364 mesh->unset_has_boundary_id_sets();
365
366 // Orphaned nodes (the collapsed ones) are removed while preparing the mesh for use
367 mesh->contract();
368
369 return num_collapsed;
370}
registerMooseObject("MooseApp", CoarsenSurfaceMeshAlongSidesetGenerator)
MeshGenerator that coarsens a 2D-element (TRI3/QUAD4) surface mesh along a sideset by collapsing alte...
const bool _has_max_element_area
Whether a maximum merged element area is enforced.
const bool _verbose
Whether the mesh generator should be verbose to the console.
std::unique_ptr< MeshBase > generate() override
Generate / modify the mesh.
const bool _coarsen_more_than_two_elements
Whether to repeat the coarsening pass so that more than two elements can be merged together.
const std::vector< BoundaryName > _exclude_boundaries
Sideset(s) to exclude when coarsening along all the sidesets of the mesh.
const std::vector< BoundaryName > _boundaries
Sideset(s) to coarsen the mesh along.
CoarsenSurfaceMeshAlongSidesetGenerator(const InputParameters &parameters)
const bool _has_max_normal_deviation
Whether a maximum normal deviation between merged elements is enforced.
const Real _max_merged_element_area
Maximum area of an element created by merging two elements.
const bool _has_max_side_length
Whether a maximum merged side length is enforced.
const Real _max_normal_deviation
Maximum angle (degrees) between the normals of the two elements merged together.
unsigned int coarsenAlongSidesets(std::unique_ptr< MeshBase > &mesh, const std::set< boundary_id_type > &boundary_id_set)
Performs a single coarsening pass: collapse non-adjacent sideset nodes, merging pairs of elements.
std::unique_ptr< MeshBase > & _input
Input mesh to coarsen.
const Real _max_merged_side_length
Maximum length of the side created by merging two elements.
const ConsoleStream _console
An instance of helper class to write streams to the Console objects.
Class used for caching additional information for elements such as the volume and centroid.
Definition ElemInfo.h:26
The main MOOSE class responsible for handling user-defined parameters in almost every MOOSE system.
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 addRequiredParam(const std::string &name, const std::string &doc_string)
This method adds a parameter and documentation string to the InputParameters object that will be extr...
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.
void addRangeCheckedParam(const std::string &name, const T &value, const std::string &parsed_function, const std::string &doc_string)
MeshGenerators are objects that can modify or add to an existing mesh.
static InputParameters validParams()
const std::string & name() const
Get the name of the class.
Definition MooseBase.h:103
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
MeshBase & mesh
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.
auto index_range(const T &sizable)
const Real pi
static constexpr Real TOLERANCE
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real