LCOV - code coverage report
Current view: top level - src/meshgenerators - CombinerGenerator.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: 2bf808 Lines: 151 176 85.8 %
Date: 2025-07-17 01:28:37 Functions: 5 5 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       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             : 
      10             : #include "CombinerGenerator.h"
      11             : 
      12             : #include "CastUniquePointer.h"
      13             : #include "MooseUtils.h"
      14             : #include "DelimitedFileReader.h"
      15             : 
      16             : #include "libmesh/replicated_mesh.h"
      17             : #include "libmesh/unstructured_mesh.h"
      18             : #include "libmesh/mesh_modification.h"
      19             : #include "libmesh/point.h"
      20             : #include "libmesh/node.h"
      21             : 
      22             : registerMooseObject("MooseApp", CombinerGenerator);
      23             : 
      24             : InputParameters
      25       15501 : CombinerGenerator::validParams()
      26             : {
      27       15501 :   InputParameters params = MeshGenerator::validParams();
      28             : 
      29       15501 :   params.addClassDescription(
      30             :       "Combine multiple meshes (or copies of one mesh) together into one (disjoint) mesh.  Can "
      31             :       "optionally translate those meshes before combining them.");
      32             : 
      33       15501 :   params.addRequiredParam<std::vector<MeshGeneratorName>>(
      34             :       "inputs",
      35             :       "The input MeshGenerators.  This can either be N generators or 1 generator.  If only 1 is "
      36             :       "given then 'positions' must also be given.");
      37             : 
      38       15501 :   params.addParam<std::vector<Point>>(
      39             :       "positions",
      40             :       "The (optional) position of each given mesh.  If N 'inputs' were given then this must either "
      41             :       "be left blank or N positions must be given.  If 1 input was given then this MUST be "
      42             :       "provided.");
      43             : 
      44       15501 :   params.addParam<std::vector<FileName>>(
      45             :       "positions_file", "Alternative way to provide the position of each given mesh.");
      46             : 
      47       46503 :   params.addParam<bool>("avoid_merging_subdomains",
      48       31002 :                         false,
      49             :                         "Whether to prevent merging subdomains by offsetting ids. The first mesh "
      50             :                         "in the input will keep the same subdomains ids, the others will have "
      51             :                         "offsets. All subdomain names will remain valid");
      52       46503 :   params.addParam<bool>("avoid_merging_boundaries",
      53       31002 :                         false,
      54             :                         "Whether to prevent merging sidesets by offsetting ids. The first mesh "
      55             :                         "in the input will keep the same boundary ids, the others will have "
      56             :                         "offsets. All boundary names will remain valid");
      57             : 
      58       15501 :   return params;
      59           0 : }
      60             : 
      61         618 : CombinerGenerator::CombinerGenerator(const InputParameters & parameters)
      62             :   : MeshGenerator(parameters),
      63         618 :     _meshes(getMeshes("inputs")),
      64         618 :     _input_names(getParam<std::vector<MeshGeneratorName>>("inputs")),
      65         618 :     _avoid_merging_subdomains(getParam<bool>("avoid_merging_subdomains")),
      66        1854 :     _avoid_merging_boundaries(getParam<bool>("avoid_merging_boundaries"))
      67             : {
      68         618 :   if (_input_names.empty())
      69           0 :     paramError("input_names", "You need to specify at least one MeshGenerator as an input.");
      70             : 
      71         618 :   if (isParamValid("positions") && isParamValid("positions_file"))
      72           0 :     mooseError("Both 'positions' and 'positions_file' cannot be specified simultaneously in "
      73             :                "CombinerGenerator ",
      74           0 :                _name);
      75             : 
      76         618 :   if (_input_names.size() == 1)
      77          48 :     if (!isParamValid("positions") && !isParamValid("positions_file"))
      78           0 :       paramError("positions",
      79             :                  "If only one input mesh is given, then 'positions' or 'positions_file' must also "
      80             :                  "be supplied");
      81         618 : }
      82             : 
      83             : void
      84         601 : CombinerGenerator::fillPositions()
      85             : {
      86         601 :   if (isParamValid("positions"))
      87             :   {
      88         241 :     _positions = getParam<std::vector<Point>>("positions");
      89             : 
      90             :     // the check in the constructor wont catch error where the user sets positions = ''
      91         241 :     if ((_input_names.size() == 1) && _positions.empty())
      92           4 :       paramError("positions", "If only one input mesh is given, then 'positions' cannot be empty.");
      93             : 
      94         237 :     if (_input_names.size() != 1)
      95         208 :       if (_positions.size() && (_input_names.size() != _positions.size()))
      96           4 :         paramError(
      97             :             "positions",
      98             :             "If more than one input mesh is provided then the number of positions provided must "
      99             :             "exactly match the number of input meshes.");
     100             :   }
     101         360 :   else if (isParamValid("positions_file"))
     102             :   {
     103          28 :     std::vector<FileName> positions_file = getParam<std::vector<FileName>>("positions_file");
     104             : 
     105             :     // the check in the constructor wont catch error where the user sets positions_file = ''
     106          28 :     if ((_input_names.size() == 1) && positions_file.empty())
     107           4 :       paramError("positions_file",
     108             :                  "If only one input mesh is given, then 'positions_file' cannot be empty.");
     109             : 
     110          44 :     for (const auto & f : positions_file)
     111             :     {
     112          24 :       MooseUtils::DelimitedFileReader file(f, &_communicator);
     113          24 :       file.setFormatFlag(MooseUtils::DelimitedFileReader::FormatFlag::ROWS);
     114          24 :       file.read();
     115             : 
     116          24 :       const std::vector<Point> & data = file.getDataAsPoints();
     117             : 
     118          24 :       if (_input_names.size() != 1)
     119          14 :         if (data.size() && (_input_names.size() != data.size()))
     120           4 :           paramError("positions_file",
     121             :                      "If more than one input mesh is provided then the number of positions must "
     122             :                      "exactly match the number of input meshes.");
     123             : 
     124          80 :       for (const auto & d : data)
     125          60 :         _positions.push_back(d);
     126          20 :     }
     127          20 :   }
     128         585 : }
     129             : 
     130             : std::unique_ptr<MeshBase>
     131         601 : CombinerGenerator::generate()
     132             : {
     133             :   // Two cases:
     134             :   // 1. Multiple input meshes and optional positions
     135             :   // 2. One input mesh and multiple positions
     136         601 :   fillPositions();
     137             : 
     138             :   // Case 1
     139         585 :   if (_meshes.size() != 1)
     140             :   {
     141             :     // merge all meshes into the first one
     142         546 :     auto mesh = dynamic_pointer_cast<UnstructuredMesh>(*_meshes[0]);
     143             : 
     144         546 :     if (!mesh)
     145           0 :       paramError("inputs", _input_names[0], " is not a valid unstructured mesh");
     146             : 
     147             :     // Move the first input mesh if applicable
     148         546 :     if (_positions.size())
     149             :     {
     150         214 :       MeshTools::Modification::translate(
     151         214 :           *mesh, _positions[0](0), _positions[0](1), _positions[0](2));
     152             :     }
     153             : 
     154             :     // Read in all of the other meshes
     155        2420 :     for (MooseIndex(_meshes) i = 1; i < _meshes.size(); ++i)
     156             :     {
     157        1874 :       auto other_mesh = dynamic_pointer_cast<UnstructuredMesh>(*_meshes[i]);
     158             : 
     159        1874 :       if (!other_mesh)
     160           0 :         paramError("inputs", _input_names[i], " is not a valid unstructured mesh");
     161             : 
     162             :       // Move It
     163        1874 :       if (_positions.size())
     164             :       {
     165         698 :         MeshTools::Modification::translate(
     166         698 :             *other_mesh, _positions[i](0), _positions[i](1), _positions[i](2));
     167             :       }
     168             : 
     169        1874 :       copyIntoMesh(*mesh, *other_mesh);
     170        1874 :     }
     171             : 
     172         546 :     mesh->set_isnt_prepared();
     173         546 :     return dynamic_pointer_cast<MeshBase>(mesh);
     174         546 :   }
     175             :   else // Case 2
     176             :   {
     177          39 :     auto input_mesh = dynamic_pointer_cast<UnstructuredMesh>(*_meshes[0]);
     178             : 
     179          39 :     if (!input_mesh)
     180           0 :       paramError("inputs", _input_names[0], " is not a valid unstructured mesh");
     181             : 
     182             :     // Make a copy and displace it in order to get the final mesh started
     183             :     auto copy =
     184          39 :         input_mesh->clone(); // This is required because dynamic_pointer_cast() requires an l-value
     185          39 :     auto final_mesh = dynamic_pointer_cast<UnstructuredMesh>(copy);
     186             : 
     187          39 :     if (!final_mesh)
     188           0 :       mooseError("Unable to copy mesh!");
     189             : 
     190          39 :     MeshTools::Modification::translate(
     191          39 :         *final_mesh, _positions[0](0), _positions[0](1), _positions[0](2));
     192             : 
     193             :     // Here's the way this is going to work:
     194             :     // I'm going to make one more copy of the input_mesh so that I can move it and copy it in
     195             :     // Then, after it's copied in I'm going to reset its coordinates by looping over the input_mesh
     196             :     // and resetting the nodal positions.
     197             :     // This could be done without the copy - you would translate the mesh then translate it back...
     198             :     // However, I'm worried about floating point roundoff.  If you were doing this 100,000 times or
     199             :     // more then the mesh could "drift" away from its original position.  I really want the
     200             :     // translations to be exact each time.
     201             :     // I suppose that it is technically possible to just save off a datastructure (map, etc.) that
     202             :     // could hold the nodal positions only (instead of a copy of the mesh) but I'm not sure that
     203             :     // would really save much... we'll see if it shows up in profiling somewhere
     204          39 :     copy = input_mesh->clone();
     205          39 :     auto translated_mesh = dynamic_pointer_cast<UnstructuredMesh>(copy);
     206             : 
     207          39 :     if (!translated_mesh)
     208           0 :       mooseError("Unable to copy mesh!");
     209             : 
     210          79 :     for (MooseIndex(_meshes) i = 1; i < _positions.size(); ++i)
     211             :     {
     212             :       // Move
     213          40 :       MeshTools::Modification::translate(
     214          40 :           *translated_mesh, _positions[i](0), _positions[i](1), _positions[i](2));
     215             : 
     216             :       // Copy into final mesh
     217          40 :       copyIntoMesh(*final_mesh, *translated_mesh);
     218             : 
     219             :       // Reset nodal coordinates
     220        9016 :       for (auto translated_node_ptr : translated_mesh->node_ptr_range())
     221             :       {
     222        4488 :         auto & translated_node = *translated_node_ptr;
     223        4488 :         auto & input_node = input_mesh->node_ref(translated_node_ptr->id());
     224             : 
     225       17952 :         for (MooseIndex(LIBMESH_DIM) i = 0; i < LIBMESH_DIM; i++)
     226       13464 :           translated_node(i) = input_node(i);
     227          40 :       }
     228             :     }
     229             : 
     230          39 :     final_mesh->set_isnt_prepared();
     231          39 :     return dynamic_pointer_cast<MeshBase>(final_mesh);
     232          39 :   }
     233             : }
     234             : 
     235             : void
     236        1914 : CombinerGenerator::copyIntoMesh(UnstructuredMesh & destination, const UnstructuredMesh & source)
     237             : {
     238        1914 :   dof_id_type node_delta = destination.max_node_id();
     239        1914 :   dof_id_type elem_delta = destination.max_elem_id();
     240             : 
     241             :   unique_id_type unique_delta =
     242             : #ifdef LIBMESH_ENABLE_UNIQUE_ID
     243        1914 :       destination.parallel_max_unique_id();
     244             : #else
     245             :       0;
     246             : #endif
     247             : 
     248             :   // Prevent overlaps by offsetting the subdomains in
     249        1914 :   std::unordered_map<subdomain_id_type, subdomain_id_type> id_remapping;
     250        1914 :   unsigned int block_offset = 0;
     251        1914 :   if (_avoid_merging_subdomains)
     252             :   {
     253             :     // Note: if performance becomes an issue, this is overkill for just getting the max node id
     254         136 :     std::set<subdomain_id_type> source_ids;
     255         136 :     std::set<subdomain_id_type> dest_ids;
     256         136 :     source.subdomain_ids(source_ids, true);
     257         136 :     destination.subdomain_ids(dest_ids, true);
     258             :     mooseAssert(source_ids.size(), "Should have a subdomain");
     259             :     mooseAssert(dest_ids.size(), "Should have a subdomain");
     260         136 :     unsigned int max_dest_bid = *dest_ids.rbegin();
     261         136 :     unsigned int min_source_bid = *source_ids.begin();
     262         136 :     _communicator.max(max_dest_bid);
     263         136 :     _communicator.min(min_source_bid);
     264         136 :     block_offset = 1 + max_dest_bid - min_source_bid;
     265         272 :     for (const auto bid : source_ids)
     266         136 :       id_remapping[bid] = block_offset + bid;
     267         136 :   }
     268             : 
     269             :   // Copy mesh data over from the other mesh
     270        1914 :   destination.copy_nodes_and_elements(source,
     271             :                                       // Skipping this should cause the neighbors
     272             :                                       // to simply be copied from the other mesh
     273             :                                       // (which makes sense and is way faster)
     274             :                                       /*skip_find_neighbors = */ true,
     275             :                                       elem_delta,
     276             :                                       node_delta,
     277             :                                       unique_delta,
     278        1914 :                                       _avoid_merging_subdomains ? &id_remapping : nullptr);
     279             : 
     280             :   // Get an offset to prevent overlaps / wild merging between boundaries
     281        1914 :   BoundaryInfo & boundary = destination.get_boundary_info();
     282        1914 :   const BoundaryInfo & other_boundary = source.get_boundary_info();
     283             : 
     284        1914 :   unsigned int bid_offset = 0;
     285        1914 :   if (_avoid_merging_boundaries)
     286             :   {
     287         166 :     const auto boundary_ids = boundary.get_boundary_ids();
     288         166 :     const auto other_boundary_ids = other_boundary.get_boundary_ids();
     289         166 :     unsigned int max_dest_bid = boundary_ids.size() ? *boundary_ids.rbegin() : 0;
     290         166 :     unsigned int min_source_bid = other_boundary_ids.size() ? *other_boundary_ids.begin() : 0;
     291         166 :     _communicator.max(max_dest_bid);
     292         166 :     _communicator.min(min_source_bid);
     293         166 :     bid_offset = 1 + max_dest_bid - min_source_bid;
     294         166 :   }
     295             : 
     296             :   // Note: the code below originally came from ReplicatedMesh::stitch_mesh_helper()
     297             :   // in libMesh replicated_mesh.C around line 1203
     298             : 
     299             :   // Copy BoundaryInfo from other_mesh too.  We do this via the
     300             :   // list APIs rather than element-by-element for speed.
     301       54638 :   for (const auto & t : other_boundary.build_node_list())
     302       54638 :     boundary.add_node(std::get<0>(t) + node_delta, bid_offset + std::get<1>(t));
     303             : 
     304       36956 :   for (const auto & t : other_boundary.build_side_list())
     305       36956 :     boundary.add_side(std::get<0>(t) + elem_delta, std::get<1>(t), bid_offset + std::get<2>(t));
     306             : 
     307        1914 :   for (const auto & t : other_boundary.build_edge_list())
     308        1914 :     boundary.add_edge(std::get<0>(t) + elem_delta, std::get<1>(t), bid_offset + std::get<2>(t));
     309             : 
     310        1914 :   for (const auto & t : other_boundary.build_shellface_list())
     311           0 :     boundary.add_shellface(
     312        1914 :         std::get<0>(t) + elem_delta, std::get<1>(t), bid_offset + std::get<2>(t));
     313             : 
     314             :   // Check for the case with two block ids sharing the same name
     315        1914 :   if (_avoid_merging_subdomains)
     316             :   {
     317         303 :     for (const auto & [block_id, block_name] : destination.get_subdomain_name_map())
     318         219 :       for (const auto & [source_id, source_name] : source.get_subdomain_name_map())
     319          52 :         if (block_name == source_name)
     320           0 :           paramWarning("avoid_merging_subdomains",
     321           0 :                        "Not merging subdomains is creating two subdomains with the same name '" +
     322           0 :                            block_name + "' but different ids: " + std::to_string(source_id) +
     323           0 :                            " & " + std::to_string(block_id + block_offset) +
     324             :                            ".\n We recommend using a RenameBlockGenerator to prevent this as you "
     325             :                            "will get errors reading the Exodus output later.");
     326             :   }
     327             : 
     328        2029 :   for (const auto & [block_id, block_name] : source.get_subdomain_name_map())
     329         230 :     destination.set_subdomain_name_map().insert(
     330         230 :         std::make_pair<SubdomainID, SubdomainName>(block_id + block_offset, block_name));
     331             : 
     332             :   // Check for the case with two boundary ids sharing the same name
     333        1914 :   if (_avoid_merging_boundaries)
     334             :   {
     335         850 :     for (const auto & [b_id, b_name] : other_boundary.get_sideset_name_map())
     336        4852 :       for (const auto & [source_id, source_name] : boundary.get_sideset_name_map())
     337        4168 :         if (b_name == source_name)
     338           0 :           paramWarning(
     339             :               "avoid_merging_boundaries",
     340           0 :               "Not merging boundaries is creating two sidesets with the same name '" + b_name +
     341           0 :                   "' but different ids: " + std::to_string(source_id) + " & " +
     342           0 :                   std::to_string(b_id + bid_offset) +
     343             :                   ".\n We recommend using a RenameBoundaryGenerator to prevent this as you "
     344             :                   "will get errors reading the Exodus output later.");
     345         850 :     for (const auto & [b_id, b_name] : other_boundary.get_nodeset_name_map())
     346        4852 :       for (const auto & [source_id, source_name] : boundary.get_nodeset_name_map())
     347        4168 :         if (b_name == source_name)
     348           0 :           paramWarning(
     349             :               "avoid_merging_boundaries",
     350           0 :               "Not merging boundaries is creating two nodesets with the same name '" + b_name +
     351           0 :                   "' but different ids: " + std::to_string(source_id) + " & " +
     352           0 :                   std::to_string(b_id + bid_offset) +
     353             :                   ".\n We recommend using a RenameBoundaryGenerator to prevent this as you "
     354             :                   "will get errors reading the Exodus output later.");
     355             :   }
     356             : 
     357        8824 :   for (const auto & [nodeset_id, nodeset_name] : other_boundary.get_nodeset_name_map())
     358       13820 :     boundary.set_nodeset_name_map().insert(
     359       13820 :         std::make_pair<BoundaryID, BoundaryName>(nodeset_id + bid_offset, nodeset_name));
     360             : 
     361        9084 :   for (const auto & [sideset_id, sideset_name] : other_boundary.get_sideset_name_map())
     362       14340 :     boundary.set_sideset_name_map().insert(
     363       14340 :         std::make_pair<BoundaryID, BoundaryName>(sideset_id + bid_offset, sideset_name));
     364             : 
     365        1914 :   for (const auto & [edgeset_id, edgeset_name] : other_boundary.get_edgeset_name_map())
     366           0 :     boundary.set_edgeset_name_map().insert(
     367           0 :         std::make_pair<BoundaryID, BoundaryName>(edgeset_id + bid_offset, edgeset_name));
     368        1914 : }

Generated by: LCOV version 1.14