libMesh
point_locator_nanoflann.C
Go to the documentation of this file.
1 // The libMesh Finite Element Library.
2 // Copyright (C) 2002-2025 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
3 
4 // This library is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU Lesser General Public
6 // License as published by the Free Software Foundation; either
7 // version 2.1 of the License, or (at your option) any later version.
8 
9 // This library is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 // Lesser General Public License for more details.
13 
14 // You should have received a copy of the GNU Lesser General Public
15 // License along with this library; if not, write to the Free Software
16 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 
18 
19 #include "libmesh/libmesh_config.h"
20 
21 // This class is not defined unless libmesh is built with Nanoflann support
22 #ifdef LIBMESH_HAVE_NANOFLANN
23 
24 // libmesh includes
25 #include "libmesh/point_locator_nanoflann.h"
26 #include "libmesh/elem.h"
27 #include "libmesh/libmesh_logging.h"
28 #include "libmesh/mesh_base.h"
29 #include "libmesh/mesh_tools.h"
30 #include "libmesh/int_range.h" // make_range
31 
32 // C++ includes
33 #include <array>
34 #include <numeric> // std::iota
35 #include <algorithm> // std::sort
36 
37 namespace libMesh
38 {
39 
41  const PointLocatorBase * master) :
42  PointLocatorBase (mesh, master),
43  _out_of_mesh_mode(false),
44  _num_results(8)
45 {
46  this->init();
47 }
48 
49 
50 
52 
53 
54 
55 void
57 {
58  this->_initialized = false;
59  this->_out_of_mesh_mode = false;
60 
61  // reset() actually frees the memory if we are master, otherwise it
62  // just reduces the ref. count.
63  _elems.reset();
64  _point_cloud.reset();
65  _kd_tree.reset();
66 }
67 
68 
69 
70 void
72 {
73  LOG_SCOPE("init()", "PointLocatorNanoflann");
74 
75  if (!_initialized)
76  {
77  // If _master == nullptr, then we _are_ the master, and thus
78  // responsible for initializing.
79  bool we_are_master = (_master == nullptr);
80 
81  // If we are the master PointLocator, fill in the _point_cloud
82  // data structure with active, local element vertex averages.
83  if (we_are_master)
84  {
85  _elems = std::make_shared<std::vector<const Elem *>>();
86  _point_cloud = std::make_shared<std::vector<Point>>();
87 
88  // Make the KD-Tree out of active element vertex averages.
89  //
90  // Note: we use active elements rather than active+local
91  // elements since we sometimes need to be able to locate
92  // points in ghosted elements (for example in Periodic BCs).
93  // Active elements are also natural to use in the
94  // DistributedMesh case. The size of the KD-Tree on each
95  // processor therefore scales with the number of elements on
96  // each processor in either case.
97  //
98  // Note 2: the approximate amount of space we should reserve
99  // here is going to be different for ReplicatedMesh
100  // (n_active_elem()) vs. DistributedMesh (n_active_local_elem())
101  // so we just let the containers resize themselves
102  // automatically.
103  for (const auto & elem : _mesh.active_element_ptr_range())
104  {
105  _elems->push_back(elem);
106  _point_cloud->push_back(elem->vertex_average());
107  }
108 
109  // Construct the KD-Tree
110  _kd_tree = std::make_shared<kd_tree_t>
111  (LIBMESH_DIM, *this, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
112 
113  _kd_tree->buildIndex();
114  }
115  else // we are not master
116  {
117  // Cast the _master to the appropriate type, and point to
118  // its data arrays.
119  const auto my_master =
120  cast_ptr<const PointLocatorNanoflann *>(this->_master);
121 
122  // Point our data structures at the master's
123  _elems = my_master->_elems;
124  _point_cloud = my_master->_point_cloud;
125  _kd_tree = my_master->_kd_tree;
126  }
127 
128  // We are initialized now
129  this->_initialized = true;
130  }
131 }
132 
133 nanoflann::KNNResultSet<Real>
135  std::size_t num_results) const
136 {
137  // We are searching for the Point(s) closest to Point p.
138  //
139  // TODO: The kd_tree's findNeighbors() routine needs a pointer to
140  // Real of length LIBMESH_DIM. It might be convenient if libMesh
141  // Points had a data() member that provided this, for now we just
142  // copy the coordinates into a std::array of the appropriate size.
143  std::array<Real, LIBMESH_DIM> query_pt;
144  for (int i=0; i<LIBMESH_DIM; ++i)
145  query_pt[i] = p(i);
146 
147  // Allocate storage for the indices and distances
148  _ret_index.resize(num_results);
149  _out_dist_sqr.resize(num_results);
150 
151  // nanoflann::KNNResultSet cannot be resized/reused easily, I think
152  // it is just meant to be re-created for each search.
153  nanoflann::KNNResultSet<Real> result_set(num_results);
154 
155  // Initialize the result_set
156  result_set.init(_ret_index.data(), _out_dist_sqr.data());
157 
158  // Do the search
159  // We leave all the SearchParams() ctor args on their default values
160  // (note that the first arg is ignored anyway).
161  _kd_tree->findNeighbors(result_set, query_pt.data(), nanoflann::SearchParameters());
162 
163  return result_set;
164 }
165 
166 
167 
168 const Elem *
170  const std::set<subdomain_id_type> * allowed_subdomains) const
171 {
173 
174  LOG_SCOPE("operator()", "PointLocatorNanoflann");
175 
176  // Keep track of the number of elements checked in detail. This can
177  // be useful for determining an optimal _num_results for
178  // specific applications, by looking at the average and best/worse
179  // case of each linear search.
180  unsigned int n_elems_checked = 0;
181 
182  // If a containing Elem is found locally, we will set this pointer.
183  const Elem * found_elem = nullptr;
184 
185  auto result_set = this->kd_tree_find_neighbors(p, _num_results);
186 
187  // The results from Nanoflann are already sorted by (squared)
188  // distance, but within that list of results, there may be some
189  // vertex averages which are equidistant from the searched-for
190  // Point. Therefore, we will now indirect_sort the results based on
191  // elem id, so that the lowest-id Elem from the class of Elems which
192  // are the same distance away from the search Point is always
193  // selected.
194 
195  // operator< comparison lambda used in the indirect sort. The
196  // inputs to this vector are indices of the result_set array.
197  auto comp = [this](std::size_t lhs, std::size_t rhs) -> bool
198  {
199  // First we sort by squared distance
200  if (_out_dist_sqr[lhs] < _out_dist_sqr[rhs])
201  return true;
202  if (_out_dist_sqr[rhs] < _out_dist_sqr[lhs])
203  return false;
204 
205  // If we made it here without returning, then the Points were
206  // equidistant, so we sort based on Elem id instead.
207  auto nanoflann_index_lhs = _ret_index[lhs];
208  auto elem_id_lhs = (*_elems)[nanoflann_index_lhs]->id();
209 
210  auto nanoflann_index_rhs = _ret_index[rhs];
211  auto elem_id_rhs = (*_elems)[nanoflann_index_rhs]->id();
212 
213  if (elem_id_lhs < elem_id_rhs)
214  return true;
215 
216  return false;
217  };
218 
219  // Set up the indices and do the indirect sort. The results are
220  // stored in _b so that _b[0] is the first result to check, _b[1]
221  // is second, etc.
222  _b.resize(result_set.size());
223  std::iota(_b.begin(), _b.end(), 0);
224  std::sort(_b.begin(), _b.end(), comp);
225 
226  // Note: we use result_set.size() here rather than
227  // _num_results, in case the result returns fewer than
228  // _num_results results!
229  for (auto r : make_range(result_set.size()))
230  {
231  // Translate the Nanoflann index, which is from [0..n_points),
232  // into the corresponding Elem id from the mesh. Note that we
233  // use index _b[r] instead of r, since we want to process the
234  // Elems in order based on Elem id.
235  auto nanoflann_index = _ret_index[_b[r]];
236  const Elem * candidate_elem = (*_elems)[nanoflann_index];
237 
238  // Debugging: print the results
239  // libMesh::err << "Vertex average/Elem id = " << candidate_elem->id()
240  // << ", dist^2 = " << _out_dist_sqr[r]
241  // << std::endl;
242 
243  // Before we even check whether the candidate Elem actually
244  // contains the Point, we may need to check whether the
245  // candidate Elem is from an allowed subdomain. If the
246  // candidate Elem is not from an allowed subdomain, we continue
247  // to the next one.
248  if (allowed_subdomains && !allowed_subdomains->count(candidate_elem->subdomain_id()))
249  {
250  // Debugging
251  // libMesh::err << "Elem " << candidate_elem->id() << " was not from an allowed subdomain, continuing search." << std::endl;
252  continue;
253  }
254 
255  // If we made it here, then the candidate Elem is from an
256  // allowed subdomain, so let's next check whether it contains
257  // the point. If the user set a custom tolerance, then we
258  // actually check close_to_point() rather than contains_point(),
259  // since this latter function warns about using non-default
260  // tolerances, but otherwise does the same test.
261  bool inside = _use_contains_point_tol ?
262  candidate_elem->close_to_point(p, _contains_point_tol) :
263  candidate_elem->contains_point(p);
264 
265  // Increment the number of elements checked
266  n_elems_checked++;
267 
268  // If the point is inside an Elem from an allowed subdomain, we are done.
269  if (inside)
270  {
271  // Debugging: report the number of Elems checked
272  // libMesh::err << "Checked " << n_elems_checked << " nearby Elems before finding a containing Elem." << std::endl;
273 
274  found_elem = candidate_elem;
275  break;
276  }
277  } // end for(r)
278 
279  // If we made it here, then at least one of the following happened:
280  // .) All the candidate elements were from non-allowed subdomains.
281  // .) The Point was not inside _any_ of the _num_results candidate Elems.
282  // Thus, if we are not in _out_of_mesh_mode, throw an error,
283  // otherwise return nullptr to indicate that no suitable element was
284  // found.
285  if (!_out_of_mesh_mode && !found_elem)
286  {
287  // Debugging: we are about to throw an error, but before we do,
288  // print information about the closest elements (by vertex average
289  // distance) that the Point was not found in.
290  // for (auto r : make_range(result_set.size()))
291  // {
292  // auto nanoflann_index = _ret_index[_b[r]];
293  // const Elem * candidate_elem = (*_elems)[nanoflann_index];
294  //
295  // libMesh::err << "Vertex average/Elem id = " << candidate_elem->id()
296  // << ", dist = " << std::sqrt(_out_dist_sqr[_b[r]])
297  // << std::endl;
298  // } // end for(r)
299 
300 
301  libmesh_error_msg("Point " << p << " was not contained within the closest " << n_elems_checked <<
302  " elems (by vertex average distance), and _out_of_mesh_mode was not enabled.");
303  }
304 
305  return found_elem;
306 }
307 
308 
309 void
311  std::set<const Elem *> & candidate_elements,
312  const std::set<subdomain_id_type> * allowed_subdomains) const
313 {
315 
316  LOG_SCOPE("operator() returning set", "PointLocatorNanoflann");
317 
318  // Make sure the output set is initially empty
319  candidate_elements.clear();
320 
321  // Keep track of the number of elements checked in detail
322  // unsigned int n_elems_checked = 0;
323 
324  // Do the KD-Tree search
325  auto result_set = this->kd_tree_find_neighbors(p, _num_results);
326 
327  for (auto r : make_range(result_set.size()))
328  {
329  // Translate the Nanoflann index, which is from [0..n_points),
330  // into the corresponding Elem id from the mesh.
331  auto nanoflann_index = _ret_index[r];
332  const Elem * candidate_elem = (*_elems)[nanoflann_index];
333 
334  // Before we even check whether the candidate Elem actually
335  // contains the Point, we may need to check whether the
336  // candidate Elem is from an allowed subdomain. If the
337  // candidate Elem is not from an allowed subdomain, we continue
338  // to the next one.
339  if (allowed_subdomains && !allowed_subdomains->count(candidate_elem->subdomain_id()))
340  continue;
341 
342  // If we made it here, then the candidate Elem is from an
343  // allowed subdomain, so let's next check whether it contains
344  // the point. If the user set a custom tolerance, then we
345  // actually check close_to_point() rather than contains_point(),
346  // since this latter function warns about using non-default
347  // tolerances, but otherwise does the same test.
348  bool inside = _use_contains_point_tol ?
349  candidate_elem->close_to_point(p, _contains_point_tol) :
350  candidate_elem->contains_point(p);
351 
352  // Increment the number of elements checked
353  // n_elems_checked++;
354 
355  // If the point is contained in/close to an Elem from an
356  // allowed subdomain, add it to the list.
357  if (inside)
358  candidate_elements.insert(candidate_elem);
359  } // end for(r)
360 
361  // Debugging: for performance reasons, it may be useful to print the
362  // number of Elems actually checked during the search.
363  // libMesh::err << "Checked " << n_elems_checked << " nearby Elems before finding a containing Elem." << std::endl;
364 }
365 
366 
367 void
369 {
370  // Out-of-mesh mode should now work properly even on meshes with
371  // non-affine elements.
372  _out_of_mesh_mode = true;
373 }
374 
375 
376 void
378 {
379  _out_of_mesh_mode = false;
380 }
381 
382 
383 
384 std::size_t
386 {
387  return _num_results;
388 }
389 
390 
391 
392 void
394 {
395  // Must request at least 1 result
396  _num_results = std::max(static_cast<std::size_t>(1), val);
397 }
398 
399 //
400 // Required Nanoflann APIs
401 //
402 
404 {
405  return _point_cloud->size();
406 }
407 
408 
409 
412  const std::size_t idx_p2,
413  std::size_t libmesh_dbg_var(size)) const
414 {
415  // We only consider LIBMESH_DIM dimensional KD-Trees, so just make
416  // sure we were called consistently.
417  libmesh_assert_equal_to(size, LIBMESH_DIM);
418 
419  // Construct a libmesh Point object from the LIBMESH_DIM-dimensional
420  // input object, p1.
421  Point point1;
422  for (int i=0; i<LIBMESH_DIM; ++i)
423  point1(i) = p1[i];
424 
425  // Compute Euclidean distance, squared
426  return (point1 - (*_point_cloud)[idx_p2]).norm_sq();
427 }
428 
429 
430 
432 PointLocatorNanoflann::kdtree_get_pt(const std::size_t idx, int dim) const
433 {
434  libmesh_assert_less (idx, _point_cloud->size());
435  libmesh_assert_less (dim, LIBMESH_DIM);
436 
437  return (*_point_cloud)[idx](dim);
438 }
439 
440 } // namespace libMesh
441 
442 #endif
nanoflann::KNNResultSet< Real > kd_tree_find_neighbors(const Point &p, std::size_t num_results) const
Helper function that wraps the call to the KDTree&#39;s findNeighbors() routine.
std::vector< std::size_t > _b
Vector of indices used by indirect sort.
coord_t kdtree_distance(const coord_t *p1, const std::size_t idx_p2, std::size_t size) const
Returns the squared distance between the vector (p1[0], p1[1], p1[2]) and the vertex average of Elem ...
bool _out_of_mesh_mode
true if out-of-mesh mode is enabled.
unsigned int dim
This is the base class from which all geometric element types are derived.
Definition: elem.h:94
bool _initialized
true when properly initialized, false otherwise.
MeshBase & mesh
virtual void init() override final
Initializes the locator, so that the operator() methods can be used.
std::size_t get_num_results() const
Set/get the number of results returned by each Nanoflann findNeighbors() search.
virtual const Elem * operator()(const Point &p, const std::set< subdomain_id_type > *allowed_subdomains=nullptr) const override final
Locates the element in which the point with global coordinates p is located, optionally restricted to...
The libMesh namespace provides an interface to certain functionality in the library.
void iota(ForwardIter first, ForwardIter last, T value)
Utility::iota was created back when std::iota was just an SGI STL extension.
Definition: utility.h:229
This is the MeshBase class.
Definition: mesh_base.h:75
std::size_t _num_results
The number of results returned by Nanoflann when operator() is called.
auto norm_sq() const -> decltype(std::norm(T()))
Definition: type_vector.h:926
virtual bool contains_point(const Point &p, Real tol=TOLERANCE) const
Definition: elem.C:2751
const MeshBase & _mesh
constant reference to the mesh in which the point is looked for.
libmesh_assert(ctx)
Real coord_t
Floating point type used for storing coordinates.
virtual ~PointLocatorNanoflann()
Destructor.
This is the base class for point locators.
std::size_t kdtree_get_point_count() const
Must return the number of data points.
std::vector< std::size_t > _ret_index
The operator() functions on PointLocator-derived classes are const, so to avoid re-allocating these r...
virtual bool close_to_point(const Point &p, Real tol) const
Definition: elem.C:2776
PointLocatorNanoflann(const MeshBase &mesh, const PointLocatorBase *master=nullptr)
Constructor.
subdomain_id_type subdomain_id() const
Definition: elem.h:2582
std::shared_ptr< kd_tree_t > _kd_tree
IntRange< T > make_range(T beg, T end)
The 2-parameter make_range() helper function returns an IntRange<T> when both input parameters are of...
Definition: int_range.h:140
const PointLocatorBase * _master
Const pointer to our master, initialized to nullptr if none given.
virtual void disable_out_of_mesh_mode() override final
Disables out-of-mesh mode (default).
coord_t kdtree_get_pt(const std::size_t idx, int dim) const
Returns the dim&#39;th component of the idx&#39;th vertex average.
std::shared_ptr< std::vector< const Elem * > > _elems
Lists of Points and ids which make up the "point cloud" that is to be searched via Nanoflann...
std::shared_ptr< std::vector< Point > > _point_cloud
A Point defines a location in LIBMESH_DIM dimensional Real space.
Definition: point.h:39
bool _use_contains_point_tol
true if we will use a user-specified tolerance for locating the element.
unsigned int idx(const ElemType type, const unsigned int nx, const unsigned int i, const unsigned int j)
A useful inline function which replaces the macros used previously.
virtual void enable_out_of_mesh_mode() override final
Enables out-of-mesh mode.
virtual void clear() override final
Restore to PointLocator to a just-constructed state.
Real _contains_point_tol
The tolerance to use when locating an element in the tree.