LCOV - code coverage report
Current view: top level - src/transfers - MultiAppGeneralFieldFunctorTransfer.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: #32971 (54bef8) with base c6cf66 Lines: 211 242 87.2 %
Date: 2026-05-29 20:35:17 Functions: 8 8 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 "MultiAppGeneralFieldFunctorTransfer.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             : #include "MooseFunctorArguments.h"
      21             : 
      22             : using namespace libMesh;
      23             : 
      24             : registerMooseObject("MooseApp", MultiAppGeneralFieldFunctorTransfer);
      25             : 
      26             : InputParameters
      27        4819 : MultiAppGeneralFieldFunctorTransfer::validParams()
      28             : {
      29        4819 :   InputParameters params = MultiAppGeneralFieldKDTreeTransferBase::validParams();
      30        4819 :   params += NonADFunctorInterface::validParams();
      31        9638 :   params.addClassDescription(
      32             :       "Transfers functor data at the MultiApp position by evaluating the functor inside its domain "
      33             :       "of definition and extrapolating with a user-selected behavior outside");
      34             : 
      35             :   // Input variables as functors instead
      36        9638 :   params.suppressParameter<std::vector<VariableName>>("source_variable");
      37             :   // NOTE: could rename this instead once we support array or vector functor component transfer
      38        9638 :   params.suppressParameter<std::vector<unsigned int>>("source_variable_components");
      39             : 
      40       19276 :   params.addRequiredParam<std::vector<MooseFunctorName>>(
      41             :       "source_functors", "Functors providing the values to transfer to the target variables");
      42             : 
      43             :   // Potential additional parameters:
      44             :   // - functor evaluation spatial argument type
      45             :   // - functor evaluation time argument type
      46             :   // - number of points to use when creating extrapolation 'patches'
      47             :   // - other 'nearest' locations: node, element, side or a general 'location'
      48             :   // - options for a radius based search and build patches instead of 'nearest'
      49             : 
      50       19276 :   MooseEnum extrapolation("flat evaluate_oob nearest-node nearest-elem", "nearest-node");
      51       19276 :   params.addParam<MooseEnum>("extrapolation_behavior",
      52             :                              extrapolation,
      53             :                              "How to extrapolate the functors when a target point for the transfer "
      54             :                              "is outside the domain of evaluation");
      55             : 
      56             :   // This is a convenient heuristic to limit communication
      57       24095 :   params.renameParam("use_nearest_app", "assume_nearest_app_holds_evaluation_location", "");
      58             : 
      59        9638 :   return params;
      60        4819 : }
      61             : 
      62         879 : MultiAppGeneralFieldFunctorTransfer::MultiAppGeneralFieldFunctorTransfer(
      63         879 :     const InputParameters & parameters)
      64             :   : MultiAppGeneralFieldKDTreeTransferBase(parameters),
      65             :     NonADFunctorInterface(this),
      66        1758 :     _functor_names(getParam<std::vector<MooseFunctorName>>("source_functors")),
      67        2637 :     _extrapolation_behavior(getParam<MooseEnum>("extrapolation_behavior"))
      68             : {
      69             :   // Check extrapolation options
      70        2637 :   if (isParamSetByUser("extrapolation_constant") && _extrapolation_behavior != "flat")
      71           0 :     paramError("extrapolation_behavior",
      72             :                "Flat (single-constant) extrapolation must be selected if an extrapolation constant "
      73             :                "is specified");
      74         879 :   if (_post_transfer_extrapolation != "none" && _extrapolation_behavior != "flat")
      75           0 :     paramError("extrapolation_behavior",
      76             :                "Flat (single-constant) extrapolation must be selected if an extrapolation post-"
      77             :                "treatment is specified");
      78             : 
      79             :   // Check size
      80         879 :   if (_functor_names.size() != _to_var_names.size())
      81           0 :     paramError("source_functors", "Should be the same size as target 'variable'");
      82         879 : }
      83             : 
      84             : void
      85         879 : MultiAppGeneralFieldFunctorTransfer::initialSetup()
      86             : {
      87         879 :   MultiAppGeneralFieldKDTreeTransferBase::initialSetup();
      88             : 
      89             :   // Retrieve the functors
      90         879 :   _functors.resize(_from_problems.size());
      91         879 :   _functor_is_variable.resize(_functor_names.size());
      92        1758 :   for (const auto i_functor : index_range(_functor_names))
      93             :   {
      94         879 :     const auto & fname = _functor_names[i_functor];
      95             : 
      96             :     // Different functors for every source
      97        2178 :     for (const auto i_from : index_range(_from_problems))
      98        1299 :       _functors[i_from].push_back(
      99        1299 :           &_from_problems[i_from]->getFunctor<Real>(fname, /*thread*/ 0, name(), false));
     100             : 
     101             :     // Need to keep track of variables because of ghosting needs
     102             :     // NOTE: we don't really expect the functor type to vary between problems
     103        2178 :     for (const auto i_from : index_range(_from_problems))
     104           0 :       _functor_is_variable[i_functor] =
     105        1299 :           _functor_is_variable[i_functor] || _from_problems[i_from]->hasVariable(fname);
     106             :   }
     107         879 : }
     108             : 
     109             : void
     110         804 : MultiAppGeneralFieldFunctorTransfer::execute()
     111             : {
     112             :   // Execute the user object if it was specified to execute on TRANSFER
     113        1608 :   for (const auto & fname : _functor_names)
     114         804 :     switch (_current_direction)
     115             :     {
     116         270 :       case TO_MULTIAPP:
     117             :       {
     118         270 :         if (!_fe_problem.hasUserObject(fname))
     119          28 :           continue;
     120         242 :         checkParentAppUserObjectExecuteOn(fname);
     121         242 :         _fe_problem.computeUserObjectByName(EXEC_TRANSFER, Moose::PRE_AUX, fname);
     122         242 :         _fe_problem.computeUserObjectByName(EXEC_TRANSFER, Moose::POST_AUX, fname);
     123         242 :         break;
     124             :       }
     125         270 :       case FROM_MULTIAPP:
     126         270 :         errorIfObjectExecutesOnTransferInSourceApp(fname);
     127             :     }
     128             : 
     129             :   // Perfom the actual transfer
     130         804 :   MultiAppGeneralFieldKDTreeTransferBase::execute();
     131         798 : }
     132             : 
     133             : void
     134         804 : MultiAppGeneralFieldFunctorTransfer::prepareEvaluationOfInterpValues(const unsigned int var_index)
     135             : {
     136         804 :   MultiAppGeneralFieldKDTreeTransferBase::prepareEvaluationOfInterpValues(var_index);
     137             : 
     138             :   // Get the point locators
     139         804 :   _point_locators.resize(_from_problems.size());
     140        1965 :   for (const auto app_index : index_range(_from_problems))
     141        1161 :     _point_locators[app_index] =
     142        2322 :         _from_problems[app_index]->mesh(_displaced_source_mesh).getPointLocator();
     143         804 : }
     144             : 
     145             : void
     146         804 : MultiAppGeneralFieldFunctorTransfer::buildKDTrees(const unsigned int var_index)
     147             : {
     148         804 :   computeNumSources();
     149         804 :   const auto num_apps_per_tree = getNumAppsPerTree();
     150         804 :   _local_kdtrees.resize(_num_sources);
     151         804 :   _local_points.resize(_num_sources);
     152         804 :   _local_values.resize(_num_sources);
     153         804 :   unsigned int max_leaf_size = 0;
     154             : 
     155             :   // Construct a local KDTree for each source. A source can be a single app or multiple apps
     156             :   // combined (option for nearest-position / mesh-divisions)
     157        2073 :   for (const auto i_source : make_range(_num_sources))
     158             :   {
     159             :     // Nest a loop on apps in case multiple apps contribute to the same KD-Tree source
     160        2578 :     for (const auto app_i : make_range(num_apps_per_tree))
     161             :     {
     162             :       // Get the current app index
     163        1309 :       const auto i_from = getAppIndex(i_source, app_i);
     164             :       // Current position index, if using nearest positions (not used for use_nearest_app)
     165        1309 :       const auto i_pos = _group_subapps ? i_source : (i_source % getNumDivisions());
     166             : 
     167             :       // Get access to the variable and some variable information
     168        1309 :       FEProblemBase & from_problem = *_from_problems[i_from];
     169        1309 :       auto & from_mesh = from_problem.mesh(_displaced_source_mesh);
     170             :       // No need for displaced mesh for checking domain of definition
     171        1309 :       const auto & node_to_elem_map = from_problem.mesh().nodeToActiveSemilocalElemMap();
     172             : 
     173             :       // Get functor for that app
     174        1309 :       const auto & functor = _functors[i_from][var_index];
     175             : 
     176             :       // Form the block restriction for evaluation. We need to prevent evaluation outside the
     177             :       // domain of evaluation of the functor (could crash) or the transfer (disobeys user)
     178             :       // Note: the functor subdomains of evaluation are checked below as well, so we
     179             :       // should be fairly safe
     180        1309 :       std::set<SubdomainID> from_blocks;
     181        1309 :       if (_from_blocks.size())
     182             :       {
     183         770 :         for (const auto bl : _from_blocks)
     184         420 :           if (functor->hasBlocks(bl))
     185         420 :             from_blocks.insert(bl);
     186             :       }
     187             :       else
     188         959 :         from_blocks = Moose::NodeArg::undefined_subdomain_connection;
     189             : 
     190             :       // We need to loop over the nodes at the edge of the domain of definition of the
     191             :       // current functor
     192        1309 :       if (_extrapolation_behavior == 2) // nearest-node
     193       11001 :         for (const auto & node : from_mesh.getMesh().local_node_ptr_range())
     194             :         {
     195             :           // No way to check number of dofs for a functor
     196             :           // Functor should be defined on at least one block by the block to have a value
     197             :           // Node should be either on a functor or a mesh boundary to be relevant for extrapolation
     198        5376 :           bool on_at_least_one_block = false;
     199        5376 :           bool on_boundary = false;
     200       19296 :           for (const auto eid : libmesh_map_find(node_to_elem_map, node->id()))
     201             :           {
     202       13920 :             bool has_block = functor->hasBlocks(from_mesh.elemPtr(eid)->subdomain_id());
     203       13920 :             if (has_block)
     204       12576 :               on_at_least_one_block = true;
     205             :             else
     206        1344 :               on_boundary = true;
     207             :             // Detect a mesh boundary
     208       13920 :             const auto elem = from_mesh.elemPtr(eid);
     209       69600 :             for (const auto side : elem->side_index_range())
     210       69120 :               if (!elem->neighbor_ptr(side) &&
     211       13440 :                   elem->is_node_on_side(elem->get_node_index(node), side))
     212        6720 :                 on_boundary = true;
     213             :           }
     214             : 
     215             :           // Not on a boundary
     216        5376 :           if (!on_at_least_one_block || !on_boundary)
     217        3244 :             continue;
     218             : 
     219        3168 :           if (!_from_blocks.empty() && !inBlocks(_from_blocks, from_mesh, node))
     220           0 :             continue;
     221             : 
     222        3168 :           if (!_from_boundaries.empty() && !onBoundaries(_from_boundaries, from_mesh, node))
     223           0 :             continue;
     224             : 
     225             :           // Handle the various source mesh divisions behaviors
     226             :           // NOTE: This could be more efficient, as instead of rejecting points in the
     227             :           // wrong division, we could just be adding them to the tree for the right division
     228        3168 :           if (!_from_mesh_divisions.empty())
     229             :           {
     230         528 :             const auto tree_division_index = i_source % getNumDivisions();
     231         528 :             const auto node_div_index = _from_mesh_divisions[i_from]->divisionIndex(*node);
     232             : 
     233             :             // Spatial restriction is always active
     234         528 :             if (node_div_index == MooseMeshDivision::INVALID_DIVISION_INDEX)
     235           0 :               continue;
     236             :             // We fill one tree per division index for matching subapp index or division index. We
     237             :             // only accept source data from the division index
     238         528 :             else if ((_from_mesh_division_behavior ==
     239         528 :                           MeshDivisionTransferUse::MATCH_DIVISION_INDEX ||
     240         528 :                       _to_mesh_division_behavior == MeshDivisionTransferUse::MATCH_DIVISION_INDEX ||
     241         528 :                       _from_mesh_division_behavior ==
     242        1056 :                           MeshDivisionTransferUse::MATCH_SUBAPP_INDEX) &&
     243             :                      tree_division_index != node_div_index)
     244         396 :               continue;
     245             :           }
     246             : 
     247             :           // Transformed node is in the reference space, as is the _nearest_positions_obj
     248        2772 :           const auto transformed_node = (*_from_transforms[getGlobalSourceAppIndex(i_from)])(*node);
     249             : 
     250             :           // Only add to the KDTree nodes that are closest to the 'position'
     251             :           // When querying values at a target point, the KDTree associated to the closest
     252             :           // position to the target point is queried
     253             :           // We do not need to check the positions when using nearest app as we will assume
     254             :           // (somewhat incorrectly) that all the points in each subapp are closer to that subapp
     255             :           // than to any other
     256        4052 :           if (!_use_nearest_app && _nearest_positions_obj &&
     257        1280 :               !closestToPosition(i_pos, transformed_node))
     258         640 :             continue;
     259             : 
     260        2132 :           _local_points[i_source].push_back(transformed_node);
     261             : 
     262             :           // Evaluate the functor on the boundary node
     263        2132 :           Moose::NodeArg node_arg = {node, &from_blocks};
     264        2132 :           Moose::StateArg time_arg(0, Moose::SolutionIterationType::Time);
     265        2132 :           _local_values[i_source].push_back((*functor)(node_arg, time_arg));
     266         249 :         }
     267             :       // Nearest-element option
     268             :       // We also use this for 'evaluate_oob', which will influence the distance found and used to
     269             :       // select the nearest value from out of bounds evaluations. It will not influence the result
     270             :       // of the evaluation
     271             :       else
     272      128492 :         for (const auto & elem : from_mesh.getMesh().local_element_ptr_range())
     273             :         {
     274             :           // No way to check number of dofs for a functor
     275       63716 :           if (!functor->hasBlocks(elem->subdomain_id()))
     276       24148 :             continue;
     277             : 
     278             :           // Make sure sure it is at a boundary, for either the mesh or a functor
     279       63380 :           bool at_a_boundary = false;
     280      239842 :           for (const auto side : elem->side_index_range())
     281             :           {
     282             :             // Boundary of the mesh
     283      219334 :             if (!elem->neighbor_ptr(side))
     284             :             {
     285       42776 :               at_a_boundary = true;
     286       42776 :               break;
     287             :             }
     288             :             // Boundary of the domain of definition of the functor
     289      176558 :             else if (!functor->hasBlocks(elem->neighbor_ptr(side)->subdomain_id()))
     290             :             {
     291          96 :               at_a_boundary = true;
     292          96 :               break;
     293             :             }
     294             :           }
     295             :           // Non boundary elements are not relevant as we can just evaluate the functor
     296       63380 :           if (!at_a_boundary)
     297       20508 :             continue;
     298             : 
     299       42872 :           if (!_from_blocks.empty() && !inBlocks(_from_blocks, from_mesh, elem))
     300        2528 :             continue;
     301             : 
     302       40344 :           if (!_from_boundaries.empty() && !onBoundaries(_from_boundaries, from_mesh, elem))
     303           0 :             continue;
     304             : 
     305             :           // Handle the various source mesh divisions behaviors
     306             :           // NOTE: This could be more efficient, as instead of rejecting points in the
     307             :           // wrong division, we could just be adding them to the tree for the right division
     308       40344 :           if (!_from_mesh_divisions.empty())
     309             :           {
     310         352 :             const auto tree_division_index = i_source % getNumDivisions();
     311             :             const auto node_div_index =
     312         352 :                 _from_mesh_divisions[i_from]->divisionIndex(elem->vertex_average());
     313             : 
     314             :             // Spatial restriction is always active
     315         352 :             if (node_div_index == MooseMeshDivision::INVALID_DIVISION_INDEX)
     316           0 :               continue;
     317             :             // We fill one tree per division index for matching subapp index or division index. We
     318             :             // only accept source data from the division index
     319         352 :             else if ((_from_mesh_division_behavior ==
     320         352 :                           MeshDivisionTransferUse::MATCH_DIVISION_INDEX ||
     321         352 :                       _to_mesh_division_behavior == MeshDivisionTransferUse::MATCH_DIVISION_INDEX ||
     322         352 :                       _from_mesh_division_behavior ==
     323         704 :                           MeshDivisionTransferUse::MATCH_SUBAPP_INDEX) &&
     324             :                      tree_division_index != node_div_index)
     325         264 :               continue;
     326             :           }
     327             : 
     328             :           // Transformed centroid is in the reference space, as is the _nearest_positions_obj
     329             :           const auto transformed_centroid =
     330       40080 :               (*_from_transforms[getGlobalSourceAppIndex(i_from)])(elem->vertex_average());
     331             : 
     332             :           // Only add to the KDTree nodes that are closest to the 'position'
     333             :           // When querying values at a target point, the KDTree associated to the closest
     334             :           // position to the target point is queried
     335             :           // We do not need to check the positions when using nearest app as we will assume
     336             :           // (somewhat incorrectly) that all the points in each subapp are closer to that subapp
     337             :           // than to any other
     338       41104 :           if (!_use_nearest_app && _nearest_positions_obj &&
     339        1024 :               !closestToPosition(i_pos, transformed_centroid))
     340         512 :             continue;
     341             : 
     342       39568 :           _local_points[i_source].push_back(transformed_centroid);
     343             : 
     344             :           // Evaluate the functor on the boundary node
     345       39568 :           Moose::ElemArg elem_arg = {elem, /*sknewness*/ false};
     346       39568 :           Moose::StateArg time_arg(0, Moose::SolutionIterationType::Time);
     347       39568 :           _local_values[i_source].push_back((*functor)(elem_arg, time_arg));
     348        1060 :         }
     349             : 
     350        1309 :       max_leaf_size = std::max(max_leaf_size, from_mesh.getMaxLeafSize());
     351        1309 :     }
     352             : 
     353             :     // Make a KDTree from the accumulated points data
     354             :     std::shared_ptr<KDTree> _kd_tree =
     355        1269 :         std::make_shared<KDTree>(_local_points[i_source], max_leaf_size);
     356        1269 :     _local_kdtrees[i_source] = _kd_tree;
     357        1269 :   }
     358         804 : }
     359             : 
     360             : void
     361        1194 : MultiAppGeneralFieldFunctorTransfer::evaluateInterpValues(
     362             :     const unsigned int var_index,
     363             :     const std::vector<std::pair<Point, unsigned int>> & incoming_points,
     364             :     std::vector<std::pair<Real, Real>> & outgoing_vals)
     365             : {
     366        1194 :   evaluateValues(var_index, incoming_points, outgoing_vals);
     367        1194 : }
     368             : 
     369             : void
     370        1194 : MultiAppGeneralFieldFunctorTransfer::evaluateValues(
     371             :     const unsigned int var_index,
     372             :     const std::vector<std::pair<Point, unsigned int>> & incoming_points,
     373             :     std::vector<std::pair<Real, Real>> & outgoing_vals)
     374             : {
     375        1194 :   dof_id_type i_pt = 0;
     376        1194 :   std::set<const Elem *> elem_candidates;
     377        1194 :   const auto num_apps_per_tree = getNumAppsPerTree();
     378             : 
     379      110484 :   for (const auto & [pt, mesh_div] : incoming_points)
     380             :   {
     381             :     // Reset distance
     382      109290 :     outgoing_vals[i_pt].second = std::numeric_limits<Real>::max();
     383      109290 :     bool point_found = false;
     384             : 
     385             :     // Loop on all sources: locate the point and evaluate the functor if it is in-domain.
     386             :     // Extrapolation (all modes) is handled after this loop, only when no in-domain hit is found.
     387      261661 :     for (const auto i_source : make_range(_num_sources))
     388             :     {
     389             :       // Examine all restrictions for the point. This source (KDTree+values) could be ruled out
     390      152371 :       if (!checkRestrictionsForSource(pt, mesh_div, i_source))
     391        7568 :         continue;
     392             :       // Note: because this transfer is intended for extrapolation,
     393             :       // this will usually not restrict the source. The distance comparisons will be crucial
     394             : 
     395             :       // Inner loop: when group_subapps is true, multiple apps contribute to the same source
     396             :       // (one KD-tree per position). Mirror the structure of buildKDTrees.
     397      290826 :       for (const auto app_i : make_range(num_apps_per_tree))
     398             :       {
     399      146023 :         const auto app_index = getAppIndex(i_source, app_i);
     400             : 
     401             :         // Point locators and functor evaluation work in each app's local frame
     402             :         const Point app_local_pt =
     403      292046 :             getPointInSourceAppFrame(pt, app_index, "Functor value evaluation");
     404             : 
     405             :         // Retrieve the functor
     406      146023 :         const auto & functor = *_functors[app_index][var_index];
     407             : 
     408             :         // Get the intersection of the functor and transfer source block restrictions
     409      146023 :         std::set<SubdomainID> from_blocks;
     410      146023 :         for (const auto bl :
     411      146023 :              _from_blocks.size()
     412      146023 :                  ? _from_blocks
     413      495186 :                  : _from_problems[app_index]->mesh().getMesh().get_mesh_subdomains())
     414      203140 :           if (functor.hasBlocks(bl))
     415      202304 :             from_blocks.insert(bl);
     416             : 
     417             :         // If in domain, use the functor evaluation at the point
     418             :         // Locate the point and an element
     419             :         // Clear the nearest candidates
     420      146023 :         elem_candidates.clear();
     421      146023 :         if (from_blocks.size())
     422      146023 :           (*_point_locators[app_index])(app_local_pt, elem_candidates, &from_blocks);
     423             :         else
     424           0 :           (*_point_locators[app_index])(app_local_pt, elem_candidates);
     425      146023 :         if (elem_candidates.size())
     426             :         {
     427             :           // Register conflict if any
     428       68084 :           if (point_found && _search_value_conflicts)
     429             :           {
     430             :             // In the nearest-position/app mode, we save conflicts in the reference frame
     431          18 :             if (_nearest_positions_obj)
     432           0 :               registerConflict(i_source, /*dof*/ 0, pt, 0, true);
     433             :             else
     434          18 :               registerConflict(i_source, 0, app_local_pt, 0, true);
     435             :           }
     436             : 
     437             :           // Average the result for now
     438             :           // TODO: if we knew the functor were continuous, we could return earlier
     439       68084 :           Real value = 0;
     440       68084 :           unsigned int num_values = 0;
     441      143897 :           for (const auto elem : elem_candidates)
     442             :           {
     443             :             // Variables would hit a ghosting error; compare against the sub-app communicator rank,
     444             :             // not the parent communicator rank - each sub-app runs in its own sub-communicator
     445             :             // where ranks start at 0, regardless of the global rank of the owning process
     446      156462 :             if (_functor_is_variable[var_index] &&
     447       80649 :                 elem->processor_id() != _from_problems[app_index]->processor_id())
     448        1816 :               continue;
     449             :             // Avoid evaluating outside of element
     450       75325 :             if (!elem->contains_point(app_local_pt, libMesh::TOLERANCE * libMesh::TOLERANCE))
     451        1328 :               continue;
     452       73997 :             Moose::ElemPointArg elem_pt_arg = {elem, app_local_pt, /*correct skewness*/ false};
     453       73997 :             Moose::StateArg time_arg(0, Moose::SolutionIterationType::Time);
     454       73997 :             value += functor(elem_pt_arg, time_arg);
     455       73997 :             num_values++;
     456             :           }
     457       68084 :           if (num_values == 0)
     458         728 :             continue;
     459             : 
     460       67356 :           value /= num_values;
     461       67356 :           point_found = true;
     462       67356 :           outgoing_vals[i_pt] = {value, 0};
     463             :         }
     464      146023 :       }
     465             :     }
     466             : 
     467             :     // Extrapolation: only reached when no in-domain functor evaluation was found.
     468             :     //   flat:          return OutOfMeshValue; the base class post-transfer step handles
     469             :     //                  any user-specified constant or nearest-node fill on the target mesh
     470             :     //   evaluate_oob:  find the nearest boundary point via KD-tree, then evaluate the functor
     471             :     //                  there (out-of-bounds evaluation with a nullptr element)
     472             :     //   nearest-node / nearest-elem: delegate to the shared KD-tree method on the base class,
     473             :     //                  which also handles search_value_conflicts detection
     474      109290 :     if (!point_found)
     475             :     {
     476       41952 :       if (_extrapolation_behavior == 0) /*flat*/
     477             :         // The base class will take care of replacing the value
     478       37387 :         outgoing_vals[i_pt] = {GeneralFieldTransfer::OutOfMeshValue,
     479       74774 :                                GeneralFieldTransfer::OutOfMeshValue};
     480             : 
     481        4565 :       else if (_extrapolation_behavior == 1) /*evaluate_oob*/
     482           0 :         for (const auto i_source : make_range(_num_sources))
     483             :         {
     484           0 :           if (!checkRestrictionsForSource(pt, mesh_div, i_source))
     485           0 :             continue;
     486             : 
     487             :           // TODO: Pre-allocate these two work arrays. They will be regularly resized by the
     488             :           // searches
     489           0 :           std::vector<std::size_t> return_index(_num_nearest_points);
     490           0 :           std::vector<Real> return_dist_sqr(_num_nearest_points);
     491             : 
     492             :           // KD-tree neighbor search uses global pt (KD-trees store global coords);
     493             :           // functor evaluation needs the per-app local coordinate
     494           0 :           const auto first_app = getAppIndex(i_source, 0);
     495             :           const Point oob_local_pt =
     496           0 :               getPointInSourceAppFrame(pt, first_app, "Out-of-bounds functor extrapolation");
     497           0 :           const auto & functor = *_functors[first_app][var_index];
     498             : 
     499           0 :           if (_local_kdtrees[i_source]->numberCandidatePoints())
     500             :           {
     501           0 :             point_found = true;
     502           0 :             _local_kdtrees[i_source]->neighborSearch(
     503             :                 pt, _num_nearest_points, return_index, return_dist_sqr);
     504           0 :             Real dist_sum = 0;
     505           0 :             for (const auto index : return_index)
     506           0 :               dist_sum += (_local_points[i_source][index] - pt).norm();
     507             : 
     508           0 :             const auto new_distance = dist_sum / return_dist_sqr.size();
     509           0 :             if (new_distance < outgoing_vals[i_pt].second)
     510             :             {
     511           0 :               Moose::ElemPointArg elem_pt_arg = {nullptr, oob_local_pt, /*correct skewness*/ false};
     512           0 :               Moose::StateArg time_arg(0, Moose::SolutionIterationType::Time);
     513           0 :               outgoing_vals[i_pt] = {functor(elem_pt_arg, time_arg), new_distance};
     514             :             }
     515             :           }
     516           0 :         }
     517        6340 :       else if (_extrapolation_behavior == 2 /*nearest-node*/ ||
     518        1775 :                _extrapolation_behavior == 3 /*nearest-elem*/)
     519        4565 :         evaluateNearestNodeFromKDTrees(pt, mesh_div, outgoing_vals[i_pt], point_found);
     520             :       else
     521             :         mooseAssert(false,
     522             :                     "Unexpected extrapolation behavior '" << std::to_string(_extrapolation_behavior)
     523             :                                                           << "'");
     524             :     }
     525             : 
     526             :     // Move to next point
     527      109290 :     i_pt++;
     528             :   }
     529        1194 : }

Generated by: LCOV version 1.14