preCICE v3.2.0
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
CreateClustering.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <Eigen/Core>
4
5#include <algorithm>
6
7#include "math/math.hpp"
8#include "mesh/Mesh.hpp"
10#include "query/Index.hpp"
11// required for the squared distance computation
13
15
17
23// Internal helper functions
24namespace {
25
37{
38 return (ids[0] * nCells[1] + ids[1]) * nCells[2] + ids[2];
39}
40
41// Overload to handle the offsets (via ints) properly
43{
44 return (ids[0] * nCells[1] + ids[1]) * nCells[2] + ids[2];
45}
46
54template <int dim>
55std::array<int, math::pow_int<dim>(3) - 1> zCurveNeighborOffsets(std::array<unsigned int, 3> nCells);
56
58template <>
59std::array<int, 8> zCurveNeighborOffsets<2>(std::array<unsigned int, 3> nCells)
60{
61 // All neighbor directions
62 const std::array<std::array<int, 3>, 8> neighbors2DIndex = {{{-1, -1, 0}, {-1, 0, 0}, {-1, 1, 0}, {0, -1, 0}, {0, 1, 0}, {1, -1, 0}, {1, 0, 0}, {1, 1, 0}}};
63 std::array<int, 8> neighbors2D{{}};
64 // Uses the int overload of the zCurve
65 // @todo: mark the function as constexpr, when moving to C++20 (transform is non-constexpr)
66 std::transform(neighbors2DIndex.begin(), neighbors2DIndex.end(), neighbors2D.begin(), [&nCells](auto &v) { return zCurve(v, nCells); });
67
68 return neighbors2D;
69}
70
72template <>
73std::array<int, 26> zCurveNeighborOffsets<3>(std::array<unsigned int, 3> nCells)
74{
75 // All neighbor directions
76 const std::array<std::array<int, 3>, 26> neighbors3DIndex = {{{-1, -1, -1}, {-1, -1, 0}, {-1, -1, 1}, {-1, 0, 0}, {-1, 0, 1}, {-1, 0, -1}, {-1, 1, 0}, {-1, 1, 1}, {-1, 1, -1}, {0, -1, -1}, {0, -1, 0}, {0, -1, 1}, {0, 0, 1}, {0, 0, -1}, {0, 1, 0}, {0, 1, 1}, {0, 1, -1}, {1, -1, -1}, {1, -1, 0}, {1, -1, 1}, {1, 0, 0}, {1, 0, 1}, {1, 0, -1}, {1, 1, 0}, {1, 1, 1}, {1, 1, -1}}};
77 std::array<int, 26> neighbors3D{{}};
78 // Uses the int overload of the zCurve
79 std::transform(neighbors3DIndex.begin(), neighbors3DIndex.end(), neighbors3D.begin(), [&nCells](auto &v) { return zCurve(v, nCells); });
80
81 return neighbors3D;
82}
96template <typename ArrayType>
97std::vector<VertexID> getNeighborsWithinSphere(const Vertices &vertices, VertexID centerID, double radius, const ArrayType neighborOffsets)
98{
99 // number of neighbors = (3^dim)-1
100 static_assert((neighborOffsets.size() == 26) || (neighborOffsets.size() == 8));
102 for (auto off : neighborOffsets) {
103 // corner case where we have 'dead axis' in the bounding box
104 if (off == 0) {
105 continue;
106 }
107 // compute neighbor ID
108 // @todo target type should be an unsigned int
109 PRECICE_ASSERT(off != 0);
110 // Assumption, vertex position in the container == vertex ID
111 VertexID neighborID = centerID + off;
112 // We might run out of the array bounds along the edges, so we only consider vertices inside
113 if (neighborID >= 0 && neighborID < static_cast<int>(vertices.size())) {
114 auto &neighborVertex = vertices[neighborID];
115 auto &center = vertices[centerID];
116 if (!neighborVertex.isTagged() && (computeSquaredDifference(center.rawCoords(), neighborVertex.rawCoords()) < math::pow_int<2>(radius))) {
117 result.emplace_back(neighborID);
118 }
119 }
120 }
121 return result;
122}
123
132void tagEmptyClusters(Vertices &clusterCenters, double clusterRadius, mesh::PtrMesh mesh)
133{
134 // Alternative implementation: mesh->index().getVerticesInsideBox() == 0
135 std::for_each(clusterCenters.begin(), clusterCenters.end(), [&](auto &v) {
136 if (!v.isTagged() && !mesh->index().isAnyVertexInsideBox(v, clusterRadius)) {
137 v.tag();
138 }
139 });
140}
141
150void projectClusterCentersToinputMesh(Vertices &clusterCenters, mesh::PtrMesh mesh)
151{
152 std::transform(clusterCenters.begin(), clusterCenters.end(), clusterCenters.begin(), [&](auto &v) {
153 if (!v.isTagged()) {
154 auto closestCenter = mesh->index().getClosestVertex(v.getCoords()).index;
155 return mesh::Vertex{mesh->vertex(closestCenter).getCoords(), v.getID()};
156 } else {
157 return v;
158 }
159 });
160}
161
173template <int dim>
174void tagDuplicateCenters(Vertices &centers, std::array<unsigned int, 3> nCells, double threshold)
175{
176 PRECICE_ASSERT(threshold >= 0);
177 if (centers.empty())
178 return;
179
180 // Get the index offsets for the z curve
181 auto neighborOffsets = zCurveNeighborOffsets<dim>(nCells);
182 static_assert((neighborOffsets.size() == 8 && dim == 2) || (neighborOffsets.size() == 26 && dim == 3));
183
184 // For the following to work, the vertexID has to correspond to the position in the centers
185 PRECICE_ASSERT(std::all_of(centers.begin(), centers.end(), [idx = 0](auto &v) mutable { return v.getID() == idx++; }));
186 // we check all neighbors
187 for (auto &v : centers) {
188 if (!v.isTagged()) {
189 auto ids = getNeighborsWithinSphere(centers, v.getID(), threshold, neighborOffsets);
190 // @todo check more selective which one to remove
191 if (ids.size() > 0)
192 v.tag();
193 }
194 }
195}
196
205void removeTaggedVertices(Vertices &container)
206{
207 container.erase(std::remove_if(container.begin(), container.end(), [](auto &v) { return v.isTagged(); }), container.end());
208}
209} // namespace
210
223inline double estimateClusterRadius(unsigned int verticesPerCluster, mesh::PtrMesh inMesh, const mesh::BoundingBox &bb)
224{
225 // Step 1: Generate random samples from the input mesh
226 std::vector<VertexID> randomSamples;
227
228 PRECICE_ASSERT(!bb.isDefault(), "Invalid bounding box.");
229 // All samples are 'moved' to the closest vertex from the input mesh in order
230 // to avoid empty clusters when we have shell-like meshes (as in surface coupling)
231 // The first sample: the vertex closest to the bounding box center
232 const auto bbCenter = bb.center();
233 randomSamples.emplace_back(inMesh->index().getClosestVertex(bbCenter).index);
234 // Now we place samples for each space dimension above and below the bounding box center
235 // and look for the closest vertex in the input mesh
236 // In 2D the sampling would like this
237 // +---------------------+
238 // | x |
239 // | |
240 // | x c x |
241 // | |
242 // | x |
243 // +---------------------+
244
245 for (int d = 0; d < inMesh->getDimensions(); ++d) {
246 auto sample = bbCenter;
247 // Lower as the center
248 sample[d] -= bb.getEdgeLength(d) * 0.25;
249 randomSamples.emplace_back(inMesh->index().getClosestVertex(sample).index);
250 // Higher than the center
251 sample[d] += bb.getEdgeLength(d) * 0.5;
252 randomSamples.emplace_back(inMesh->index().getClosestVertex(sample).index);
253 }
254
255 // Step 2: Compute the radius of the randomSamples ('centers'), which would have verticesPerCluster vertices
256 std::vector<double> sampledClusterRadii;
257 for (auto s : randomSamples) {
258 // ask the index tree for the k-nearest neighbors in order to estimate the point density
259 auto kNearestVertexIDs = inMesh->index().getClosestVertices(inMesh->vertex(s).getCoords(), verticesPerCluster);
260 // compute the distance of each point to the center
261 std::vector<double> squaredRadius(kNearestVertexIDs.size());
262 std::transform(kNearestVertexIDs.begin(), kNearestVertexIDs.end(), squaredRadius.begin(), [&inMesh, s](auto i) {
263 return computeSquaredDifference(inMesh->vertex(i).rawCoords(), inMesh->vertex(s).rawCoords());
264 });
265 // Store the maximum distance
266 auto maxRadius = std::max_element(squaredRadius.begin(), squaredRadius.end());
267 sampledClusterRadii.emplace_back(std::sqrt(*maxRadius));
268 }
269
270 // Step 3: Sort (using nth_element) the sampled radii and select the median as cluster radius
271 PRECICE_ASSERT(sampledClusterRadii.size() % 2 != 0, "Median calculation is only valid for odd number of elements.");
272 PRECICE_ASSERT(sampledClusterRadii.size() > 0);
273 unsigned int middle = sampledClusterRadii.size() / 2;
274 std::nth_element(sampledClusterRadii.begin(), sampledClusterRadii.begin() + middle, sampledClusterRadii.end());
275 double clusterRadius = sampledClusterRadii[middle];
276
277 return clusterRadius;
278}
279
303 double relativeOverlap, unsigned int verticesPerCluster,
304 bool projectClustersToInput)
305{
306 precice::logging::Logger _log{"impl::createClustering"};
308 PRECICE_ASSERT(relativeOverlap < 1);
309 PRECICE_ASSERT(verticesPerCluster > 0);
310 PRECICE_ASSERT(inMesh->getDimensions() == outMesh->getDimensions());
311
312 // If we have either no input or no output vertices, we return immediately
313 if (inMesh->empty() || (outMesh->empty() && !outMesh->isJustInTime())) {
314 return {double{}, Vertices{}};
315 }
316
317 PRECICE_ASSERT(!inMesh->empty());
318 PRECICE_ASSERT(!outMesh->empty() || outMesh->isJustInTime());
319 // startGridAtEdge boolean switch in order to decide either to start the clustering at the edge of the bounding box in each direction
320 // (true) or start the clustering inside the bounding box (edge + 0.5 radius). The latter approach leads to fewer clusters,
321 // but might result in a worse clustering
322 const bool startGridAtEdge = projectClustersToInput;
323
324 PRECICE_DEBUG("Relative overlap: {}", relativeOverlap);
325 PRECICE_DEBUG("Vertices per cluster: {}", verticesPerCluster);
326
327 // Step 1: Compute the local bounding box of the input mesh manually
328 // Note that we don't use the corresponding bounding box functions from
329 // precice::mesh (e.g. ::getBoundingBox), as the stored bounding box might
330 // have the wrong size (e.g. direct access)
331 // @todo: Which mesh should be used in order to determine the cluster centers:
332 // pro outMesh: we want perfectly fitting clusters around our output vertices
333 // however, this makes the cluster distribution/mapping dependent on the output
334 precice::mesh::BoundingBox localBB = inMesh->index().getRtreeBounds();
335
336#ifndef NDEBUG
337 // Safety check
338 precice::mesh::BoundingBox bb_check(inMesh->getDimensions());
339 for (const mesh::Vertex &vertex : inMesh->vertices()) {
340 bb_check.expandBy(vertex);
341 }
342 PRECICE_ASSERT(bb_check == localBB);
343#endif
344
345 // If we have very few vertices in the domain, (in this case twice our cluster
346 // size as we decompose most probably at least in 4 clusters) we just use a
347 // single cluster. The clustering result of the algorithm further down is in
348 // this case not optimal and might lead to too many clusters.
349 // The single cluster has in principle a radius of inf. We use here twice the
350 // length of the longest bounding box edge length and the center of the bounding
351 // box for the center point.
352 if (inMesh->nVertices() < verticesPerCluster * 2) {
353 double cRadius = localBB.longestEdgeLength();
354 if (cRadius == 0) {
355 PRECICE_WARN("Determining a cluster radius failed. This is most likely a result of a coupling mesh consisting of just a single vertex. Setting the cluster radius to 1.");
356 cRadius = 1;
357 }
358 return {cRadius * 2, Vertices{mesh::Vertex({localBB.center(), 0})}};
359 }
360 // We define a convenience alias for the localBB. In case we need to synchronize the clustering across ranks later on, we need
361 // to work with the global bounding box of the whole domain.
362 auto globalBB = localBB;
363
364 // Step 2: Now we pick random samples from the input mesh and ask the index tree for the k-nearest neighbors
365 // in order to estimate the point density and determine a proper cluster radius (see also the function documentation)
366 double clusterRadius = estimateClusterRadius(verticesPerCluster, inMesh, localBB);
367 PRECICE_DEBUG("Vertex cluster radius: {}", clusterRadius);
368
369 // maximum distance between cluster centers lying diagonal to each other. The maximum distance takes the overlap condition into
370 // account: example for 2D: if the distance between the centers is sqrt( 4 / 2 ) * radius, we violate the overlap condition between
371 // diagonal clusters
372 const int inDim = inMesh->getDimensions();
373 const double maximumCenterDistance = std::sqrt(4. / inDim) * clusterRadius * (1 - relativeOverlap);
374
375 // Step 3: using the maximum distance and the bounding box, compute the number of clusters in each direction
376 // we ceil the number of clusters in order to guarantee the desired overlap
377 std::array<unsigned int, 3> nClustersGlobal{1, 1, 1};
378 for (int d = 0; d < globalBB.getDimension(); ++d)
379 nClustersGlobal[d] = std::ceil(std::max(1., globalBB.getEdgeLength(d) / maximumCenterDistance));
380
381 // Step 4: Determine the centers of the clusters
382 Vertices centers;
383 // Vector used to temporarily store each center coordinates
384 std::vector<double> centerCoords(inDim);
385 // Vector storing the distances between the cluster in each direction
386 std::vector<double> distances(inDim);
387 // Vector storing the starting coordinates in each direction
388 std::vector<double> start(inDim);
389 // Fill the constant vectos, i.e., the distances and the start
390 for (unsigned int d = 0; d < distances.size(); ++d) {
391 // this distance calculation guarantees that we have at least the minimal specified overlap, as we ceil the division for the number of clusters
392 // as an alternative, once could use distance = const = maximumCenterDistance, which might lead to better results when projecting and removing duplicates (?)
393 distances[d] = globalBB.getEdgeLength(d) / (nClustersGlobal[d]);
394 start[d] = globalBB.minCorner()(d);
395 }
396 PRECICE_DEBUG("Distances: {}", distances);
397
398 // Step 5: Take care of the starting layer: if we start the grid at the globalBB edge we have an additional layer in each direction
399 if (startGridAtEdge) {
400 for (int d = 0; d < inDim; ++d) {
401 if (globalBB.getEdgeLength(d) > math::NUMERICAL_ZERO_DIFFERENCE) {
402 nClustersGlobal[d] += 1;
403 }
404 }
405 } // If we don't start at the edge, we shift the starting layer into the middle by 0.5 * distance
406 else {
407 std::transform(start.begin(), start.end(), distances.begin(), start.begin(), [](auto s, auto d) { return s + 0.5 * d; });
408 }
409
410 // Print some information
411 PRECICE_DEBUG("Global cluster distribution: {}", nClustersGlobal);
412 unsigned int nTotalClustersGlobal = std::accumulate(nClustersGlobal.begin(), nClustersGlobal.end(), 1U, std::multiplies<unsigned int>());
413 PRECICE_DEBUG("Global number of total clusters (tentative): {}", nTotalClustersGlobal);
414
415 // Step 6: transform the global metrics (number of clusters and starting layer) into local ones
416 // Since the local and global bounding boxes are the same in the current implementation, the
417 // global metrics and the local metrics will be the same.
418 // First, we need to update the starting points of our clustering scheme
419 std::array<unsigned int, 3> nClustersLocal{1, 1, 1};
420 for (unsigned int d = 0; d < start.size(); ++d) {
421 // Exclude the case where we have no distances (nClustersGlobal[d] == 1 )
422 if (distances[d] > 0) {
423 start[d] += std::ceil(((localBB.minCorner()(d) - start[d]) / distances[d]) - math::NUMERICAL_ZERO_DIFFERENCE) * distances[d];
424 // One cluster for the starting point and a further one for each distance we can fit into the BB
425 nClustersLocal[d] = 1 + std::floor(((localBB.maxCorner()(d) - start[d]) / distances[d]) + math::NUMERICAL_ZERO_DIFFERENCE);
426 }
427 }
428
429 unsigned int nTotalClustersLocal = std::accumulate(nClustersLocal.begin(), nClustersLocal.end(), 1U, std::multiplies<unsigned int>());
430 centers.resize(nTotalClustersLocal, mesh::Vertex({centerCoords, -1}));
431
432 // Print some information
433 PRECICE_DEBUG("Local cluster distribution: {}", nClustersLocal);
434 PRECICE_DEBUG("Local number of total clusters (tentative): {}", nTotalClustersLocal);
435
436 // Step 7: fill the center container using the zCurve for indexing. For the further processing, it is also important to ensure
437 // that the ID within the center Vertex class coincides with the position of the center vertex in the container.
438 // We start with the (bottom left) corner and iterate over all dimension
439 PRECICE_ASSERT(inDim >= 2);
440 if (inDim == 2) {
441 centerCoords[0] = start[0];
442 for (unsigned int x = 0; x < nClustersLocal[0]; ++x, centerCoords[0] += distances[0]) {
443 centerCoords[1] = start[1];
444 for (unsigned int y = 0; y < nClustersLocal[1]; ++y, centerCoords[1] += distances[1]) {
445 auto id = zCurve(std::array<unsigned int, 3>{{x, y, 0}}, nClustersLocal);
446 PRECICE_ASSERT(id < static_cast<int>(centers.size()) && id >= 0);
447 centers[id] = mesh::Vertex({centerCoords, id});
448 }
449 }
450 } else {
451 centerCoords[0] = start[0];
452 for (unsigned int x = 0; x < nClustersLocal[0]; ++x, centerCoords[0] += distances[0]) {
453 centerCoords[1] = start[1];
454 for (unsigned int y = 0; y < nClustersLocal[1]; ++y, centerCoords[1] += distances[1]) {
455 centerCoords[2] = start[2];
456 for (unsigned int z = 0; z < nClustersLocal[2]; ++z, centerCoords[2] += distances[2]) {
457 auto id = zCurve(std::array<unsigned int, 3>{{x, y, z}}, nClustersLocal);
458 PRECICE_ASSERT(id < static_cast<int>(centers.size()) && id >= 0);
459 centers[id] = mesh::Vertex({centerCoords, id});
460 }
461 }
462 }
463 }
464
465 // Step 8: tag vertices, which should be removed later on
466
467 // Step 8a: tag empty clusters
468 tagEmptyClusters(centers, clusterRadius, inMesh);
469 if (!outMesh->isJustInTime()) {
470 tagEmptyClusters(centers, clusterRadius, outMesh);
471 }
472 PRECICE_DEBUG("Number of non-tagged centers after empty clusters were filtered: {}", std::count_if(centers.begin(), centers.end(), [](auto &v) { return !v.isTagged(); }));
473
474 // Step 9: Move the cluster centers if desired
475 if (projectClustersToInput) {
476 projectClusterCentersToinputMesh(centers, inMesh);
477 // @todo: The duplication should probably be defined in terms of the target distance, not the actual distance. Find good default values
478 const auto duplicateThreshold = 0.4 * (*std::min_element(distances.begin(), distances.end()));
479 PRECICE_DEBUG("Tagging duplicates using the threshold value {} for the regular distances {}", duplicateThreshold, distances);
480 // usually inMesh->getDimensions() == 2
481 // to also cover the case where we have only a single layer in a 3D computation we use the number of clusters in z direction
482 // Step 8b or 9a: Moving cluster centers might lead to duplicate centers or centers being too close to each other. Therefore,
483 // we tag duplicate centers (see also the documentation of \ref tagDuplicateCenters ).
484 if (nClustersLocal[2] == 1) {
485 tagDuplicateCenters<2>(centers, nClustersLocal, duplicateThreshold);
486 } else {
487 tagDuplicateCenters<3>(centers, nClustersLocal, duplicateThreshold);
488 }
489 // the tagging here won't filter out a lot of clusters, but finding them anyway allows us to allocate the right amount of
490 // memory for the cluster vector in the mapping, which would otherwise be expensive
491 // we cannot hit any empty vertices in the inputmesh
492 if (!outMesh->isJustInTime()) {
493 tagEmptyClusters(centers, clusterRadius, outMesh);
494 }
495 PRECICE_DEBUG("Number of non-tagged centers after duplicate tagging : {}", std::count_if(centers.begin(), centers.end(), [](auto &v) { return !v.isTagged(); }));
496 }
497 PRECICE_ASSERT(std::all_of(centers.begin(), centers.end(), [idx = 0](auto &v) mutable { return v.getID() == idx++; }));
498
499 // Step 10: finally, remove the vertices from the center container
500 removeTaggedVertices(centers);
501 PRECICE_ASSERT(std::none_of(centers.begin(), centers.end(), [](auto &v) { return v.isTagged(); }));
502 PRECICE_CHECK(centers.size() > 0, "Too many vertices have been filtered out.");
503
504 return {clusterRadius, centers};
505}
506} // namespace precice::mapping::impl
#define PRECICE_WARN(...)
Definition LogMacros.hpp:12
#define PRECICE_DEBUG(...)
Definition LogMacros.hpp:61
#define PRECICE_TRACE(...)
Definition LogMacros.hpp:92
#define PRECICE_CHECK(check,...)
Definition LogMacros.hpp:32
T accumulate(T... args)
T all_of(T... args)
#define PRECICE_ASSERT(...)
Definition assertion.hpp:85
T begin(T... args)
T ceil(T... args)
This class provides a lightweight logger.
Definition Logger.hpp:17
An axis-aligned bounding box around a (partition of a) mesh.
Eigen::VectorXd maxCorner() const
the max corner of the bounding box
Eigen::VectorXd minCorner() const
the min corner of the bounding box
double longestEdgeLength() const
returns the maximum length of the bounding box in any dimension
void expandBy(const BoundingBox &otherBB)
Expand bounding box using another bounding box.
Eigen::VectorXd center() const
Returns the Center Of Gravity of the mesh.
double getEdgeLength(int axis) const
returns the edge length of a specific axis
Vertex of a mesh.
Definition Vertex.hpp:16
T count_if(T... args)
T emplace_back(T... args)
T end(T... args)
T floor(T... args)
T for_each(T... args)
T make_unique(T... args)
T max_element(T... args)
T max(T... args)
T min_element(T... args)
std::tuple< double, Vertices > createClustering(mesh::PtrMesh inMesh, mesh::PtrMesh outMesh, double relativeOverlap, unsigned int verticesPerCluster, bool projectClustersToInput)
Creates a clustering as a collection of Vertices (representing the cluster centers) and a cluster rad...
double estimateClusterRadius(unsigned int verticesPerCluster, mesh::PtrMesh inMesh, const mesh::BoundingBox &bb)
Computes an estimate for the cluster radius, which results in approximately verticesPerCluster vertic...
std::vector< mesh::Vertex > Vertices
double computeSquaredDifference(const std::array< double, 3 > &u, std::array< double, 3 > v, const std::array< bool, 3 > &activeAxis={{true, true, true}})
Deletes all dead directions from fullVector and returns a vector of reduced dimensionality.
int VertexID
Definition Types.hpp:13
T nth_element(T... args)
static precice::logging::Logger _log("precicec")
T remove_if(T... args)
T resize(T... args)
T size(T... args)
T sqrt(T... args)
T transform(T... args)