LCOV - code coverage report
Current view: top level - src/meshgenerators - Boundary2DDelaunayGenerator.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: #32971 (54bef8) with base c6cf66 Lines: 169 175 96.6 %
Date: 2026-05-29 20:35:17 Functions: 4 4 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://www.mooseframework.org
       3             : //*
       4             : //* All rights reserved, see COPYRIGHT for full restrictions
       5             : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
       6             : //*
       7             : //* Licensed under LGPL 2.1, please see LICENSE for details
       8             : //* https://www.gnu.org/licenses/lgpl-2.1.html
       9             : 
      10             : #include "Boundary2DDelaunayGenerator.h"
      11             : 
      12             : #include "MooseMeshUtils.h"
      13             : #include "CastUniquePointer.h"
      14             : 
      15             : #include "libmesh/boundary_info.h"
      16             : #include "libmesh/poly2tri_triangulator.h"
      17             : #include "libmesh/mesh_triangle_holes.h"
      18             : #include "libmesh/mesh_modification.h"
      19             : #include "libmesh/parallel_algebra.h"
      20             : 
      21             : registerMooseObject("MooseApp", Boundary2DDelaunayGenerator);
      22             : 
      23             : InputParameters
      24        3421 : Boundary2DDelaunayGenerator::validParams()
      25             : {
      26        3421 :   InputParameters params = SurfaceDelaunayGeneratorBase::validParams();
      27        3421 :   params += LevelSetMeshingHelper::validParams();
      28             : 
      29        6842 :   params.addClassDescription(
      30             :       "Mesh generator that convert a 2D surface given as one or a few boundaries of a 3D mesh into "
      31             :       "a 2D mesh using Delaunay triangulation.");
      32       13684 :   params.addRequiredParam<MeshGeneratorName>("input", "The mesh we want to modify");
      33       13684 :   params.addRequiredParam<std::vector<BoundaryName>>("boundary_names", "The boundaries to be used");
      34       10263 :   params.addParam<std::vector<std::vector<BoundaryName>>>(
      35             :       "hole_boundary_names",
      36        6842 :       std::vector<std::vector<BoundaryName>>(),
      37             :       "The optional boundaries to be used as the holes in the mesh during triangulation. Note that "
      38             :       "this is a vector of vectors, which allows each hole to be defined as a combination of "
      39             :       "multiple boundaries.");
      40             : 
      41       10263 :   params.addParam<BoundaryName>(
      42             :       "output_external_boundary_name",
      43             :       "",
      44             :       "The optional name of the external boundary of the mesh to generate. If not provided, the "
      45             :       "external boundary will be have a trivial id of 0.");
      46             : 
      47        3421 :   return params;
      48           0 : }
      49             : 
      50         180 : Boundary2DDelaunayGenerator::Boundary2DDelaunayGenerator(const InputParameters & parameters)
      51             :   : SurfaceDelaunayGeneratorBase(parameters),
      52             :     LevelSetMeshingHelper(parameters),
      53         180 :     _input(getMesh("input")),
      54         360 :     _boundary_names(getParam<std::vector<BoundaryName>>("boundary_names")),
      55         360 :     _hole_boundary_names(getParam<std::vector<std::vector<BoundaryName>>>("hole_boundary_names")),
      56         540 :     _output_external_boundary_name(getParam<BoundaryName>("output_external_boundary_name"))
      57             : {
      58         180 : }
      59             : 
      60             : std::unique_ptr<MeshBase>
      61         100 : Boundary2DDelaunayGenerator::generate()
      62             : {
      63         100 :   std::unique_ptr<MeshBase> mesh_3d = std::move(_input);
      64             : 
      65             :   // Generate a new 2D block based on the sidesets
      66         100 :   const auto new_block_id = MooseMeshUtils::getNextFreeSubdomainID(*mesh_3d);
      67             :   try
      68             :   {
      69         209 :     MooseMeshUtils::createSubdomainFromSidesets(
      70         303 :         *mesh_3d, _boundary_names, new_block_id, SubdomainName(), type());
      71             :   }
      72           3 :   catch (MooseException & e)
      73             :   {
      74           6 :     if (((std::string)e.what()).compare(0, 12, "The sideset ") == 0)
      75           6 :       paramError("boundary_names", e.what());
      76             :     else
      77           0 :       mooseError(e.what());
      78           0 :   }
      79             : 
      80             :   // If holes are provided, we need to create new blocks for them too
      81          97 :   std::vector<subdomain_id_type> hole_block_ids;
      82         111 :   for (const auto & hole : _hole_boundary_names)
      83             :   {
      84          17 :     hole_block_ids.push_back(MooseMeshUtils::getNextFreeSubdomainID(*mesh_3d));
      85             :     try
      86             :     {
      87          57 :       MooseMeshUtils::createSubdomainFromSidesets(
      88          54 :           *mesh_3d, hole, hole_block_ids.back(), SubdomainName(), type());
      89             :     }
      90           3 :     catch (MooseException & e)
      91             :     {
      92           6 :       if (((std::string)e.what()).compare(0, 12, "The sideset ") == 0)
      93           6 :         paramError("hole_boundary_names", e.what());
      94             :       else
      95           0 :         mooseError(e.what());
      96           0 :     }
      97             :   }
      98             : 
      99             :   // Create a 2D mesh form the 2D block
     100          94 :   auto mesh_2d = buildMeshBaseObject();
     101         282 :   MooseMeshUtils::convertBlockToMesh(*mesh_3d, *mesh_2d, {std::to_string(new_block_id)});
     102             :   // If holes are provided, we need to create a 2D mesh for each hole
     103          94 :   std::vector<std::unique_ptr<MeshBase>> hole_meshes_2d;
     104         108 :   for (const auto & hole_block_id : hole_block_ids)
     105             :   {
     106          14 :     hole_meshes_2d.push_back(buildMeshBaseObject());
     107          56 :     MooseMeshUtils::convertBlockToMesh(
     108          28 :         *mesh_3d, *hole_meshes_2d.back(), {std::to_string(hole_block_id)});
     109             :   }
     110             : 
     111             :   // We do not need the 3D mesh anymore
     112          94 :   mesh_3d->clear();
     113             : 
     114         182 :   return General2DDelaunay(mesh_2d, hole_meshes_2d);
     115         196 : }
     116             : 
     117             : std::unique_ptr<MeshBase>
     118          94 : Boundary2DDelaunayGenerator::General2DDelaunay(
     119             :     std::unique_ptr<MeshBase> & mesh_2d, std::vector<std::unique_ptr<MeshBase>> & hole_meshes_2d)
     120             : {
     121             :   // If a level set is provided, we need to check if the nodes in the original 2D mesh match the
     122             :   // level set
     123          94 :   if (_func_level_set)
     124             :   {
     125         843 :     for (const auto & node : mesh_2d->node_ptr_range())
     126             :     {
     127         803 :       if (std::abs(levelSetEvaluator(*node)) > libMesh::TOLERANCE)
     128             :       {
     129           6 :         paramError("level_set",
     130             :                    "The level set function does not match the nodes in the given boundary of the "
     131             :                    "input mesh.");
     132             :       }
     133          40 :     }
     134             :   }
     135             : 
     136             :   // Easier to work with a TRI3 mesh
     137             :   // all_tri() also prepares the mesh for use
     138          91 :   mesh_2d->prepare_for_use();
     139          91 :   MeshTools::Modification::all_tri(*mesh_2d);
     140          91 :   const auto mesh_2d_ext_bdry = MooseMeshUtils::getNextFreeBoundaryID(*mesh_2d);
     141        3083 :   for (const auto & elem : mesh_2d->active_element_ptr_range())
     142       11968 :     for (const auto & i_side : elem->side_index_range())
     143        8976 :       if (elem->neighbor_ptr(i_side) == nullptr)
     144        1475 :         mesh_2d->get_boundary_info().add_side(elem, i_side, mesh_2d_ext_bdry);
     145             : 
     146             :   // Create a clone of the 2D mesh to be used for the 1D mesh generation
     147          91 :   auto mesh_2d_dummy = dynamic_pointer_cast<MeshBase>(mesh_2d->clone());
     148             :   // Generate a new 1D block based on the external boundary
     149          91 :   const auto new_block_id_1d = MooseMeshUtils::getNextFreeSubdomainID(*mesh_2d_dummy);
     150             : 
     151         455 :   MooseMeshUtils::createSubdomainFromSidesets(
     152         364 :       *mesh_2d_dummy, {std::to_string(mesh_2d_ext_bdry)}, new_block_id_1d, SubdomainName(), type());
     153             : 
     154             :   // Create a 1D mesh form the 1D block
     155          91 :   auto mesh_1d = buildMeshBaseObject();
     156         273 :   MooseMeshUtils::convertBlockToMesh(*mesh_2d_dummy, *mesh_1d, {std::to_string(new_block_id_1d)});
     157          91 :   mesh_2d_dummy->clear();
     158             : 
     159             :   // If we have holes, we need to create a 1D mesh for each hole
     160          91 :   std::vector<std::unique_ptr<MeshBase>> hole_meshes_1d;
     161         102 :   for (auto & hole_mesh_2d : hole_meshes_2d)
     162             :   {
     163             :     // As we do not need these holes for reverse projection, we do not need to convert them to TRI3
     164             :     // meshes, but we still need to create a 1D mesh for each hole
     165          11 :     hole_mesh_2d->find_neighbors();
     166          11 :     const auto hole_mesh_2d_ext_bdry = MooseMeshUtils::getNextFreeBoundaryID(*hole_mesh_2d);
     167          55 :     for (const auto & elem : hole_mesh_2d->active_element_ptr_range())
     168         220 :       for (const auto & i_side : elem->side_index_range())
     169         176 :         if (elem->neighbor_ptr(i_side) == nullptr)
     170          99 :           hole_mesh_2d->get_boundary_info().add_side(elem, i_side, hole_mesh_2d_ext_bdry);
     171          11 :     const auto new_hole_block_id_1d = MooseMeshUtils::getNextFreeSubdomainID(*hole_mesh_2d);
     172          44 :     MooseMeshUtils::createSubdomainFromSidesets(*hole_mesh_2d,
     173          11 :                                                 {std::to_string(hole_mesh_2d_ext_bdry)},
     174             :                                                 new_hole_block_id_1d,
     175          22 :                                                 SubdomainName(),
     176          11 :                                                 type());
     177             :     // Create a 1D mesh form the 1D block
     178          11 :     hole_meshes_1d.push_back(buildMeshBaseObject());
     179          44 :     MooseMeshUtils::convertBlockToMesh(
     180          22 :         *hole_mesh_2d, *hole_meshes_1d.back(), {std::to_string(new_hole_block_id_1d)});
     181          11 :     hole_mesh_2d->clear();
     182             :   }
     183             : 
     184             :   // Find centroid of the 2D mesh
     185          91 :   const Point centroid = MooseMeshUtils::meshCentroidCalculator(*mesh_2d);
     186             :   // calculate an average normal vector of the 2D mesh
     187          91 :   const Point mesh_norm = meshNormal2D(*mesh_2d);
     188             :   // Check the deviation of the mesh normal vector from the global average normal vector
     189          91 :   if (meshNormalDeviation2D(*mesh_2d, mesh_norm) > _max_angle_deviation)
     190           3 :     paramError("boundary_names",
     191             :                "The normal vector of some elements in the 2D mesh deviates too much from the "
     192           3 :                "global average normal vector. The maximum deviation is " +
     193           6 :                    std::to_string(_max_angle_deviation) +
     194             :                    ". Consider dividing the boundary into several parts to "
     195             :                    "reduce the angle deviation.");
     196             : 
     197             :   // Move both 2d and 1d meshes to the centroid of the 2D mesh
     198          88 :   MeshTools::Modification::translate(*mesh_1d, -centroid(0), -centroid(1), -centroid(2));
     199          88 :   MeshTools::Modification::translate(*mesh_2d, -centroid(0), -centroid(1), -centroid(2));
     200             :   // Alsop need to translate the 1D hole meshes if applicable
     201          96 :   for (auto & hole_mesh_1d : hole_meshes_1d)
     202           8 :     MeshTools::Modification::translate(*hole_mesh_1d, -centroid(0), -centroid(1), -centroid(2));
     203             : 
     204             :   // Calculate the Euler angles to rotate the meshes so that the 2D mesh is close to the XY plane
     205             :   // (i.e., the normal vector of the 2D mesh is aligned with the Z axis)
     206          88 :   const Real theta = std::acos(mesh_norm(2)) / M_PI * 180.0;
     207             :   const Real phi =
     208          88 :       (MooseUtils::absoluteFuzzyLessThan(mesh_norm(2), 1.0) ? std::atan2(mesh_norm(1), mesh_norm(0))
     209          88 :                                                             : 0.0) /
     210          88 :       M_PI * 180.0;
     211          88 :   MeshTools::Modification::rotate(*mesh_1d, 90.0 - phi, theta, 0.0);
     212          88 :   MeshTools::Modification::rotate(*mesh_2d, 90.0 - phi, theta, 0.0);
     213             :   // Also rotate the 1D hole meshes if applicable
     214          96 :   for (auto & hole_mesh_1d : hole_meshes_1d)
     215           8 :     MeshTools::Modification::rotate(*hole_mesh_1d, 90.0 - phi, theta, 0.0);
     216             : 
     217             :   // Clone the 2D mesh to be used for reverse projection later
     218          88 :   auto mesh_2d_xyz = dynamic_pointer_cast<MeshBase>(mesh_2d->clone());
     219             : 
     220             :   // Project the 2D mesh to the XY plane so that XYDelaunay can be used
     221        2256 :   for (const auto & node : mesh_2d->node_ptr_range())
     222        2256 :     (*node)(2) = 0;
     223             :   // Project the 1D mesh to the XY plane as well
     224        1400 :   for (const auto & node : mesh_1d->node_ptr_range())
     225        1400 :     (*node)(2) = 0;
     226             :   // Also project the 1D hole meshes to the XY plane if applicable
     227          96 :   for (auto & hole_mesh_1d : hole_meshes_1d)
     228             :   {
     229          72 :     for (const auto & node : hole_mesh_1d->node_ptr_range())
     230          72 :       (*node)(2) = 0;
     231             :   }
     232             : 
     233          88 :   std::vector<libMesh::TriangulatorInterface::MeshedHole> meshed_holes;
     234          88 :   std::vector<libMesh::TriangulatorInterface::Hole *> triangulator_hole_ptrs(hole_meshes_1d.size());
     235          88 :   meshed_holes.reserve(hole_meshes_1d.size());
     236          96 :   for (auto hole_i : index_range(hole_meshes_1d))
     237             :   {
     238           8 :     hole_meshes_1d[hole_i]->prepare_for_use();
     239           8 :     meshed_holes.emplace_back(*hole_meshes_1d[hole_i]);
     240           8 :     triangulator_hole_ptrs[hole_i] = &meshed_holes.back();
     241             :   }
     242             : 
     243             :   // Finally, triangulation
     244             :   std::unique_ptr<UnstructuredMesh> mesh =
     245          88 :       dynamic_pointer_cast<UnstructuredMesh>(std::move(mesh_1d));
     246             : 
     247          88 :   Poly2TriTriangulator poly2tri(*mesh);
     248          88 :   poly2tri.triangulation_type() = TriangulatorInterface::PSLG;
     249             : 
     250          88 :   poly2tri.set_interpolate_boundary_points(0);
     251          88 :   poly2tri.set_refine_boundary_allowed(false);
     252          88 :   poly2tri.set_verify_hole_boundaries(false);
     253          88 :   poly2tri.desired_area() = 0;
     254          88 :   poly2tri.minimum_angle() = 0; // Not yet supported
     255          88 :   poly2tri.smooth_after_generating() = false;
     256          88 :   if (!triangulator_hole_ptrs.empty())
     257           8 :     poly2tri.attach_hole_list(&triangulator_hole_ptrs);
     258             :   // Future TODO: correct the area function based on the local normal vector
     259          88 :   if (_use_auto_area_func)
     260          80 :     poly2tri.set_auto_area_function(this->comm(),
     261          80 :                                     _auto_area_function_num_points,
     262          80 :                                     _auto_area_function_power,
     263          80 :                                     _auto_area_func_default_size,
     264          80 :                                     _auto_area_func_default_size_dist);
     265          88 :   poly2tri.triangulate();
     266             : 
     267             :   // Reverse the projection based on the original 2D mesh
     268        2464 :   for (const auto & node : mesh->node_ptr_range())
     269             :   {
     270        2376 :     bool node_mod = false;
     271             :     // Try to find the element in mesh_2d that contains the new node
     272       41984 :     for (const auto & elem : mesh_2d->active_element_ptr_range())
     273             :     {
     274       41984 :       if (elem->contains_point(Point((*node)(0), (*node)(1), 0.0)))
     275             :       {
     276             :         // Element id
     277        2376 :         const auto elem_id = elem->id();
     278             :         // element in xyz_in_xyz
     279        2376 :         const Elem & elem_xyz = *mesh_2d_xyz->elem_ptr(elem_id);
     280             : 
     281        2376 :         const Point elem_normal = elemNormal(elem_xyz);
     282        2376 :         const Point & elem_p = *mesh_2d_xyz->elem_ptr(elem_id)->node_ptr(0);
     283             : 
     284             :         // if the x and y values of the node is the same as the elem_p's first node, we can just
     285             :         // move it to that node's position
     286        3176 :         if (MooseUtils::absoluteFuzzyEqual((*node)(0), elem_p(0)) &&
     287         800 :             MooseUtils::absoluteFuzzyEqual((*node)(1), elem_p(1)))
     288             :         {
     289         168 :           (*node)(2) = elem_p(2);
     290         168 :           node_mod = true;
     291         168 :           break;
     292             :         }
     293             :         // Otherwise, we need to find a position inside the 2D element
     294             :         // It has the same x and y coordinates as the node in the projected mesh;
     295        2208 :         (*node)(2) = elem_p(2) - (((*node)(0) - elem_p(0)) * elem_normal(0) +
     296        2208 :                                   ((*node)(1) - elem_p(1)) * elem_normal(1)) /
     297        2208 :                                      elem_normal(2);
     298        2208 :         node_mod = true;
     299        2208 :         break;
     300             :       }
     301        2376 :     }
     302        2376 :     if (!node_mod)
     303           0 :       mooseError("Node not found in mesh_in_xy");
     304          88 :   }
     305             : 
     306             :   // Rotate the mesh back
     307          88 :   MeshTools::Modification::rotate(*mesh, 0.0, -theta, phi - 90.0);
     308             :   // Translate the mesh back
     309          88 :   MeshTools::Modification::translate(*mesh, centroid(0), centroid(1), centroid(2));
     310             : 
     311             :   // Correct the nodes based on the level set function
     312          88 :   if (_func_level_set)
     313             :   {
     314        1040 :     for (const auto & node : mesh->node_ptr_range())
     315             :     {
     316        1000 :       unsigned int iter_ct = 0;
     317        4000 :       while (iter_ct < _max_level_set_correction_iterations &&
     318        1800 :              std::abs(levelSetEvaluator(*node)) > libMesh::TOLERANCE * libMesh::TOLERANCE)
     319             :       {
     320        1200 :         levelSetCorrection(*node);
     321        1200 :         ++iter_ct;
     322             :       }
     323          40 :     }
     324             :   }
     325             : 
     326             :   // Assign the external boundary name if provided
     327          88 :   if (!_output_external_boundary_name.empty())
     328             :   {
     329             :     const std::vector<BoundaryID> output_boundary_id =
     330          48 :         MooseMeshUtils::getBoundaryIDs(*mesh, {_output_external_boundary_name}, true);
     331             : 
     332          16 :     libMesh::MeshTools::Modification::change_boundary_id(*mesh, 0, output_boundary_id[0]);
     333          16 :     mesh->get_boundary_info().sideset_name(output_boundary_id[0]) = _output_external_boundary_name;
     334          16 :   }
     335             : 
     336          88 :   mesh->unset_is_prepared();
     337             : 
     338         176 :   return mesh;
     339         308 : }

Generated by: LCOV version 1.14