libMesh
Loading...
Searching...
No Matches
mesh_netgen_interface.C
Go to the documentation of this file.
1// The libMesh Finite Element Library.
2// Copyright (C) 2002-2023 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
3
4// This library is free software; you can redistribute it and/or
5// modify it under the terms of the GNU Lesser General Public
6// License as published by the Free Software Foundation; either
7// version 2.1 of the License, or (at your option) any later version.
8
9// This library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12// Lesser General Public License for more details.
13
14// You should have received a copy of the GNU Lesser General Public
15// License along with this library; if not, write to the Free Software
16// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
18#include "libmesh/libmesh_config.h"
19#ifdef LIBMESH_HAVE_NETGEN
20
21
22// C++ includes
23#include <sstream>
24
25// Local includes
26#include "libmesh/mesh_netgen_interface.h"
27
28#include "libmesh/boundary_info.h"
29#include "libmesh/cell_tet4.h"
30#include "libmesh/cell_tet10.h"
31#include "libmesh/elem.h"
32#include "libmesh/face_tri3.h"
33#include "libmesh/face_tri6.h"
34#include "libmesh/libmesh_logging.h"
35#include "libmesh/mesh_communication.h"
36#include "libmesh/threads.h"
37#include "libmesh/unstructured_mesh.h"
38#include "libmesh/utility.h" // libmesh_map_find
39
40namespace nglib {
41#include "netgen/nglib/nglib.h"
42}
43
44namespace {
45
46// RAII for exception safety
47class WrappedNgMesh
48{
49public:
50 WrappedNgMesh() {
51 _ngmesh = nglib::Ng_NewMesh();
52 }
53
54 ~WrappedNgMesh() {
55 nglib::Ng_DeleteMesh(_ngmesh);
56 }
57
58 void clear() {
59 nglib::Ng_DeleteMesh(_ngmesh);
60 _ngmesh = nglib::Ng_NewMesh();
61 }
62
63 operator nglib::Ng_Mesh* () {
64 return _ngmesh;
65 }
66
67private:
68 nglib::Ng_Mesh * _ngmesh;
69};
70
71}
72
73namespace libMesh
74{
75
76//----------------------------------------------------------------------
77// NetGenMeshInterface class members
83
84
85
87{
88 using namespace nglib;
89
90 LOG_SCOPE("triangulate()", "NetGenMeshInterface");
91
92 if (_elem_type != TET4 &&
93 _elem_type != TET10 &&
95 libmesh_not_implemented();
96
97 // We're hoping to do volume_to_surface_mesh in parallel at least,
98 // but then we'll need to serialize any hole meshes to rank 0 so it
99 // can use them in serial.
100
101 // If the user wants higher-order output, record midpoints from any
102 // quadratic boundary elements before stripping them to TRI3 for
103 // NetGen. We key by sorted position pairs (not node IDs) so that
104 // outer mesh and hole mesh node namespaces cannot conflict.
105 std::map<std::pair<Point,Point>, Point> edge_midpoints;
106
107 // TRI7 boundary faces additionally carry a face-centroid node (local index
108 // 6) that TET14 output reproduces (its faces are TRI7). Record those too,
109 // keyed by the sorted triple of the face's three corner positions so the
110 // key is independent of node id and rotation.
111 std::map<std::array<Point,3>, Point> face_centroids;
112
113 auto record_and_strip =
114 [&edge_midpoints, &face_centroids](UnstructuredMesh & m)
115 {
116 bool has_quadratic = false;
117 for (const auto & elem : m.element_ptr_range())
118 {
119 if (elem->type() != TRI6 && elem->type() != TRI7) continue;
120 has_quadratic = true;
121 // TRI6/TRI7: edge e runs node[e]→node[(e+1)%3], midpoint=node[e+3]
122 for (auto e : make_range(3u))
123 {
124 const Point & pa = elem->point(e);
125 const Point & pb = elem->point((e+1)%3);
126 auto key = pa < pb
127 ? std::make_pair(pa,pb) : std::make_pair(pb,pa);
128 edge_midpoints[key] = elem->point(e+3);
129 }
130 // TRI7: node 6 is the face-centroid node.
131 if (elem->type() == TRI7)
132 {
133 std::array<Point,3> corners =
134 {elem->point(0), elem->point(1), elem->point(2)};
135 std::sort(corners.begin(), corners.end());
136 face_centroids[corners] = elem->point(6);
137 }
138 }
139 if (has_quadratic)
140 m.all_first_order();
141 };
142
143 const BoundingBox mesh_bb =
145
146 if (_elem_type != TET4)
147 record_and_strip(this->_mesh);
148
149 std::vector<MeshSerializer> hole_serializers;
150 if (_holes)
151 for (std::unique_ptr<UnstructuredMesh> & hole : *_holes)
152 {
153 const BoundingBox hole_bb =
155
156 if (_elem_type != TET4)
157 record_and_strip(*hole);
158
159 libmesh_error_msg_if
160 (!mesh_bb.contains(hole_bb),
161 "Found hole with bounding box " << hole_bb <<
162 "\nextending outside of mesh bounding box " << mesh_bb);
163
164 hole_serializers.emplace_back
165 (*hole, /* need_serial */ true,
166 /* serial_only_needed_on_proc_0 */ true);
167 }
168
169 // Increasing the element order (all_second_order()/all_complete_order()
170 // via increase_tet_order()) performs collective MPI communication, so it
171 // must run on every rank in lockstep -- it cannot run on rank 0 alone
172 // while the other ranks wait in broadcast(). We therefore broadcast
173 // NetGen's TET4 result first, then increase the order and restore the
174 // curved-boundary midpoints identically on all ranks. This mirrors what
175 // the 2D interfaces do (see TriangulatorInterface::increase_triangle_order,
176 // which is likewise called on all ranks). edge_midpoints was built while
177 // the mesh was serialized on every rank, so it is identical everywhere and
178 // this post-broadcast fixup is deterministic.
179 auto increase_order_and_restore_midpoints =
180 [this, &edge_midpoints, &face_centroids]()
181 {
182 if (_elem_type == TET4)
183 return;
184
185 // find_neighbors() is needed before all_second_order() can place
186 // shared edge midpoints correctly.
187 this->_mesh.find_neighbors();
188
189 // Refresh the cached element dimensions. We just replaced the 2D
190 // TRI3 surface elements with 3D TET4 volume elements, but the mesh's
191 // cached dimension is still that of the original surface (2).
192 // all_second_order()/all_complete_order() derive the per-element
193 // unique_id reservation width from mesh_dimension(): a stale value of
194 // 2 reserves only 9-4=5 slots per element, too few for the 6 new edge
195 // nodes of a TET10, so adjacent elements' unique_id ranges overlap and
196 // collide. cache_elem_data() recomputes the dimension to 3 (reserving
197 // 27-8=19 slots) before the order increase runs.
198 this->_mesh.cache_elem_data();
199
200 this->increase_tet_order();
201
202 // Move auto-placed geometric midpoints to the recorded positions,
203 // preserving any curvature from the original quadratic boundary.
204 if (!edge_midpoints.empty() || !face_centroids.empty())
205 for (Elem * elem : _mesh.element_ptr_range())
206 for (auto s : elem->side_index_range())
207 {
208 if (elem->neighbor_ptr(s)) continue;
209
210 // build_side_ptr() returns a TRI6 (TET10) or TRI7 (TET14)
211 // whose node pointers reference the actual mesh nodes; point()
212 // assignments update mesh node coordinates in place.
213 auto side = elem->build_side_ptr(s);
214 for (auto e : make_range(3u))
215 {
216 const Point & pa = side->point(e);
217 const Point & pb = side->point((e+1)%3);
218 auto key = pa < pb
219 ? std::make_pair(pa,pb) : std::make_pair(pb,pa);
220 if (auto it = edge_midpoints.find(key);
221 it != edge_midpoints.end())
222 side->point(e+3) = it->second;
223 }
224
225 // TRI7 faces (TET14 output) also carry a face-centroid node
226 // at local index 6; restore its recorded curved position.
227 if (side->type() == TRI7)
228 {
229 std::array<Point,3> corners =
230 {side->point(0), side->point(1), side->point(2)};
231 std::sort(corners.begin(), corners.end());
232 if (auto it = face_centroids.find(corners);
233 it != face_centroids.end())
234 side->point(6) = it->second;
235 }
236 }
237 };
238
239 // This should probably only be done on rank 0, but the API is
240 // designed with the hope that we'll parallelize it eventually
241 auto integrity = this->improve_hull_integrity();
242 this->process_hull_integrity_result(integrity);
243
244 // If we're not rank 0, we're just going to wait for rank 0 to call
245 // Netgen, then receive its data afterward, we're not going to hope
246 // that Netgen does the exact same thing on every processor.
247 if (this->_mesh.processor_id() != 0)
248 {
249 // We don't need our holes anymore. Delete their serializers
250 // first to avoid dereferencing dangling pointers.
251 hole_serializers.clear();
252 if (_holes)
253 _holes->clear();
254
255 // Receive the TET4 mesh data rank 0 will send later.
257
258 // If we got an empty mesh here then our tetrahedralization
259 // failed.
260 libmesh_error_msg_if (!this->_mesh.n_elem(),
261 "NetGen failed to generate any tetrahedra");
262
263 // Increase the element order collectively, in lockstep with rank 0.
264 increase_order_and_restore_midpoints();
265
266 this->_mesh.prepare_for_use();
267 return;
268 }
269
270 Ng_Meshing_Parameters params;
271
272 Ng_SetNumThreads(cast_int<int>(libMesh::n_threads()));
273
274 // Override any default parameters we might need to, to avoid
275 // inserting nodes we don't want.
276 params.uselocalh = false;
277 params.minh = 0;
278 params.elementsperedge = 1;
279 params.elementspercurve = 1;
280 params.closeedgeenable = false;
281 params.closeedgefact = 0;
282 params.minedgelenenable = false;
283 params.minedgelen = 0;
284
285 // Try to get a no-extra-nodes mesh if we're asked to, or try to
286 // translate our desired volume into NetGen terms otherwise.
287 //
288 // Spoiler alert: all we can do is try; NetGen uses a marching front
289 // algorithm that can insert extra nodes despite all my best
290 // efforts.
291 if (_desired_volume == 0) // shorthand for "no refinement"
292 {
293 params.maxh = std::numeric_limits<double>::max();
294 params.fineness = 0; // "coarse" in the docs
295 params.grading = 1; // "aggressive local grading" to avoid smoothing??
296
297 // Turning off optimization steps avoids another opportunity for
298 // Netgen to try to add more nodes.
299 params.optsteps_3d = 0;
300 }
301 else
302 params.maxh = double(std::pow(_desired_volume, 1./3.));
303
304 // Keep track of how NetGen copies of nodes map back to our original
305 // nodes, so we can connect new elements to nodes correctly.
306 std::unordered_map<int, dof_id_type> ng_to_libmesh_id;
307
308 auto handle_ng_result = [](Ng_Result result) {
309 static const std::vector<std::string> result_types =
310 {"Netgen error", "Netgen success", "Netgen surface input error",
311 "Netgen volume failure", "Netgen STL input error",
312 "Netgen surface failure", "Netgen file not found"};
313
314 if (result+1 >= 0 &&
315 std::size_t(result+1) < result_types.size())
316 libmesh_error_msg_if
317 (result, "Ng_GenerateVolumeMesh failed: " <<
318 result_types[result+1]);
319 else
320 libmesh_error_msg
321 ("Ng_GenerateVolumeMesh failed with an unknown error code");
322 };
323
324 // Keep track of what boundary ids we want to assign to each new
325 // triangle. We'll give the outer boundary BC 0, and give holes ids
326 // starting from 1.
327 // We key on sorted tuples of node ids to identify a side.
328 std::unordered_map<std::array<dof_id_type,3>,
329 boundary_id_type, libMesh::hash> side_boundary_id;
330
331 auto insert_id = []
332 (std::array<dof_id_type,3> & array,
333 dof_id_type n_id)
334 {
335 libmesh_assert_less(n_id, DofObject::invalid_id);
336 unsigned int i=0;
337 while (array[i] < n_id)
338 ++i;
339 while (i < 3)
340 std::swap(array[i++], n_id);
341 };
342
343 WrappedNgMesh ngmesh;
344
345 // Create surface mesh in the WrappedNgMesh
346 {
347 // NetGen appears to use ONE-BASED numbering for its nodes, and
348 // since it doesn't return an id when adding nodes we'll have to
349 // track the numbering ourselves.
350 int ng_id = 1;
351
352 auto create_surface_component =
353 [this, &ng_id, &ng_to_libmesh_id, &ngmesh, &side_boundary_id, &insert_id]
354 (UnstructuredMesh & srcmesh, bool hole_mesh,
355 boundary_id_type bcid)
356 {
357 LOG_SCOPE("create_surface_component()", "NetGenMeshInterface");
358
359 // Keep track of what nodes we've already added to the Netgen
360 // mesh vs what nodes we need to add. We'll keep track by id,
361 // not by point location. I don't know if Netgen can handle
362 // multiple nodes with the same point location, but if they can
363 // it's not going to be *us* who breaks that feature.
364 std::unordered_map<dof_id_type, int> libmesh_to_ng_id;
365
366 // Keep track of what nodes we've already added to the main
367 // mesh from a hole mesh.
368 std::unordered_map<dof_id_type, dof_id_type> hole_to_main_mesh_id;
369
370 // Use a separate array for passing points to NetGen, just in case
371 // we're not using double-precision ourselves.
372 std::array<double, 3> point_val;
373
374 // And an array for element vertices
375 std::array<int, 3> elem_nodes;
376
377 for (const auto * elem : srcmesh.element_ptr_range())
378 {
379 // If someone has triangles we can't triangulate, we have a
380 // problem
381 if (elem->type() == TRI6 ||
382 elem->type() == TRI7)
383 libmesh_not_implemented_msg
384 ("Netgen tetrahedralization currently only supports TRI3 boundaries");
385
386 // If someone has non-triangles, let's just ignore them.
387 if (elem->type() != TRI3)
388 continue;
389
390 std::array<dof_id_type,3> sorted_ids =
393
394 for (int ni : make_range(3))
395 {
396 // Just using the "invert_trigs" option in NetGen params
397 // doesn't work for me, so we'll have to have properly
398 // oriented the tris earlier.
399 auto & elem_node = hole_mesh ? elem_nodes[2-ni] : elem_nodes[ni];
400
401 const Node & n = elem->node_ref(ni);
402 auto n_id = n.id();
403 if (hole_mesh)
404 {
405 if (auto it = hole_to_main_mesh_id.find(n_id);
406 it != hole_to_main_mesh_id.end())
407 {
408 n_id = it->second;
409 }
410 else
411 {
412 Node * n_new = this->_mesh.add_point(n);
413 const dof_id_type n_new_id = n_new->id();
414 hole_to_main_mesh_id.emplace(n_id, n_new_id);
415 n_id = n_new_id;
416 }
417 }
418
419 if (auto it = libmesh_to_ng_id.find(n_id);
420 it != libmesh_to_ng_id.end())
421 {
422 const int existing_ng_id = it->second;
423 elem_node = existing_ng_id;
424 }
425 else
426 {
427 for (auto i : make_range(3))
428 point_val[i] = double(n(i));
429
430 Ng_AddPoint(ngmesh, point_val.data());
431
432 ng_to_libmesh_id[ng_id] = n_id;
433 libmesh_to_ng_id[n_id] = ng_id;
434 elem_node = ng_id;
435 ++ng_id;
436 }
437
438 insert_id(sorted_ids, n_id);
439 }
440
441 side_boundary_id[sorted_ids] = bcid;
442
443 Ng_AddSurfaceElement(ngmesh, NG_TRIG, elem_nodes.data());
444 }
445 };
446
447 // Number the outer boundary 0, and the holes starting from 1
448 boundary_id_type bcid = 0;
449
450 create_surface_component(this->_mesh, false, bcid);
451
452 if (_holes)
453 for (const std::unique_ptr<UnstructuredMesh> & h : *_holes)
454 create_surface_component(*h, true, ++bcid);
455 }
456
457 {
458 LOG_SCOPE("Ng_GenerateVolumeMesh()", "NetGenMeshInterface");
459
460 auto result = Ng_GenerateVolumeMesh(ngmesh, &params);
461 handle_ng_result(result);
462 }
463
464 const int n_elem = Ng_GetNE(ngmesh);
465
466 // If Netgen fails us, we're likely to get n_elem <= 0. This is a
467 // common enough failure from bad setups that I want to make sure
468 // it's thrown in parallel so as to not desynchronize any unit tests
469 // that trigger it. So we'll broadcast the empty mesh to indicate
470 // the problem and enable throwing exceptions in parallel.
471 if (n_elem <= 0)
472 {
473 this->_mesh.clear();
475 libmesh_error_msg ("NetGen failed to generate any tetrahedra");
476 }
477
478 const dof_id_type n_points = Ng_GetNP(ngmesh);
479 const dof_id_type old_nodes = this->_mesh.n_nodes();
480
481 // Netgen may have generated new interior nodes
482 if (n_points != old_nodes)
483 {
484 std::array<double, 3> point_val;
485
486 // We should only be getting new nodes if we asked for them
487 if (!_desired_volume)
488 {
489 std::cout <<
490 "NetGen output " << n_points <<
491 " points when we gave it " <<
492 old_nodes << " and disabled refinement\n" <<
493 "If new interior points are acceptable in your mesh, please set\n" <<
494 "a non-zero desired_volume to indicate that. If new interior\n" <<
495 "points are not acceptable in your mesh, you may need a different\n" <<
496 "(non-advancing-front?) mesh generator." << std::endl;
497 libmesh_error();
498 }
499 else
500 for (auto i : make_range(old_nodes, n_points))
501 {
502 // i+1 since ng uses ONE-BASED numbering
503 Ng_GetPoint (ngmesh, i+1, point_val.data());
504 const Point p(point_val[0], point_val[1], point_val[2]);
505 Node * n_new = this->_mesh.add_point(p);
506 const dof_id_type n_new_id = n_new->id();
507 ng_to_libmesh_id[i+1] = n_new_id;
508 }
509 }
510
511 for (auto * elem : this->_mesh.element_ptr_range())
512 this->_mesh.delete_elem(elem);
513
514 BoundaryInfo * bi = & this->_mesh.get_boundary_info();
515
516 for (auto i : make_range(n_elem))
517 {
518 // Enough data to return even a Tet10 without a segfault if nglib
519 // went nuts
520 int ngnodes[11];
521
522 // i+1 since we must be 1-based with these ids too...
523 Ng_Volume_Element_Type ngtype =
524 Ng_GetVolumeElement(ngmesh, i+1, ngnodes);
525
526 // But really nglib shouldn't go nuts
527 libmesh_assert(ngtype == NG_TET);
528 libmesh_ignore(ngtype);
529
530 auto elem = this->_mesh.add_elem(Elem::build_with_id(TET4, i));
531 for (auto n : make_range(4))
532 {
533 const dof_id_type node_id =
534 libmesh_map_find(ng_to_libmesh_id, ngnodes[n]);
535 elem->set_node(n, this->_mesh.node_ptr(node_id));
536 }
537
538 // NetGen and we disagree about node numbering orientation
539 elem->orient(bi);
540
541 for (auto s : make_range(4))
542 {
543 std::array<dof_id_type,3> sorted_ids =
546
547 std::vector<unsigned int> nos = elem->nodes_on_side(s);
548 for (auto n : nos)
549 insert_id(sorted_ids, elem->node_id(n));
550
551 if (auto it = side_boundary_id.find(sorted_ids);
552 it != side_boundary_id.end())
553 bi->add_side(elem, s, it->second);
554 }
555 }
556
557 // We don't need our holes anymore. Delete their serializers
558 // first to avoid dereferencing dangling pointers.
559 hole_serializers.clear();
560 if (_holes)
561 _holes->clear();
562
563 // Send NetGen's TET4 result to the other ranks, then increase the element
564 // order collectively on all ranks together. increase_tet_order() performs
565 // collective communication, so it must not run on rank 0 alone -- doing so
566 // would deadlock against the other ranks waiting in broadcast() above.
568 increase_order_and_restore_midpoints();
569 this->_mesh.prepare_for_use();
570}
571
572
573
574} // namespace libMesh
575
576
577#endif // #ifdef LIBMESH_HAVE_NETGEN
The BoundaryInfo class contains information relevant to boundary conditions including storing faces,...
void add_side(const dof_id_type elem, const unsigned short int side, const boundary_id_type id)
Add side side of element number elem with boundary id id to the boundary information data structure.
Defines a Cartesian bounding box by the two corner extremum.
bool contains(const BoundingBox &) const
static constexpr dof_id_type invalid_id
An invalid id to distinguish an uninitialized DofObject.
Definition dof_object.h:473
dof_id_type id() const
Definition dof_object.h:819
This is the base class from which all geometric element types are derived.
Definition elem.h:96
static std::unique_ptr< Elem > build_with_id(const ElemType type, dof_id_type id)
Calls the build() method above with a nullptr parent, and additionally sets the newly-created Elem's ...
Definition elem.C:556
const BoundaryInfo & get_boundary_info() const
The information about boundary ids on the mesh.
Definition mesh_base.h:170
virtual const Node * node_ptr(const dof_id_type i) const =0
virtual dof_id_type n_elem() const =0
void prepare_for_use(const bool skip_renumber_nodes_and_elements, const bool skip_find_neighbors)
Prepare a newly created (or read) mesh for use.
Definition mesh_base.C:824
virtual dof_id_type n_nodes() const =0
virtual void delete_elem(Elem *e)=0
Removes element e from the mesh.
virtual Node * add_point(const Point &p, const dof_id_type id=DofObject::invalid_id, const processor_id_type proc_id=DofObject::invalid_processor_id)=0
Add a new Node at Point p to the end of the vertex array, with processor_id procid.
virtual void clear()
Deletes all the element and node data that is currently stored.
Definition mesh_base.C:1036
virtual Elem * add_elem(Elem *e)=0
Add elem e to the end of the element array.
This is the MeshCommunication class.
void broadcast(MeshBase &) const
This method takes a mesh (which is assumed to reside on processor 0) and broadcasts it to all the oth...
Class MeshTetInterface provides an abstract interface for tetrahedralization of meshes by subclasses.
void process_hull_integrity_result(const std::set< SurfaceIntegrity > &result) const
This function prints an informative message and throws an exception based on the output of the check_...
ElemType _elem_type
The exact type of tetrahedra we intend to construct.
UnstructuredMesh & _mesh
Local reference to the mesh we are working with.
std::unique_ptr< std::vector< std::unique_ptr< UnstructuredMesh > > > _holes
A pointer to a vector of meshes each defining a hole.
Real _desired_volume
The desired volume for the elements in the resulting mesh.
std::set< SurfaceIntegrity > improve_hull_integrity()
This function checks the integrity of the current set of elements in the Mesh, and corrects what it c...
void increase_tet_order()
Converts all linear tet elements to the type requested by _elem_type, if that type differs from TET4.
static BoundingBox volume_to_surface_mesh(UnstructuredMesh &mesh)
Remove volume elements from the given mesh, after converting their outer boundary faces to surface el...
NetGenMeshInterface(UnstructuredMesh &mesh)
Constructor.
virtual void triangulate() override
Method invokes NetGen library to compute a tetrahedralization.
A Node is like a Point, but with more information.
Definition node.h:55
processor_id_type processor_id() const
A Point defines a location in LIBMESH_DIM dimensional Real space.
Definition point.h:40
The UnstructuredMesh class is derived from the MeshBase class.
virtual void find_neighbors(const bool reset_remote_elements=false, const bool reset_current_list=true, const bool assert_valid=true) override
Other functions from MeshBase requiring re-definition.
MeshBase & mesh
The libMesh namespace provides an interface to certain functionality in the library.
int8_t boundary_id_type
Definition id_types.h:51
void libmesh_ignore(const Args &...)
libmesh_assert(ctx)
uint8_t dof_id_type
Definition id_types.h:67
IntRange< T > make_range(T beg, T end)
The 2-parameter make_range() helper function returns an IntRange<T> when both input parameters are of...
Definition int_range.h:176
unsigned int n_threads()