LCOV - code coverage report
Current view: top level - src/transfers - MultiAppGeneralFieldKDTreeTransferBase.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: #32971 (54bef8) with base c6cf66 Lines: 150 166 90.4 %
Date: 2026-05-29 20:35:17 Functions: 12 12 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 "MultiAppGeneralFieldKDTreeTransferBase.h"
      11             : 
      12             : // MOOSE includes
      13             : #include "FEProblem.h"
      14             : #include "MooseMesh.h"
      15             : #include "MooseTypes.h"
      16             : #include "MooseVariableFE.h"
      17             : #include "SystemBase.h"
      18             : #include "Positions.h"
      19             : #include "MooseAppCoordTransform.h"
      20             : 
      21             : #include "libmesh/system.h"
      22             : 
      23             : using namespace libMesh;
      24             : 
      25             : InputParameters
      26       17671 : MultiAppGeneralFieldKDTreeTransferBase::validParams()
      27             : {
      28       17671 :   InputParameters params = MultiAppGeneralFieldTransfer::validParams();
      29             : 
      30       53013 :   params.addParam<unsigned int>("num_nearest_points",
      31       35342 :                                 1,
      32             :                                 "Number of nearest source (from) points will be chosen to "
      33             :                                 "construct a value for the target point. All points will be "
      34             :                                 "selected from the same origin mesh!");
      35             : 
      36             :   // choose whether to include data from multiple apps when performing nearest-position/
      37             :   // mesh-divisions based transfers
      38       35342 :   params.addParam<bool>("group_subapps",
      39       35342 :                         false,
      40             :                         "Whether to group source locations and values from all subapps "
      41             :                         "when working with a nearest-position or source mesh-division");
      42             : 
      43       17671 :   return params;
      44           0 : }
      45             : 
      46        4241 : MultiAppGeneralFieldKDTreeTransferBase::MultiAppGeneralFieldKDTreeTransferBase(
      47        4241 :     const InputParameters & parameters)
      48             :   : MultiAppGeneralFieldTransfer(parameters),
      49        8482 :     _num_nearest_points(getParam<unsigned int>("num_nearest_points")),
      50       12723 :     _group_subapps(getParam<bool>("group_subapps"))
      51             : {
      52        4241 :   if (_source_app_must_contain_point && _nearest_positions_obj)
      53           6 :     paramError("use_nearest_position",
      54             :                "We do not support using both nearest positions matching and checking if target "
      55             :                "points are within an app domain because the KDTrees for nearest-positions matching "
      56             :                "are (currently) built with data from multiple applications.");
      57        4505 :   if (_nearest_positions_obj &&
      58        5573 :       (isParamValid("from_mesh_divisions") || isParamValid("to_mesh_divisions")))
      59           0 :     paramError("use_nearest_position", "Cannot use nearest positions with mesh divisions");
      60             : 
      61             :   // Parameter checks on grouping subapp values
      62        4238 :   if (_group_subapps && _from_mesh_divisions.empty() && !_nearest_positions_obj)
      63           0 :     paramError(
      64             :         "group_subapps",
      65             :         "This option is only available for using mesh divisions or nearest positions regions");
      66        4361 :   else if (_group_subapps &&
      67         123 :            (_from_mesh_division_behavior == MeshDivisionTransferUse::MATCH_DIVISION_INDEX ||
      68         123 :             _from_mesh_division_behavior == MeshDivisionTransferUse::MATCH_SUBAPP_INDEX))
      69           0 :     paramError("group_subapps",
      70             :                "Cannot group subapps when considering nearest-location data as we would lose "
      71             :                "track of the division index of the source locations");
      72        4238 :   else if (_group_subapps && _use_nearest_app)
      73           0 :     paramError(
      74             :         "group_subapps",
      75             :         "When using the 'nearest child application' data, the source data (positions and values) "
      76             :         "are grouped on a per-application basis, so it cannot be agglomerated over all child "
      77             :         "applications.\nNote that the option to use nearest applications for source restrictions, "
      78             :         "but further split each child application's domain by regions closest to each position "
      79             :         "(here the the child application's centroid), which could be conceived when "
      80             :         "'group_subapps' = false, is also not available.");
      81        4238 : }
      82             : 
      83             : void
      84        4127 : MultiAppGeneralFieldKDTreeTransferBase::initialSetup()
      85             : {
      86        4127 :   MultiAppGeneralFieldTransfer::initialSetup();
      87             : 
      88             :   // We need to improve the indexing if we are to allow this
      89        4091 :   if (!_from_mesh_divisions.empty())
      90         444 :     for (const auto mesh_div : _from_mesh_divisions)
      91         270 :       if (mesh_div->getNumDivisions() != _from_mesh_divisions[0]->getNumDivisions())
      92           0 :         paramError("from_mesh_division",
      93             :                    "This transfer has only been implemented with a uniform number of source mesh "
      94             :                    "divisions across all source applications");
      95        4091 : }
      96             : 
      97             : void
      98       48536 : MultiAppGeneralFieldKDTreeTransferBase::prepareEvaluationOfInterpValues(
      99             :     const unsigned int var_index)
     100             : {
     101       48536 :   _local_kdtrees.clear();
     102       48536 :   _local_points.clear();
     103       48536 :   _local_values.clear();
     104       48536 :   buildKDTrees(var_index);
     105       48536 : }
     106             : 
     107             : bool
     108       40768 : MultiAppGeneralFieldKDTreeTransferBase::inBlocks(const std::set<SubdomainID> & blocks,
     109             :                                                  const MooseMesh & mesh,
     110             :                                                  const Elem * elem) const
     111             : {
     112             :   // We need to override the definition of block restriction for an element
     113             :   // because we have to consider whether each node of an element is adjacent to a block
     114      109024 :   for (const auto & i_node : make_range(elem->n_nodes()))
     115             :   {
     116       99120 :     const auto & node = elem->node_ptr(i_node);
     117       99120 :     const auto & node_blocks = mesh.getNodeBlockIds(*node);
     118       99120 :     std::set<SubdomainID> u;
     119       99120 :     std::set_intersection(blocks.begin(),
     120             :                           blocks.end(),
     121             :                           node_blocks.begin(),
     122             :                           node_blocks.end(),
     123             :                           std::inserter(u, u.begin()));
     124       99120 :     if (!u.empty())
     125       30864 :       return true;
     126       99120 :   }
     127        9904 :   return false;
     128             : }
     129             : 
     130             : void
     131       48536 : MultiAppGeneralFieldKDTreeTransferBase::computeNumSources()
     132             : {
     133             :   // Number of source = number of KDTrees.
     134             :   // Using mesh divisions or nearest-positions, for every app we use 1 tree per division
     135       96912 :   if (!_from_mesh_divisions.empty() ||
     136       48376 :       (!_use_nearest_app && _nearest_positions_obj && !_group_subapps))
     137         226 :     _num_sources = _from_problems.size() * getNumDivisions();
     138             :   // If we group apps, then we only use one tree per division (nearest-position region)
     139       48310 :   else if (_nearest_positions_obj && _group_subapps)
     140         113 :     _num_sources = _nearest_positions_obj->getNumPositions(_fe_problem.getCurrentExecuteOnFlag() ==
     141             :                                                            EXEC_INITIAL);
     142             :   // Regular case: 1 KDTree per app
     143             :   // Also if use_nearest_app = true, the number of problems is better than the number of positions,
     144             :   // because some of the positions are positions of child applications that are not local
     145             :   else
     146       48197 :     _num_sources = _from_problems.size();
     147       48536 : }
     148             : 
     149             : unsigned int
     150     6539500 : MultiAppGeneralFieldKDTreeTransferBase::getAppIndex(unsigned int kdtree_index,
     151             :                                                     unsigned int nested_loop_on_app_index) const
     152             : {
     153             :   // Each app is mapped to a single KD Tree
     154     6539500 :   if (_use_nearest_app)
     155        7720 :     return kdtree_index;
     156             :   // We are looping over all the apps that are grouped together
     157     6531780 :   else if (_group_subapps)
     158       25636 :     return nested_loop_on_app_index;
     159             :   // There are num_divisions trees for each app, inner ordering is divisions, so dividing by the
     160             :   // number of divisions gets us the index of the application
     161             :   else
     162     6506144 :     return kdtree_index / getNumDivisions();
     163             : }
     164             : 
     165             : unsigned int
     166       49730 : MultiAppGeneralFieldKDTreeTransferBase::getNumAppsPerTree() const
     167             : {
     168       49730 :   if (_use_nearest_app)
     169          66 :     return 1;
     170       49664 :   else if (_group_subapps)
     171         181 :     return _from_meshes.size();
     172             :   else
     173       49483 :     return 1;
     174             : }
     175             : 
     176             : unsigned int
     177    13283435 : MultiAppGeneralFieldKDTreeTransferBase::getNumDivisions() const
     178             : {
     179             :   // This is not used currently, but conceptually it is better to only divide the domain with the
     180             :   // local of local applications rather than the global number of positions (# global applications
     181             :   // here)
     182    13283435 :   if (_use_nearest_app)
     183        7720 :     return _from_meshes.size();
     184             :   // Each nearest-position region is a division
     185    13275715 :   else if (_nearest_positions_obj && !_group_subapps)
     186       46314 :     return _nearest_positions_obj->getNumPositions(_fe_problem.getCurrentExecuteOnFlag() ==
     187       46314 :                                                    EXEC_INITIAL);
     188             :   // Assume all mesh divisions (on each sub-app) has the same number of divisions. This is checked
     189    13229401 :   else if (!_from_mesh_divisions.empty())
     190      790554 :     return _from_mesh_divisions[0]->getNumDivisions();
     191             :   // Grouping subapps or no special mode, we do not subdivide
     192             :   else
     193    12438847 :     return 1;
     194             : }
     195             : 
     196             : Point
     197       15395 : MultiAppGeneralFieldKDTreeTransferBase::getPointInSourceKDTreeFrame(unsigned int i_source,
     198             :                                                                     const Point & pt) const
     199             : {
     200       15395 :   if (_nearest_positions_obj && _group_subapps)
     201             :     // KD-trees are built in global coords when grouping subapps with nearest-positions
     202        6075 :     return pt;
     203             :   else
     204       27960 :     return getPointInSourceAppFrame(pt, i_source, "KD-tree neighbor search");
     205             : }
     206             : 
     207             : bool
     208     6364238 : MultiAppGeneralFieldKDTreeTransferBase::checkRestrictionsForSource(const Point & pt,
     209             :                                                                    const unsigned int mesh_div,
     210             :                                                                    const unsigned int i_from) const
     211             : {
     212             :   // Only use the KDTree from the closest position if in "nearest-position" mode
     213     6364238 :   if (_nearest_positions_obj)
     214             :   {
     215             :     // See computeNumSources for the number of sources. i_from is the index in the source loop
     216             :     // i_from is local if looping on _from_problems as sources, positions are indexed globally
     217             :     // i_from is already indexing in positions if using group_subapps
     218       70889 :     auto position_index = i_from; // if _group_subapps
     219       70889 :     if (_use_nearest_app)
     220        9949 :       position_index = getGlobalSourceAppIndex(i_from);
     221       60940 :     else if (!_group_subapps)
     222       19760 :       position_index = i_from % getNumDivisions();
     223             : 
     224             :     // NOTE: if two positions are equi-distant to the point, this will chose one
     225             :     // This problem is detected if using search_value_conflicts in this call
     226       70889 :     if (!closestToPosition(position_index, pt))
     227       39976 :       return false;
     228             :   }
     229             : 
     230             :   // Application index depends on which source/grouping mode we are using
     231     6324262 :   const unsigned int app_index = getAppIndex(i_from, i_from / getNumDivisions());
     232             : 
     233             :   // Check mesh restriction before anything
     234     6324262 :   if (_source_app_must_contain_point)
     235             :   {
     236           0 :     if (_nearest_positions_obj)
     237           0 :       mooseError("Nearest-positions + source_app_must_contain_point not implemented");
     238             :     // Transform the point to place it in the local coordinate system
     239           0 :     const auto local_pt = getPointInSourceAppFrame(pt, app_index, "Source mesh containment check");
     240           0 :     if (!inMesh(_from_point_locators[app_index].get(), local_pt))
     241           0 :       return false;
     242             :   }
     243             : 
     244             :   // Check the mesh division. We have handled the restriction of the source locations when
     245             :   // building the nearest-neighbor trees. We only need to check that we meet the required
     246             :   // source division index.
     247     6324262 :   if (!_from_mesh_divisions.empty())
     248             :   {
     249             :     mooseAssert(mesh_div != MooseMeshDivision::INVALID_DIVISION_INDEX,
     250             :                 "We should not be receiving point requests with an invalid "
     251             :                 "source mesh division index");
     252      208800 :     const unsigned int kd_div_index = i_from % getNumDivisions();
     253             : 
     254             :     // If matching source mesh divisions to target apps, we check that the index of the target
     255             :     // application, which was passed in the point request, is equal to the current mesh division
     256      208800 :     if (_from_mesh_division_behavior == MeshDivisionTransferUse::MATCH_SUBAPP_INDEX &&
     257             :         mesh_div != kd_div_index)
     258       25917 :       return false;
     259             :     // If matching source mesh divisions to target mesh divisions, we check that the index of the
     260             :     // target mesh division, which was passed in the point request, is equal to the current mesh
     261             :     // division
     262      182883 :     else if ((_from_mesh_division_behavior == MeshDivisionTransferUse::MATCH_DIVISION_INDEX ||
     263      182883 :               _to_mesh_division_behavior == MeshDivisionTransferUse::MATCH_DIVISION_INDEX) &&
     264             :              mesh_div != kd_div_index)
     265      133152 :       return false;
     266             :   }
     267             : 
     268             :   // If matching target apps to source mesh divisions, we check that the global index of the
     269             :   // application is equal to the target mesh division index, which was passed in the point request
     270     6169165 :   if (_to_mesh_division_behavior == MeshDivisionTransferUse::MATCH_SUBAPP_INDEX &&
     271        3972 :       mesh_div != getGlobalSourceAppIndex(app_index))
     272        2916 :     return false;
     273             : 
     274     6162277 :   return true;
     275             : }
     276             : 
     277             : void
     278     5895129 : MultiAppGeneralFieldKDTreeTransferBase::evaluateNearestNodeFromKDTrees(
     279             :     const Point & pt,
     280             :     unsigned int source_index,
     281             :     std::pair<Real, Real> & outgoing_val,
     282             :     bool & point_found)
     283             : {
     284             :   // First pass: find the nearest neighbor value across all local sources
     285    12072089 :   for (const auto i_from : make_range(_num_sources))
     286             :   {
     287     6176960 :     if (!checkRestrictionsForSource(pt, source_index, i_from))
     288      176101 :       continue;
     289             : 
     290             :     // TODO: Pre-allocate these two work arrays. They will be regularly resized by the searches
     291    12001718 :     std::vector<std::size_t> return_index(_num_nearest_points);
     292     6000859 :     std::vector<Real> return_dist_sqr(_num_nearest_points);
     293             : 
     294             :     // KD-Tree can be empty if no points are within block/boundary/bounding box restrictions
     295     6000859 :     if (_local_kdtrees[i_from]->numberCandidatePoints())
     296             :     {
     297     5993033 :       point_found = true;
     298             :       // Note that we do not need to use the transformed_pt (in the source app frame)
     299             :       // because the KDTree has been created in the reference frame
     300     5993033 :       _local_kdtrees[i_from]->neighborSearch(
     301             :           pt, _num_nearest_points, return_index, return_dist_sqr);
     302     5993033 :       Real val_sum = 0, dist_sum = 0;
     303    11986066 :       for (const auto index : return_index)
     304             :       {
     305     5993033 :         val_sum += _local_values[i_from][index];
     306     5993033 :         dist_sum += (_local_points[i_from][index] - pt).norm();
     307             :       }
     308             : 
     309             :       // If the new value found is closer than for other sources, use it
     310     5993033 :       const auto new_distance = dist_sum / return_dist_sqr.size();
     311     5993033 :       if (new_distance < outgoing_val.second)
     312     5926971 :         outgoing_val = {val_sum / return_index.size(), new_distance};
     313             :     }
     314     6000859 :   }
     315             : 
     316             :   // Second pass: search-value-conflict detection
     317     5895129 :   if (point_found && _search_value_conflicts)
     318             :   {
     319       15032 :     unsigned int num_equidistant_problems = 0;
     320             : 
     321       49939 :     for (const auto i_from : make_range(_num_sources))
     322             :     {
     323       34907 :       if (!checkRestrictionsForSource(pt, source_index, i_from))
     324       18292 :         continue;
     325             : 
     326             :       // TODO: Pre-allocate these two work arrays. They will be regularly resized by the searches
     327       33230 :       std::vector<std::size_t> return_index(_num_nearest_points + 1);
     328       16615 :       std::vector<Real> return_dist_sqr(_num_nearest_points + 1);
     329             : 
     330       16615 :       const auto app_index = getAppIndex(i_from, i_from / getNumDivisions());
     331       16615 :       const auto num_search = _num_nearest_points + 1;
     332             : 
     333       16615 :       if (_local_kdtrees[i_from]->numberCandidatePoints())
     334             :       {
     335       15395 :         _local_kdtrees[i_from]->neighborSearch(pt, num_search, return_index, return_dist_sqr);
     336       15395 :         auto num_found = return_dist_sqr.size();
     337             : 
     338             :         // Local coordinates only accessible when not using nearest-position,
     339             :         // as we did not keep the index of the source app, only the position index
     340       15395 :         const Point local_pt = getPointInSourceKDTreeFrame(app_index, pt);
     341             : 
     342       20811 :         if (!_nearest_positions_obj &&
     343        5416 :             _from_transforms[getGlobalSourceAppIndex(app_index)]->hasCoordinateSystemTypeChange())
     344           0 :           if (!_skip_coordinate_collapsing)
     345           0 :             mooseInfo("Search value conflict cannot find the origin point due to the "
     346             :                       "non-uniqueness of the coordinate collapsing reverse mapping");
     347             : 
     348             :         // Look for too many equidistant nodes within a problem. First zip then sort by distance
     349       15395 :         std::vector<std::pair<Real, std::size_t>> zipped_nearest_points;
     350       46040 :         for (const auto i : make_range(num_found))
     351       30645 :           zipped_nearest_points.push_back(std::make_pair(return_dist_sqr[i], return_index[i]));
     352       15395 :         std::sort(zipped_nearest_points.begin(), zipped_nearest_points.end());
     353             : 
     354             :         // If two furthest are equally far from target point, then we have an indetermination in
     355             :         // what is sent in this communication round from this process. However, it may not
     356             :         // materialize to an actual conflict, as values sent from another process for the
     357             :         // desired target point could be closer (nearest). There is no way to know at this point
     358             :         // in the communication that a closer value exists somewhere else
     359       30645 :         if (num_found > 1 && num_found == num_search &&
     360       15250 :             MooseUtils::absoluteFuzzyEqual(zipped_nearest_points[num_found - 1].first,
     361       15250 :                                            zipped_nearest_points[num_found - 2].first))
     362             :         {
     363         288 :           if (_nearest_positions_obj)
     364          12 :             registerConflict(app_index, 0, pt, outgoing_val.second, true);
     365             :           else
     366         276 :             registerConflict(app_index, 0, local_pt, outgoing_val.second, true);
     367             :         }
     368             : 
     369             :         // Recompute the distance for this problem. If it matches the cached value more than
     370             :         // once it means multiple problems provide equidistant values for this point
     371       15395 :         Real dist_sum = 0;
     372       30790 :         for (const auto i : make_range(num_search - 1))
     373             :         {
     374       15395 :           auto index = zipped_nearest_points[i].second;
     375       15395 :           dist_sum += (_local_points[i_from][index] - pt).norm();
     376             :         }
     377             : 
     378             :         // Compare to the selected value found after looking at all the problems
     379       15395 :         if (MooseUtils::absoluteFuzzyEqual(dist_sum / return_dist_sqr.size(), outgoing_val.second))
     380             :         {
     381         211 :           num_equidistant_problems++;
     382         211 :           if (num_equidistant_problems > 1)
     383             :           {
     384           0 :             if (_nearest_positions_obj)
     385           0 :               registerConflict(app_index, 0, pt, outgoing_val.second, true);
     386             :             else
     387           0 :               registerConflict(app_index, 0, local_pt, outgoing_val.second, true);
     388             :           }
     389             :         }
     390       15395 :       }
     391       16615 :     }
     392             :   }
     393     5895129 : }

Generated by: LCOV version 1.14