libMesh
mesh_smoother_test.C
Go to the documentation of this file.
1 #include <libmesh/boundary_info.h>
2 #include <libmesh/elem.h>
3 #include <libmesh/enum_elem_type.h>
4 #include <libmesh/function_base.h>
5 #include <libmesh/libmesh.h>
6 #include <libmesh/mesh_generation.h>
7 #include <libmesh/mesh_modification.h>
8 #include <libmesh/mesh_smoother_laplace.h>
9 #include <libmesh/mesh_smoother_vsmoother.h>
10 #include <libmesh/mesh_tools.h>
11 #include <libmesh/node.h>
12 #include <libmesh/reference_elem.h>
13 #include <libmesh/replicated_mesh.h>
14 #include <libmesh/system.h> // LIBMESH_HAVE_SOLVER define
15 #include "libmesh/face_tri.h"
16 #include "libmesh/utility.h"
17 #include "libmesh/enum_to_string.h"
18 
19 #include "test_comm.h"
20 #include "libmesh_cppunit.h"
21 
22 namespace
23 {
24 using namespace libMesh;
25 using Utility::pow;
26 
27 // Distortion function that doesn't distort boundary nodes
28 // 2D only, use for LaplaceMeshSmoother
29 class DistortSquare : public FunctionBase<Real>
30 {
31  std::unique_ptr<FunctionBase<Real>> clone() const override
32  {
33  return std::make_unique<DistortSquare>();
34  }
35 
36  Real operator()(const Point &, const Real = 0.) override
37  {
38  libmesh_not_implemented();
39  } // scalar-only API
40 
41  // Skew inward based on a cubic function
42  void operator()(const Point & p, const Real, DenseVector<Real> & output)
43  {
44  output.resize(3);
45  const Real eta = 2 * p(0) - 1;
46  const Real zeta = 2 * p(1) - 1;
47  output(0) = p(0) + (pow<3>(eta) - eta) * p(1) * (1 - p(1));
48  output(1) = p(1) + (pow<3>(zeta) - zeta) * p(0) * (1 - p(0));
49  output(2) = 0;
50  }
51 };
52 
53 // Distortion function supporting 1D, 2D, and 3D
54 // Use for VariationalMeshSmoother
55 class DistortHyperCube : public FunctionBase<Real>
56 {
57 public:
58  DistortHyperCube(const unsigned int dim) : _dim(dim) {}
59 
60 private:
61  std::unique_ptr<FunctionBase<Real>> clone() const override
62  {
63  return std::make_unique<DistortHyperCube>(_dim);
64  }
65 
66  Real operator()(const Point &, const Real = 0.) override { libmesh_not_implemented(); }
67 
68  void operator()(const Point & p, const Real, DenseVector<Real> & output) override
69  {
70  output.resize(3);
71  output.zero();
72 
73  // Count how many coordinates are exactly on the boundary (0 or 1)
74  unsigned int boundary_dims = 0;
75  std::array<bool, 3> is_on_boundary = {false, false, false};
76  for (unsigned int i = 0; i < _dim; ++i)
77  {
78  if (std::abs(p(i)) < TOLERANCE || std::abs(p(i) - 1.) < TOLERANCE)
79  {
80  ++boundary_dims;
81  is_on_boundary[i] = true;
82  }
83  }
84 
85  // If all coordinates are on the boundary, treat as vertex — leave unchanged
86  if (boundary_dims == _dim)
87  {
88  for (unsigned int i = 0; i < _dim; ++i)
89  output(i) = p(i);
90  return;
91  }
92 
93  // Distort only those directions not fixed on the boundary
94  for (unsigned int i = 0; i < _dim; ++i)
95  {
96  if (!is_on_boundary[i]) // only distort free dimensions
97  {
98  Real xi = 2. * p(i) - 1.;
99  // This value controls the strength of the distortion
100  Real modulation = 0.3;
101  for (unsigned int j = 0; j < _dim; ++j)
102  {
103  if (j != i)
104  {
105  Real pj = std::clamp(p(j), 0., 1.); // ensure numeric safety
106  modulation *= (pj - 0.5) * (pj - 0.5) * 4.; // quadratic bump centered at 0.5
107  }
108  }
109  const auto delta = (pow<3>(xi) - xi) * modulation;
110  // Check for delta = 0 to make sure we perturb every point
111  output(i) = (delta > TOLERANCE) ? p(i) + delta : 1.05 * p(i);
112  }
113  else
114  output(i) = p(i); // dimension on boundary remains unchanged
115  }
116  }
117 
118  const unsigned int _dim;
119 };
120 
121 class SquareToParallelogram : public FunctionBase<Real>
122 {
123  std::unique_ptr<FunctionBase<Real>> clone() const override
124  {
125  return std::make_unique<SquareToParallelogram>();
126  }
127 
128  Real operator()(const Point &, const Real = 0.) override
129  {
130  libmesh_not_implemented();
131  } // scalar-only API
132 
133  // Has the effect that a square, meshed into right triangles with diagonals
134  // rising from lower-left to upper-right, is transformed into a left-leaning
135  // parallelogram of eqilateral triangles.
136  void operator()(const Point & p, const Real, DenseVector<Real> & output)
137  {
138  output.resize(3);
139  output(0) = p(0) - 0.5 * p(1);
140  output(1) = 0.5 * std::sqrt(3) * p(1);
141  output(2) = 0;
142  }
143 };
144 
145 class ParallelogramToSquare : public FunctionBase<Real>
146 {
147  std::unique_ptr<FunctionBase<Real>> clone() const override
148  {
149  return std::make_unique<ParallelogramToSquare>();
150  }
151 
152  Real operator()(const Point &, const Real = 0.) override
153  {
154  libmesh_not_implemented();
155  } // scalar-only API
156 
157  // Has the effect that a left-leaning parallelogram of equilateral triangles is transformed
158  // into a square of right triangle with diagonals rising from lower-left to upper-right.
159  // This is the inversion of the SquareToParallelogram mapping.
160  void operator()(const Point & p, const Real, DenseVector<Real> & output)
161  {
162  output.resize(3);
163  output(0) = p(0) + p(1) / std::sqrt(3.);
164  output(1) = (2. / std::sqrt(3)) * p(1);
165  output(2) = 0;
166  }
167 };
168 
169 }
170 
171 using namespace libMesh;
172 
173 class MeshSmootherTest : public CppUnit::TestCase
174 {
179 public:
180  LIBMESH_CPPUNIT_TEST_SUITE(MeshSmootherTest);
181 
182 #if LIBMESH_DIM > 2
183  CPPUNIT_TEST(testLaplaceQuad);
184  CPPUNIT_TEST(testLaplaceTri);
185 #if defined(LIBMESH_ENABLE_VSMOOTHER) && defined(LIBMESH_HAVE_SOLVER)
186  CPPUNIT_TEST(testVariationalEdge2);
187  CPPUNIT_TEST(testVariationalEdge3);
188  CPPUNIT_TEST(testVariationalEdge3MultipleSubdomains);
189 
190  CPPUNIT_TEST(testVariationalQuad);
191  CPPUNIT_TEST(testVariationalQuadMultipleSubdomains);
192 
193  CPPUNIT_TEST(testVariationalTri3);
194  CPPUNIT_TEST(testVariationalTri6);
195  CPPUNIT_TEST(testVariationalTri6MultipleSubdomains);
196 
197  CPPUNIT_TEST(testVariationalHex8);
198  CPPUNIT_TEST(testVariationalHex20);
199  CPPUNIT_TEST(testVariationalHex27);
200  CPPUNIT_TEST(testVariationalHex27MultipleSubdomains);
201 #endif // LIBMESH_ENABLE_VSMOOTHER
202 #endif
203 
204  CPPUNIT_TEST_SUITE_END();
205 
206 public:
207  void setUp() {}
208 
209  void tearDown() {}
210 
212  {
213  LOG_UNIT_TEST;
214 
215  constexpr unsigned int n_elems_per_side = 4;
216 
218  mesh, n_elems_per_side, n_elems_per_side, 0., 1., 0., 1., type);
219 
220  // Move it around so we have something that needs smoothing
221  DistortSquare ds;
223 
224  // Assert the distortion is as expected
225  auto center_distortion_is =
226  [](const Node & node, int d, bool distortion, Real distortion_tol = TOLERANCE) {
227  const Real r = node(d);
228  const Real R = r * n_elems_per_side;
229  CPPUNIT_ASSERT_GREATER(-TOLERANCE * TOLERANCE, r);
230  CPPUNIT_ASSERT_GREATER(-TOLERANCE * TOLERANCE, 1 - r);
231 
232  // If we're at the boundaries we should *never* be distorted
233  if (std::abs(node(0)) < TOLERANCE * TOLERANCE ||
234  std::abs(node(0) - 1) < TOLERANCE * TOLERANCE)
235  {
236  const Real R1 = node(1) * n_elems_per_side;
237  CPPUNIT_ASSERT_LESS(TOLERANCE * TOLERANCE, std::abs(R1 - std::round(R1)));
238  return true;
239  }
240 
241  if (std::abs(node(1)) < TOLERANCE * TOLERANCE ||
242  std::abs(node(1) - 1) < TOLERANCE * TOLERANCE)
243  {
244  const Real R0 = node(0) * n_elems_per_side;
245  CPPUNIT_ASSERT_LESS(TOLERANCE * TOLERANCE, std::abs(R0 - std::round(R0)));
246 
247  return true;
248  }
249 
250  // If we're at the center we're fine
251  if (std::abs(r - 0.5) < TOLERANCE * TOLERANCE)
252  return true;
253 
254  return ((std::abs(R - std::round(R)) > distortion_tol) == distortion);
255  };
256 
257  for (auto node : mesh.node_ptr_range())
258  {
259  CPPUNIT_ASSERT(center_distortion_is(*node, 0, true));
260  CPPUNIT_ASSERT(center_distortion_is(*node, 1, true));
261  }
262 
263  // Enough iterations to mostly fix us up. Laplace seems to be at 1e-3
264  // tolerance by iteration 6, so hopefully everything is there on any
265  // system by 8.
266  for (unsigned int i = 0; i != 8; ++i)
267  smoother.smooth();
268 
269  // Make sure we're not too distorted anymore.
270  for (auto node : mesh.node_ptr_range())
271  {
272  CPPUNIT_ASSERT(center_distortion_is(*node, 0, false, 1e-3));
273  CPPUNIT_ASSERT(center_distortion_is(*node, 1, false, 1e-3));
274  }
275  }
276 
278  MeshSmoother & smoother,
279  const ElemType type,
280  const bool multiple_subdomains = false)
281  {
282  LOG_UNIT_TEST;
283 
284  // Get mesh dimension, determine whether type is triangular
285  const auto * ref_elem = &(ReferenceElem::get(type));
286  const auto dim = ref_elem->dim();
287  const bool type_is_tri = Utility::enum_to_string(type).compare(0, 3, "TRI") == 0;
288 
289  unsigned int n_elems_per_side = 5;
290 
291  // The current distortion mechanism in DistortHyperCube, combined with the
292  // way multiple subdomains are assigned below, has the property that:
293  // - When n_elems_per_side is even, some of the subdomain boundary nodes
294  // are colinear, leading to sliding node constraints.
295  // - When n_elems_per_side is odd, none of the subdomain boundary nodes
296  // are colinear, leading to all subdomain boundary nodes being fixed.
297  // Our check for preserved subdomain boundaries is overly restrictive
298  // because we error if any subdomain boundary nodes move during smoothing.
299  // To avoid this error, instead of implementing a more elaborate check for
300  // sliding subdomain boundary nodes, we require n_elems_per_side to be odd.
301  libmesh_error_msg_if(n_elems_per_side % 2 != 1,
302  "n_elems_per_side should be odd.");
303 
304  switch (dim)
305  {
306  case 1:
307  MeshTools::Generation::build_line(mesh, n_elems_per_side, 0., 1., type);
308  break;
309  case 2:
311  mesh, n_elems_per_side, n_elems_per_side, 0., 1., 0., 1., type);
312  break;
313 
314  case 3:
316  n_elems_per_side,
317  n_elems_per_side,
318  n_elems_per_side,
319  0.,
320  1.,
321  0.,
322  1.,
323  0.,
324  1.,
325  type);
326  break;
327 
328  default:
329  libmesh_error_msg("Unsupported dimension " << dim);
330  }
331 
332  // Move it around so we have something that needs smoothing
333  DistortHyperCube dh(dim);
335 
336  // Add multiple subdomains if requested
337  std::unordered_map<dof_id_type, Point> subdomain_boundary_node_id_to_point;
338  if (multiple_subdomains)
339  {
340  // Modify the subdomain ids in an interesting way
341  for (auto * elem : mesh.active_element_ptr_range())
342  {
343  unsigned int subdomain_id = 0;
344  for (const auto d : make_range(dim))
345  if (elem->vertex_average()(d) > 0.5)
346  ++subdomain_id;
347  elem->subdomain_id() += subdomain_id;
348  }
349 
350  // This loop should NOT be combined with the one above because we need to
351  // finish checking and updating subdomain ids for all elements before
352  // recording the final subdomain boundary.
353  for (auto * elem : mesh.active_element_ptr_range())
354  for (const auto & s : elem->side_index_range())
355  {
356  const auto * neighbor = elem->neighbor_ptr(s);
357  if (neighbor == nullptr)
358  continue;
359 
360  if (elem->subdomain_id() != neighbor->subdomain_id())
361  // This side is part of a subdomain boundary, record the
362  // corresponding node locations
363  for (const auto & n : elem->nodes_on_side(s))
364  subdomain_boundary_node_id_to_point[elem->node_id(n)] =
365  Point(*(elem->get_nodes()[n]));
366  }
367  }
368 
369 
370  // Get the mesh order
371  const auto &elem_orders = mesh.elem_default_orders();
372  libmesh_error_msg_if(
373  elem_orders.size() != 1,
374  "The variational smoother cannot be used for mixed-order meshes!");
375  const auto fe_order = *elem_orders.begin();
376 
377  // Function to assert the distortion is as expected
378  const auto &boundary_info = mesh.get_boundary_info();
379  auto distortion_is = [
380  &n_elems_per_side, &dim, &boundary_info, &fe_order
381  ](const Node &node, bool distortion, Real distortion_tol = TOLERANCE)
382  {
383  // Get boundary ids associated with the node
384  std::vector<boundary_id_type> boundary_ids;
385  boundary_info.boundary_ids(&node, boundary_ids);
386 
387  // This tells us what type of node we are: internal, sliding, or fixed
388  const auto num_dofs = dim - boundary_ids.size();
389  /*
390  * The following cases of num_dofs are possible, ASSUMING all boundaries
391  * are non-overlapping
392  * 3D: 3-0, 3-1, 3-2, 3-3
393  * = 3 2 1 0
394  * internal sliding, sliding, fixed
395  * 2D: 2-0, 2-1, 2-2
396  * = 2 1 0
397  * internal sliding, fixed
398  * 1D: 1-0, 1-1
399  * = 1 0
400  * internal fixed
401  *
402  * We expect that R is an integer in [0, n_elems_per_side] for
403  * num_dofs of the node's cooridinantes, while the remaining coordinates
404  * are fixed to the boundary with value 0 or 1. In other words, at LEAST
405  * dim - num_dofs coordinantes should be 0 or 1.
406  */
407 
408  std::size_t num_zero_or_one = 0;
409 
410  bool distorted = false;
411  for (const auto d : make_range(dim))
412  {
413  const Real r = node(d);
414  const Real R = r * n_elems_per_side * fe_order;
415  CPPUNIT_ASSERT_GREATER(-distortion_tol * distortion_tol, r);
416  CPPUNIT_ASSERT_GREATER(-distortion_tol * distortion_tol, 1 - r);
417 
418  const bool d_distorted = std::abs(R - std::round(R)) > distortion_tol;
419  distorted |= d_distorted;
420  num_zero_or_one +=
421  (absolute_fuzzy_equals(r, 0.) || absolute_fuzzy_equals(r, 1.));
422  }
423 
424  CPPUNIT_ASSERT_GREATEREQUAL(dim - num_dofs, num_zero_or_one);
425 
426  // We can never expect a fixed node to be distorted
427  if (num_dofs == 0)
428  // if (num_dofs < dim)
429  return true;
430  return distorted == distortion;
431  };
432 
433  // Function to check if a given node has changed based on previous mapping
434  auto is_subdomain_boundary_node_the_same = [&subdomain_boundary_node_id_to_point](
435  const Node & node) {
436  auto it = subdomain_boundary_node_id_to_point.find(node.id());
437  if (it != subdomain_boundary_node_id_to_point.end())
438  return (relative_fuzzy_equals(Point(node), subdomain_boundary_node_id_to_point[node.id()]));
439  else
440  // node is not a subdomain boundary node, just return true
441  return true;
442  };
443 
444  // Make sure our DistortSquare transformation has distorted the mesh
445  for (auto node : mesh.node_ptr_range())
446  CPPUNIT_ASSERT(distortion_is(*node, true));
447 
448  // Transform the square mesh of triangles to a parallelogram mesh of
449  // triangles. This will allow the Variational Smoother to smooth the mesh
450  // to the optimal case of equilateral triangles
451  if (type_is_tri)
452  {
453  SquareToParallelogram stp;
455  }
456 
457  smoother.smooth();
458 
459  // Transform the parallelogram mesh back to a square mesh. In the case of
460  // the Variational Smoother, equilateral triangular elements will be
461  // transformed into right triangular elements that align with the original
462  // undistorted mesh.
463  if (type_is_tri)
464  {
465  ParallelogramToSquare pts;
467  }
468 
469  // Make sure we're not too distorted anymore OR that interval subdomain boundary nodes did not
470  // change.
471  for (auto node : mesh.node_ptr_range())
472  {
473  if (multiple_subdomains)
474  CPPUNIT_ASSERT(is_subdomain_boundary_node_the_same(*node));
475  else
476  CPPUNIT_ASSERT(distortion_is(*node, false, 1e-3));
477  }
478  }
479 
481  {
483  LaplaceMeshSmoother laplace(mesh);
484 
485  testLaplaceSmoother(mesh, laplace, QUAD4);
486  }
487 
489  {
491  LaplaceMeshSmoother laplace(mesh);
492 
493  testLaplaceSmoother(mesh, laplace, TRI3);
494  }
495 
496 #ifdef LIBMESH_ENABLE_VSMOOTHER
498  {
500  VariationalMeshSmoother variational(mesh);
501 
502  testVariationalSmoother(mesh, variational, EDGE2);
503  }
504 
506  {
508  VariationalMeshSmoother variational(mesh);
509 
510  testVariationalSmoother(mesh, variational, EDGE3);
511  }
512 
514  {
516  VariationalMeshSmoother variational(mesh);
517 
518  testVariationalSmoother(mesh, variational, EDGE3, true);
519  }
520 
522  {
524  VariationalMeshSmoother variational(mesh);
525 
526  testVariationalSmoother(mesh, variational, QUAD4);
527  }
528 
530  {
532  VariationalMeshSmoother variational(mesh);
533 
534  testVariationalSmoother(mesh, variational, QUAD4, true);
535  }
536 
538  {
540  VariationalMeshSmoother variational(mesh);
541 
542  testVariationalSmoother(mesh, variational, TRI3);
543  }
544 
546  {
548  VariationalMeshSmoother variational(mesh);
549 
550  testVariationalSmoother(mesh, variational, TRI6);
551  }
552 
554  {
556  VariationalMeshSmoother variational(mesh);
557 
558  testVariationalSmoother(mesh, variational, TRI3, true);
559  }
560 
562  {
564  VariationalMeshSmoother variational(mesh);
565 
566  testVariationalSmoother(mesh, variational, HEX8);
567  }
568 
570  {
572  VariationalMeshSmoother variational(mesh);
573 
574  testVariationalSmoother(mesh, variational, HEX20);
575  }
576 
578  {
580  VariationalMeshSmoother variational(mesh);
581 
582  testVariationalSmoother(mesh, variational, HEX27);
583  }
584 
586  {
588  VariationalMeshSmoother variational(mesh);
589 
590  testVariationalSmoother(mesh, variational, HEX27, true);
591  }
592 #endif // LIBMESH_ENABLE_VSMOOTHER
593 };
594 
ElemType
Defines an enum for geometric element types.
const std::set< Order > & elem_default_orders() const
Definition: mesh_base.h:289
The ReplicatedMesh class is derived from the MeshBase class, and is used to store identical copies of...
A Node is like a Point, but with more information.
Definition: node.h:52
virtual void zero() override final
Set every element in the vector to 0.
Definition: dense_vector.h:420
void testLaplaceSmoother(ReplicatedMesh &mesh, MeshSmoother &smoother, ElemType type)
libMesh::Parallel::Communicator * TestCommWorld
Definition: driver.C:171
static constexpr Real TOLERANCE
unsigned int dim
void resize(const unsigned int n)
Resize the vector.
Definition: dense_vector.h:396
MeshBase & mesh
void build_square(UnstructuredMesh &mesh, const unsigned int nx, const unsigned int ny, const Real xmin=0., const Real xmax=1., const Real ymin=0., const Real ymax=1., const ElemType type=INVALID_ELEM, const bool gauss_lobatto_grid=false)
A specialized build_cube() for 2D meshes.
void boundary_ids(const Node *node, std::vector< boundary_id_type > &vec_to_fill) const
Fills a user-provided std::vector with the boundary ids associated with Node node.
The libMesh namespace provides an interface to certain functionality in the library.
const BoundaryInfo & get_boundary_info() const
The information about boundary ids on the mesh.
Definition: mesh_base.h:165
virtual void smooth()=0
Function which actually performs the smoothing operations.
This class defines the data structures necessary for Laplace smoothing.
T pow(const T &x)
Definition: utility.h:328
void testVariationalSmoother(ReplicatedMesh &mesh, MeshSmoother &smoother, const ElemType type, const bool multiple_subdomains=false)
CPPUNIT_TEST_SUITE_REGISTRATION(MeshSmootherTest)
std::string enum_to_string(const T e)
bool absolute_fuzzy_equals(const T &var1, const T2 &var2, const Real tol=TOLERANCE *TOLERANCE)
Function to check whether two variables are equal within an absolute tolerance.
Definition: fuzzy_equals.h:64
void testVariationalHex27MultipleSubdomains()
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
This is an implementation of Larisa Branets&#39; smoothing algorithms.
void testVariationalQuadMultipleSubdomains()
void testVariationalTri6MultipleSubdomains()
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
void build_line(UnstructuredMesh &mesh, const unsigned int nx, const Real xmin=0., const Real xmax=1., const ElemType type=INVALID_ELEM, const bool gauss_lobatto_grid=false)
A specialized build_cube() for 1D meshes.
void redistribute(MeshBase &mesh, const FunctionBase< Real > &mapfunc)
Deterministically perturb the nodal locations.
This class provides the necessary interface for mesh smoothing.
Definition: mesh_smoother.h:38
bool relative_fuzzy_equals(const T &var1, const T2 &var2, const Real tol=TOLERANCE *TOLERANCE)
Function to check whether two variables are equal within a relative tolerance.
Definition: fuzzy_equals.h:78
Defines a dense vector for use in Finite Element-type computations.
Definition: dof_map.h:74
Base class for functors that can be evaluated at a point and (optionally) time.
A Point defines a location in LIBMESH_DIM dimensional Real space.
Definition: point.h:39
const Elem & get(const ElemType type_in)
void build_cube(UnstructuredMesh &mesh, const unsigned int nx=0, const unsigned int ny=0, const unsigned int nz=0, const Real xmin=0., const Real xmax=1., const Real ymin=0., const Real ymax=1., const Real zmin=0., const Real zmax=1., const ElemType type=INVALID_ELEM, const bool gauss_lobatto_grid=false)
Builds a (elements) cube.
void testVariationalEdge3MultipleSubdomains()