From f3ddb10458c3fec11bade2ce8ef3cb1db4d2a03b Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Sat, 5 Oct 2024 17:53:48 -0600 Subject: [PATCH 01/17] Optimize nearest position retrieval refs #23587 --- framework/include/positions/Positions.h | 5 +++ framework/src/positions/Positions.C | 44 +++++++++++-------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/framework/include/positions/Positions.h b/framework/include/positions/Positions.h index 35069bd4d1ec..e6dbce568d07 100644 --- a/framework/include/positions/Positions.h +++ b/framework/include/positions/Positions.h @@ -11,6 +11,7 @@ // Moose includes #include "GeneralReporter.h" +#include "KDTree.h" /** * Positions objects are under the hood Reporters. @@ -99,6 +100,10 @@ class Positions : public GeneralReporter /// 4D storage for all the positions : space & time std::vector>>> _positions_4d; + /// KDTree to be able to find the nearest position to a point in a fast and scalable manner + std::unique_ptr _initial_positions_kd_tree; + std::unique_ptr _positions_kd_tree; + /// Whether generation of positions is distributed or not (and therefore needs a broadcast) bool _need_broadcast; diff --git a/framework/src/positions/Positions.C b/framework/src/positions/Positions.C index 44c650074f6f..e0d0a825a2e2 100644 --- a/framework/src/positions/Positions.C +++ b/framework/src/positions/Positions.C @@ -50,6 +50,12 @@ Positions::Positions(const InputParameters & parameters) _need_sort(getParam("auto_sort")), _initialized(false) { + + if (_initial_positions) + { + auto initial_positions_copy = *_initial_positions; + _initial_positions_kd_tree = std::make_unique(initial_positions_copy, 1); + } } const Point & @@ -93,37 +99,22 @@ unsigned int Positions::getNearestPositionIndex(const Point & target, const bool initial) const { mooseAssert(initialized(initial), "Positions vector has not been initialized."); - const auto & positions = (initial && _initial_positions) ? *_initial_positions : _positions; - // To keep track of indetermination due to equidistant positions - std::pair conflict_index(-1, -1); + std::vector return_index(1); - // TODO Use faster & fancier machinery such as a KNN-partition - std::size_t nearest_index = 0; - auto nearest_distance_sq = std::numeric_limits::max(); - for (const auto i : index_range(positions)) + // Note that we do not need to use the transformed_pt (in the source app frame) + // because the KDTree has been created in the reference frame + if (initial) { - const auto & pt = positions[i]; - if (MooseUtils::absoluteFuzzyLessThan((pt - target).norm_sq(), nearest_distance_sq)) - { - nearest_index = i; - nearest_distance_sq = (pt - target).norm_sq(); - } - // Check that no two positions are equidistant to the target - else if (MooseUtils::absoluteFuzzyEqual((positions[i] - target).norm_sq(), nearest_distance_sq)) - conflict_index = std::make_pair(cast_int(i), cast_int(nearest_index)); + mooseAssert(_initial_positions_kd_tree, "Should have an initial positions KDTree"); + _initial_positions_kd_tree->neighborSearch(target, 1, return_index); } - - // If there were multiple "closest" positions, report a warning - if (cast_int(nearest_index) == conflict_index.second) + else { - mooseWarning("Search for nearest position found at least two matches: " + - Moose::stringify(positions[conflict_index.first]) + " and " + - Moose::stringify(positions[nearest_index]), - " for point " + Moose::stringify(target) + " at a distance of " + - std::to_string(std::sqrt(nearest_distance_sq))); + mooseAssert(_positions_kd_tree, "Should have a positions KDTree"); + _positions_kd_tree->neighborSearch(target, 1, return_index); } - return nearest_index; + return return_index[0]; } const std::vector & @@ -244,6 +235,9 @@ Positions::finalize() // Sort positions by X then Y then Z if (_need_sort) std::sort(_positions.begin(), _positions.end()); + + // Make a KDTree with the positions + _positions_kd_tree = std::make_unique(_positions, 1); } Real From 6d2805ffff1cd737f5f364cddfac405cadc10fba Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Sat, 5 Oct 2024 17:54:51 -0600 Subject: [PATCH 02/17] Add support for nearest-application mode in NL transfer: - only use the minimal number of KDTree (# local apps) rather than the number of positions (global #of apps) refs #23585 Apply suggestions from code review --- ...tiAppGeneralFieldNearestLocationTransfer.h | 13 +- ...tiAppGeneralFieldNearestLocationTransfer.C | 131 ++++++++++++------ 2 files changed, 100 insertions(+), 44 deletions(-) diff --git a/framework/include/transfers/MultiAppGeneralFieldNearestLocationTransfer.h b/framework/include/transfers/MultiAppGeneralFieldNearestLocationTransfer.h index 64a918a7a869..81c381b114a6 100644 --- a/framework/include/transfers/MultiAppGeneralFieldNearestLocationTransfer.h +++ b/framework/include/transfers/MultiAppGeneralFieldNearestLocationTransfer.h @@ -60,11 +60,12 @@ class MultiAppGeneralFieldNearestLocationTransfer : public MultiAppGeneralFieldT const std::vector> & incoming_points, std::vector> & outgoing_vals); + /// Pre-compute the number of sources /// Number of KDTrees used to hold the locations and variable value data - unsigned int getNumSources() const; + void computeNumSources(); - /// Transform a point towards the local frame - Point getPointInLocalSourceFrame(unsigned int i_from, const Point & pt) const; + /// Get the index of the app in the loop over the trees and the apps contributing data to each tree + unsigned int getAppIndex(unsigned int kdtree_index, unsigned int app_index) const; /// Number of applications which contributed nearest-locations to each KD-tree unsigned int getNumAppsPerTree() const; @@ -72,6 +73,9 @@ class MultiAppGeneralFieldNearestLocationTransfer : public MultiAppGeneralFieldT /// Number of divisions (nearest-positions or source mesh divisions) used when building KD-Trees unsigned int getNumDivisions() const; + /// Transform a point towards the local frame + Point getPointInLocalSourceFrame(unsigned int i_from, const Point & pt) const; + /** * @brief Examine all spatial restrictions that could preclude this source from being * a valid source for this point @@ -107,4 +111,7 @@ class MultiAppGeneralFieldNearestLocationTransfer : public MultiAppGeneralFieldT /// Whether we can just use the local zero-indexed dof to get the value from the solution std::vector _use_zero_dof_for_value; + + /// Number of KD-Trees to create + unsigned int _num_sources; }; diff --git a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C index be2f86aba1e7..ff7b38869ca2 100644 --- a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C @@ -42,9 +42,13 @@ MultiAppGeneralFieldNearestLocationTransfer::validParams() // Nearest node is historically more an extrapolation transfer params.set("extrapolation_constant") = GeneralFieldTransfer::BetterOutOfMeshValue; params.suppressParameter("extrapolation_constant"); - // We dont keep track of both point distance to app and to nearest node - params.set("use_nearest_app") = false; - params.suppressParameter("use_nearest_app"); + // We dont keep track of both point distance to app and to nearest node, so we cannot guarantee + // that the nearest app (among the local apps, not globally) will hold the nearest location. + // However, if the user knows this is true, we can use this heuristic to reduce the number of apps + // that are requested to provide a candidate value. If the user is wrong, then the nearest + // location is used, which can be from the non-nearest app. + params.set("use_nearest_app") = true; + params.renameParam("use_nearest_app", "assume_nearest_app_holds_nearest_location", ""); // the default of node/centroid switching based on the variable is causing lots of mistakes and // bad results @@ -57,7 +61,7 @@ MultiAppGeneralFieldNearestLocationTransfer::validParams() // mesh-divisions based transfers params.addParam("group_subapps", false, - "Whether to group data from subapps " + "Whether to group source locations and values from all subapps " "when considering a nearest-position algorithm"); return params; @@ -90,6 +94,15 @@ MultiAppGeneralFieldNearestLocationTransfer::MultiAppGeneralFieldNearestLocation paramError("group_subapps", "Cannot group subapps when considering nearest-location data as we would lose " "track of the division index of the source locations"); + else if (_group_subapps && _use_nearest_app) + paramError( + "group_subapps", + "When using the 'nearest child application' data, the source data (positions and values) " + "are grouped on a per-application basis, so it cannot be agglomerated over all child " + "applications.\nNote that the option to use nearest applications for source restrictions, " + "but further split each child application's domain by regions closest to each position " + "(here the the child application's centroid), which could be conceived when " + "'group_subapps' = false, is also not available."); } void @@ -205,23 +218,23 @@ MultiAppGeneralFieldNearestLocationTransfer::prepareEvaluationOfInterpValues( void MultiAppGeneralFieldNearestLocationTransfer::buildKDTrees(const unsigned int var_index) { - const auto num_sources = getNumSources(); + computeNumSources(); const auto num_apps_per_tree = getNumAppsPerTree(); - _local_kdtrees.resize(num_sources); - _local_points.resize(num_sources); - _local_values.resize(num_sources); + _local_kdtrees.resize(_num_sources); + _local_points.resize(_num_sources); + _local_values.resize(_num_sources); unsigned int max_leaf_size = 0; // Construct a local KDTree for each source. A source can be a single app or multiple apps // combined (option for nearest-position / mesh-divisions) - for (const auto i_source : make_range(num_sources)) + for (const auto i_source : make_range(_num_sources)) { // Nest a loop on apps in case multiple apps contribute to the same KD-Tree source for (const auto app_i : make_range(num_apps_per_tree)) { // Get the current app index - const auto i_from = _group_subapps ? app_i : (i_source / getNumDivisions()); - // Current position index, if using nearest positions + const auto i_from = getAppIndex(i_source, app_i); + // Current position index, if using nearest positions (not used for use_nearest_app) const auto i_pos = _group_subapps ? i_source : (i_source % getNumDivisions()); // Get access to the variable and some variable information @@ -277,7 +290,11 @@ MultiAppGeneralFieldNearestLocationTransfer::buildKDTrees(const unsigned int var // Only add to the KDTree nodes that are closest to the 'position' // When querying values at a target point, the KDTree associated to the closest // position to the target point is queried - if (_nearest_positions_obj && !closestToPosition(i_pos, transformed_node)) + // We do not need to check the positions when using nearest app as we will assume + // (somewhat incorrectly) that all the points in each subapp are closest to that subapp + // than to any other + if (!_use_nearest_app && _nearest_positions_obj && + !closestToPosition(i_pos, transformed_node)) continue; _local_points[i_source].push_back(transformed_node); @@ -329,7 +346,10 @@ MultiAppGeneralFieldNearestLocationTransfer::buildKDTrees(const unsigned int var const auto transformed_vertex_average = (*_from_transforms[getGlobalSourceAppIndex(i_from)])(vertex_average); - if (_nearest_positions_obj && !closestToPosition(i_pos, transformed_vertex_average)) + // We do not need to check the positions when using nearest app as we will assume + // (somewhat incorrectly) that all the points in each subapp are closest to that subapp + if (!_use_nearest_app && _nearest_positions_obj && + !closestToPosition(i_pos, transformed_vertex_average)) continue; _local_points[i_source].push_back(transformed_vertex_average); @@ -367,16 +387,14 @@ MultiAppGeneralFieldNearestLocationTransfer::evaluateInterpValuesNearestNode( std::vector> & outgoing_vals) { dof_id_type i_pt = 0; - for (auto & [pt, mesh_div] : incoming_points) + for (const auto & [pt, mesh_div] : incoming_points) { // Reset distance outgoing_vals[i_pt].second = std::numeric_limits::max(); bool point_found = false; - const unsigned int num_sources = getNumSources(); - // Loop on all sources - for (const auto i_from : make_range(num_sources)) + for (const auto i_from : make_range(_num_sources)) { // Examine all restrictions for the point. This source (KDTree+values) could be ruled out if (!checkRestrictionsForSource(pt, mesh_div, i_from)) @@ -420,7 +438,7 @@ MultiAppGeneralFieldNearestLocationTransfer::evaluateInterpValuesNearestNode( // point as the num_nearest_points + 1'th closest node unsigned int num_equidistant_problems = 0; - for (const auto i_from : make_range(num_sources)) + for (const auto i_from : make_range(_num_sources)) { // Examine all restrictions for the point. This source (KDTree+values) could be ruled out if (!checkRestrictionsForSource(pt, mesh_div, i_from)) @@ -514,41 +532,47 @@ MultiAppGeneralFieldNearestLocationTransfer::inBlocks(const std::setgetNumPositions(_fe_problem.getCurrentExecuteOnFlag() == - EXEC_INITIAL); + _num_sources = _nearest_positions_obj->getNumPositions(_fe_problem.getCurrentExecuteOnFlag() == + EXEC_INITIAL); // Regular case: 1 KDTree per app + // Also if use_nearest_app = true, the number of problems is better than the number of positions, + // because some of the positions are positions of child applications that are not local else - return _from_problems.size(); + _num_sources = _from_problems.size(); } -Point -MultiAppGeneralFieldNearestLocationTransfer::getPointInLocalSourceFrame(unsigned int i_from, - const Point & pt) const +unsigned int +MultiAppGeneralFieldNearestLocationTransfer::getAppIndex( + unsigned int kdtree_index, unsigned int nested_loop_on_app_index) const { - - if (!_nearest_positions_obj && - (!_from_transforms[getGlobalSourceAppIndex(i_from)]->hasCoordinateSystemTypeChange() || - _skip_coordinate_collapsing)) - return _from_transforms[getGlobalSourceAppIndex(i_from)]->mapBack(pt); - else if (!_nearest_positions_obj || !_group_subapps) - return pt - _from_positions[i_from]; + // Each app is mapped to a single KD Tree + if (_use_nearest_app) + return kdtree_index; + // We are looping over all the apps that are grouped together + else if (_group_subapps) + return nested_loop_on_app_index; + // There are num_divisions trees for each app, inner ordering is divisions, so dividing by the + // number of divisions gets us the index of the application else - return pt; + return kdtree_index / getNumDivisions(); } unsigned int MultiAppGeneralFieldNearestLocationTransfer::getNumAppsPerTree() const { - if (_group_subapps) + if (_use_nearest_app) + return 1; + else if (_group_subapps) return _from_meshes.size(); else return 1; @@ -557,25 +581,50 @@ MultiAppGeneralFieldNearestLocationTransfer::getNumAppsPerTree() const unsigned int MultiAppGeneralFieldNearestLocationTransfer::getNumDivisions() const { - if (_nearest_positions_obj && !_group_subapps) + // This is not used currently, but conceptually it is better to only divide the domain with the + // local of local applications rather than the global number of positions (# global applications + // here) + if (_use_nearest_app) + return _from_meshes.size(); + // Each nearest-position region is a division + else if (_nearest_positions_obj && !_group_subapps) return _nearest_positions_obj->getNumPositions(_fe_problem.getCurrentExecuteOnFlag() == EXEC_INITIAL); + // Assume all mesh divisions (on each sub-app) has the same number of divisions. This is checked else if (!_from_mesh_divisions.empty()) return _from_mesh_divisions[0]->getNumDivisions(); + // Grouping subapps or no special mode, we do not subdivide else return 1; } +Point +MultiAppGeneralFieldNearestLocationTransfer::getPointInLocalSourceFrame(unsigned int i_from, + const Point & pt) const +{ + + if (!_nearest_positions_obj && + (!_from_transforms[getGlobalSourceAppIndex(i_from)]->hasCoordinateSystemTypeChange() || + _skip_coordinate_collapsing)) + return _from_transforms[getGlobalSourceAppIndex(i_from)]->mapBack(pt); + else if (!_nearest_positions_obj || !_group_subapps) + return pt - _from_positions[i_from]; + else + return pt; +} + bool MultiAppGeneralFieldNearestLocationTransfer::checkRestrictionsForSource( const Point & pt, const unsigned int mesh_div, const unsigned int i_from) const { // Only use the KDTree from the closest position if in "nearest-position" mode - const auto i_pos = _group_subapps ? i_from : i_from % getNumDivisions(); - if (_nearest_positions_obj && !closestToPosition(i_pos, pt)) + const auto position_index = + (_group_subapps || _use_nearest_app) ? i_from : i_from % getNumDivisions(); + if (_nearest_positions_obj && !closestToPosition(position_index, pt)) return false; - const unsigned int app_index = i_from / getNumDivisions(); + // Application index depends on which source/grouping mode we are using + const unsigned int app_index = getAppIndex(i_from, i_from / getNumDivisions()); // Check mesh restriction before anything if (_source_app_must_contain_point) From 6e53e5b473ae4b221010b1b01a549d6fed47df2c Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Sat, 5 Oct 2024 17:55:43 -0600 Subject: [PATCH 03/17] Improve nearest-position check by only comparing index Add a special case bypassing bounding boxes for nearest-app mode to determine the source processors refs #28782 --- .../transfers/MultiAppGeneralFieldTransfer.C | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/framework/src/transfers/MultiAppGeneralFieldTransfer.C b/framework/src/transfers/MultiAppGeneralFieldTransfer.C index a054f43d1188..81ad59bfb95a 100644 --- a/framework/src/transfers/MultiAppGeneralFieldTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldTransfer.C @@ -123,7 +123,7 @@ MultiAppGeneralFieldTransfer::validParams() "use_nearest_position", "Name of the the Positions object (in main app) such that transfers to/from a child " "application will work by finding the nearest position to a target and query only the " - "app / points closer to this position than any other position for the value to transfer."); + "app / points closer to this position than to any other position for the value to transfer."); params.addParam( "from_app_must_contain_point", false, @@ -437,6 +437,8 @@ MultiAppGeneralFieldTransfer::getAppInfo() void MultiAppGeneralFieldTransfer::execute() { + TIME_SECTION( + "MultiAppGeneralFieldTransfer::execute()_" + name(), 5, "Transfer execution " + name()); getAppInfo(); // Set up bounding boxes, etc @@ -560,20 +562,43 @@ MultiAppGeneralFieldTransfer::locatePointReceivers(const Point point, bool found = false; // Additional process-restriction techniques we could use (TODOs): - // - nearest_positions/_app could use its own heuristic + // - create a heuristic for using nearest-positions // - from_mesh_divisions could be polled for which divisions they possess on each // process, depending on the behavior chosen. This could limit potential senders. // This should be done ahead of this function call, for all points at once - // Select the method for determining the apps receiving points/sending values - if (_use_bounding_boxes) + // Determine the apps which will be receiving points (then sending values) using various + // heuristics + if (_use_nearest_app) + { + // Find the nearest position for the point + const bool initial = _fe_problem.getCurrentExecuteOnFlag() == EXEC_INITIAL; + // The apps form the nearest positions here, this is the index of the nearest app + const auto nearest_index = _nearest_positions_obj->getNearestPositionIndex(point, initial); + + // Find the apps that are nearest to the same position + // Global search over all applications + unsigned int from0 = 0; + for (processor_id_type i_proc = 0; i_proc < n_processors(); + from0 += _froms_per_proc[i_proc], ++i_proc) + for (unsigned int i_from = from0; i_from < from0 + _froms_per_proc[i_proc]; ++i_from) + { + if (_greedy_search || i_from == nearest_index) + { + processors.insert(i_proc); + found = true; + } + } + mooseAssert(processors.size() == 1 || _greedy_search, "Should only be one nearest app"); + } + else if (_use_bounding_boxes) { - // We examine all bounding boxes and find the maximum distance within a bounding box - // from the point. This creates a sphere around the point of interest. Any app with a bounding - // box that intersects this sphere (with a bboxMinDistance < nearest_max_distance) will be - // considered a potential source. + // We examine all (global) bounding boxes and find the minimum of the maximum distances within a + // bounding box from the point. This creates a sphere around the point of interest. Any app + // with a bounding box that intersects this sphere (with a bboxMinDistance < + // nearest_max_distance) will be considered a potential source // NOTE: This is a heuristic. We could try others - // NOTE: from_bboxes are in the reference space, as is the point + // NOTE: from_bboxes are in the reference space, as is the point. Real nearest_max_distance = std::numeric_limits::max(); for (const auto & bbox : _from_bboxes) { @@ -640,7 +665,12 @@ MultiAppGeneralFieldTransfer::locatePointReceivers(const Point point, // Error out if we could not find this point when ask us to do so if (!found && _error_on_miss) - mooseError("Cannot locate point ", point, " \n ", "mismatched meshes are used"); + mooseError( + "Cannot find a source application to provide a value at point: ", + point, + " \n ", + "It must be that mismatched matches are used.\nIf you are using bounding boxes, " + "nearest-app or mesh-divisions, please consider using the greedy_search to confirm."); } void @@ -677,7 +707,7 @@ MultiAppGeneralFieldTransfer::cacheOutgoingPointInfo(const Point point, outgoing_points[pid].push_back(std::pair(point, required_source_division)); // Store point information locally for processing received data - // We can use these information when insert values to solution vector + // We can use these information when inserting values into the solution vector PointInfo pointinfo; pointinfo.problem_id = problem_id; pointinfo.dof_object_id = dof_object_id; @@ -891,7 +921,9 @@ MultiAppGeneralFieldTransfer::cacheIncomingInterpVals( MooseUtils::absoluteFuzzyEqual(distance_cache[p], incoming_vals[val_offset].second)) registerConflict(problem_id, dof_object_id, p, incoming_vals[val_offset].second, false); - if ((!GeneralFieldTransfer::isBetterOutOfMeshValue(val) || _use_nearest_app) && + // if we use the nearest app, even if the value is bad we want to save the distance because + // it's the distance to the app, if that's the closest app then so be it with the bad value + if ((!GeneralFieldTransfer::isBetterOutOfMeshValue(val)) && MooseUtils::absoluteFuzzyGreaterThan(distance_cache[p], incoming_vals[val_offset].second)) { // NOTE: We store the distance as well as the value. We really only need the @@ -962,7 +994,7 @@ MultiAppGeneralFieldTransfer::cacheIncomingInterpVals( } // We adopt values that are, in order of priority - // - valid + // - valid (or from nearest app) // - closest distance // - the smallest rank with the same distance if (!GeneralFieldTransfer::isBetterOutOfMeshValue(incoming_vals[val_offset].first) && @@ -1692,8 +1724,7 @@ MultiAppGeneralFieldTransfer::closestToPosition(unsigned int pos_index, const Po if (!_skip_coordinate_collapsing) paramError("skip_coordinate_collapsing", "Coordinate collapsing not implemented"); bool initial = _fe_problem.getCurrentExecuteOnFlag() == EXEC_INITIAL; - return _nearest_positions_obj->getPosition(pos_index, initial) == - _nearest_positions_obj->getNearestPosition(pt, initial); + return pos_index == _nearest_positions_obj->getNearestPositionIndex(pt, initial); } Real From fc1b319838f38e42acf5c18797ee3f6833caf5e7 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Sat, 5 Oct 2024 18:46:57 -0600 Subject: [PATCH 04/17] Avoid copying initial positions reporter vector --- framework/include/utils/KDTree.h | 2 +- framework/src/positions/Positions.C | 5 +---- framework/src/utils/KDTree.C | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/framework/include/utils/KDTree.h b/framework/include/utils/KDTree.h index 993132c25e05..b808f37ae98e 100644 --- a/framework/include/utils/KDTree.h +++ b/framework/include/utils/KDTree.h @@ -28,7 +28,7 @@ using ResultItem = std::pair; class KDTree { public: - KDTree(std::vector & master_points, unsigned int max_leaf_size); + KDTree(const std::vector & master_points, unsigned int max_leaf_size); virtual ~KDTree() = default; diff --git a/framework/src/positions/Positions.C b/framework/src/positions/Positions.C index e0d0a825a2e2..41b76774affc 100644 --- a/framework/src/positions/Positions.C +++ b/framework/src/positions/Positions.C @@ -52,10 +52,7 @@ Positions::Positions(const InputParameters & parameters) { if (_initial_positions) - { - auto initial_positions_copy = *_initial_positions; - _initial_positions_kd_tree = std::make_unique(initial_positions_copy, 1); - } + _initial_positions_kd_tree = std::make_unique(*_initial_positions, 1); } const Point & diff --git a/framework/src/utils/KDTree.C b/framework/src/utils/KDTree.C index 3b3e6ab15ebc..10e00b423929 100644 --- a/framework/src/utils/KDTree.C +++ b/framework/src/utils/KDTree.C @@ -24,7 +24,7 @@ using ResultItem = std::pair; } #endif -KDTree::KDTree(std::vector & master_points, unsigned int max_leaf_size) +KDTree::KDTree(const std::vector & master_points, unsigned int max_leaf_size) : _point_list_adaptor(master_points.begin(), master_points.end()), _kd_tree(std::make_unique( LIBMESH_DIM, _point_list_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(max_leaf_size))) From ceafbcf798639c1fcc82393f467efee45734f392 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Sun, 6 Oct 2024 10:12:19 -0600 Subject: [PATCH 05/17] Add an automatic, but possibly constrained method for the GridPartitioner Avoid ignoring user parameters when they dont match the number of MPI closes #28787 --- framework/src/partitioner/GridPartitioner.C | 82 ++++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/framework/src/partitioner/GridPartitioner.C b/framework/src/partitioner/GridPartitioner.C index 40903a3d9d40..570bbc07a004 100644 --- a/framework/src/partitioner/GridPartitioner.C +++ b/framework/src/partitioner/GridPartitioner.C @@ -22,14 +22,23 @@ registerMooseObject("MooseApp", GridPartitioner); InputParameters GridPartitioner::validParams() { - // These two are in this order because they are from different systems - // so you have to apply _this_ system's second to override the base InputParameters params = MoosePartitioner::validParams(); + MooseEnum method("manual automatic", "manual"); + params.addParam( + "grid_computation", + method, + "Whether to determine the grid manually (using nx, ny and nz) or automatically. When using " + "the automatic mode, the user can impose a certain value for nx, ny or nz, and the automatic " + "factorization will adjust the number of processors in the other directions."); + // Users specify how many processors they need along each direction - params.addParam("nx", 1, "Number of processors in the X direction"); - params.addParam("ny", 1, "Number of processors in the Y direction"); - params.addParam("nz", 1, "Number of processors in the Z direction"); + params.addParam( + "nx", "Number of processors in the X direction. Defaults to 1 in manual mode"); + params.addParam( + "ny", "Number of processors in the Y direction. Defaults to 1 in manual mode"); + params.addParam( + "nz", "Number of processors in the Z direction. Defaults to 1 in manual mode"); params.addClassDescription("Create a uniform grid that overlays the mesh to be partitioned. " "Assign all elements within each cell of the grid to the same " @@ -66,22 +75,57 @@ GridPartitioner::_do_partition(MeshBase & mesh, const unsigned int /*n*/) const auto & max = bounding_box.max(); auto dim = mesh.mesh_dimension(); - // Need to make sure the number of cells in the grid matches the number of procs to partition for - nx = getParam("nx"); + // Gather user parameters + nx = isParamValid("nx") ? getParam("nx") : nx; if (dim >= 2) - ny = getParam("ny"); - + ny = isParamValid("ny") ? getParam("ny") : ny; if (dim == 3) - nz = getParam("nz"); + nz = isParamValid("nz") ? getParam("nz") : nz; + + // error on unused parameters + if (dim < 2 && isParamValid("ny") && getParam("ny") > 1) + paramError("ny", "Should not be specified for a mesh of dimension less than 2."); + if (dim < 3 && isParamValid("nz") && getParam("nz") > 1) + paramError("nz", "Should not be specified for a mesh of dimension less than 3."); + + // User parameters, which should match the number of partitions needed + if (getParam("grid_computation") == "manual") + { + // Need to make sure the number of grid cells matches the number of procs to partition for + if ((nx * ny * nz) != mesh.n_partitions()) + mooseError("Number of grid cells (" + std::to_string(nx * ny * nz) + + ") does not match the number of MPI processes (" + + std::to_string(mesh.n_partitions()) + ")"); + } - // We should compute a balanced factorization so - // that we can assign proper processors to each direction. - // I just want to make grid partitioner smarter. - if ((nx * ny * nz) != mesh.n_partitions()) + else if (getParam("grid_computation") == "automatic") { - // Anybody knows we are living in a 3D space. - int dims[] = {0, 0, 0}; + // remove over-constraint and tell user + if (nx * ny * nz > mesh.n_partitions()) + { + nx = 0; + ny = 0; + nz = 0; + paramWarning("grid_computation", + "User specified (nx,ny,nz) grid exceeded number of partitions, these parameters " + "will be ignored."); + } + if (nx > 0 && ny > 0 && nz > 0 && nx * ny * nz != mesh.n_partitions()) + { + nx = 0; + ny = 0; + nz = 0; + paramWarning("grid_computation", + "User specified (nx,ny,nz) grid do not match the number of partitions and " + "constrain the grid partitioner in every direction, these parameters " + "will be ignored."); + } + // 0 means no restriction on which number to choose + int dims[] = {isParamValid("nx") ? int(nx) : 0, + isParamValid("ny") ? int(ny) : 0, + isParamValid("nz") ? int(nz) : 0}; + // This will error if the factorization is not possible MPI_Dims_create(mesh.n_partitions(), dim, dims); nx = dims[0]; @@ -147,9 +191,9 @@ GridPartitioner::_do_partition(MeshBase & mesh, const unsigned int /*n*/) if (dim == 3) k = (coordz - min(2)) / hz; - mooseAssert(i < nx, "Index caculation is wrong along x direction"); - mooseAssert(j < ny, "Index caculation is wrong along y direction"); - mooseAssert(k < nz, "Index caculation is wrong along z direction"); + mooseAssert(i < nx, "Index calculation is wrong along x direction"); + mooseAssert(j < ny || ny == 0, "Index calculation is wrong along y direction"); + mooseAssert(k < nz || nz == 0, "Index calculation is wrong along z direction"); // Assign processor ID to current element elem_ptr->processor_id() = k * nx * ny + j * nx + i; } From b1abdf7caa9eda8941d3e9e866cb7a0049daa862 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Sun, 6 Oct 2024 11:09:09 -0600 Subject: [PATCH 06/17] Make hierarchical grid partitioner behavior similar to grid partitioner - add an automatic option, for each level - make sure we can constrain the selections --- .../partitioner/HierarchicalGridPartitioner.h | 19 ++- .../partitioner/HierarchicalGridPartitioner.C | 132 +++++++++++++----- 2 files changed, 113 insertions(+), 38 deletions(-) diff --git a/framework/include/partitioner/HierarchicalGridPartitioner.h b/framework/include/partitioner/HierarchicalGridPartitioner.h index a76d6cf5ad84..b96290a7209a 100644 --- a/framework/include/partitioner/HierarchicalGridPartitioner.h +++ b/framework/include/partitioner/HierarchicalGridPartitioner.h @@ -33,12 +33,19 @@ class HierarchicalGridPartitioner : public MoosePartitioner protected: virtual void _do_partition(MeshBase & mesh, const unsigned int n) override; + /// Mesh to partition MooseMesh & _mesh; - const unsigned int _nx_nodes; - const unsigned int _ny_nodes; - const unsigned int _nz_nodes; - const unsigned int _nx_procs; - const unsigned int _ny_procs; - const unsigned int _nz_procs; + /// Number of nodes in the X direction + unsigned int _nx_nodes; + /// Number of nodes in the Y direction + unsigned int _ny_nodes; + /// Number of nodes in the Z direction + unsigned int _nz_nodes; + /// Number of processors on each node in the X direction + unsigned int _nx_procs; + /// Number of processors on each node in the Y direction + unsigned int _ny_procs; + /// Number of processors on each node in the Z direction + unsigned int _nz_procs; }; diff --git a/framework/src/partitioner/HierarchicalGridPartitioner.C b/framework/src/partitioner/HierarchicalGridPartitioner.C index ca21d5b24d81..58714170d444 100644 --- a/framework/src/partitioner/HierarchicalGridPartitioner.C +++ b/framework/src/partitioner/HierarchicalGridPartitioner.C @@ -27,19 +27,43 @@ HierarchicalGridPartitioner::validParams() { auto params = MoosePartitioner::validParams(); - params.addRequiredRangeCheckedParam( - "nx_nodes", "nx_nodes > 0", "Number of compute nodes in the X direction"); + // Options for automatic grid computations + MooseEnum method("manual automatic", "manual"); + params.addParam( + "nodes_grid_computation", + method, + "Whether to determine the compute node grid manually (using nx_nodes, ny_nodes and nz_nodes) " + "or automatically. When using the automatic mode, the user can impose a certain value for " + "nx, ny or nz, and the automatic factorization will adjust the number of processors in the " + "other directions."); + params.addParam( + "number_nodes", "Number of nodes. Used for determining the node grid automatically"); + params.addParam( + "processors_grid_computation", + method, + "Whether to determine the processors grid on each node manually (using nx_procs, ny_procs " + "and nz_procs) or automatically. When using the automatic mode, the user can impose a " + "certain value for nx, ny or nz, and the automatic factorization will adjust the number of " + "processors in the other directions."); + params.addParam("number_procs_per_node", + "Number of processors per node. Used for determining the processor " + "grid on each node automatically"); + + // Node grid params.addRangeCheckedParam( - "ny_nodes", 0, "ny_nodes >= 0", "Number of compute nodes in the Y direction"); + "nx_nodes", "nx_nodes >= 1", "Number of compute nodes in the X direction"); params.addRangeCheckedParam( - "nz_nodes", 0, "nz_nodes >= 0", "Number of compute nodes in the Z direction"); + "ny_nodes", "ny_nodes >= 1", "Number of compute nodes in the Y direction"); + params.addRangeCheckedParam( + "nz_nodes", "nz_nodes >= 1", "Number of compute nodes in the Z direction"); - params.addRequiredRangeCheckedParam( - "nx_procs", "nx_procs > 0", "Number of processors in the X direction within each node"); + // Processor grid on each node + params.addRangeCheckedParam( + "nx_procs", "nx_procs >= 1", "Number of processors in the X direction within each node"); params.addRangeCheckedParam( - "ny_procs", 0, "ny_procs >= 0", "Number of processors in the Y direction within each node"); + "ny_procs", "ny_procs >= 1", "Number of processors in the Y direction within each node"); params.addRangeCheckedParam( - "nz_procs", 0, "nz_procs >= 0", "Number of processors in the Z direction within each node"); + "nz_procs", "nz_procs >= 1", "Number of processors in the Z direction within each node"); params.addClassDescription("Partitions a mesh into sub-partitions for each computational node" " then into partitions within that node. All partitions are made" @@ -49,14 +73,7 @@ HierarchicalGridPartitioner::validParams() } HierarchicalGridPartitioner::HierarchicalGridPartitioner(const InputParameters & params) - : MoosePartitioner(params), - _mesh(*getCheckedPointerParam("mesh")), - _nx_nodes(getParam("nx_nodes")), - _ny_nodes(getParam("ny_nodes")), - _nz_nodes(getParam("nz_nodes")), - _nx_procs(getParam("nx_procs")), - _ny_procs(getParam("ny_procs")), - _nz_procs(getParam("nz_procs")) + : MoosePartitioner(params), _mesh(*getCheckedPointerParam("mesh")) { } @@ -72,36 +89,87 @@ void HierarchicalGridPartitioner::_do_partition(MeshBase & mesh, const unsigned int /*n*/) { const auto dim = mesh.spatial_dimension(); - if (_ny_procs == 0 && dim > 1) - paramError("ny_procs", "Required for ", dim, "D meshes"); - if (_ny_nodes == 0 && dim > 1) - paramError("ny_nodes", "Required for ", dim, "D meshes"); - if (_nz_procs == 0 && dim == 3) - paramError("nz_procs", "Required for 3D meshes"); - if (_nz_nodes == 0 && dim == 3) - paramError("nz_nodes", "Required for 3D meshes"); - auto total_nodes = _nx_nodes; + // Process user parameters + _nx_nodes = isParamValid("nx_nodes") ? getParam("nx_nodes") : 0; + _ny_nodes = isParamValid("ny_nodes") ? getParam("ny_nodes") : 0; + _nz_nodes = isParamValid("nz_nodes") ? getParam("nz_nodes") : 0; + _nx_procs = isParamValid("nx_procs") ? getParam("nx_procs") : 0; + _ny_procs = isParamValid("ny_procs") ? getParam("ny_procs") : 0; + _nz_procs = isParamValid("nz_procs") ? getParam("nz_procs") : 0; + + if (getParam("nodes_grid_computation") == "manual") + { + if (_nx_nodes == 0) + paramError("nx_nodes", "Required for manual nodes grid specification"); + if (_ny_nodes == 0 && dim > 1) + paramError("ny_nodes", "Required for ", dim, "D meshes"); + if (_nz_nodes == 0 && dim == 3) + paramError("nz_nodes", "Required for 3D meshes"); + } + else + { + if (!isParamValid("number_nodes")) + paramError("number_nodes", "Required for automatic nodes grid computation"); + // 0 means no restriction on which number to choose + int dims[] = {int(_nx_nodes), int(_ny_nodes), int(_nz_nodes)}; + // This will error if the factorization is not possible + MPI_Dims_create(getParam("number_nodes"), dim, dims); + + _nx_nodes = dims[0]; + _ny_nodes = (dim >= 2) ? dims[1] : 0; + _nz_nodes = (dim == 3) ? dims[2] : 0; + } + if (getParam("processors_grid_computation") == "manual") + { + if (_nx_procs == 0) + paramError("nx_procs", "Required for manual processors grid specification"); + if (_ny_procs == 0 && dim > 1) + paramError("ny_procs", "Required for ", dim, "D meshes"); + if (_nz_procs == 0 && dim == 3) + paramError("nz_procs", "Required for 3D meshes"); + } + else + { + if (!isParamValid("number_procs_per_node")) + paramError("number_procs_per_node", "Required for automatic processors grid computation"); + // 0 means no restriction on which number to choose + int dims[] = {int(_nx_procs), int(_ny_procs), int(_nz_procs)}; + // This will error if the factorization is not possible + MPI_Dims_create(getParam("number_procs_per_node"), dim, dims); + + _nx_procs = dims[0]; + _ny_procs = (dim >= 2) ? dims[1] : 0; + _nz_procs = (dim == 3) ? dims[2] : 0; + } + + // Sanity checks on both grids + auto total_nodes = _nx_nodes; if (mesh.spatial_dimension() >= 2) total_nodes *= _ny_nodes; - if (mesh.spatial_dimension() == 3) total_nodes *= _nz_nodes; + if (isParamValid("number_nodes") && total_nodes != getParam("number_nodes")) + paramError("number_nodes", + "Computed number of nodes (" + std::to_string(total_nodes) + ") does not match"); auto procs_per_node = _nx_procs; - if (mesh.spatial_dimension() >= 2) procs_per_node *= _ny_procs; - if (mesh.spatial_dimension() == 3) procs_per_node *= _nz_procs; + if (isParamValid("number_procs_per_node") && + procs_per_node != getParam("number_procs_per_node")) + paramError("number_procs_per_node", + "Computed number of processors per node (" + std::to_string(procs_per_node) + + ") does not match"); if (procs_per_node * total_nodes != mesh.n_partitions()) - mooseError("Partitioning in ", - name(), - " doesn't add up to the total number of processors: ", - procs_per_node * total_nodes); + mooseError("Partitioning creates ", + procs_per_node * total_nodes, + " partitions, which does not add up to the total number of processors: ", + mesh.n_partitions()); // Figure out the physical bounds of the given mesh auto nodes_bounding_box = MeshTools::create_bounding_box(mesh); From 24cfba287e531020b637358529e36c988037bd03 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Thu, 5 Dec 2024 16:29:56 -0700 Subject: [PATCH 07/17] Add tests for grid partitioner automatic mode + exceptions --- framework/src/partitioner/GridPartitioner.C | 7 +-- test/tests/bcs/periodic/tests | 2 +- .../custom_partition_generated_mesh.i | 2 + .../tests/partitioners/grid_partitioner/tests | 51 +++++++++++++++++-- test/tests/reporters/mesh_info/mesh_info.i | 3 +- .../point_value_sampler/tests | 2 +- 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/framework/src/partitioner/GridPartitioner.C b/framework/src/partitioner/GridPartitioner.C index 570bbc07a004..a60d42533aac 100644 --- a/framework/src/partitioner/GridPartitioner.C +++ b/framework/src/partitioner/GridPartitioner.C @@ -83,11 +83,12 @@ GridPartitioner::_do_partition(MeshBase & mesh, const unsigned int /*n*/) if (dim == 3) nz = isParamValid("nz") ? getParam("nz") : nz; - // error on unused parameters + // simple info message unused parameters as this can be normal: we could be partitioning + // a 2D mesh before extruding it to 3D. The nz parameter is needed for the 3D mesh if (dim < 2 && isParamValid("ny") && getParam("ny") > 1) - paramError("ny", "Should not be specified for a mesh of dimension less than 2."); + paramInfo("ny", "Parameter ignored as mesh is currently of dimension less than 2."); if (dim < 3 && isParamValid("nz") && getParam("nz") > 1) - paramError("nz", "Should not be specified for a mesh of dimension less than 3."); + paramInfo("nz", "Parameter ignored as mesh is currently of dimension less than 3."); // User parameters, which should match the number of partitions needed if (getParam("grid_computation") == "manual") diff --git a/test/tests/bcs/periodic/tests b/test/tests/bcs/periodic/tests index a33f0d450fec..3f885856f110 100644 --- a/test/tests/bcs/periodic/tests +++ b/test/tests/bcs/periodic/tests @@ -43,7 +43,7 @@ abs_zero = 1e-6 # This test is very sensitive to partitioner (parmetis). # GridPartitioner is robust and beautiful - cli_args = 'Mesh/Partitioner/type=GridPartitioner' + cli_args = 'Mesh/Partitioner/type=GridPartitioner Mesh/Partitioner/grid_computation=automatic' requirement = "The system shall support periodic boundary conditions with transforms that are computed automatically in the 'x', 'y', and 'z' directions." [] diff --git a/test/tests/partitioners/custom_partition_generated_mesh/custom_partition_generated_mesh.i b/test/tests/partitioners/custom_partition_generated_mesh/custom_partition_generated_mesh.i index f683297e9532..589964718cb3 100644 --- a/test/tests/partitioners/custom_partition_generated_mesh/custom_partition_generated_mesh.i +++ b/test/tests/partitioners/custom_partition_generated_mesh/custom_partition_generated_mesh.i @@ -16,6 +16,8 @@ nx = 1 ny = 1 nz = 4 + # So we can use the same partitioner for the 2D and 3D mesh + grid_computation = automatic [] [] diff --git a/test/tests/partitioners/grid_partitioner/tests b/test/tests/partitioners/grid_partitioner/tests index 1792a129211f..d6d5f06127ab 100644 --- a/test/tests/partitioners/grid_partitioner/tests +++ b/test/tests/partitioners/grid_partitioner/tests @@ -1,12 +1,55 @@ [Tests] - [./test] - requirement = 'The system shall provide a simple regular grid-based partitioner' - design = '/GridPartitioner.md' + design = '/GridPartitioner.md' + [test] + type = 'Exodiff' + input = 'grid_partitioner.i' + exodiff = 'grid_partitioner_out.e' + min_parallel = 4 + max_parallel = 4 issues = '#11437' + requirement = 'The system shall provide a simple regular grid-based partitioner' + [] + [automatic_with_constraint] type = 'Exodiff' input = 'grid_partitioner.i' exodiff = 'grid_partitioner_out.e' + # Same output name as 'test' + prereq = 'test' + recover = false + cli_args = 'Mesh/Partitioner/grid_computation=automatic Mesh/Partitioner/nx=2 Mesh/Partitioner/ny=0 Mesh/Partitioner/nz=0' + allow_warnings = true min_parallel = 4 max_parallel = 4 - [../] + issues = '#28787' + requirement = 'The system shall be able to create a simple rectangular grid partitioning automatically.' + [] + + [exceptions] + requirement = "The system shall throw an error if" + issues = '#11437 #28787' + [wrong_size] + type = RunException + input = 'grid_partitioner.i' + expect_err = 'Number of grid cells \(10\) does not match the number of MPI processes' + cli_args = 'Mesh/Partitioner/nx=5' + min_parallel = 2 + max_parallel = 9 + detail = 'the user-specified grid does not match the number of processes.' + [] + [] + + [warnings] + requirement = "The system shall emit a warning if" + issues = '#28787' + [override_in_automatic] + type = 'RunApp' + input = 'grid_partitioner.i' + cli_args = 'Mesh/Partitioner/grid_computation=automatic Mesh/Partitioner/nx=5' + expect_out = "User specified \(nx,ny,nz\) grid exceeded number of partition" + allow_warnings = true + min_parallel = 4 + max_parallel = 4 + detail = 'the user provides a grid with more partitions than processors in the automatic model of the grid-based partitioner.' + [] + [] [] diff --git a/test/tests/reporters/mesh_info/mesh_info.i b/test/tests/reporters/mesh_info/mesh_info.i index 73b2fecf8edc..ddb59a572b83 100644 --- a/test/tests/reporters/mesh_info/mesh_info.i +++ b/test/tests/reporters/mesh_info/mesh_info.i @@ -11,8 +11,7 @@ # For consistent partitioning across platforms [Partitioner] type = GridPartitioner - nx = 2 - ny = 1 + grid_computation = 'automatic' [] [] diff --git a/test/tests/vectorpostprocessors/point_value_sampler/tests b/test/tests/vectorpostprocessors/point_value_sampler/tests index 969a047d3c3f..6fdd5ad16da6 100644 --- a/test/tests/vectorpostprocessors/point_value_sampler/tests +++ b/test/tests/vectorpostprocessors/point_value_sampler/tests @@ -75,7 +75,7 @@ type = 'CSVDiff' input = 'point_value_sampler_fv.i' csvdiff = 'sampler_discontinuous_process_boundaries_point_sample_0001.csv' - cli_args = "Mesh/Partitioner/type=GridPartitioner Mesh/Partitioner/nx=2 Mesh/Partitioner/ny=2 VectorPostprocessors/point_sample/points='0.5 0.5 0' Outputs/file_base=sampler_discontinuous_process_boundaries" + cli_args = "Mesh/Partitioner/type=GridPartitioner Mesh/Partitioner/nx=2 VectorPostprocessors/point_sample/points='0.5 0.5 0' Outputs/file_base=sampler_discontinuous_process_boundaries" detail = 'on process domain boundaries.' min_parallel = 2 max_parallel = 2 From ad9a7c5beb0a603ffdc648c283e325dfcee4683d Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Thu, 5 Dec 2024 18:06:19 -0700 Subject: [PATCH 08/17] Add tests for hierarchical grid partitioner automated modes + exceptions refs #28787 --- .../hierarchical_grid_partitioner_errors.i | 55 +++++++++ .../hierarchical_grid_partitioner/tests | 110 +++++++++++++++++- 2 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 test/tests/partitioners/hierarchical_grid_partitioner/hierarchical_grid_partitioner_errors.i diff --git a/test/tests/partitioners/hierarchical_grid_partitioner/hierarchical_grid_partitioner_errors.i b/test/tests/partitioners/hierarchical_grid_partitioner/hierarchical_grid_partitioner_errors.i new file mode 100644 index 000000000000..fac7f8d98c6a --- /dev/null +++ b/test/tests/partitioners/hierarchical_grid_partitioner/hierarchical_grid_partitioner_errors.i @@ -0,0 +1,55 @@ +[Mesh] + type = GeneratedMesh + dim = 2 + nx = 8 + ny = 8 + [Partitioner] + type = HierarchicalGridPartitioner + [] +[] + +[Variables/u] +[] + +[Kernels/diff] + type = Diffusion + variable = u +[] + +[BCs] + [left] + type = DirichletBC + variable = u + boundary = 'left' + value = 0 + [] + [right] + type = DirichletBC + variable = u + boundary = 'right' + value = 1 + [] +[] + +[Executioner] + type = Steady +[] + +[Outputs] + exodus = true +[] + +[AuxVariables/pid] + family = MONOMIAL + order = CONSTANT +[] + +[Problem] + solve = false +[] + +[AuxKernels/pid] + type = ProcessorIDAux + variable = pid + execute_on = 'INITIAL' +[] diff --git a/test/tests/partitioners/hierarchical_grid_partitioner/tests b/test/tests/partitioners/hierarchical_grid_partitioner/tests index e2797e037600..60fbd82c7023 100644 --- a/test/tests/partitioners/hierarchical_grid_partitioner/tests +++ b/test/tests/partitioners/hierarchical_grid_partitioner/tests @@ -1,14 +1,118 @@ [Tests] - [./test] + design = 'HierarchicalGridPartitioner.md' + [test] type = 'Exodiff' input = 'hierarchical_grid_partitioner.i' exodiff = 'hierarchical_grid_partitioner_out.e' requirement = "The system shall have the ability to do hierarchical partitioning based on a regular grid." - design = 'HierarchicalGridPartitioner.md' issues = '#12531' min_parallel = 16 max_parallel = 16 - [../] + [] + [automatic_procs_grid] + type = 'Exodiff' + input = 'hierarchical_grid_partitioner.i' + exodiff = 'hierarchical_grid_partitioner_out.e' + cli_args = 'Mesh/Partitioner/processors_grid_computation=automatic Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/number_procs_per_node=4' + + requirement = "The system shall have the ability to do hierarchical partitioning based on a regular grid with an automatic selection of the within-node processor grid." + issues = '#12531' + + min_parallel = 16 + max_parallel = 16 + [] + [automatic_nodes_grid] + type = 'Exodiff' + input = 'hierarchical_grid_partitioner.i' + exodiff = 'hierarchical_grid_partitioner_out.e' + cli_args = 'Mesh/Partitioner/nodes_grid_computation=automatic Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/number_procs_per_node=4' + + requirement = "The system shall have the ability to do hierarchical partitioning based on a regular grid with an automatic selection of the node-based grid." + issues = '#12531' + + min_parallel = 16 + max_parallel = 16 + [] + + [exceptions] + requirement = 'The system shall throw an error if' + issues = '#12531 #28787' + [missing_nx_procs] + type = 'RunException' + input = 'hierarchical_grid_partitioner_errors.i' + expect_err = 'Required for manual processors grid specification' + cli_args = 'Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/nodes_grid_computation=automatic' + detail = 'the number of processors in the X-direction for a 1D manual grid mesh partition is missing,' + min_parallel = 2 + [] + [missing_ny_procs] + type = 'RunException' + input = 'hierarchical_grid_partitioner_errors.i' + expect_err = 'ny_procs: Required for 2D meshes' + cli_args = 'Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/nodes_grid_computation=automatic Mesh/Partitioner/nx_procs=2' + detail = 'the number of processors in the Y-direction for a 2D manual grid mesh partition is missing,' + min_parallel = 2 + [] + [missing_nz_procs] + type = 'RunException' + input = 'hierarchical_grid_partitioner_errors.i' + expect_err = 'Required for 3D meshes' + cli_args = 'Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/nodes_grid_computation=automatic Mesh/dim=3 Mesh/nz=1 Mesh/Partitioner/nx_procs=2 Mesh/Partitioner/ny_procs=2' + detail = 'the number of processors in the Z-direction for a 3D manual grid mesh partition is missing,' + min_parallel = 2 + [] + [missing_nx_nodes] + type = 'RunException' + input = 'hierarchical_grid_partitioner_errors.i' + expect_err = 'Required for manual nodes grid specification' + cli_args = 'Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/processors_grid_computation=automatic' + detail = 'the number of nodes in the X-direction for a 1D manual grid mesh partition is missing,' + min_parallel = 2 + [] + [missing_ny_nodes] + type = 'RunException' + input = 'hierarchical_grid_partitioner_errors.i' + expect_err = 'ny_nodes: Required for 2D meshes' + cli_args = 'Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/processors_grid_computation=automatic Mesh/Partitioner/nx_nodes=2' + detail = 'the number of nodes in the Y-direction for a 2D manual grid mesh partition is missing,' + min_parallel = 2 + [] + [missing_nz_nodes] + type = 'RunException' + input = 'hierarchical_grid_partitioner_errors.i' + expect_err = 'Required for 3D meshes' + cli_args = 'Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/processors_grid_computation=automatic Mesh/dim=3 Mesh/Partitioner/nx_nodes=2 Mesh/Partitioner/ny_nodes=2' + detail = 'the number of nodes in the Z-direction for a 3D manual grid mesh partition is missing,' + min_parallel = 2 + [] + [wrong_manual_procs_partitioning] + type = 'RunException' + input = 'hierarchical_grid_partitioner.i' + expect_err = 'Computed number of processors per node \(4\) does not match' + detail = 'the number of partitions for a manual hierarchical grid partitioning does not match the number of processes,' + cli_args = 'Mesh/Partitioner/number_procs_per_node=8' + min_parallel = 8 + max_parallel = 8 + [] + [wrong_manual_nodes_partitioning] + type = 'RunException' + input = 'hierarchical_grid_partitioner.i' + expect_err = 'Computed number of nodes \(20\) does not match' + detail = 'the number of partitions for a manual hierarchical grid node partitioning does not match the number of nodes,' + cli_args = 'Mesh/Partitioner/number_nodes=8 Mesh/Partitioner/nx_nodes=10' + min_parallel = 8 + max_parallel = 8 + [] + [wrong_numbers] + type = 'RunException' + input = 'hierarchical_grid_partitioner.i' + expect_err = "Partitioning creates 16 partitions, which does not add up to the total number of processors: 8" + cli_args = 'Mesh/Partitioner/number_nodes=4 Mesh/Partitioner/number_procs_per_node=4 Mesh/Partitioner/processors_grid_computation=automatic Mesh/Partitioner/nodes_grid_computation=automatic Mesh/Partitioner/nx=12' + min_parallel = 8 + max_parallel = 8 + detail = 'the user specified partitioning parameters for more partitions than there are processes.' + [] + [] [] From 6c34cee7abe2b45ac58c67161d2d761842331ee9 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Thu, 5 Dec 2024 18:50:48 -0700 Subject: [PATCH 09/17] Fix quirks in positions behavior at initial and for multiapp postions not initializing --- framework/include/positions/MultiAppPositions.h | 7 +++++++ framework/src/positions/FilePositions.C | 2 ++ framework/src/positions/InputPositions.C | 2 ++ framework/src/positions/Positions.C | 4 +--- unit/src/PositionsTest.C | 1 - 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/framework/include/positions/MultiAppPositions.h b/framework/include/positions/MultiAppPositions.h index 3095e1ff9c92..aedebee7743e 100644 --- a/framework/include/positions/MultiAppPositions.h +++ b/framework/include/positions/MultiAppPositions.h @@ -25,6 +25,13 @@ class MultiAppPositions : public Positions // MultiApp positions are not known at construction void initialize() override; + // Since we did not initialize on construction, initialize on setup by default + void initialSetup() override + { + initialize(); + finalize(); + } + /// Whether to use the subapp mesh centroids to compute the positions, further translated by the positions const bool _use_apps_centroid; }; diff --git a/framework/src/positions/FilePositions.C b/framework/src/positions/FilePositions.C index de0ef6f29405..c92d601786bd 100644 --- a/framework/src/positions/FilePositions.C +++ b/framework/src/positions/FilePositions.C @@ -49,4 +49,6 @@ FilePositions::FilePositions(const InputParameters & parameters) : Positions(par } } _initialized = true; + // Sort (if requested) and create KDTree + finalize(); } diff --git a/framework/src/positions/InputPositions.C b/framework/src/positions/InputPositions.C index 5056c7fefb7d..b11ffb9c67c2 100644 --- a/framework/src/positions/InputPositions.C +++ b/framework/src/positions/InputPositions.C @@ -30,4 +30,6 @@ InputPositions::InputPositions(const InputParameters & parameters) : Positions(p { _positions = getParam>("positions"); _initialized = true; + // Sort (if requested) and create KDTree + finalize(); } diff --git a/framework/src/positions/Positions.C b/framework/src/positions/Positions.C index 41b76774affc..bcea31163bcd 100644 --- a/framework/src/positions/Positions.C +++ b/framework/src/positions/Positions.C @@ -98,9 +98,7 @@ Positions::getNearestPositionIndex(const Point & target, const bool initial) con mooseAssert(initialized(initial), "Positions vector has not been initialized."); std::vector return_index(1); - // Note that we do not need to use the transformed_pt (in the source app frame) - // because the KDTree has been created in the reference frame - if (initial) + if (initial && _initial_positions) { mooseAssert(_initial_positions_kd_tree, "Should have an initial positions KDTree"); _initial_positions_kd_tree->neighborSearch(target, 1, return_index); diff --git a/unit/src/PositionsTest.C b/unit/src/PositionsTest.C index 2e4a9edca0e0..382a3ec9c812 100644 --- a/unit/src/PositionsTest.C +++ b/unit/src/PositionsTest.C @@ -69,7 +69,6 @@ TEST_F(PositionsTest, getters) { InputParameters params = _factory.getValidParams("InputPositions"); params.set>("positions") = {Point(1, 0, 0), Point(0, 0, 1)}; - params.set("auto_sort") = true; auto & positions = addObject("InputPositions", "test", params); // Test getters From 1c596bdb5b70d1cfd9a8d040b78a0eaf3d6380fb Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Thu, 5 Dec 2024 19:53:22 -0700 Subject: [PATCH 10/17] Add testing for nearest-app refactoring --- .../gold/main_between_multiapp_out_ma10.e | 1 + .../gold/main_between_multiapp_out_ma11.e | 1 + .../gold/main_between_multiapp_out_ma20.e | 1 + .../gold/main_between_multiapp_out_ma21.e | 1 + .../nearest_node/nearest_app/gold/main_out.e | 1 + .../nearest_node/nearest_app/main.i | 80 +++++++++++++++ .../nearest_app/main_between_multiapp.i | 97 +++++++++++++++++++ .../nearest_node/nearest_app/sub.i | 1 + .../nearest_app/sub_between_diffusion.i | 1 + .../nearest_node/nearest_app/tests | 41 ++++++++ 10 files changed, 225 insertions(+) create mode 120000 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e create mode 120000 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma11.e create mode 120000 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e create mode 120000 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e create mode 120000 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_out.e create mode 100644 test/tests/transfers/general_field/nearest_node/nearest_app/main.i create mode 100644 test/tests/transfers/general_field/nearest_node/nearest_app/main_between_multiapp.i create mode 120000 test/tests/transfers/general_field/nearest_node/nearest_app/sub.i create mode 120000 test/tests/transfers/general_field/nearest_node/nearest_app/sub_between_diffusion.i create mode 100644 test/tests/transfers/general_field/nearest_node/nearest_app/tests diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e new file mode 120000 index 000000000000..b152530507b7 --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e @@ -0,0 +1 @@ +../../nearest_position/gold/main_between_multiapp_out_ma10.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma11.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma11.e new file mode 120000 index 000000000000..5290330037e5 --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma11.e @@ -0,0 +1 @@ +../../nearest_position/gold/main_between_multiapp_out_ma11.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e new file mode 120000 index 000000000000..2852f3437cae --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e @@ -0,0 +1 @@ +../../nearest_position/gold/main_between_multiapp_out_ma20.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e new file mode 120000 index 000000000000..b838e01283bc --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e @@ -0,0 +1 @@ +../../nearest_position/gold/main_between_multiapp_out_ma21.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_out.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_out.e new file mode 120000 index 000000000000..bf27b9ba836d --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_out.e @@ -0,0 +1 @@ +../../nearest_position/gold/main_out.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/main.i b/test/tests/transfers/general_field/nearest_node/nearest_app/main.i new file mode 100644 index 000000000000..60c841d1adaf --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/main.i @@ -0,0 +1,80 @@ +# Base input for testing transfers. It has the following complexities: +# - more than one subapp +# - transfers from the subapps +# - both nodal and elemental variables +# - uses the nearest subapp as the source for any given point +# Tests derived from this input may add complexities through command line arguments + +[Mesh] + type = GeneratedMesh + dim = 2 + nx = 10 + ny = 10 +[] + +[AuxVariables] + [from_sub] + initial_condition = -1 + [] + [from_sub_elem] + order = CONSTANT + family = MONOMIAL + initial_condition = -1 + [] +[] + +[Executioner] + type = Steady +[] + +[Problem] + solve = false + verbose_multiapps = true +[] + +[Outputs] + [out] + type = Exodus + overwrite = true + [] +[] + +[Positions] + [input] + type = InputPositions + positions = '1e-6 0 0 0.4 0.6001 0' + [] +[] + +[MultiApps] + [sub] + type = FullSolveMultiApp + positions_objects = input + app_type = MooseTestApp + input_files = sub.i + output_in_position = true + execute_on = TIMESTEP_END + [] +[] + +[Transfers] + [from_sub] + type = MultiAppGeneralFieldNearestLocationTransfer + from_multi_app = sub + source_variable = to_main + variable = from_sub + assume_nearest_app_holds_nearest_location = true + bbox_factor = 100 + # Transfer relies on two nodes that are equidistant to the target point + search_value_conflicts = false + [] + + [from_sub_elem] + type = MultiAppGeneralFieldNearestLocationTransfer + from_multi_app = sub + source_variable = to_main_elem + variable = from_sub_elem + assume_nearest_app_holds_nearest_location = true + bbox_factor = 100 + [] +[] diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/main_between_multiapp.i b/test/tests/transfers/general_field/nearest_node/nearest_app/main_between_multiapp.i new file mode 100644 index 000000000000..5ad2d77a445b --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/main_between_multiapp.i @@ -0,0 +1,97 @@ +# Base input for testing between-multiapp transfers. It has the following complexities: +# - multiapps may not be run with the same number of ranks +# - both nodal and elemental variables +# Tests derived from this input may add or remove complexities through command line arguments + +[Problem] + solve = false +[] + +[Mesh] + type = GeneratedMesh + dim = 2 +[] + +[Positions] + [input_app1] + type = InputPositions + positions = '0 0.1 0 + 0.5 0.5 0' + [] + [input_app2] + type = InputPositions + # offsets to avoid indetermination + # but small enough to remain below to bounding box factor bump + positions = '0.0000001 0.30000000001 0 + 0.60000000001 0.5003 0' + [] +[] + +# This application uses at most 3 processes +[MultiApps/ma1] + type = TransientMultiApp + input_files = sub_between_diffusion.i + max_procs_per_app = 3 + positions_objects = 'input_app1' + output_in_position = true +[] + +# This application will use as many processes as the main app +[MultiApps/ma2] + type = TransientMultiApp + input_files = sub_between_diffusion.i + positions_objects = 'input_app2' + output_in_position = true +[] + +[Transfers] + # Nodal to nodal variables + [app1_to_2_nodal_nodal] + type = MultiAppGeneralFieldNearestLocationTransfer + from_multi_app = ma1 + to_multi_app = ma2 + source_variable = sent_nodal + variable = received_nodal + assume_nearest_app_holds_nearest_location = true + search_value_conflicts = true + # slight inflation to avoid floating point issues on borders + bbox_factor = 1.000001 + [] + [app2_to_1_nodal_nodal] + type = MultiAppGeneralFieldNearestLocationTransfer + from_multi_app = ma2 + to_multi_app = ma1 + source_variable = sent_nodal + variable = received_nodal + bbox_factor = 1.000001 + search_value_conflicts = true + assume_nearest_app_holds_nearest_location = true + [] + + # Elemental to elemental variables + [app1_to_2_elem_elem] + type = MultiAppGeneralFieldNearestLocationTransfer + from_multi_app = ma1 + to_multi_app = ma2 + source_variable = sent_elem + variable = received_elem + assume_nearest_app_holds_nearest_location = true + search_value_conflicts = true + bbox_factor = 1.000001 + [] + [app2_to_1_elem_elem] + type = MultiAppGeneralFieldNearestLocationTransfer + from_multi_app = ma2 + to_multi_app = ma1 + source_variable = sent_elem + variable = received_elem + assume_nearest_app_holds_nearest_location = true + search_value_conflicts = true + bbox_factor = 1.000001 + [] +[] + +[Executioner] + type = Transient + num_steps = 1 +[] diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/sub.i b/test/tests/transfers/general_field/nearest_node/nearest_app/sub.i new file mode 120000 index 000000000000..3077cebb2cf9 --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/sub.i @@ -0,0 +1 @@ +../nearest_position/sub.i \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/sub_between_diffusion.i b/test/tests/transfers/general_field/nearest_node/nearest_app/sub_between_diffusion.i new file mode 120000 index 000000000000..46eaa4e92070 --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/sub_between_diffusion.i @@ -0,0 +1 @@ +../nearest_position/sub_between_diffusion.i \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/tests b/test/tests/transfers/general_field/nearest_node/nearest_app/tests new file mode 100644 index 000000000000..9243faa21b5c --- /dev/null +++ b/test/tests/transfers/general_field/nearest_node/nearest_app/tests @@ -0,0 +1,41 @@ +[Tests] + issues = '#23587' + design = 'transfers/MultiAppGeneralFieldUserObjectTransfer.md' + + [2d_overlay] + requirement = "The system shall be able to transfer a spatial field using a nearest-node " + "(source) algorithm combined with a nearest-application heuristic to limit the " + "source of the origin values " + [nearest_apps] + type = 'Exodiff' + input = 'main.i' + exodiff = 'main_out.e' + detail = "for a 2D case with multiple subapps." + [] + [] + + [between_subapps] + type = 'Exodiff' + input = 'main_between_multiapp.i' + exodiff = 'main_between_multiapp_out_ma10.e main_between_multiapp_out_ma11.e + main_between_multiapp_out_ma20.e main_between_multiapp_out_ma21.e' + requirement = 'The system shall be able to send data in a subapp to another subapp using a nearest-application heuristic for finding the source values.' + [] + # Ideally we would have used the between_subapps test under nearest_node/nearest_position but it uses + # the group_subapps option, which does not make sense for the 'nearest-app' mode + [between_subapps_match_nearest_positions] + type = 'Exodiff' + input = 'main_between_multiapp.i' + exodiff = 'main_between_multiapp_out_ma10.e main_between_multiapp_out_ma11.e + main_between_multiapp_out_ma20.e main_between_multiapp_out_ma21.e' + cli_args = "Transfers/app1_to_2_nodal_nodal/assume_nearest_app_holds_nearest_location=false + Transfers/app2_to_1_nodal_nodal/assume_nearest_app_holds_nearest_location=false + Transfers/app1_to_2_elem_elem/assume_nearest_app_holds_nearest_location=false + Transfers/app2_to_1_elem_elem/assume_nearest_app_holds_nearest_location=false + Transfers/app1_to_2_nodal_nodal/use_nearest_position=input_app1 + Transfers/app2_to_1_nodal_nodal/use_nearest_position=input_app2 + Transfers/app1_to_2_elem_elem/use_nearest_position=input_app1 + Transfers/app2_to_1_elem_elem/use_nearest_position=input_app2" + requirement = 'The system shall be able to match the nearest-app heuristic using a nearest-position heuristic for field transfers.' + [] +[] From 8005ef5753294b787ff3b3e719a2ae731334db4b Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Thu, 5 Dec 2024 19:53:40 -0700 Subject: [PATCH 11/17] Fix an issue where shape evaluation transfer was not saving value conflicts in reference frame when using nearest-app/positions also: - create multiapp positions with factory - check distance to positions when comparing nearest target position and nearest app position to handle equidistant case (find the value conflicts) - accept infinite values if coming from the nearest-app --- ...tiAppGeneralFieldNearestLocationTransfer.C | 14 ++++++- ...tiAppGeneralFieldShapeEvaluationTransfer.C | 8 +++- .../transfers/MultiAppGeneralFieldTransfer.C | 37 ++++++++++++------- .../MultiAppGeneralFieldUserObjectTransfer.C | 10 ++++- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C index ff7b38869ca2..628c9d2a2bf5 100644 --- a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C @@ -481,7 +481,12 @@ MultiAppGeneralFieldNearestLocationTransfer::evaluateInterpValuesNearestNode( if (num_found > 1 && num_found == num_search && MooseUtils::absoluteFuzzyEqual(zipped_nearest_points[num_found - 1].first, zipped_nearest_points[num_found - 2].first)) - registerConflict(app_index, 0, local_pt, outgoing_vals[i_pt].second, true); + { + if (_nearest_positions_obj) + registerConflict(app_index, 0, pt, outgoing_vals[i_pt].second, true); + else + registerConflict(app_index, 0, local_pt, outgoing_vals[i_pt].second, true); + } // Recompute the distance for this problem. If it matches the cached value more than // once it means multiple problems provide equidistant values for this point @@ -498,7 +503,12 @@ MultiAppGeneralFieldNearestLocationTransfer::evaluateInterpValuesNearestNode( { num_equidistant_problems++; if (num_equidistant_problems > 1) - registerConflict(app_index, 0, local_pt, outgoing_vals[i_pt].second, true); + { + if (_nearest_positions_obj) + registerConflict(app_index, 0, pt, outgoing_vals[i_pt].second, true); + else + registerConflict(app_index, 0, local_pt, outgoing_vals[i_pt].second, true); + } } } } diff --git a/framework/src/transfers/MultiAppGeneralFieldShapeEvaluationTransfer.C b/framework/src/transfers/MultiAppGeneralFieldShapeEvaluationTransfer.C index 65955d4b6607..985cb6b68da2 100644 --- a/framework/src/transfers/MultiAppGeneralFieldShapeEvaluationTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldShapeEvaluationTransfer.C @@ -143,7 +143,13 @@ MultiAppGeneralFieldShapeEvaluationTransfer::evaluateInterpValuesWithMeshFunctio // NOTE: There is no guarantee this will be the final value used among all problems // but for shape evaluation we really do expect only one value to even be valid if (detectConflict(val, outgoing_vals[i_pt].first, distance, outgoing_vals[i_pt].second)) - registerConflict(i_from, 0, local_pt, distance, true); + { + // In the nearest-position/app mode, we save conflicts in the reference frame + if (_nearest_positions_obj) + registerConflict(i_from, 0, pt, distance, true); + else + registerConflict(i_from, 0, local_pt, distance, true); + } // No need to consider decision factors if value is invalid if (val == GeneralFieldTransfer::BetterOutOfMeshValue) diff --git a/framework/src/transfers/MultiAppGeneralFieldTransfer.C b/framework/src/transfers/MultiAppGeneralFieldTransfer.C index 81ad59bfb95a..3d05cbe686d8 100644 --- a/framework/src/transfers/MultiAppGeneralFieldTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldTransfer.C @@ -17,7 +17,7 @@ #include "MooseVariableFE.h" #include "MeshDivision.h" #include "Positions.h" -#include "MultiAppPositions.h" // remove after use_nearest_app deprecation +#include "Factory.h" #include "MooseAppCoordTransform.h" // libmesh includes @@ -200,10 +200,8 @@ MultiAppGeneralFieldTransfer::MultiAppGeneralFieldTransfer(const InputParameters if (!hasFromMultiApp()) paramError("use_nearest_app", "Should have a 'from_multiapp' when using the nearest-app informed search"); - auto pos_params = MultiAppPositions::validParams(); - pos_params.set>("multiapps") = {getMultiApp()->name()}; - pos_params.set("_moose_app") = - parameters.getCheckedPointerParam("_moose_app"); + auto pos_params = _app.getFactory().getValidParams("MultiAppPositions"); + pos_params.set>("multiapps") = {getFromMultiApp()->name()}; _fe_problem.addReporter("MultiAppPositions", "_created_for_" + name(), pos_params); _nearest_positions_obj = &_fe_problem.getPositionsObject("_created_for_" + name()); } @@ -583,13 +581,14 @@ MultiAppGeneralFieldTransfer::locatePointReceivers(const Point point, from0 += _froms_per_proc[i_proc], ++i_proc) for (unsigned int i_from = from0; i_from < from0 + _froms_per_proc[i_proc]; ++i_from) { - if (_greedy_search || i_from == nearest_index) + if (_greedy_search || _search_value_conflicts || i_from == nearest_index) { processors.insert(i_proc); found = true; } } - mooseAssert(processors.size() == 1 || _greedy_search, "Should only be one nearest app"); + mooseAssert(processors.size() == 1 || _greedy_search || _search_value_conflicts, + "Should only be one nearest app"); } else if (_use_bounding_boxes) { @@ -669,8 +668,9 @@ MultiAppGeneralFieldTransfer::locatePointReceivers(const Point point, "Cannot find a source application to provide a value at point: ", point, " \n ", - "It must be that mismatched matches are used.\nIf you are using bounding boxes, " - "nearest-app or mesh-divisions, please consider using the greedy_search to confirm."); + "It must be that mismatched meshes, between the source and target application, are being " + "used.\nIf you are using bounding boxes, nearest-app or mesh-divisions, please consider " + "using the greedy_search to confirm. Then consider choosing a different transfer type."); } void @@ -923,7 +923,7 @@ MultiAppGeneralFieldTransfer::cacheIncomingInterpVals( // if we use the nearest app, even if the value is bad we want to save the distance because // it's the distance to the app, if that's the closest app then so be it with the bad value - if ((!GeneralFieldTransfer::isBetterOutOfMeshValue(val)) && + if ((!GeneralFieldTransfer::isBetterOutOfMeshValue(val) || _use_nearest_app) && MooseUtils::absoluteFuzzyGreaterThan(distance_cache[p], incoming_vals[val_offset].second)) { // NOTE: We store the distance as well as the value. We really only need the @@ -997,7 +997,9 @@ MultiAppGeneralFieldTransfer::cacheIncomingInterpVals( // - valid (or from nearest app) // - closest distance // - the smallest rank with the same distance - if (!GeneralFieldTransfer::isBetterOutOfMeshValue(incoming_vals[val_offset].first) && + // TODO Fix + if ((!GeneralFieldTransfer::isBetterOutOfMeshValue(incoming_vals[val_offset].first) || + _use_nearest_app) && (MooseUtils::absoluteFuzzyGreaterThan(val.distance, incoming_vals[val_offset].second) || ((val.pid > pid) && MooseUtils::absoluteFuzzyEqual(val.distance, incoming_vals[val_offset].second)))) @@ -1551,13 +1553,20 @@ MultiAppGeneralFieldTransfer::acceptPointInOriginMesh(unsigned int i_from, _from_transforms[from_global_num]->hasNonTranslationTransformation()) mooseError("Rotation and scaling currently unsupported with nearest positions transfer."); + // Compute distance to nearest position and nearest position source + const Real distance_to_position_nearest_source = (pt - nearest_position_source).norm(); + const Real distance_to_nearest_position = (pt - nearest_position).norm(); + // Source (usually app position) is not closest to the same positions as the target, dont - // send values - if (nearest_position != nearest_position_source) + // send values. We check the distance instead of the positions because if they are the same + // that means there's two equidistant positions and we would want to capture that as a "value + // conflict" + if (!MooseUtils::absoluteFuzzyEqual(distance_to_position_nearest_source, + distance_to_nearest_position)) return false; // Set the distance as the distance from the nearest position to the target point - distance = (pt - nearest_position_source).norm(); + distance = distance_to_position_nearest_source; } // Check that the app actually contains the origin point diff --git a/framework/src/transfers/MultiAppGeneralFieldUserObjectTransfer.C b/framework/src/transfers/MultiAppGeneralFieldUserObjectTransfer.C index 0abeb5d3ba40..96d457c3731f 100644 --- a/framework/src/transfers/MultiAppGeneralFieldUserObjectTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldUserObjectTransfer.C @@ -117,14 +117,20 @@ MultiAppGeneralFieldUserObjectTransfer::evaluateInterpValuesWithUserObjects( _from_problems[i_from]->getUserObjectBase(_user_object_name); // Use spatial value routine to compute the origin value to transfer - auto val = user_object.spatialValue(_from_transforms[from_global_num]->mapBack(pt)); + const auto local_pt = _from_transforms[from_global_num]->mapBack(pt); + auto val = user_object.spatialValue(local_pt); // Look for overlaps. The check is not active outside of overlap search because in that // case we accept the first value from the lowest ranked process // NOTE: There is no guarantee this will be the final value used among all problems // but we register an overlap as soon as two values are possible from this rank if (detectConflict(val, outgoing_vals[i_pt].first, distance, outgoing_vals[i_pt].second)) - registerConflict(i_from, 0, _from_transforms[from_global_num]->mapBack(pt), 1, true); + { + if (_nearest_positions_obj) + registerConflict(i_from, 0, pt, distance, true); + else + registerConflict(i_from, 0, local_pt, distance, true); + } // No need to consider decision factors if value is invalid if (val == GeneralFieldTransfer::BetterOutOfMeshValue) From 8d6da3545b9eaf300dfcf1799cee32f031c8783d Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Fri, 6 Dec 2024 09:31:51 -0700 Subject: [PATCH 12/17] Separate test for value-conflict + nearest app in nodal & elemental --- .../shape_evaluation/regular/tests | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/test/tests/transfers/general_field/shape_evaluation/regular/tests b/test/tests/transfers/general_field/shape_evaluation/regular/tests index 3434d7b82daa..ff1a40c3bc52 100644 --- a/test/tests/transfers/general_field/shape_evaluation/regular/tests +++ b/test/tests/transfers/general_field/shape_evaluation/regular/tests @@ -116,7 +116,7 @@ cli_args = "Transfers/active='from_sub' Transfers/from_sub/search_value_conflicts=true " "MultiApps/sub/positions='0 0 0 0.0001 0 0'" expect_err = "multiple valid values from equidistant points were " - detail = 'overlapping origin child application, causing multiple shape evaluations to be valid' + detail = 'overlapping origin child application, causing multiple shape evaluations to be valid,' # Both apps need to be on the same process max_parallel = 1 [] @@ -128,25 +128,36 @@ cli_args = "Transfers/active='from_sub' Transfers/from_sub/search_value_conflicts=true " "MultiApps/sub/positions='0 0 0 0.01 0 0'" expect_err = "multiple valid values from equidistant points were received" - detail = 'multiple source problems sending valid values for a given target point' + detail = 'multiple source problems sending valid values for a given target point,' # One app per process min_parallel = 2 [] - [nearest_app] + [nearest_app_nodal] type = RunException input = main.i - # The element variable has point evaluations at 0.05 0.05 0. This is within both subapps' domain - # and equidistant from 0 0 0 and 0.1 0 0, the subapp positions - cli_args = "Outputs/file_base=nearest_subapp Transfers/active='from_sub from_sub_elem' - Transfers/from_sub/use_nearest_app=true Transfers/from_sub_elem/use_nearest_app=true - Transfers/from_sub/bbox_factor=1 Transfers/from_sub_elem/bbox_factor=1 - MultiApps/sub/positions='0 0 0 0.1 0 0' - Transfers/from_sub/search_value_conflicts=true Transfers/from_sub_elem/search_value_conflicts=true - MultiApps/sub/cli_args='Mesh/xmin=-0.4;Mesh/xmax=0.8;Mesh/ymax=0.7'" + # The element variable has point evaluations at 0 0 0. This is within both subapps' domain + # and equidistant from 0.05 0.05 0 and 0.15 0.15 0, the subapp positions + cli_args = "Outputs/file_base=nearest_subapp Transfers/active='from_sub' + Transfers/from_sub/use_nearest_app=true Transfers/from_sub/bbox_factor=1 + MultiApps/sub/positions='0.05 0.05 0 0.15 0.15 0' Transfers/from_sub/search_value_conflicts=true + MultiApps/sub/cli_args='Mesh/xmin=-0.4;Mesh/xmax=0.6;Mesh/ymax=0.7'" expect_err = "(Search for nearest position found at least two matches|multiple valid values " "from equidistant points were )" - detail = "when using the closest app rather than the first valid value to chose a value" + detail = "when using the closest app rather than the first valid value to chose a value for nodal variables," + [] + [nearest_app_elem] + type = RunException + input = main.i + # The element variable has point evaluations at 0.05 0 0. This is within both subapps' domain + # and equidistant from -0.15 0 0 and 0.25 0 0, the subapp positions + cli_args = "Outputs/file_base=nearest_subapp Transfers/active='from_sub_elem' + Transfers/from_sub_elem/use_nearest_app=true Transfers/from_sub_elem/bbox_factor=1 + MultiApps/sub/positions='-0.15 0 0 0.25 0 0' Transfers/from_sub_elem/search_value_conflicts=true + MultiApps/sub/cli_args='Mesh/xmin=-0.4;Mesh/xmax=0.6;Mesh/ymax=0.7'" + expect_err = "(Search for nearest position found at least two matches|multiple valid values " + "from equidistant points were )" + detail = "when using the closest app rather than the first valid value to chose a value for constant monomial variables." [] [] [] From ab811e46de51952fc14d9a4d677b727ac2779546 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Fri, 6 Dec 2024 10:28:05 -0700 Subject: [PATCH 13/17] Modify test to show between multiapps test agrees between > nearest-app mode and nearest-position mode > Thus validating the better search heuristic in nearest-ap --- .../gold/main_between_multiapp_out_ma10.e | Bin 60 -> 59228 bytes .../gold/main_between_multiapp_out_ma11.e | Bin 60 -> 59228 bytes .../gold/main_between_multiapp_out_ma20.e | Bin 60 -> 59228 bytes .../gold/main_between_multiapp_out_ma21.e | Bin 60 -> 59228 bytes .../nearest_app/main_between_multiapp.i | 20 ++++++++++++------ 5 files changed, 14 insertions(+), 6 deletions(-) mode change 120000 => 100644 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e mode change 120000 => 100644 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma11.e mode change 120000 => 100644 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e mode change 120000 => 100644 test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e deleted file mode 120000 index b152530507b7..000000000000 --- a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e +++ /dev/null @@ -1 +0,0 @@ -../../nearest_position/gold/main_between_multiapp_out_ma10.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma10.e new file mode 100644 index 0000000000000000000000000000000000000000..9f8f2bb32cb3701cd99f81476737662f6ec7ab05 GIT binary patch literal 59228 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 BH4Oj& literal 0 HcmV?d00001 diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e deleted file mode 120000 index 2852f3437cae..000000000000 --- a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e +++ /dev/null @@ -1 +0,0 @@ -../../nearest_position/gold/main_between_multiapp_out_ma20.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma20.e new file mode 100644 index 0000000000000000000000000000000000000000..f733a9300573f39595a72238fad4df95df5135f5 GIT binary patch literal 59228 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 IdaFP8zg5AMyZ`_I literal 0 HcmV?d00001 diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e deleted file mode 120000 index b838e01283bc..000000000000 --- a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e +++ /dev/null @@ -1 +0,0 @@ -../../nearest_position/gold/main_between_multiapp_out_ma21.e \ No newline at end of file diff --git a/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e b/test/tests/transfers/general_field/nearest_node/nearest_app/gold/main_between_multiapp_out_ma21.e new file mode 100644 index 0000000000000000000000000000000000000000..514047f02875375a6a0c9709d0dd6e0a7fe6fe0d GIT binary patch literal 59228 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 Date: Fri, 6 Dec 2024 13:50:58 -0700 Subject: [PATCH 14/17] Fix indexing issue in nearest app: use the global app (=the position index) when checking if the point is nearest to an app --- ...tiAppGeneralFieldNearestLocationTransfer.C | 27 ++++++++++---- .../transfers/MultiAppGeneralFieldTransfer.C | 35 +++++++++++++++++-- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C index 628c9d2a2bf5..319b1e1672b9 100644 --- a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C @@ -47,7 +47,6 @@ MultiAppGeneralFieldNearestLocationTransfer::validParams() // However, if the user knows this is true, we can use this heuristic to reduce the number of apps // that are requested to provide a candidate value. If the user is wrong, then the nearest // location is used, which can be from the non-nearest app. - params.set("use_nearest_app") = true; params.renameParam("use_nearest_app", "assume_nearest_app_holds_nearest_location", ""); // the default of node/centroid switching based on the variable is causing lots of mistakes and @@ -400,7 +399,8 @@ MultiAppGeneralFieldNearestLocationTransfer::evaluateInterpValuesNearestNode( if (!checkRestrictionsForSource(pt, mesh_div, i_from)) continue; - // TODO: Pre-allocate these two work arrays. They will be regularly resized by the searches + // TODO: Pre-allocate these two work arrays. They will be regularly resized by the + // searches std::vector return_index(_num_nearest_points); std::vector return_dist_sqr(_num_nearest_points); @@ -628,10 +628,22 @@ MultiAppGeneralFieldNearestLocationTransfer::checkRestrictionsForSource( const Point & pt, const unsigned int mesh_div, const unsigned int i_from) const { // Only use the KDTree from the closest position if in "nearest-position" mode - const auto position_index = - (_group_subapps || _use_nearest_app) ? i_from : i_from % getNumDivisions(); - if (_nearest_positions_obj && !closestToPosition(position_index, pt)) - return false; + if (_nearest_positions_obj) + { + // See computeNumSources for the number of sources. i_from is the index in the source loop + // i_from is local if looping on _from_problems as sources, positions are indexed globally + // i_from is already indexing in positions if using group_subapps + auto position_index = i_from; // if _group_subapps + if (_use_nearest_app) + position_index = getGlobalSourceAppIndex(i_from); + else if (!_group_subapps) + position_index = i_from % getNumDivisions(); + + // NOTE: if two positions are equi-distant to the point, this will chose one + // This problem is detected if using search_value_conflicts in this call + if (!closestToPosition(position_index, pt)) + return false; + } // Application index depends on which source/grouping mode we are using const unsigned int app_index = getAppIndex(i_from, i_from / getNumDivisions()); @@ -639,6 +651,9 @@ MultiAppGeneralFieldNearestLocationTransfer::checkRestrictionsForSource( // Check mesh restriction before anything if (_source_app_must_contain_point) { + // We have to be careful that getPointInLocalSourceFrame returns in the reference frame + if (_nearest_positions_obj) + mooseError("Nearest-positions + source_app_must_contain_point not implemented"); // Transform the point to place it in the local coordinate system const auto local_pt = getPointInLocalSourceFrame(app_index, pt); if (!inMesh(_from_point_locators[app_index].get(), local_pt)) diff --git a/framework/src/transfers/MultiAppGeneralFieldTransfer.C b/framework/src/transfers/MultiAppGeneralFieldTransfer.C index 3d05cbe686d8..908915b34c20 100644 --- a/framework/src/transfers/MultiAppGeneralFieldTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldTransfer.C @@ -207,7 +207,7 @@ MultiAppGeneralFieldTransfer::MultiAppGeneralFieldTransfer(const InputParameters } // Dont let users get wrecked by bounding boxes if it looks like they are trying to extrapolate - if (!_source_app_must_contain_point && + if (!_source_app_must_contain_point && _use_bounding_boxes && (_nearest_positions_obj || isParamSetByUser("from_app_must_contain_point"))) if (!isParamSetByUser("bbox_factor") && !isParamSetByUser("fixed_bounding_box_size")) mooseWarning( @@ -997,7 +997,8 @@ MultiAppGeneralFieldTransfer::cacheIncomingInterpVals( // - valid (or from nearest app) // - closest distance // - the smallest rank with the same distance - // TODO Fix + // It is debatable whether we want invalid values from the nearest app. It could just be + // that the app position was closer but the extent of another child app was large enough if ((!GeneralFieldTransfer::isBetterOutOfMeshValue(incoming_vals[val_offset].first) || _use_nearest_app) && (MooseUtils::absoluteFuzzyGreaterThan(val.distance, incoming_vals[val_offset].second) || @@ -1733,7 +1734,35 @@ MultiAppGeneralFieldTransfer::closestToPosition(unsigned int pos_index, const Po if (!_skip_coordinate_collapsing) paramError("skip_coordinate_collapsing", "Coordinate collapsing not implemented"); bool initial = _fe_problem.getCurrentExecuteOnFlag() == EXEC_INITIAL; - return pos_index == _nearest_positions_obj->getNearestPositionIndex(pt, initial); + if (!_search_value_conflicts) + // Faster to just compare the index + return pos_index == _nearest_positions_obj->getNearestPositionIndex(pt, initial); + else + { + // Get the distance to the position and see if we are missing a value just because the position + // is not officially the closest, but it is actually at the same distance + const auto nearest_position = _nearest_positions_obj->getNearestPosition(pt, initial); + const auto nearest_position_at_index = _nearest_positions_obj->getPosition(pos_index, initial); + Real distance_to_position_at_index = (pt - nearest_position_at_index).norm(); + const Real distance_to_nearest_position = (pt - nearest_position).norm(); + + if (!MooseUtils::absoluteFuzzyEqual(distance_to_position_at_index, + distance_to_nearest_position)) + return false; + // Actually the same position (point) + else if (nearest_position == nearest_position_at_index) + return true; + else + { + mooseWarning("Two equidistant positions ", + nearest_position, + " and ", + nearest_position_at_index, + " detected near point ", + pt); + return true; + } + } } Real From 1d31c9e2ac5a8f984270e219ad29e6a47af2ff5d Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Fri, 6 Dec 2024 15:22:56 -0700 Subject: [PATCH 15/17] Fix indexing for nearest-app communication restriction --- .../src/transfers/MultiAppGeneralFieldTransfer.C | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/framework/src/transfers/MultiAppGeneralFieldTransfer.C b/framework/src/transfers/MultiAppGeneralFieldTransfer.C index 908915b34c20..54a17b0cab08 100644 --- a/framework/src/transfers/MultiAppGeneralFieldTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldTransfer.C @@ -576,9 +576,10 @@ MultiAppGeneralFieldTransfer::locatePointReceivers(const Point point, // Find the apps that are nearest to the same position // Global search over all applications - unsigned int from0 = 0; - for (processor_id_type i_proc = 0; i_proc < n_processors(); - from0 += _froms_per_proc[i_proc], ++i_proc) + for (processor_id_type i_proc = 0; i_proc < n_processors(); ++i_proc) + { + // We need i_from to correspond to the global app index + unsigned int from0 = _global_app_start_per_proc[i_proc]; for (unsigned int i_from = from0; i_from < from0 + _froms_per_proc[i_proc]; ++i_from) { if (_greedy_search || _search_value_conflicts || i_from == nearest_index) @@ -586,9 +587,12 @@ MultiAppGeneralFieldTransfer::locatePointReceivers(const Point point, processors.insert(i_proc); found = true; } + mooseAssert(i_from < getFromMultiApp()->numGlobalApps(), "We should not reach this"); } - mooseAssert(processors.size() == 1 || _greedy_search || _search_value_conflicts, - "Should only be one nearest app"); + } + mooseAssert((getFromMultiApp()->numGlobalApps() < n_processors() || processors.size() == 1) || + _greedy_search || _search_value_conflicts, + "Should only be one source processor when using more processors than source apps"); } else if (_use_bounding_boxes) { @@ -609,6 +613,7 @@ MultiAppGeneralFieldTransfer::locatePointReceivers(const Point point, unsigned int from0 = 0; for (processor_id_type i_proc = 0; i_proc < n_processors(); from0 += _froms_per_proc[i_proc], ++i_proc) + // i_from here is a hybrid index based on the cumulative sum of the apps per processor for (unsigned int i_from = from0; i_from < from0 + _froms_per_proc[i_proc]; ++i_from) { Real distance = bboxMinDistance(point, _from_bboxes[i_from]); From af576334a0f86e3c830fb7fde4992594f740ec69 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Thu, 12 Dec 2024 12:46:47 -0700 Subject: [PATCH 16/17] Tune the grid partitioner warning message on ill-specified grid more --- framework/src/partitioner/GridPartitioner.C | 27 ++++++++++++------- .../custom_partition_generated_mesh/tests | 4 +++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/framework/src/partitioner/GridPartitioner.C b/framework/src/partitioner/GridPartitioner.C index a60d42533aac..26eddb7f43df 100644 --- a/framework/src/partitioner/GridPartitioner.C +++ b/framework/src/partitioner/GridPartitioner.C @@ -112,20 +112,27 @@ GridPartitioner::_do_partition(MeshBase & mesh, const unsigned int /*n*/) "User specified (nx,ny,nz) grid exceeded number of partitions, these parameters " "will be ignored."); } - if (nx > 0 && ny > 0 && nz > 0 && nx * ny * nz != mesh.n_partitions()) - { - nx = 0; - ny = 0; - nz = 0; - paramWarning("grid_computation", - "User specified (nx,ny,nz) grid do not match the number of partitions and " - "constrain the grid partitioner in every direction, these parameters " - "will be ignored."); - } // 0 means no restriction on which number to choose int dims[] = {isParamValid("nx") ? int(nx) : 0, isParamValid("ny") ? int(ny) : 0, isParamValid("nz") ? int(nz) : 0}; + + if ((dims[0] > 0 && dim == 1 && dims[0] != int(mesh.n_partitions())) || + (dims[0] > 0 && dims[1] > 0 && dim == 2 && dims[0] * dims[1] != int(mesh.n_partitions())) || + (dims[0] > 0 && dims[1] > 0 && dims[2] > 0 && dim == 3 && + dims[0] * dims[1] * dims[2] != int(mesh.n_partitions()))) + { + dims[0] = 0; + dims[1] = 0; + dims[2] = 0; + paramWarning("grid_computation", + "User specified grid for the current dimension of the mesh (" + + std::to_string(dim) + ") does not fit the number of partitions (" + + std::to_string(mesh.n_partitions()) + + ") and constrain the grid partitioner in every direction, these parameters " + "will be ignored."); + } + // This will error if the factorization is not possible MPI_Dims_create(mesh.n_partitions(), dim, dims); diff --git a/test/tests/partitioners/custom_partition_generated_mesh/tests b/test/tests/partitioners/custom_partition_generated_mesh/tests index 404ee409137b..bcbfd7af2bab 100644 --- a/test/tests/partitioners/custom_partition_generated_mesh/tests +++ b/test/tests/partitioners/custom_partition_generated_mesh/tests @@ -9,5 +9,9 @@ issues = '#18696' design = 'Mesh/Partitioner/index.md' requirement = 'The system shall allow custom partitioners to work with mesh generators' + + # The 3D grid is good for the final partitioning of the extruded mesh, but it is not good + # for the partitioning triggered during mesh generation when the mesh is still 2D + allow_warnings = true [../] [] From ec548c26afd9a2940b04ef0d425d5541cb19c837 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Thu, 16 Jan 2025 19:15:00 -0700 Subject: [PATCH 17/17] Address Alex's review - better comments Co-authored-by: Alex Lindsay --- .../transfers/MultiAppGeneralFieldNearestLocationTransfer.C | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C index 319b1e1672b9..e8b76dcbd600 100644 --- a/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C +++ b/framework/src/transfers/MultiAppGeneralFieldNearestLocationTransfer.C @@ -290,7 +290,7 @@ MultiAppGeneralFieldNearestLocationTransfer::buildKDTrees(const unsigned int var // When querying values at a target point, the KDTree associated to the closest // position to the target point is queried // We do not need to check the positions when using nearest app as we will assume - // (somewhat incorrectly) that all the points in each subapp are closest to that subapp + // (somewhat incorrectly) that all the points in each subapp are closer to that subapp // than to any other if (!_use_nearest_app && _nearest_positions_obj && !closestToPosition(i_pos, transformed_node)) @@ -346,7 +346,7 @@ MultiAppGeneralFieldNearestLocationTransfer::buildKDTrees(const unsigned int var (*_from_transforms[getGlobalSourceAppIndex(i_from)])(vertex_average); // We do not need to check the positions when using nearest app as we will assume - // (somewhat incorrectly) that all the points in each subapp are closest to that subapp + // (somewhat incorrectly) that all the points in each subapp are closer to that subapp if (!_use_nearest_app && _nearest_positions_obj && !closestToPosition(i_pos, transformed_vertex_average)) continue;