diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index c903a1c02c..2bcfc02afd 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -22,14 +22,369 @@ #include "libmesh/system.h" #include "libmesh/dof_map.h" +// C++ includes +#include + namespace libMesh { -/* + +// Forward declarations +class PointConstraint; +class LineConstraint; +class PlaneConstraint; +class InvalidConstraint; + +/** + * Type used to store a constraint that may be a PlaneConstraint, + * LineConstraint, or PointConstraint. std::variant is an alternative to using + * the classic polymorphic approach where these constraints inherit from an + * common base class. + */ +using ConstraintVariant = std::variant; + +/** + * Represents a fixed point constraint. + */ +class PointConstraint +{ + +public: + PointConstraint() = default; + + /** + * Constructor + * @param point The point defining the constraint. + * @param tol The tolerance to use for numerical comparisons. + */ + PointConstraint(const Point &point, const Real &tol = TOLERANCE); + + /** + * Comparison operator for ordering PointConstraint objects. + * A PointConstraint is considered less than another if its location + * is lexicographically less than the other's location. + * @param other The PointConstraint to compare with. + * @param tol The tolerance to use for numerical comparisons. + * @return True if this PointConstraint is less than the other. + */ + bool operator<(const PointConstraint &other) const; + + /** + * Equality operator. + * @param other The PointConstraint to compare with. + * @return True if both PointConstraints have the same location. + */ + bool operator==(const PointConstraint &other) const; + + /** + * Query whether a point lies on another point. + * @param p The point in question + * @return bool indicating whether p lies on this point. + */ + bool contains_point(const PointConstraint &p) const { return *this == p; } + + /** + * Computes the intersection of this point with another constraint. + * Handles intersection with PointConstraint, LineConstraint, or + * PlaneConstraint. + * @param other The constraint to intersect with. + * @return The most specific ConstraintVariant that satisfies both + * constraints. constraints. If no intersection exists, return an + * InvalidConstraint. + */ + ConstraintVariant intersect(const ConstraintVariant &other) const; + + /** + * Const getter for the _point attribute + */ + const Point &point() const { return _point; } + + /** + * Const getter for the _tol attribute + */ + const Real &tol() const { return _tol; } + +private: + // Life is easier if we don't make this const + /** + * Location of constraint + */ + Point _point; + + /** + * Tolerance to use for numerical comparisons + */ + Real _tol; +}; + +/** + * Represents a line constraint defined by a base point and direction vector. + */ +class LineConstraint +{ +public: + LineConstraint() = default; + + /** + * Constructor + * @param point A point on the constraining line. + * @param direction the direction of the constraining line. + * @param tol The tolerance to use for numerical comparisons. + */ + LineConstraint(const Point &point, const Point &direction, + const Real &tol = TOLERANCE); + + /** + * Comparison operator for ordering LineConstraint objects. + * The comparison is primarily based on the direction vector. If the direction + * vectors are equal (within tolerance), the tie is broken using the dot + * product of the direction with the base point. + * @param other The LineConstraint to compare with. + * @return True if this LineConstraint is less than the other. + */ + bool operator<(const LineConstraint &other) const; + + /** + * Equality operator. + * @param other The LineConstraint to compare with. + * @return True if both LineConstraints represent the same line. + */ + bool operator==(const LineConstraint &other) const; + + /** + * Query whether a point lies on the line. + * @param p The point in question + * @return bool indicating whether p lies on the line. + */ + bool contains_point(const PointConstraint &p) const; + + /** + * Query whether a line is parallel to this line + * @param l The line in question + * @return bool indicating whether l is parallel to this line. + */ + bool is_parallel(const LineConstraint &l) const; + + /** + * Query whether a plane is parallel to this line + * @param p The plane in question + * @return bool indicating whether p is parallel to this line. + */ + bool is_parallel(const PlaneConstraint &p) const; + + /** + * Computes the intersection of this line with another constraint. + * Handles intersection with LineConstraint, PlaneConstraint, or + * PointConstraint. + * @param other The constraint to intersect with. + * @return The most specific ConstraintVariant that satisfies both + * constraints. constraints. If no intersection exists, return an + * InvalidConstraint. + */ + ConstraintVariant intersect(const ConstraintVariant &other) const; + + /** + * Const getter for the _point attribute + */ + const Point &point() const { return _point; } + + /** + * Const getter for the _direction attribute + */ + const Point &direction() const { return _direction; } + + /** + * Const getter for the _tol attribute + */ + const Real &tol() const { return _tol; } + +private: + // Life is easier if we don't make these const + /** + * A point on the constraining line + */ + Point _point; + + /** + * Direction of the constraining line + */ + Point _direction; + + /** + * Tolerance to use for numerical comparisons + */ + Real _tol; +}; + +/** + * Represents a plane constraint defined by a point and normal vector. + */ +class PlaneConstraint +{ + +public: + PlaneConstraint() = default; + + /** + * Constructor + * @param point A point on the constraining plane. + * @param normal the direction normal to the constraining plane. + * @param tol The tolerance to use for numerical comparisons. + */ + PlaneConstraint(const Point &point, const Point &normal, + const Real &tol = TOLERANCE); + + /** + * Comparison operator for ordering PlaneConstraint objects. + * The comparison is primarily based on the normal vector. If the normal + * vectors are equal (within tolerance), the tie is broken using the dot + * product of the normal with the point on the plane. + * @param other The PlaneConstraint to compare with. + * @return True if this PlaneConstraint is less than the other. + */ + bool operator<(const PlaneConstraint &other) const; + + /** + * Equality operator. + * @param other The PlaneConstraint to compare with. + * @return True if both PlaneConstraints represent the same plane. + */ + bool operator==(const PlaneConstraint &other) const; + + /** + * Query whether a point lies on the plane. + * @param p The point in question + * @return bool indicating whether p lies on the plane. + */ + bool contains_point(const PointConstraint &p) const; + + /** + * Query whether a line lies on the plane. + * @param l The line in question + * @return bool indicating whether l lies on the plane. + */ + bool contains_line(const LineConstraint &l) const; + + /** + * Query whether a plane is parallel to this plane + * @param p The plane in question + * @return bool indicating whether p is parallel to this plane. + */ + bool is_parallel(const PlaneConstraint &p) const; + + /** + * Query whether a line is parallel to this plane + * @param l The line in question + * @return bool indicating whether l is parallel to this plane. + */ + bool is_parallel(const LineConstraint &l) const; + + /** + * Computes the intersection of this plane with another constraint. + * Handles intersection with PlaneConstraint, LineConstraint, or + * PointConstraint. + * @param other The constraint to intersect with. + * @return The most specific ConstraintVariant that satisfies both + * constraints. constraints. If no intersection exists, return an + * InvalidConstraint. + */ + ConstraintVariant intersect(const ConstraintVariant &other) const; + + /** + * Const getter for the _point attribute + */ + const Point &point() const { return _point; } + + /** + * Const getter for the _normal attribute + */ + const Point &normal() const { return _normal; } + + /** + * Const getter for the _tol attribute + */ + const Real &tol() const { return _tol; } + +private: + // Life is easier if we don't make these const + /** + * A point on the constraining plane + */ + Point _point; + + /** + * The direction normal to the constraining plane + */ + Point _normal; + + /** + * Tolerance to use for numerical comparisons + */ + Real _tol; +}; + +/** + * Represents an invalid constraint (i.e., when the two constraints don't + * intersect) + */ +class InvalidConstraint +{ + +public: + InvalidConstraint() + : _err_msg("We should never get here! The InvalidConstraint object should be " + "detected and replaced with a valid ConstraintVariant prior to calling " + "any class methods.") + { + } + + /** + * Dummy intersect method that should never be called. + */ + ConstraintVariant intersect(const ConstraintVariant &) const { + libmesh_assert_msg(false, _err_msg); + return *this; + } + + /** + * Dummy contains_point method that should never be called. + */ + bool contains_point(const PointConstraint &) const { + libmesh_assert_msg(false, _err_msg); + return false; + } + +private: + std::string _err_msg; +}; + +/** + * Dispatch intersection between two constraint variants. + * Resolves to the appropriate method based on the type of the first operand. + * @param a First constraint. + * @param b Constraint to combine with a. + * @return Combination (intersection) of constraint a and b. + */ +inline ConstraintVariant intersect_constraints(const ConstraintVariant &a, + const ConstraintVariant &b) { + // std::visit applies the visitor v (a Callable that can be called with any + // combination of types from Variants) to the active value inside a + // std::Variant. This circumvents the issue that the literal ConstraintVariant + // type does not have a method called 'intersect' (but the types defining + // ConstraintVariant do) + return std::visit( + [](const auto &lhs, const auto &rhs) -> ConstraintVariant { + return lhs.intersect(rhs); + }, + a, b); +} + +/** * Constraint class for the VariationalMeshSmoother. * - * Currently, all mesh boundary nodes are constrained to not move during smoothing. - * If requested (preserve_subdomain_boundaries = true), nodes on subdomain boundaries - * are also constrained to not move. + * Currently, all mesh boundary nodes are constrained to not move during + * smoothing. If requested (preserve_subdomain_boundaries = true), nodes on + * subdomain boundaries are also constrained to not move. */ class VariationalSmootherConstraint : public System::Constraint { @@ -37,21 +392,112 @@ class VariationalSmootherConstraint : public System::Constraint System & _sys; - /// Whether nodes on subdomain boundaries are subject to change via smoothing + /** + * Whether nodes on subdomain boundaries are subject to change via smoothing + */ const bool _preserve_subdomain_boundaries; - /* - * Constrain (fix) a node to not move during mesh smoothing. + /** + * Constrain (i.e., fix) a node to not move during mesh smoothing. * @param node Node to fix. */ void fix_node(const Node & node); -public: + /** + * Constrain a node to remain in the given plane during mesh smoothing. + * @param node Node to constrain + * @param ref_normal_vec Reference normal vector to the constraining plane. + * This, along with the coordinates of node, are used to define the + * constraining plane. + */ + void constrain_node_to_plane(const Node & node, const Point & ref_normal_vec); + + /** + * Constrain a node to remain on the given line during mesh smoothing. + * @param node Node to constrain + * @param line_vec vector parallel to the constraining line. + * This, along with the coordinates of node, are used to define the + * constraining line. + */ + void constrain_node_to_line(const Node & node, const Point & line_vec); + + /** + * Determines whether two neighboring nodes share a common boundary id. + * @param boundary_node The first of the two prospective nodes. + * @param neighbor_node The second of the two prospective nodes. + * @param containing_elem The element containing node1 and node2. + * @param boundary_info The mesh's BoundaryInfo. + * @return nodes_share_bid Whether node1 and node2 share a common boundary id. + */ + static bool nodes_share_boundary_id( + const Node & boundary_node, + const Node & neighbor_node, + const Elem & containing_elem, + const BoundaryInfo & boundary_info); + + /** + * Get the relevant nodal neighbors for a subdomain constraint. + * @param mesh The mesh being smoothed. + * @param node The node (on the subdomain boundary) being constrained. + * @param sub_id The subdomain id of the block on one side of the subdomain + * boundary. + * @param nodes_to_elem_map A mapping from node id to containing element ids. + * @return A set of node pointer sets containing nodal neighbors to 'node' on + * the sub_id1-sub_id2 boundary. The subsets are grouped by element faces + * that form the subdomain boundary. Note that 'node' itself does not appear + * in this set. + */ + static std::set> + get_neighbors_for_subdomain_constraint( + const MeshBase &mesh, const Node &node, const subdomain_id_type sub_id, + const std::unordered_map> + &nodes_to_elem_map); + + /** + * Get the relevant nodal neighbors for an external boundary constraint. + * @param mesh The mesh being smoothed. + * @param node The node (on the external boundary) being constrained. + * @param boundary_node_ids The set of mesh's external boundary node ids. + * @param boundary_info The mesh's BoundaryInfo. + * @param nodes_to_elem_map A mapping from node id to containing element ids. + * @return A set of node pointer sets containing nodal neighbors to 'node' on + * the external boundary. The subsets are grouped by element faces that form + * the external boundary. Note that 'node' itself does not appear in this + * set. + */ + static std::set> get_neighbors_for_boundary_constraint( + const MeshBase &mesh, const Node &node, + const std::unordered_set &boundary_node_ids, + const BoundaryInfo &boundary_info, + const std::unordered_map> + &nodes_to_elem_map); + + /** + * Determines the appropriate constraint (PointConstraint, LineConstraint, or + * PlaneConstraint) for a node based on its neighbors. + * @param node The node to constrain. + * @param dim The mesh dimension. + * @return The best-fit constraint for the given geometry. + */ + static ConstraintVariant determine_constraint( + const Node &node, const unsigned int dim, + const std::set> &side_grouped_boundary_neighbors); + + /** + * Applies a given constraint to a node (e.g., fixing it, restricting it to a + * line or plane). + * @param node The node to constrain. + * @param constraint The geometric constraint variant to apply. + * @throw libMesh::logicError If the constraint cannot be imposed. + */ + void impose_constraint(const Node &node, const ConstraintVariant &constraint); - /* +public: + /** * Constructor * @param sys System to constrain. - * @param preserve_subdomain_boundaries Whether to constrain nodes on subdomain boundaries to not move. + * @param preserve_subdomain_boundaries Whether to constrain nodes on + * subdomain boundaries to not move. */ VariationalSmootherConstraint(System & sys, const bool & preserve_subdomain_boundaries); diff --git a/include/systems/variational_smoother_system.h b/include/systems/variational_smoother_system.h index 71d2b2141e..0f68300324 100644 --- a/include/systems/variational_smoother_system.h +++ b/include/systems/variational_smoother_system.h @@ -81,9 +81,12 @@ class VariationalSmootherSystem : public libMesh::FEMSystem libMesh::DiffContext & context) override; /* Computes the element reference volume used in the dilation metric - * The reference value is set to the averaged value of all elements' average |J|. + * The reference value is set to the averaged value of all elements' average + * |J|. Also computes any applicable target element inverse Jacobians. Target + * elements are relavant when the reference element does not minimize the + * distortion metric. */ - void compute_element_reference_volume(); + void prepare_for_smoothing(); /// The small nonzero constant to prevent zero denominators (degenerate elements only) const Real _epsilon_squared; @@ -93,6 +96,11 @@ class VariationalSmootherSystem : public libMesh::FEMSystem /// The relative weight to give the dilation metric. The distortion metric is given weight 1 - _dilation_weight. Real _dilation_weight; + + /* Map to hold target qp-dependent element inverse reference-to-target mapping + * Jacobians, if any + */ + std::map> _target_inverse_jacobians; }; } // namespace libMesh diff --git a/src/mesh/mesh_smoother_vsmoother.C b/src/mesh/mesh_smoother_vsmoother.C index 146dbb84e6..c9a8b217a8 100644 --- a/src/mesh/mesh_smoother_vsmoother.C +++ b/src/mesh/mesh_smoother_vsmoother.C @@ -118,9 +118,10 @@ void VariationalMeshSmoother::smooth(unsigned int) es.init(); // More debugging options - DiffSolver & solver = *(sys.time_solver->diff_solver().get()); + //DiffSolver & solver = *(sys.time_solver->diff_solver().get()); //solver.quiet = false; - solver.verbose = true; + //solver.verbose = true; + sys.time_solver->diff_solver()->relative_residual_tolerance = TOLERANCE*TOLERANCE; sys.solve(); diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index dc54d30140..781b2a786b 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -18,110 +18,833 @@ // Local Includes #include "libmesh/variational_smoother_constraint.h" #include "libmesh/mesh_tools.h" +#include "libmesh/boundary_info.h" namespace libMesh { -VariationalSmootherConstraint::VariationalSmootherConstraint(System & sys, const bool & preserve_subdomain_boundaries) - : - Constraint(), - _sys(sys), - _preserve_subdomain_boundaries(preserve_subdomain_boundaries) - {} +// Helper function to orient a vector in the positive x/y/z direction +auto get_positive_vector = [](const Point & vec) -> Point { + libmesh_error_msg_if(vec.norm() < TOLERANCE, + "Can't define a positively-oriented vector with a zero vector."); + + // Choose sign such that direction vector points in the positive x/y/z + // direction This helps to eliminate duplicate lines/planes + Point canonical{0, 0, 0}; + // Choose the canonical dimension to ensure the dot product below is nonzero + for (const auto dim_id : make_range(3)) + if (!absolute_fuzzy_equals(vec(dim_id), 0.)) + { + canonical(dim_id) = 1.; + break; + } + + const auto dot_prod = vec * canonical; + libmesh_assert(!absolute_fuzzy_equals(dot_prod, 0.)); + + return (dot_prod > 0) ? vec.unit() : -vec.unit(); +}; + +PointConstraint::PointConstraint(const Point & point, const Real & tol) : _point(point), _tol(tol) +{ +} + +bool PointConstraint::operator<(const PointConstraint & other) const +{ + if (*this == other) + return false; + + return _point < other.point(); +} + +bool PointConstraint::operator==(const PointConstraint & other) const +{ + return _point.absolute_fuzzy_equals(other.point(), _tol); +} + +ConstraintVariant PointConstraint::intersect(const ConstraintVariant & other) const +{ + // using visit to resolve the variant to its actual type + return std::visit( + [&](auto && o) -> ConstraintVariant { + if (!o.contains_point(*this)) + // Point is not on the constraint + return InvalidConstraint(); + + return *this; + }, + other); +} + +LineConstraint::LineConstraint(const Point & point, const Point & direction, const Real & tol) + : _point(point), _direction(get_positive_vector(direction)), _tol(tol) +{ + libmesh_error_msg_if(_direction.norm() < _tol, + "Can't define a line with zero magnitude direction vector."); +} + +bool LineConstraint::operator<(const LineConstraint & other) const +{ + if (*this == other) + return false; + + if (!(_direction.absolute_fuzzy_equals(other.direction(), _tol))) + return _direction < other.direction(); + return (_direction * _point) < (other.direction() * other.point()); +} + +bool LineConstraint::operator==(const LineConstraint & other) const +{ + if (!(_direction.absolute_fuzzy_equals(other.direction(), _tol))) + return false; + return this->contains_point(other.point()); +} + +bool LineConstraint::contains_point(const PointConstraint & p) const +{ + // If the point lies on the line, then the vector p - point is parallel to the + // line In that case, the cross product of p - point with the line's direction + // will be zero. + return _direction.cross(p.point() - _point).norm() < _tol; +} + +bool LineConstraint::is_parallel(const LineConstraint & l) const +{ + return _direction.absolute_fuzzy_equals(l.direction(), _tol); +} + +bool LineConstraint::is_parallel(const PlaneConstraint & p) const +{ + return _direction * p.normal() < _tol; +} + +ConstraintVariant LineConstraint::intersect(const ConstraintVariant & other) const +{ + // using visit to resolve the variant to its actual type + return std::visit( + [&](auto && o) -> ConstraintVariant { + // Use std::decay_t to strip references/const/volatile from o's type, + // so we can match the actual stored variant type in std::is_same_v. + using T = std::decay_t; + if constexpr (std::is_same_v) + { + if (*this == o) + return *this; + + if (this->is_parallel(o)) + // Lines are parallel and do not intersect + return InvalidConstraint(); + + // Solve for t in the equation p1 + t·d1 = p2 + s·d2 + // The shortest vector between skew lines lies along the normal vector + // (d1 × d2). Projecting the vector (p2 - p1) onto this normal gives a + // scalar proportional to the distance. This is equivalent to solving: + // ((p2 - p1) × d2) · (d1 × d2) = t · |d1 × d2|² + // ⇒ t = ((delta × d2) · (d1 × d2)) / |d1 × d2|² + + const Point delta = o.point() - _point; + const Point cross_d1_d2 = _direction.cross(o.direction()); + const Real cross_dot = (delta.cross(o.direction())) * cross_d1_d2; + const Real denom = cross_d1_d2.norm_sq(); + + const Real t = cross_dot / denom; + const Point intersection = _point + t * _direction; + + // Verify that intersection lies on both lines + if (o.direction().cross(intersection - o.point()).norm() > _tol) + // Lines do not intersect at a single point + return InvalidConstraint(); + + return PointConstraint{intersection}; + } + + else if constexpr (std::is_same_v) + return o.intersect(*this); + + else if constexpr (std::is_same_v) + { + if (!this->contains_point(o)) + // Point is not on the line + return InvalidConstraint(); + + return o; + } + + else + libmesh_error_msg("Unsupported constraint type in Line::intersect."); + }, + other); +} + +PlaneConstraint::PlaneConstraint(const Point & point, const Point & normal, const Real & tol) + : _point(point), _normal(get_positive_vector(normal)), _tol(tol) +{ + libmesh_error_msg_if(_normal.norm() < _tol, + "Can't define a plane with zero magnitude direction vector."); +} + +bool PlaneConstraint::operator<(const PlaneConstraint & other) const +{ + if (*this == other) + return false; + + if (!(_normal.absolute_fuzzy_equals(other.normal(), _tol))) + return _normal < other.normal(); + return (_normal * _point) < (other.normal() * other.point()); +} + +bool PlaneConstraint::operator==(const PlaneConstraint & other) const +{ + if (!(_normal.absolute_fuzzy_equals(other.normal(), _tol))) + return false; + return this->contains_point(other.point()); +} + +bool PlaneConstraint::is_parallel(const PlaneConstraint & p) const +{ + return _normal.absolute_fuzzy_equals(p.normal(), _tol); +} + +bool PlaneConstraint::is_parallel(const LineConstraint & l) const { return l.is_parallel(*this); } + +bool PlaneConstraint::contains_point(const PointConstraint & p) const +{ + // distance between the point and the plane + const Real dist = (p.point() - _point) * _normal; + return std::abs(dist) < _tol; +} + +bool PlaneConstraint::contains_line(const LineConstraint & l) const +{ + const bool base_on_plane = this->contains_point(PointConstraint(l.point())); + const bool dir_orthogonal = std::abs(_normal * l.direction()) < _tol; + return base_on_plane && dir_orthogonal; +} + +ConstraintVariant PlaneConstraint::intersect(const ConstraintVariant & other) const +{ + // using visit to resolve the variant to its actual type + return std::visit( + [&](auto && o) -> ConstraintVariant { + // Use std::decay_t to strip references/const/volatile from o's type, + // so we can match the actual stored variant type in std::is_same_v. + using T = std::decay_t; + if constexpr (std::is_same_v) + { + // If planes are identical, return one of them + if (*this == o) + return *this; + + if (this->is_parallel(o)) + // Planes are parallel and do not intersect + return InvalidConstraint(); + + // Solve for a point on the intersection line of two planes. + // Given planes: + // Plane 1: n1 · (x - p1) = 0 + // Plane 2: n2 · (x - p2) = 0 + // The line of intersection has direction dir = n1 × n2. + // To find a point on this line, we assume: + // x = p1 + s·n1 = p2 + t·n2 + // ⇒ p1 - p2 = t·n2 - s·n1 + // Taking dot products with n1 and n2 leads to: + // [-n1·n1 n1·n2] [s] = [n1 · (p1 - p2)] + // [-n1·n2 n2·n2] [t] [n2 · (p1 - p2)] + + const Point dir = this->_normal.cross(o.normal()); // direction of line of intersection + libmesh_assert(dir.norm() > _tol); + const Point w = _point - o.point(); + + // Dot product terms used in 2x2 system + const Real n1_dot_n1 = _normal * _normal; + const Real n1_dot_n2 = _normal * o.normal(); + const Real n2_dot_n2 = o.normal() * o.normal(); + const Real n1_dot_w = _normal * w; + const Real n2_dot_w = o.normal() * w; + + const Real denom = -(n1_dot_n1 * n2_dot_n2 - n1_dot_n2 * n1_dot_n2); + libmesh_assert(std::abs(denom) > _tol); + + const Real s = -(n1_dot_n2 * n2_dot_w - n2_dot_n2 * n1_dot_w) / denom; + const Point p0 = _point + s * _normal; + + return LineConstraint{p0, dir}; + } + + else if constexpr (std::is_same_v) + { + if (this->contains_line(o)) + return o; + + if (this->is_parallel(o)) + // Line is parallel and does not intersect the plane + return InvalidConstraint(); + + // Solve for t in the parametric equation: + // p(t) = point + t·d + // such that this point also satisfies the plane equation: + // n · (p(t) - p0) = 0 + // which leads to: + // t = (n · (p0 - point)) / (n · d) + + const Real denom = _normal * o.direction(); + libmesh_assert(std::abs(denom) > _tol); + const Real t = (_normal * (_point - o.point())) / denom; + return PointConstraint{o.point() + t * o.direction()}; + } + + else if constexpr (std::is_same_v) + { + if (!this->contains_point(o)) + // Point is not on the plane + return InvalidConstraint(); + + return o; + } + + else + libmesh_error_msg("Unsupported constraint type in Plane::intersect."); + }, + other); +} + +VariationalSmootherConstraint::VariationalSmootherConstraint( + System & sys, const bool & preserve_subdomain_boundaries) + : Constraint(), _sys(sys), _preserve_subdomain_boundaries(preserve_subdomain_boundaries) +{ +} VariationalSmootherConstraint::~VariationalSmootherConstraint() = default; void VariationalSmootherConstraint::constrain() { - auto & mesh = _sys.get_mesh(); + const auto & mesh = _sys.get_mesh(); + const auto dim = mesh.mesh_dimension(); // Only compute the node to elem map once std::unordered_map> nodes_to_elem_map; MeshTools::build_nodes_to_elem_map(mesh, nodes_to_elem_map); - const auto boundary_node_ids = MeshTools::find_boundary_nodes (mesh); + const auto & boundary_info = mesh.get_boundary_info(); + + const auto boundary_node_ids = MeshTools::find_boundary_nodes(mesh); + + // Identify/constrain subdomain boundary nodes, if requested + std::unordered_map subdomain_boundary_map; + if (_preserve_subdomain_boundaries) + { + for (const auto * elem : mesh.active_element_ptr_range()) + { + const auto & sub_id1 = elem->subdomain_id(); + for (const auto side : elem->side_index_range()) + { + const auto * neighbor = elem->neighbor_ptr(side); + if (neighbor == nullptr) + continue; + + const auto & sub_id2 = neighbor->subdomain_id(); + if (sub_id1 == sub_id2) + continue; + + // elem and neighbor are in different subdomains, and share nodes + // that need to be constrained + for (const auto local_node_id : elem->nodes_on_side(side)) + { + const auto & node = mesh.node_ref(elem->node_id(local_node_id)); + // Make sure we haven't already processed this node + if (subdomain_boundary_map.count(node.id())) + continue; + + // Get the relevant nodal neighbors for the subdomain constraint + const auto side_grouped_boundary_neighbors = + get_neighbors_for_subdomain_constraint( + mesh, node, sub_id1, nodes_to_elem_map); + + // Determine which constraint should be imposed + const auto subdomain_constraint = + determine_constraint(node, dim, side_grouped_boundary_neighbors); + + // This subdomain boundary node does not lie on an external boundary, + // go ahead and impose constraint + if (boundary_node_ids.find(node.id()) == boundary_node_ids.end()) + this->impose_constraint(node, subdomain_constraint); + + // This subdomain boundary node could lie on an external boundary, save it + // for later to combine with the external boundary constraint. + // We also save constraints for non-boundary nodes so we don't try to + // re-constrain the node when accessed from the neighboring elem. + // See subdomain_boundary_map.count call above. + subdomain_boundary_map[node.id()] = subdomain_constraint; + + } // for local_node_id + + } // for side + } // for elem + } + + // Loop through boundary nodes and impose constraints for (const auto & bid : boundary_node_ids) - { - const auto & node = mesh.node_ref(bid); - // Find all the nodal neighbors... that is the nodes directly connected - // to this node through one edge - std::vector neighbors; - MeshTools::find_nodal_neighbors(mesh, node, nodes_to_elem_map, neighbors); - - // Remove any neighbors that are not boundary nodes - neighbors.erase( - std::remove_if(neighbors.begin(), neighbors.end(), - [&boundary_node_ids](const Node * neigh) { - return boundary_node_ids.find(neigh->id()) == boundary_node_ids.end(); + { + const auto & node = mesh.node_ref(bid); + + // Get the relevant nodal neighbors for the boundary constraint + const auto side_grouped_boundary_neighbors = get_neighbors_for_boundary_constraint( + mesh, node, boundary_node_ids, boundary_info, nodes_to_elem_map); + + // Determine which constraint should be imposed + const auto boundary_constraint = + determine_constraint(node, dim, side_grouped_boundary_neighbors); + + // Check for the case where this boundary node is also part of a subdomain id boundary + if (const auto it = subdomain_boundary_map.find(bid); it != subdomain_boundary_map.end()) + { + const auto & subdomain_constraint = it->second; + // Combine current boundary constraint with previously determined + // subdomain_constraint + auto combined_constraint = intersect_constraints(subdomain_constraint, boundary_constraint); + + // This will catch cases where constraints have no intersection + // Fall back to fixed node constraint + if (std::holds_alternative(combined_constraint)) + combined_constraint = PointConstraint(node); + + this->impose_constraint(node, combined_constraint); } - ), - neighbors.end() - ); - - // if 2D, determine if node and neighbors lie on the same line - // if 3D, determine if node and neighbors lie on the same plane - // if not same line/plane, then node is either part of a curved surface or - // it is the vertex where two boundary surfaces meet. In the first case, - // we should just fix the node. For the latter case: - // - 2D: just fix the node - // - 3D: If the node is at the intersection of 3 surfaces (i.e., the - // vertex of a cube), fix the node. If the node is at the intersection - // of 2 surfaces (i.e., the edge of a cube), constrain it to slide along - // this edge. - - // But for now, just fix all the boundary nodes to not move - this->fix_node(node); - - }// end bid - - // Constrain subdomain boundary nodes, if requested - if (_preserve_subdomain_boundaries) - { - auto already_constrained_node_ids = boundary_node_ids; - for (const auto * elem : mesh.active_element_ptr_range()) + + else + this->impose_constraint(node, boundary_constraint); + + } // end bid +} + +void VariationalSmootherConstraint::fix_node(const Node & node) +{ + for (const auto d : make_range(_sys.get_mesh().mesh_dimension())) { - const auto & subdomain_id = elem->subdomain_id(); - for (const auto side : elem->side_index_range()) - { - const auto * neighbor = elem->neighbor_ptr(side); - if (neighbor == nullptr) - continue; + const auto constrained_dof_index = node.dof_number(_sys.number(), d, 0); + DofConstraintRow constraint_row; + // Leave the constraint row as all zeros so this dof is independent from other dofs + const auto constrained_value = node(d); + // Simply constrain this dof to retain it's current value + _sys.get_dof_map().add_constraint_row( + constrained_dof_index, constraint_row, constrained_value, true); + } +} + +void VariationalSmootherConstraint::constrain_node_to_plane(const Node & node, + const Point & ref_normal_vec) +{ + const auto dim = _sys.get_mesh().mesh_dimension(); + // determine equation of plane: c_x * x + c_y * y + c_z * z + c = 0 + std::vector xyz_coefs; // vector to hold c_x, c_y, c_z + Real c = 0.; + + // We choose to constrain the dimension with the largest magnitude coefficient + // This approach ensures the coefficients added to the constraint_row + // (i.e., -c_xyz / c_max) have as small magnitude as possible + + // We initialize this to avoid maybe-uninitialized compiler error + unsigned int constrained_dim = 0; + + // Let's assert that we have a nonzero normal to ensure that constrained_dim + // is always set + libmesh_assert(ref_normal_vec.norm() > TOLERANCE); + + Real max_abs_coef = 0.; + for (const auto d : make_range(dim)) + { + const auto coef = ref_normal_vec(d); + xyz_coefs.push_back(coef); + c -= coef * node(d); + + const auto coef_abs = std::abs(coef); + if (coef_abs > max_abs_coef) + { + max_abs_coef = coef_abs; + constrained_dim = d; + } + } + + DofConstraintRow constraint_row; + for (const auto free_dim : make_range(dim)) + { + if (free_dim == constrained_dim) + continue; + + const auto free_dof_index = node.dof_number(_sys.number(), free_dim, 0); + constraint_row[free_dof_index] = -xyz_coefs[free_dim] / xyz_coefs[constrained_dim]; + } + + const auto inhomogeneous_part = -c / xyz_coefs[constrained_dim]; + const auto constrained_dof_index = node.dof_number(_sys.number(), constrained_dim, 0); + _sys.get_dof_map().add_constraint_row( + constrained_dof_index, constraint_row, inhomogeneous_part, true); +} + +void VariationalSmootherConstraint::constrain_node_to_line(const Node & node, + const Point & line_vec) +{ + const auto dim = _sys.get_mesh().mesh_dimension(); + + // We will free the dimension most parallel to line_vec to keep the + // constraint coefficients small + const std::vector line_vec_coefs{line_vec(0), line_vec(1), line_vec(2)}; + auto it = std::max_element(line_vec_coefs.begin(), line_vec_coefs.end(), [](double a, double b) { + return std::abs(a) < std::abs(b); + }); + const unsigned int free_dim = std::distance(line_vec_coefs.begin(), it); + const auto free_dof_index = node.dof_number(_sys.number(), free_dim, 0); + + // A line is parameterized as r(t) = node + t * line_vec, so + // x(t) = node(x) + t * line_vec(x) + // y(t) = node(y) + t * line_vec(y) + // z(t) = node(z) + t * line_vec(z) + // Let's say we leave x free. Then t = (x(t) - node(x)) / line_vec(x) + // Then y and z can be constrained as + // y = node(y) + line_vec_y * (x(t) - node(x)) / line_vec(x) + // = x(t) * line_vec(y) / line_vec(x) + (node(y) - node(x) * line_vec(y) / line_vec(x)) + // z = x(t) * line_vec(z) / line_vec(x) + (node(z) - node(x) * line_vec(z) / line_vec(x)) + + libmesh_assert(!relative_fuzzy_equals(line_vec(free_dim), 0.)); + for (const auto constrained_dim : make_range(dim)) + { + if (constrained_dim == free_dim) + continue; + + DofConstraintRow constraint_row; + constraint_row[free_dof_index] = line_vec(constrained_dim) / line_vec(free_dim); + const auto inhomogeneous_part = + node(constrained_dim) - node(free_dim) * line_vec(constrained_dim) / line_vec(free_dim); + const auto constrained_dof_index = node.dof_number(_sys.number(), constrained_dim, 0); + _sys.get_dof_map().add_constraint_row( + constrained_dof_index, constraint_row, inhomogeneous_part, true); + } +} + +// Utility function to determine whether two nodes share a boundary ID. +// The motivation for this is that a sliding boundary node on a triangular +// element can have a neighbor boundary node in the same element that is not +// part of the same boundary +// Consider the below example with nodes A, C, D, F, G that comprise elements +// E1, E2, E3, with boundaries B1, B2, B3, B4. To determine the constraint +// equations for the sliding node C, the neighboring nodes A and D need to +// be identified to construct the line that C is allowed to slide along. +// Note that neighbors A and D both share the boundary B1 with C. +// Without ensuring that neighbors share the same boundary as the current +// node, a neighboring node that does not lie on the same boundary +// (i.e. F and G) might be selected to define the constraining line, +// resulting in an incorrect constraint. +// Note that if, for example, boundaries B1 and B2 were to be combined +// into boundary B12, the node F would share a boundary id with node C +// and result in an incorrect constraint. It would be useful to design +// additional checks to detect cases like this. + +// B3 +// G-----------F +// | \ / | +// B4 | \ E2 / | B2 +// | \ / | +// | E1 \ / E3 | +// A-----C-----D +// B1 + +bool VariationalSmootherConstraint::nodes_share_boundary_id(const Node & boundary_node, + const Node & neighbor_node, + const Elem & containing_elem, + const BoundaryInfo & boundary_info) +{ + bool nodes_share_bid = false; + + // Node ids local to containing_elem + const auto node_id = containing_elem.get_node_index(&boundary_node); + const auto neighbor_id = containing_elem.get_node_index(&neighbor_node); + + for (const auto side_id : containing_elem.side_index_range()) + { + // We don't care about this side if it doesn't contain our boundary and neighbor nodes + if (!(containing_elem.is_node_on_side(node_id, side_id) && + containing_elem.is_node_on_side(neighbor_id, side_id))) + continue; + + // If the current side, containing boundary_node and neighbor_node, lies on a boundary, + // we can say that boundary_node and neighbor_node have a common boundary id. + std::vector boundary_ids; + boundary_info.boundary_ids(&containing_elem, side_id, boundary_ids); + if (boundary_ids.size()) + { + nodes_share_bid = true; + break; + } + } + return nodes_share_bid; +} + +std::set> +VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( + const MeshBase & mesh, + const Node & node, + const subdomain_id_type sub_id, + const std::unordered_map> & nodes_to_elem_map) +{ + + // Find all the nodal neighbors... that is the nodes directly connected + // to this node through one edge + std::vector neighbors; + MeshTools::find_nodal_neighbors(mesh, node, nodes_to_elem_map, neighbors); + + // Each constituent set corresponds to neighbors sharing a face on the + // subdomain boundary + std::set> side_grouped_boundary_neighbors; + + for (const auto * neigh : neighbors) + { + // Determine whether the neighbor is on the subdomain boundary + // First, find the common elements that both node and neigh belong to + const auto & elems_containing_node = nodes_to_elem_map.at(node.id()); + const auto & elems_containing_neigh = nodes_to_elem_map.at(neigh->id()); + const Elem * common_elem = nullptr; + for (const auto * neigh_elem : elems_containing_neigh) + { + if ((std::find(elems_containing_node.begin(), elems_containing_node.end(), neigh_elem) != + elems_containing_node.end()) + // We should be able to find a common element on the sub_id boundary + && (neigh_elem->subdomain_id() == sub_id)) + common_elem = neigh_elem; + else + continue; + + // Now, determine whether node and neigh are on a side coincident + // with the subdomain boundary + for (const auto common_side : common_elem->side_index_range()) + { + bool node_found_on_side = false; + bool neigh_found_on_side = false; + for (const auto local_node_id : common_elem->nodes_on_side(common_side)) + { + if (common_elem->node_id(local_node_id) == node.id()) + node_found_on_side = true; + else if (common_elem->node_id(local_node_id) == neigh->id()) + neigh_found_on_side = true; + } + + if (!(node_found_on_side && neigh_found_on_side && + common_elem->neighbor_ptr(common_side))) + continue; + + const auto matched_side = common_side; + // There could be multiple matched sides, so keep this next part + // inside the common_side loop + // + // Does matched_side, containing both node and neigh, lie on the + // sub_id subdomain boundary? + const auto matched_neighbor_sub_id = + common_elem->neighbor_ptr(matched_side)->subdomain_id(); + const bool is_matched_side_on_subdomain_boundary = matched_neighbor_sub_id != sub_id; + + if (is_matched_side_on_subdomain_boundary) + { + // Store all nodes that live on this side + const auto nodes_on_side = common_elem->nodes_on_side(common_side); + std::set node_ptrs_on_side; + for (const auto local_node_id : nodes_on_side) + node_ptrs_on_side.insert(common_elem->node_ptr(local_node_id)); + node_ptrs_on_side.erase(node_ptrs_on_side.find(&node)); + side_grouped_boundary_neighbors.insert(node_ptrs_on_side); + + continue; + } + + } // for common_side + + } // for neigh_elem + } + + return side_grouped_boundary_neighbors; +} + +std::set> +VariationalSmootherConstraint::get_neighbors_for_boundary_constraint( + const MeshBase & mesh, + const Node & node, + const std::unordered_set & boundary_node_ids, + const BoundaryInfo & boundary_info, + const std::unordered_map> & nodes_to_elem_map) +{ + + // Find all the nodal neighbors... that is the nodes directly connected + // to this node through one edge + std::vector neighbors; + MeshTools::find_nodal_neighbors(mesh, node, nodes_to_elem_map, neighbors); + + // Each constituent set corresponds to neighbors sharing a face on the + // boundary + std::set> side_grouped_boundary_neighbors; + + for (const auto * neigh : neighbors) + { + const bool is_neighbor_boundary_node = + boundary_node_ids.find(neigh->id()) != boundary_node_ids.end(); + if (!is_neighbor_boundary_node) + continue; + + // Determine whether nodes share a common boundary id + // First, find the common element that both node and neigh belong to + const auto & elems_containing_node = nodes_to_elem_map.at(node.id()); + const auto & elems_containing_neigh = nodes_to_elem_map.at(neigh->id()); + const Elem * common_elem = nullptr; + for (const auto * neigh_elem : elems_containing_neigh) + { + const bool is_neigh_common = + std::find(elems_containing_node.begin(), elems_containing_node.end(), neigh_elem) != + elems_containing_node.end(); + if (!is_neigh_common) + continue; + common_elem = neigh_elem; + // Keep this in the neigh_elem loop because there can be multiple common + // elements Now, determine whether node and neigh share a common boundary + // id + const bool nodes_have_common_bid = VariationalSmootherConstraint::nodes_share_boundary_id( + node, *neigh, *common_elem, boundary_info); + if (nodes_have_common_bid) + { + // Find the side coinciding with the shared boundary + for (const auto side : common_elem->side_index_range()) + { + // We only care about external boundaries here, make sure side doesn't + // have a neighbor + if (common_elem->neighbor_ptr(side)) + continue; + + bool node_found_on_side = false; + bool neigh_found_on_side = false; + const auto nodes_on_side = common_elem->nodes_on_side(side); + for (const auto local_node_id : nodes_on_side) + { + if (common_elem->node_id(local_node_id) == node.id()) + node_found_on_side = true; + else if (common_elem->node_id(local_node_id) == neigh->id()) + neigh_found_on_side = true; + } + if (!(node_found_on_side && neigh_found_on_side)) + continue; + + std::set node_ptrs_on_side; + for (const auto local_node_id : nodes_on_side) + node_ptrs_on_side.insert(common_elem->node_ptr(local_node_id)); + node_ptrs_on_side.erase(node_ptrs_on_side.find(&node)); + side_grouped_boundary_neighbors.insert(node_ptrs_on_side); + } + continue; + } + } + } + + return side_grouped_boundary_neighbors; +} + +ConstraintVariant VariationalSmootherConstraint::determine_constraint( + const Node & node, + const unsigned int dim, + const std::set> & side_grouped_boundary_neighbors) +{ + // Determines the appropriate geometric constraint for a node based on its + // neighbors. + + // Extract neighbors in flat vector + std::vector neighbors; + for (const auto & side : side_grouped_boundary_neighbors) + neighbors.insert(neighbors.end(), side.begin(), side.end()); - const auto & neighbor_subdomain_id = neighbor->subdomain_id(); - if (subdomain_id != neighbor_subdomain_id) + // Constrain the node to it's current location + if (dim == 1 || neighbors.size() == 1) + return PointConstraint{node}; + + if (dim == 2 || neighbors.size() == 2) + { + // Determine whether node + all neighbors are colinear + bool all_colinear = true; + const Point ref_dir = (*neighbors[0] - node).unit(); + for (const auto i : make_range(std::size_t(1), neighbors.size())) { - for (const auto local_node_id : elem->nodes_on_side(side)) + const Point delta = *(neighbors[i]) - node; + libmesh_assert(delta.norm() >= TOLERANCE); + const Point dir = delta.unit(); + if (!dir.relative_fuzzy_equals(ref_dir) && !dir.relative_fuzzy_equals(-ref_dir)) { - const auto & node = mesh.node_ref(elem->node_id(local_node_id)); - // Make sure we haven't already constrained this node - if ( - std::find(already_constrained_node_ids.begin(), - already_constrained_node_ids.end(), - node.id()) == already_constrained_node_ids.end() - ) + all_colinear = false; + break; + } + } + + if (all_colinear) + return LineConstraint{node, ref_dir}; + + return PointConstraint{node}; + } + + // dim == 3, neighbors.size() >= 3 + std::set valid_planes; + for (const auto & side_nodes : side_grouped_boundary_neighbors) + { + std::vector side_nodes_vec(side_nodes.begin(), side_nodes.end()); + for (const auto i : index_range(side_nodes_vec)) + { + const Point vec_i = (*side_nodes_vec[i] - node); + for (const auto j : make_range(i)) { - this->fix_node(node); - already_constrained_node_ids.insert(node.id()); + const Point vec_j = (*side_nodes_vec[j] - node); + Point candidate_normal = vec_i.cross(vec_j); + if (candidate_normal.norm() <= TOLERANCE) + continue; + + const PlaneConstraint candidate_plane{node, candidate_normal}; + valid_planes.emplace(candidate_plane); } - }//for local_node_id } - }// for side - }// for elem - } + } + // Fall back to point constraint + if (valid_planes.empty()) + return PointConstraint(node); + + // Combine all the planes together to get a common constraint + auto it = valid_planes.begin(); + ConstraintVariant current = *it++; + for (; it != valid_planes.end(); ++it) + { + current = intersect_constraints(current, *it); + + // This will catch cases where constraints have no intersection + // (i.e., the element surface is non-planar) + // Fall back to fixed node constraint + if (std::holds_alternative(current)) + { + current = PointConstraint(node); + break; + } + } + + return current; } -void VariationalSmootherConstraint::fix_node(const Node & node) +// Applies the computed constraint (PointConstraint, LineConstraint, or +// PlaneConstraint) to a node. +void VariationalSmootherConstraint::impose_constraint(const Node & node, + const ConstraintVariant & constraint) { - for (const auto d : make_range(_sys.get_mesh().mesh_dimension())) - { - const auto constrained_dof_index = node.dof_number(_sys.number(), d, 0); - DofConstraintRow constraint_row; - // Leave the constraint row as all zeros so this dof is independent from other dofs - const auto constrained_value = node(d); - // Simply constrain this dof to retain it's current value - _sys.get_dof_map().add_constraint_row( constrained_dof_index, constraint_row, constrained_value, true); - } + + libmesh_assert_msg(!std::holds_alternative(constraint), + "Cannot impose constraint using InvalidConstraint."); + + if (std::holds_alternative(constraint)) + fix_node(node); + else if (std::holds_alternative(constraint)) + constrain_node_to_line(node, std::get(constraint).direction()); + else if (std::holds_alternative(constraint)) + constrain_node_to_plane(node, std::get(constraint).normal()); + + else + libmesh_assert_msg(false, "Unknown constraint type."); } } // namespace libMesh diff --git a/src/systems/variational_smoother_system.C b/src/systems/variational_smoother_system.C index 8b30d5cf11..700ac091e4 100644 --- a/src/systems/variational_smoother_system.C +++ b/src/systems/variational_smoother_system.C @@ -18,15 +18,16 @@ #include "libmesh/variational_smoother_system.h" #include "libmesh/elem.h" +#include "libmesh/face_tri3.h" #include "libmesh/fe_base.h" #include "libmesh/fe_interface.h" #include "libmesh/fem_context.h" #include "libmesh/mesh.h" +#include "libmesh/numeric_vector.h" +#include "libmesh/parallel_ghost_sync.h" #include "libmesh/quadrature.h" #include "libmesh/string_to_enum.h" #include "libmesh/utility.h" -#include "libmesh/numeric_vector.h" -#include "libmesh/parallel_ghost_sync.h" // C++ includes #include // std::reference_wrapper @@ -89,16 +90,17 @@ void VariationalSmootherSystem::init_data () } } - this->compute_element_reference_volume(); + this->prepare_for_smoothing(); } -void VariationalSmootherSystem::compute_element_reference_volume() +void VariationalSmootherSystem::prepare_for_smoothing() { std::unique_ptr con = this->build_context(); FEMContext & femcontext = cast_ref(*con); this->init_context(femcontext); const auto & mesh = this->get_mesh(); + const auto dim = mesh.mesh_dimension(); Real elem_averaged_det_J_sum = 0.; @@ -108,15 +110,93 @@ void VariationalSmootherSystem::compute_element_reference_volume() const auto & fe_map = femcontext.get_element_fe(0)->get_fe_map(); const auto & JxW = fe_map.get_JxW(); + std::map> target_elem_inverse_jacobian_dets; + for (const auto * elem : mesh.active_local_element_ptr_range()) { femcontext.pre_fe_reinit(*this, elem); femcontext.elem_fe_reinit(); - const auto elem_integrated_det_J = std::accumulate(JxW.begin(), JxW.end(), 0.); + // Add target element info, if applicable + if (_target_inverse_jacobians.find(elem->type()) == _target_inverse_jacobians.end()) + { + // Create FEMap to compute target_element mapping information + FEMap fe_map_target; + + // pre-request mapping derivatives + const auto & dxyzdxi = fe_map_target.get_dxyzdxi(); + const auto & dxyzdeta = fe_map_target.get_dxyzdeta(); + // const auto & dxyzdzeta = fe_map_target.get_dxyzdzeta(); + + const auto & qrule_points = femcontext.get_element_qrule().get_points(); + const auto & qrule_weights = femcontext.get_element_qrule().get_weights(); + const auto nq_points = femcontext.get_element_qrule().n_points(); + + // If the target element is the reference element, Jacobian matrix is + // identity, det of inverse is 1. This will only be overwritten if a + // different target elemen is explicitly specified. + target_elem_inverse_jacobian_dets[elem->type()] = + std::vector(nq_points, 1.0); + + switch (elem->type()) + { + case TRI3: + { + // Build target element: an equilateral triangle + Tri3 target_elem; + + // equilateral triangle side length that preserves area of reference + // element + const Real sqrt_3 = std::sqrt(3.); + const auto ref_volume = target_elem.reference_elem()->volume(); + const auto s = std::sqrt(4. / sqrt_3 * ref_volume); + + // Set nodes of target element to form an equilateral triangle + Node node_0 = Node(0., 0.); + Node node_1 = Node(s, 0.); + Node node_2 = Node(0.5 * s, 0.5 * s * sqrt_3); + target_elem.set_node(0) = &node_0; + target_elem.set_node(1) = &node_1; + target_elem.set_node(2) = &node_2; + + // build map + fe_map_target.init_reference_to_physical_map(dim, qrule_points, + &target_elem); + fe_map_target.compute_map(dim, qrule_weights, &target_elem, + /*d2phi=*/false); + + // Yes, triangle-to-triangle mappings have constant Jacobians, but we + // will keep things general for now + _target_inverse_jacobians[target_elem.type()] = + std::vector(nq_points); + for (const auto qp : make_range(nq_points)) + { + const RealTensor H_inv = + RealTensor(dxyzdxi[qp](0), dxyzdeta[qp](0), 0, dxyzdxi[qp](1), + dxyzdeta[qp](1), 0, 0, 0, 1) + .inverse(); + + _target_inverse_jacobians[target_elem.type()][qp] = H_inv; + target_elem_inverse_jacobian_dets[target_elem.type()][qp] = + H_inv.det(); + } + + break; + } + + default: + break; + } + } + + Real elem_integrated_det_J(0.); + for (const auto qp : index_range(JxW)) + elem_integrated_det_J += + JxW[qp] * target_elem_inverse_jacobian_dets[elem->type()][qp]; const auto ref_elem_vol = elem->reference_elem()->volume(); elem_averaged_det_J_sum += elem_integrated_det_J / ref_elem_vol; - } + + } // for elem // Get contributions from elements on other processors mesh.comm().sum(elem_averaged_det_J_sum); @@ -124,8 +204,6 @@ void VariationalSmootherSystem::compute_element_reference_volume() _ref_vol = elem_averaged_det_J_sum / mesh.n_active_elem(); } - - void VariationalSmootherSystem::init_context(DiffContext & context) { FEMContext & c = cast_ref(context); @@ -264,6 +342,11 @@ bool VariationalSmootherSystem::element_time_derivative (bool request_jacobian, libmesh_error_msg("Unsupported dimension."); } + // Apply any applicable target element transformations + if (_target_inverse_jacobians.find(elem.type()) != + _target_inverse_jacobians.end()) + S = S * _target_inverse_jacobians[elem.type()][qp]; + // Compute quantities needed for the smoothing algorithm // determinant @@ -529,7 +612,8 @@ bool VariationalSmootherSystem::element_time_derivative (bool request_jacobian, Real d2E_dSdR_l = 0.; Real d2E_dSdR_p = 0.; - for (const auto a : make_range(i + 1)) { + for (const auto a : make_range(i + 1)) + { // If this condition is met, both the ijab and abij // contributions to the Jacobian are zero due to the @@ -546,7 +630,8 @@ bool VariationalSmootherSystem::element_time_derivative (bool request_jacobian, const Real alpha_ja_applied = alpha_times_dilation_weight * S_inv_ja; const auto b_limit = (a == i) ? j + 1 : dim; - for (const auto b : make_range(b_limit)) { + for (const auto b : make_range(b_limit)) + { // Combine precomputed coefficients with tensor products // to get d2(beta) / dS2 diff --git a/tests/mesh/mesh_smoother_test.C b/tests/mesh/mesh_smoother_test.C index 301b641147..6b6df42818 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -1,61 +1,134 @@ -#include +#include +#include #include #include +#include #include #include #include #include #include #include -#include +#include #include -#include #include // LIBMESH_HAVE_SOLVER define #include "test_comm.h" #include "libmesh_cppunit.h" -namespace { +namespace +{ using namespace libMesh; +// Distortion function that doesn't distort boundary nodes +// 2D only, use for LaplaceMeshSmoother class DistortSquare : public FunctionBase { - std::unique_ptr> clone () const override - { return std::make_unique(); } + std::unique_ptr> clone() const override + { + return std::make_unique(); + } - Real operator() (const Point &, - const Real = 0.) override - { libmesh_not_implemented(); } // scalar-only API + Real operator()(const Point &, const Real = 0.) override + { + libmesh_not_implemented(); + } // scalar-only API // Skew inward based on a cubic function - void operator() (const Point & p, - const Real, - DenseVector & output) + void operator()(const Point & p, const Real, DenseVector & output) { output.resize(3); - const Real eta = 2*p(0)-1; - const Real zeta = 2*p(1)-1; - output(0) = p(0) + (std::pow(eta,3)-eta)*p(1)*(1-p(1)); - output(1) = p(1) + (std::pow(zeta,3)-zeta)*p(0)*(1-p(0)); + const Real eta = 2 * p(0) - 1; + const Real zeta = 2 * p(1) - 1; + output(0) = p(0) + (std::pow(eta, 3) - eta) * p(1) * (1 - p(1)); + output(1) = p(1) + (std::pow(zeta, 3) - zeta) * p(0) * (1 - p(0)); output(2) = 0; } }; +// Distortion function supporting 1D, 2D, and 3D +// Use for VariationalMeshSmoother +class DistortHyperCube : public FunctionBase +{ +public: + DistortHyperCube(const unsigned int dim) : _dim(dim) {} + +private: + std::unique_ptr> clone() const override + { + return std::make_unique(_dim); + } + + Real operator()(const Point &, const Real = 0.) override { libmesh_not_implemented(); } + + void operator()(const Point & p, const Real, DenseVector & output) override + { + output.resize(3); + output.zero(); + + // Count how many coordinates are exactly on the boundary (0 or 1) + unsigned int boundary_dims = 0; + std::array is_on_boundary = {false, false, false}; + for (unsigned int i = 0; i < _dim; ++i) + { + if (std::abs(p(i)) < TOLERANCE || std::abs(p(i) - 1.) < TOLERANCE) + { + ++boundary_dims; + is_on_boundary[i] = true; + } + } + + // If all coordinates are on the boundary, treat as vertex — leave unchanged + if (boundary_dims == _dim) + { + for (unsigned int i = 0; i < _dim; ++i) + output(i) = p(i); + return; + } + + // Distort only those directions not fixed on the boundary + for (unsigned int i = 0; i < _dim; ++i) + { + if (!is_on_boundary[i]) // only distort free dimensions + { + Real xi = 2. * p(i) - 1.; + Real modulation = 0.3; // This value constrols the strength of the distortion + for (unsigned int j = 0; j < _dim; ++j) + { + if (j != i) + { + Real pj = std::clamp(p(j), 0., 1.); // ensure numeric safety + modulation *= (pj - 0.5) * (pj - 0.5) * 4.; // quadratic bump centered at 0.5 + } + } + output(i) = p(i) + (std::pow(xi, 3) - xi) * modulation; + } + else + { + output(i) = p(i); // dimension on boundary remains unchanged + } + } + } + + const unsigned int _dim; +}; + class SquareToParallelogram : public FunctionBase { - std::unique_ptr> clone () const override - { return std::make_unique(); } + std::unique_ptr> clone() const override + { + return std::make_unique(); + } - Real operator() (const Point &, - const Real = 0.) override - { libmesh_not_implemented(); } // scalar-only API + Real operator()(const Point &, const Real = 0.) override + { + libmesh_not_implemented(); + } // scalar-only API // Has the effect that a square, meshed into right triangles with diagonals // rising from lower-left to upper-right, is transformed into a left-leaning // parallelogram of eqilateral triangles. - void operator() (const Point & p, - const Real, - DenseVector & output) + void operator()(const Point & p, const Real, DenseVector & output) { output.resize(3); output(0) = p(0) - 0.5 * p(1); @@ -66,19 +139,20 @@ class SquareToParallelogram : public FunctionBase class ParallelogramToSquare : public FunctionBase { - std::unique_ptr> clone () const override - { return std::make_unique(); } + std::unique_ptr> clone() const override + { + return std::make_unique(); + } - Real operator() (const Point &, - const Real = 0.) override - { libmesh_not_implemented(); } // scalar-only API + Real operator()(const Point &, const Real = 0.) override + { + libmesh_not_implemented(); + } // scalar-only API // Has the effect that a left-leaning parallelogram of equilateral triangles is transformed // into a square of right triangle with diagonals rising from lower-left to upper-right. // This is the inversion of the SquareToParallelogram mapping. - void operator() (const Point & p, - const Real, - DenseVector & output) + void operator()(const Point & p, const Real, DenseVector & output) { output.resize(3); output(0) = p(0) + p(1) / std::sqrt(3.); @@ -98,16 +172,21 @@ class MeshSmootherTest : public CppUnit::TestCase * MeshSmoother subclasses. */ public: - LIBMESH_CPPUNIT_TEST_SUITE( MeshSmootherTest ); + LIBMESH_CPPUNIT_TEST_SUITE(MeshSmootherTest); #if LIBMESH_DIM > 2 - CPPUNIT_TEST( testLaplaceQuad ); - CPPUNIT_TEST( testLaplaceTri ); + CPPUNIT_TEST(testLaplaceQuad); + CPPUNIT_TEST(testLaplaceTri); #if defined(LIBMESH_ENABLE_VSMOOTHER) && defined(LIBMESH_HAVE_SOLVER) - CPPUNIT_TEST( testVariationalQuad ); - CPPUNIT_TEST( testVariationalTri ); - CPPUNIT_TEST( testVariationalQuadMultipleSubdomains ); -# endif // LIBMESH_ENABLE_VSMOOTHER + CPPUNIT_TEST(testVariationalEdge); + CPPUNIT_TEST(testVariationalEdgeMultipleSubdomains); + CPPUNIT_TEST(testVariationalQuad); + CPPUNIT_TEST(testVariationalQuadMultipleSubdomains); + CPPUNIT_TEST(testVariationalTri); + CPPUNIT_TEST(testVariationalTriMultipleSubdomains); + CPPUNIT_TEST(testVariationalHex); + CPPUNIT_TEST(testVariationalHexMultipleSubdomains); +#endif // LIBMESH_ENABLE_VSMOOTHER #endif CPPUNIT_TEST_SUITE_END(); @@ -117,210 +196,341 @@ public: void tearDown() {} - void testSmoother(ReplicatedMesh & mesh, MeshSmoother & smoother, const ElemType type, const bool multiple_subdomains=false) + void testLaplaceSmoother(ReplicatedMesh & mesh, MeshSmoother & smoother, ElemType type) { LOG_UNIT_TEST; - unsigned int n_elems_per_side = 4; + constexpr unsigned int n_elems_per_side = 4; - MeshTools::Generation::build_square(mesh, n_elems_per_side, n_elems_per_side, - 0.,1.,0.,1., type); + MeshTools::Generation::build_square( + mesh, n_elems_per_side, n_elems_per_side, 0., 1., 0., 1., type); // Move it around so we have something that needs smoothing DistortSquare ds; MeshTools::Modification::redistribute(mesh, ds); - std::unordered_map subdomain_boundary_node_id_to_point; - if (multiple_subdomains) - { - // Increment the subdomain id on the right half by 1 - for (auto * elem : mesh.active_element_ptr_range()) - if (elem->vertex_average()(0) > 0.5) - ++elem->subdomain_id(); - - // This loop should NOT be combined with the one above because we need to - // finish checking and updating subdomain ids for all elements before - // recording the final subdomain boundary. - for (auto * elem : mesh.active_element_ptr_range()) - for (const auto & s : elem->side_index_range()) - { - const auto* neighbor = elem->neighbor_ptr(s); - if (neighbor == nullptr) - continue; + // Assert the distortion is as expected + auto center_distortion_is = + [](const Node & node, int d, bool distortion, Real distortion_tol = TOLERANCE) { + const Real r = node(d); + const Real R = r * n_elems_per_side; + CPPUNIT_ASSERT_GREATER(-TOLERANCE * TOLERANCE, r); + CPPUNIT_ASSERT_GREATER(-TOLERANCE * TOLERANCE, 1 - r); + + // If we're at the boundaries we should *never* be distorted + if (std::abs(node(0)) < TOLERANCE * TOLERANCE || + std::abs(node(0) - 1) < TOLERANCE * TOLERANCE) + { + const Real R1 = node(1) * n_elems_per_side; + CPPUNIT_ASSERT_LESS(TOLERANCE * TOLERANCE, std::abs(R1 - std::round(R1))); + return true; + } - if (elem->subdomain_id() != neighbor->subdomain_id()) - // This side is part of a subdomain boundary, record the - // corresponding node locations - for (const auto & n : elem->nodes_on_side(s)) - subdomain_boundary_node_id_to_point[elem->node_id(n)] = Point(*(elem->get_nodes()[n])); - } + if (std::abs(node(1)) < TOLERANCE * TOLERANCE || + std::abs(node(1) - 1) < TOLERANCE * TOLERANCE) + { + const Real R0 = node(0) * n_elems_per_side; + CPPUNIT_ASSERT_LESS(TOLERANCE * TOLERANCE, std::abs(R0 - std::round(R0))); + + return true; + } + + // If we're at the center we're fine + if (std::abs(r - 0.5) < TOLERANCE * TOLERANCE) + return true; + + return ((std::abs(R - std::round(R)) > distortion_tol) == distortion); + }; + + for (auto node : mesh.node_ptr_range()) + { + CPPUNIT_ASSERT(center_distortion_is(*node, 0, true)); + CPPUNIT_ASSERT(center_distortion_is(*node, 1, true)); } - const auto & boundary_info = mesh.get_boundary_info(); + // Enough iterations to mostly fix us up. Laplace seems to be at 1e-3 + // tolerance by iteration 6, so hopefully everything is there on any + // system by 8. + for (unsigned int i = 0; i != 8; ++i) + smoother.smooth(); - // Function to assert the distortion is as expected - auto center_distortion_is = [&boundary_info, n_elems_per_side] - (const Node & node, int d, bool distortion, - Real distortion_tol=TOLERANCE) { - const Real r = node(d); - const Real R = r * n_elems_per_side; - CPPUNIT_ASSERT_GREATER(-distortion_tol*distortion_tol, r); - CPPUNIT_ASSERT_GREATER(-distortion_tol*distortion_tol, 1-r); - - // If we're at the center we're fine - if (std::abs(r-0.5) < distortion_tol*distortion_tol) - return true; + // Make sure we're not too distorted anymore. + for (auto node : mesh.node_ptr_range()) + { + CPPUNIT_ASSERT(center_distortion_is(*node, 0, false, 1e-3)); + CPPUNIT_ASSERT(center_distortion_is(*node, 1, false, 1e-3)); + } + } - // Boundary nodes are allowed to slide along the boundary. - // However, nodes that are part of more than one boundary (i.e., corners) should remain fixed. + void testVariationalSmoother(ReplicatedMesh & mesh, + MeshSmoother & smoother, + const ElemType type, + const bool multiple_subdomains = false) + { + LOG_UNIT_TEST; - // Get boundary ids associated with the node - std::vector boundary_ids; - boundary_info.boundary_ids(&node, boundary_ids); + const auto dim = ReferenceElem::get(type).dim(); + + unsigned int n_elems_per_side = 5; + + // If n_elems_per_side is even, then some sliding boundary nodes will have a + // coordinante with value 0.5, which is already the optimal position, + // causing distortion_is(node, true) to return false when evaluating the + // distorted mesh. To avoid this, we require n_elems_per_side to be odd. + libmesh_error_msg_if(n_elems_per_side % 2 != 1, "n_elems_per_side should be odd."); - switch (boundary_ids.size()) + switch (dim) { - // Internal node - case 0: - return ((std::abs(R-std::round(R)) > distortion_tol) == distortion); - break; - // Sliding boundary node case 1: - // Since sliding boundary nodes may or may not already be in the optimal - // position, they may or may not be different from the originally distorted - // mesh. Return true here to avoid issues. - return true; + MeshTools::Generation::build_line(mesh, n_elems_per_side, 0., 1., type); break; - // Fixed boundary node, should not have moved case 2: - if (std::abs(node(0)) < distortion_tol*distortion_tol || - std::abs(node(0)-1) < distortion_tol*distortion_tol) - { - const Real R1 = node(1) * n_elems_per_side; - CPPUNIT_ASSERT_LESS(distortion_tol*distortion_tol, std::abs(R1-std::round(R1))); - return true; - } + MeshTools::Generation::build_square( + mesh, n_elems_per_side, n_elems_per_side, 0., 1., 0., 1., type); + break; - if (std::abs(node(1)) < distortion_tol*distortion_tol || - std::abs(node(1)-1) < distortion_tol*distortion_tol) - { - const Real R0 = node(0) * n_elems_per_side; - CPPUNIT_ASSERT_LESS(distortion_tol*distortion_tol, std::abs(R0-std::round(R0))); + case 3: + MeshTools::Generation::build_cube(mesh, + n_elems_per_side, + n_elems_per_side, + n_elems_per_side, + 0., + 1., + 0., + 1., + 0., + 1., + type); + break; - return true; + default: + libmesh_error_msg("Unsupported dimension " << dim); + } + + // Move it around so we have something that needs smoothing + DistortHyperCube dh(dim); + MeshTools::Modification::redistribute(mesh, dh); + + // Add multiple subdomains if requested + std::unordered_map subdomain_boundary_node_id_to_point; + if (multiple_subdomains) + { + // Modify the subdomain ids in an interesting way + for (auto * elem : mesh.active_element_ptr_range()) + { + unsigned int subdomain_id = 0; + for (const auto d : make_range(dim)) + if (elem->vertex_average()(d) > 0.5) + ++subdomain_id; + elem->subdomain_id() += subdomain_id; + } + + // This loop should NOT be combined with the one above because we need to + // finish checking and updating subdomain ids for all elements before + // recording the final subdomain boundary. + for (auto * elem : mesh.active_element_ptr_range()) + for (const auto & s : elem->side_index_range()) + { + const auto * neighbor = elem->neighbor_ptr(s); + if (neighbor == nullptr) + continue; + + if (elem->subdomain_id() != neighbor->subdomain_id()) + // This side is part of a subdomain boundary, record the + // corresponding node locations + for (const auto & n : elem->nodes_on_side(s)) + subdomain_boundary_node_id_to_point[elem->node_id(n)] = + Point(*(elem->get_nodes()[n])); } - return false; - break; - default: - libmesh_error_msg("Node has unsupported number of boundary ids = " << boundary_ids.size()); } + + // Function to assert the distortion is as expected + const auto & boundary_info = mesh.get_boundary_info(); + auto distortion_is = [&n_elems_per_side, &dim, &boundary_info]( + const Node & node, bool distortion, Real distortion_tol = TOLERANCE) { + // Get boundary ids associated with the node + std::vector boundary_ids; + boundary_info.boundary_ids(&node, boundary_ids); + + // This tells us what type of node we are: internal, sliding, or fixed + const auto num_dofs = dim - boundary_ids.size(); + /* + * The following cases of num_dofs are possible, ASSUMING all boundaries + * are non-overlapping + * 3D: 3-0, 3-1, 3-2, 3-3 + * = 3 2 1 0 + * internal sliding, sliding, fixed + * 2D: 2-0, 2-1, 2-2 + * = 2 1 0 + * internal sliding, fixed + * 1D: 1-0, 1-1 + * = 1 0 + * internal fixed + * + * We expect that R is an integer in [0, n_elems_per_side] for + * num_dofs of the node's cooridinantes, while the remaining coordinates + * are fixed to the boundary with value 0 or 1. In other words, at LEAST + * dim - num_dofs coordinantes should be 0 or 1. + */ + + std::size_t num_zero_or_one = 0; + + bool distorted = false; + for (const auto d : make_range(dim)) + { + const Real r = node(d); + const Real R = r * n_elems_per_side; + CPPUNIT_ASSERT_GREATER(-distortion_tol * distortion_tol, r); + CPPUNIT_ASSERT_GREATER(-distortion_tol * distortion_tol, 1 - r); + + // Due to the type of distortion used, nodes on the x, y, or z plane + // of symmetry do not have their respective x, y, or z node adjusted. + // Just continue to the next dimension. + if (std::abs(r - 0.5) < distortion_tol * distortion_tol) + continue; + + const bool d_distorted = std::abs(R - std::round(R)) > distortion_tol; + distorted |= d_distorted; + num_zero_or_one += (absolute_fuzzy_equals(r, 0.) || absolute_fuzzy_equals(r, 1.)); + } + + CPPUNIT_ASSERT_GREATEREQUAL(dim - num_dofs, num_zero_or_one); + + // We can never expect a fixed node to be distorted + if (num_dofs == 0) + // if (num_dofs < dim) + return true; + return distorted == distortion; }; // Function to check if a given node has changed based on previous mapping - auto is_internal_subdomain_boundary_node_the_same = [&subdomain_boundary_node_id_to_point] - (const Node & node) { + auto is_subdomain_boundary_node_the_same = [&subdomain_boundary_node_id_to_point]( + const Node & node) { auto it = subdomain_boundary_node_id_to_point.find(node.id()); if (it != subdomain_boundary_node_id_to_point.end()) - return (Point(node) == subdomain_boundary_node_id_to_point[node.id()]); + return (relative_fuzzy_equals(Point(node), subdomain_boundary_node_id_to_point[node.id()])); else - // node is not an internal subdomain boundary node, just return true + // node is not a subdomain boundary node, just return true return true; }; // Make sure our DistortSquare transformation has distorted the mesh for (auto node : mesh.node_ptr_range()) + CPPUNIT_ASSERT(distortion_is(*node, true)); + + // Transform the square mesh of triangles to a parallelogram mesh of + // triangles. This will allow the Variational Smoother to smooth the mesh + // to the optimal case of equilateral triangles + if (type == TRI3) { - CPPUNIT_ASSERT(center_distortion_is(*node, 0, true)); - CPPUNIT_ASSERT(center_distortion_is(*node, 1, true)); + SquareToParallelogram stp; + MeshTools::Modification::redistribute(mesh, stp); } - // Transform the square mesh of triangles to a parallelogram mesh of triangles. - // This will allow the Variational Smoother to smooth the mesh to the optimal case - // of equilateral triangles - const bool is_variational_smoother_type = (dynamic_cast(&smoother) != nullptr); - if (type == TRI3 && is_variational_smoother_type) - { - SquareToParallelogram stp; - MeshTools::Modification::redistribute(mesh, stp); - } - - // Enough iterations to mostly fix us up. Laplace seems to be at 1e-3 - // tolerance by iteration 6, so hopefully everything is there on any - // system by 8. - const unsigned int num_iterations = is_variational_smoother_type ? 1 : 8; - for (unsigned int i=0; i != num_iterations; ++i) - smoother.smooth(); + smoother.smooth(); - // Transform the parallelogram mesh back to a square mesh. In the case of the - // Variational Smoother, equilateral triangular elements will be transformed - // into right triangular elements that align with the original undistorted mesh. - if (type == TRI3 && is_variational_smoother_type) - { - ParallelogramToSquare pts; - MeshTools::Modification::redistribute(mesh, pts); - } + // Transform the parallelogram mesh back to a square mesh. In the case of + // the Variational Smoother, equilateral triangular elements will be + // transformed into right triangular elements that align with the original + // undistorted mesh. + if (type == TRI3) + { + ParallelogramToSquare pts; + MeshTools::Modification::redistribute(mesh, pts); + } - // Make sure we're not too distorted anymore OR that interval subdomain boundary nodes did not change. + // Make sure we're not too distorted anymore OR that interval subdomain boundary nodes did not + // change. for (auto node : mesh.node_ptr_range()) { if (multiple_subdomains) - { - CPPUNIT_ASSERT(is_internal_subdomain_boundary_node_the_same(*node)); - } + CPPUNIT_ASSERT(is_subdomain_boundary_node_the_same(*node)); else - { - CPPUNIT_ASSERT(center_distortion_is(*node, 0, false, 1e-3)); - CPPUNIT_ASSERT(center_distortion_is(*node, 1, false, 1e-3)); - } + CPPUNIT_ASSERT(distortion_is(*node, false, 1e-3)); } } - void testLaplaceQuad() { ReplicatedMesh mesh(*TestCommWorld); LaplaceMeshSmoother laplace(mesh); - testSmoother(mesh, laplace, QUAD4); + testLaplaceSmoother(mesh, laplace, QUAD4); } - void testLaplaceTri() { ReplicatedMesh mesh(*TestCommWorld); LaplaceMeshSmoother laplace(mesh); - testSmoother(mesh, laplace, TRI3); + testLaplaceSmoother(mesh, laplace, TRI3); } - #ifdef LIBMESH_ENABLE_VSMOOTHER + void testVariationalEdge() + { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testVariationalSmoother(mesh, variational, EDGE2); + } + + void testVariationalEdgeMultipleSubdomains() + { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testVariationalSmoother(mesh, variational, EDGE2, true); + } + void testVariationalQuad() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, QUAD4); + testVariationalSmoother(mesh, variational, QUAD4); } + void testVariationalQuadMultipleSubdomains() + { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testVariationalSmoother(mesh, variational, QUAD4, true); + } void testVariationalTri() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, TRI3); + testVariationalSmoother(mesh, variational, TRI3); } - void testVariationalQuadMultipleSubdomains() + void testVariationalTriMultipleSubdomains() + { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testVariationalSmoother(mesh, variational, TRI3, true); + } + + void testVariationalHex() + { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testVariationalSmoother(mesh, variational, HEX8); + } + + void testVariationalHexMultipleSubdomains() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, QUAD4, true); + testVariationalSmoother(mesh, variational, HEX8, true); } #endif // LIBMESH_ENABLE_VSMOOTHER }; - -CPPUNIT_TEST_SUITE_REGISTRATION( MeshSmootherTest ); +CPPUNIT_TEST_SUITE_REGISTRATION(MeshSmootherTest); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy