https://mooseframework.inl.gov
Loading...
Searching...
No Matches
AutomaticContactPairingGenerator.C
Go to the documentation of this file.
1//* This file is part of the MOOSE framework
2//* https://mooseframework.inl.gov
3//*
4//* All rights reserved, see COPYRIGHT for full restrictions
5//* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6//*
7//* Licensed under LGPL 2.1, please see LICENSE for details
8//* https://www.gnu.org/licenses/lgpl-2.1.html
9
11#include "MooseMeshUtils.h"
12#include "PointListAdaptor.h"
13
14#include "libmesh/boundary_info.h"
15#include "libmesh/elem.h"
16#include "libmesh/elem_side_builder.h"
17#include "libmesh/parallel_algebra.h"
18
19#include <algorithm>
20
21// Make newer nanoflann API compatible with older nanoflann versions
22#if NANOFLANN_VERSION < 0x150
23namespace nanoflann
24{
25typedef SearchParams SearchParameters;
26
27template <typename T, typename U>
28using ResultItem = std::pair<T, U>;
29}
30#endif
31
32using NodeBoundaryIDInfo = std::pair<Point, BoundaryID>;
33
34template <>
35inline const Point &
37{
38 return item.first;
39}
40
42
45{
47 params.addRequiredParam<MeshGeneratorName>("input", "The mesh to modify");
48 params.addRequiredParam<std::vector<BoundaryName>>(
49 "automatic_pairing_boundaries",
50 "List of boundary names to consider when automatically detecting contact pairs.");
51 params.addRequiredParam<Real>(
52 "automatic_pairing_distance",
53 "Maximum distance (node-to-node or centroid-to-centroid) at which two boundaries are "
54 "considered a contact pair.");
56 "automatic_pairing_method",
57 MooseEnum("NODE CENTROID"),
58 "Strategy used to detect pairs: NODE uses a KD-tree over boundary nodes; "
59 "CENTROID uses sideset center-of-gravity distances.");
60 params.addRequiredParam<std::string>(
61 "prefix", "Prefix prepended to the names of generated subdomain blocks.");
62 params.addRequiredParam<bool>(
63 "create_lower_d_blocks",
64 "Whether to create primary and secondary lower-dimensional subdomain blocks for each "
65 "detected pair.");
67 "Detects contact surface pairs by proximity, stores them in the mesh meta-data, and "
68 "optionally creates lower-dimensional subdomain blocks for use in mortar contact.");
69 return params;
70}
71
73 const InputParameters & parameters)
74 : MeshGenerator(parameters),
75 _input(getMesh("input")),
76 _pairing_boundaries(getParam<std::vector<BoundaryName>>("automatic_pairing_boundaries")),
77 _pairing_distance(getParam<Real>("automatic_pairing_distance")),
78 _pairing_method(getParam<MooseEnum>("automatic_pairing_method")),
79 _prefix(getParam<std::string>("prefix")),
80 _create_lower_d_blocks(getParam<bool>("create_lower_d_blocks"))
81{
82 // Declared here, and assigned during generation, so that the pairs are restored from the mesh
83 // meta-data on recover, when generation is skipped
84 declareMeshProperty<ContactPairs>(contact_pairs_property);
85}
86
87std::unique_ptr<MeshBase>
89{
90 auto mesh = std::move(_input);
91
92 std::vector<std::pair<BoundaryName, BoundaryName>> pairs;
93 if (_pairing_method == "NODE")
95 else
96 pairs = findPairsCentroid(*mesh);
97
98 // Mortar contact needs at least one pair to build its lower-dimensional blocks, while node-face
99 // contact may proceed with none
100 if (pairs.empty() && _create_lower_d_blocks)
101 mooseError("No contact pairs found within distance ",
103 " among boundaries ",
105
106 setMeshProperty<ContactPairs>(contact_pairs_property, ContactPairs(pairs.begin(), pairs.end()));
107
109 return mesh;
110
111 const bool multiple_pairs = pairs.size() > 1;
112 for (const auto & pair : pairs)
113 {
114 const auto & [primary_boundary, secondary_boundary] = pair;
115 const std::string suffix = multiple_pairs ? pairSuffix(pair) : "";
116 const std::string primary_name = _prefix + "_primary_subdomain" + suffix;
117 const std::string secondary_name = _prefix + "_secondary_subdomain" + suffix;
118
120 *mesh, {primary_boundary}, MooseMeshUtils::getNextFreeSubdomainID(*mesh), primary_name);
122 *mesh, {secondary_boundary}, MooseMeshUtils::getNextFreeSubdomainID(*mesh), secondary_name);
123 }
124
125 return mesh;
126}
127
128std::string
129AutomaticContactPairingGenerator::pairSuffix(const std::pair<BoundaryName, BoundaryName> & pair)
130{
131 // Labeling each boundary keeps the suffix unambiguous when boundary names contain underscores,
132 // e.g. (top_left, bottom) -> _p_top_left_s_bottom and (top, left_bottom) -> _p_top_s_left_bottom.
133 // The single-letter labels keep generated names short, since exodus output truncates names to 32
134 // characters by default
135 return "_p_" + pair.first + "_s_" + pair.second;
136}
137
138std::vector<AutomaticContactPairingGenerator::CandidateBoundary>
140{
141 // A boundary listed more than once, either repeated or as both a name and an ID, would otherwise
142 // be paired with itself
143 std::vector<CandidateBoundary> candidates;
144 for (const auto & bname : _pairing_boundaries)
145 {
147 const auto it =
148 std::find_if(candidates.begin(),
149 candidates.end(),
150 [bid](const CandidateBoundary & candidate) { return candidate.id == bid; });
151 if (it != candidates.end())
152 {
153 if (it->name == bname)
154 mooseError("Boundary '",
155 bname,
156 "' is listed more than once in 'automatic_pairing_boundaries'. Each boundary "
157 "may be listed only once.");
158 else
159 mooseError("Boundaries '",
160 it->name,
161 "' and '",
162 bname,
163 "' in 'automatic_pairing_boundaries' refer to the same boundary (ID ",
164 bid,
165 "). Each boundary may be listed only once.");
166 }
167 candidates.push_back({bname, bid, 0, Point(0, 0, 0)});
168 }
169
170 // Accumulate the area and area-weighted centroid of each candidate sideset. On a distributed
171 // mesh, each process accumulates over the sides of its local elements so that ghosted sides are
172 // counted once, and the partial sums are then summed across processes
173 const bool serial = mesh.is_serial();
174 libMesh::ElemSideBuilder side_builder;
175 for (const auto & [eid, side, bid] : mesh.get_boundary_info().build_side_list())
176 {
177 const auto it = std::find_if(candidates.begin(),
178 candidates.end(),
179 [bid = bid](const CandidateBoundary & candidate)
180 { return candidate.id == bid; });
181 if (it == candidates.end())
182 continue;
183
184 const Elem & elem = *mesh.elem_ptr(eid);
185 if (!serial && elem.processor_id() != mesh.processor_id())
186 continue;
187
188 const Elem & side_elem = side_builder(elem, side);
189 const Real area = side_elem.volume();
190 it->area += area;
191 it->centroid += side_elem.true_centroid() * area;
192 }
193
194 if (!serial)
195 {
196 // Pack the areas and weighted centroids so that a single reduction suffices
197 std::vector<Real> sums;
198 sums.reserve(candidates.size() * (1 + Moose::dim));
199 for (const auto & candidate : candidates)
200 {
201 sums.push_back(candidate.area);
202 for (const auto d : make_range(Moose::dim))
203 sums.push_back(candidate.centroid(d));
204 }
205 mesh.comm().sum(sums);
206
207 auto sum_it = sums.begin();
208 for (auto & candidate : candidates)
209 {
210 candidate.area = *sum_it++;
211 for (const auto d : make_range(Moose::dim))
212 candidate.centroid(d) = *sum_it++;
213 }
214 }
215
216 for (auto & candidate : candidates)
217 if (candidate.area > 0)
218 candidate.centroid /= candidate.area;
219
220 return candidates;
221}
222
223std::pair<BoundaryName, BoundaryName>
225 const CandidateBoundary & b)
226{
227 // The larger surface is chosen as primary, which places the Lagrange multipliers on the smaller
228 // surface and reduces the number of secondary elements that are only partially covered by the
229 // primary surface. Areas that agree to within libMesh's relative TOLERANCE (e.g. matching
230 // surfaces whose computed areas differ only by roundoff) fall back to the larger boundary ID so
231 // that the assignment is deterministic
232 const Real area_tol = TOLERANCE * std::max(a.area, b.area);
233 const bool a_is_primary = std::abs(a.area - b.area) > area_tol ? a.area > b.area : a.id > b.id;
234 if (a_is_primary)
235 return {a.name, b.name};
236 else
237 return {b.name, a.name};
238}
239
240std::vector<std::pair<BoundaryName, BoundaryName>>
242{
243 const auto candidates = candidateBoundaries(mesh);
244 const auto find_candidate = [&candidates](const BoundaryID bid) -> const CandidateBoundary &
245 {
246 const auto it =
247 std::find_if(candidates.begin(),
248 candidates.end(),
249 [bid](const CandidateBoundary & candidate) { return candidate.id == bid; });
250 mooseAssert(it != candidates.end(), "Boundary not in candidate list");
251 return *it;
252 };
253
254 // Collect nodes on candidate boundaries, from both sidesets and nodesets. On a distributed mesh,
255 // each process contributes the boundary nodes it owns, which it sees together with all of their
256 // boundary sides through the default point-neighbor ghosting, and the lists are then gathered so
257 // that every process searches the complete set
258 const bool serial = mesh.is_serial();
259 std::vector<NodeBoundaryIDInfo> node_bid_list;
260 for (const auto & candidate : candidates)
261 for (const auto node_id : MooseMeshUtils::getBoundaryNodes(mesh, candidate.id))
262 {
263 const Node & node = mesh.node_ref(node_id);
264 if (serial || node.processor_id() == mesh.processor_id())
265 node_bid_list.emplace_back(node, candidate.id);
266 }
267
268 if (!serial)
269 mesh.comm().allgather(node_bid_list);
270
271 // Sort by boundary id for deterministic ordering
272 std::sort(node_bid_list.begin(),
273 node_bid_list.end(),
274 [](const NodeBoundaryIDInfo & a, const NodeBoundaryIDInfo & b)
275 { return a.second < b.second; });
276
277 using KDTreeType = nanoflann::KDTreeSingleIndexAdaptor<
278 nanoflann::L2_Simple_Adaptor<Real, PointListAdaptor<NodeBoundaryIDInfo>, Real, std::size_t>,
281 std::size_t>;
282
283 const unsigned int max_leaf_size = 20;
284 auto point_list =
285 PointListAdaptor<NodeBoundaryIDInfo>(node_bid_list.begin(), node_bid_list.end());
286 auto kd_tree = std::make_unique<KDTreeType>(
287 Moose::dim, point_list, nanoflann::KDTreeSingleIndexAdaptorParams(max_leaf_size));
288 kd_tree->buildIndex();
289
290 nanoflann::SearchParameters search_params;
291 std::vector<nanoflann::ResultItem<std::size_t, Real>> ret_matches;
292 std::vector<std::pair<BoundaryName, BoundaryName>> pairs;
293
294 for (const auto & [entry_node, entry_bid] : node_bid_list)
295 {
296 ret_matches.clear();
297 kd_tree->radiusSearch(
298 &entry_node(0), _pairing_distance * _pairing_distance, ret_matches, search_params);
299
300 for (const auto & [pair_idx, _] : ret_matches)
301 {
302 const auto & [pair_node, pair_bid] = node_bid_list[pair_idx];
303 libmesh_ignore(pair_node);
304
305 // Skip nodes on the same boundary
306 if (pair_bid == entry_bid)
307 continue;
308
309 pairs.push_back(orderPair(find_candidate(entry_bid), find_candidate(pair_bid)));
310 }
311 }
312
314 return pairs;
315}
316
317std::vector<std::pair<BoundaryName, BoundaryName>>
319{
320 const auto candidates = candidateBoundaries(mesh);
321 for (const auto & candidate : candidates)
322 if (candidate.area == 0)
323 mooseError("Boundary '", candidate.name, "' not found in mesh.");
324
325 // Find all pairs within distance
326 std::vector<std::pair<BoundaryName, BoundaryName>> pairs;
327 for (const auto i : index_range(candidates))
328 for (const auto j : make_range(i + 1, candidates.size()))
329 if ((candidates[i].centroid - candidates[j].centroid).norm() <= _pairing_distance)
330 pairs.push_back(orderPair(candidates[i], candidates[j]));
331
333 return pairs;
334}
335
336void
338 std::vector<std::pair<BoundaryName, BoundaryName>> & pairs)
339{
340 std::sort(pairs.begin(), pairs.end());
341 pairs.erase(std::unique(pairs.begin(), pairs.end()), pairs.end());
342}
registerMooseObject("ContactApp", AutomaticContactPairingGenerator)
std::pair< Point, BoundaryID > NodeBoundaryIDInfo
boundary_id_type BoundaryID
Detects contact surface pairs from a list of candidate boundaries using proximity,...
const Real _pairing_distance
Maximum center-to-center or node-to-node distance for pairing.
static std::pair< BoundaryName, BoundaryName > orderPair(const CandidateBoundary &a, const CandidateBoundary &b)
Order two paired boundaries as (primary, secondary), with the larger surface as primary.
static std::string pairSuffix(const std::pair< BoundaryName, BoundaryName > &pair)
Name suffix identifying a (primary, secondary) boundary pair, used to build unique object names when ...
std::vector< std::pair< BoundaryName, BoundaryName > > findPairsCentroid(const MeshBase &mesh) const
Find pairs by sideset centroid distance; returns deduplicated pairs.
std::vector< std::pair< BoundaryName, BoundaryName > > findPairsNodeProximity(const MeshBase &mesh) const
Find pairs by node-proximity KD-tree search; returns deduplicated pairs.
const bool _create_lower_d_blocks
Whether to create lower-dimensional subdomain blocks for each detected pair.
AutomaticContactPairingGenerator(const InputParameters &parameters)
static constexpr auto contact_pairs_property
Name of the mesh meta-data holding the detected contact pairs.
const std::vector< BoundaryName > & _pairing_boundaries
Candidate boundaries to pair.
static void removeDuplicatePairs(std::vector< std::pair< BoundaryName, BoundaryName > > &pairs)
const MooseEnum & _pairing_method
Pairing method: NODE or CENTROID.
std::vector< std::pair< std::string, std::string > > ContactPairs
Type of the mesh meta-data holding the detected (primary, secondary) boundary name pairs.
std::vector< CandidateBoundary > candidateBoundaries(const MeshBase &mesh) const
Resolve the candidate boundary names to boundary IDs, erroring if any boundary is listed more than on...
std::unique_ptr< MeshBase > generate() override
const std::string & _prefix
Prefix prepended to the names of generated subdomain blocks.
void addRequiredParam(const std::string &name, const std::string &doc_string)
void addClassDescription(const std::string &doc_string)
static InputParameters validParams()
void mooseError(Args &&... args) const
const Point & getPoint(const PointObject &item) const
MeshBase & mesh
void createSubdomainFromSidesets(MeshBase &mesh, std::vector< BoundaryName > boundary_names, const SubdomainID new_subdomain_id, const SubdomainName &new_subdomain_name)
std::set< dof_id_type > getBoundaryNodes(const MeshBase &mesh, const BoundaryID boundary_id)
SubdomainID getNextFreeSubdomainID(MeshBase &input_mesh)
BoundaryID getBoundaryID(const BoundaryName &boundary_name, const MeshBase &mesh)
static constexpr std::size_t dim
std::string stringify(const T &t)
std::pair< T, U > ResultItem
SearchParams SearchParameters
A candidate contact boundary with its sideset area and area-weighted centroid.