From 4a50ef6d05e6e365b52e9efe9201bb2b210d7097 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Sun, 22 Jun 2025 06:04:01 -0600 Subject: [PATCH 01/38] Sliding boundary nodes working for 2D and 3D (coplanar edges only for 3D, not sliding edge nodes. --- src/systems/variational_smoother_constraint.C | 186 +++++++++++++++++- 1 file changed, 181 insertions(+), 5 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index dc54d30140..91ba230dcc 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -33,7 +33,8 @@ 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; @@ -58,8 +59,182 @@ void VariationalSmootherConstraint::constrain() 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 + // Determine whether the current node is colinear (coplanar) with its boundary + // neighbors Start by computing the vectors from the current node to each boundary + // neighbor node + //std::cout << "Node " << node.id() << ":" << std::endl; + std::vector dist_vecs; + for (const auto & neighbor : neighbors) + { + dist_vecs.push_back((*neighbor) - node); + //std::cout << " Neighbor " << neighbor->id() << ", dist_vec = " << dist_vecs.back() << std::endl; + } + + // 2D: If the current node and all (two) boundary neighbor nodes lie on the same line, + // the magnitude of the dot product of the distance vectors will be equal + // to the product of the magnitudes of the vectors. This is because the distance + // vectors lie on the same line, so the cos(theta) term in the dot product + // evaluates to -1 or 1. + if (dim == 2) + { + // Physically, the boundary of a 2D mesh is a 1D curve. By the + // definition of a "neighbor", it is only possible for a node + // to have 2 neighbors on the boundary. + libmesh_assert_equal_to(dist_vecs.size(), dim); + const Real dot_product = dist_vecs[0] * dist_vecs[1]; + const Real norm_product = dist_vecs[0].norm() * dist_vecs[1].norm(); + + // node is not colinear with its boundary neighbors and is thus immovable + if (!relative_fuzzy_equals(std::abs(dot_product), norm_product)) + { + this->fix_node(node); + continue; + } + + // else, determine equation of line: c_x * x + c_y * y + c = 0 + Real c_x, c_y, c; + const auto & vec = dist_vecs[0]; + if (vec(0) == 0) // vertical line + { + c_y = 0.; + c_x = 1.; + c = -node(0); + } + else // not a vertical line + { + c_y = 1.; + c_x = -vec(1) / vec(0); // c_x = -m from y = mx + b + c = -c_x * node(0) - node(1); + } + + const std::vector xy_coefs{c_x, c_y}; + + // Constrain the dimension with the largest coefficient + const unsigned int constrained_dim = (std::abs(c_x) > std::abs(c_y)) ? 0 : 1; + const unsigned int free_dim = (std::abs(c_x) > std::abs(c_y)) ? 1 : 0; + + const auto constrained_dof_index = node.dof_number(_sys.number(), constrained_dim, 0); + const auto free_dof_index = node.dof_number(_sys.number(), free_dim, 0); + DofConstraintRow constraint_row; + constraint_row[free_dof_index] = -xy_coefs[free_dim] / xy_coefs[constrained_dim]; + const auto constrained_value = -c / xy_coefs[constrained_dim]; + _sys.get_dof_map().add_constraint_row( constrained_dof_index, constraint_row, constrained_value, true); + } + + // 3D: If the current node and all boundary neighbor nodes lie on the same plane, + // all the distance vectors from the current node to the boundary nodes will be + // orthogonal to the plane normal. We can obtain a reference normal by normalizing + // the cross product between two of the distance vectors. If the normalized cross + // products of all other combinations (excluding self combinations) match this + // reference normal, then the current node is coplanar with all of its boundary nodes. + else if (dim == 3) + { + // We should have at least 2 distance vectors to compute a normal with in 3D + libmesh_assert_greater_equal(dist_vecs.size(), 2); + + // Compute the reference normal by taking the cross product of two vectors in + // dist_vecs. We will use dist_vecs[0] as the first vector and the next available + // vector in dist_vecs that is not (anti)parallel to dist_vecs[0]. Without this + // check we may end up with a zero vector for the reference vector. + unsigned int vec_index; + const Point vec_0_normalized = dist_vecs[0] / dist_vecs[0].norm(); + for (const auto ii : make_range(size_t(1), dist_vecs.size())) + { + // (anti)parallel check + const bool is_parallel = + vec_0_normalized.relative_fuzzy_equals(dist_vecs[ii] / dist_vecs[ii].norm()); + const bool is_antiparallel = + vec_0_normalized.relative_fuzzy_equals(-dist_vecs[ii] / dist_vecs[ii].norm()); + if (!(is_parallel || is_antiparallel)) + { + vec_index = ii; + break; + } + } + + const Point reference_cross_prod = dist_vecs[0].cross(dist_vecs[vec_index]); + const Point reference_normal = reference_cross_prod / reference_cross_prod.norm(); + + bool node_is_coplanar = true; + for (const auto ii : index_range(dist_vecs)) + { + const Point vec_ii_normalized = dist_vecs[ii] / dist_vecs[ii].norm(); + for (const auto jj : make_range(ii + 1, dist_vecs.size())) + { + // No need to compute the cross product for this case, as it is by + // definition equal to the reference normal computed above. + // Also check for dist_vecs that are (anti)parallel, and skip, + // as their cross product will be zero. + const Point vec_jj_normalized = dist_vecs[jj] / dist_vecs[jj].norm(); + const bool is_parallel = + vec_ii_normalized.relative_fuzzy_equals(vec_jj_normalized); + const bool is_antiparallel = vec_ii_normalized.relative_fuzzy_equals( + -vec_jj_normalized); + + if ((ii == 0 and jj == vec_index) || (is_parallel || is_antiparallel)) + continue; + + const Point cross_prod = dist_vecs[ii].cross(dist_vecs[jj]); + const Point normal = cross_prod / cross_prod.norm(); + + // node is not coplanar with its boundary neighbors and is thus immovable + if (!(reference_normal.relative_fuzzy_equals(normal) || + reference_normal.relative_fuzzy_equals(-normal))) + { + node_is_coplanar = false; + break; + } + } + } + + // TODO: Need to add check for sliding edge node + + if (!node_is_coplanar) + { + this->fix_node(node); + continue; + } + + // else, determine equation of plane: c_x * x + c_y * y + c_z * z + c = 0 + const Real c_x = reference_normal(0); + const Real c_y = reference_normal(1); + const Real c_z = reference_normal(2); + const Real c = -(c_x * node(0) + c_y * node(1) + c_z * node(2)); + + const std::vector xyz_coefs{c_x, c_y, c_z}; + + // Find the dimension with the largest nonzero magnitude coefficient + auto it = std::max_element(xyz_coefs.begin(), xyz_coefs.end(), + [](double a, double b) { + return std::abs(a) < std::abs(b); + }); + const unsigned int constrained_dim = std::distance(xyz_coefs.begin(), it); + + //std::cout << "Node " << node.id() << " (" << node(0) << ", " << node(1) << ", " << node(2) + // << ") lies on the plane " << std::endl + // << c_x << " * x + " << c_y << " * y + " << c_z << " * z + " << c << " = 0" << std::endl; + + + //const std::vector dim_names{"x", "y", "z"}; + //std::cout << "Constraining " << dim_names[constrained_dim] << " = "; + + DofConstraintRow constraint_row; + auto constrained_value = -c / xyz_coefs[constrained_dim]; + for (const auto free_dim : index_range(xyz_coefs)) + { + 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]; + //std::cout << constraint_row[free_dof_index] << " * " << dim_names[free_dim] << " + "; + } + + //std::cout << constrained_value << std::endl; + 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, constrained_value, true); + + } + // 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: @@ -69,8 +244,9 @@ void VariationalSmootherConstraint::constrain() // 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); + // 1D + else + this->fix_node(node); }// end bid From ecad2717b2326edf598221ef4a34ef2cd31dfab7 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Sun, 22 Jun 2025 07:27:49 -0600 Subject: [PATCH 02/38] Moved logic to constrain node to plane into dedicated function. --- .../systems/variational_smoother_constraint.h | 11 +- src/systems/variational_smoother_constraint.C | 111 +++++++----------- 2 files changed, 55 insertions(+), 67 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index c903a1c02c..96ca19a7da 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -41,11 +41,20 @@ class VariationalSmootherConstraint : public System::Constraint 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); + /* + * 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); + public: /* diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 91ba230dcc..a9557221b1 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -91,34 +91,9 @@ void VariationalSmootherConstraint::constrain() continue; } - // else, determine equation of line: c_x * x + c_y * y + c = 0 - Real c_x, c_y, c; - const auto & vec = dist_vecs[0]; - if (vec(0) == 0) // vertical line - { - c_y = 0.; - c_x = 1.; - c = -node(0); - } - else // not a vertical line - { - c_y = 1.; - c_x = -vec(1) / vec(0); // c_x = -m from y = mx + b - c = -c_x * node(0) - node(1); - } - - const std::vector xy_coefs{c_x, c_y}; - - // Constrain the dimension with the largest coefficient - const unsigned int constrained_dim = (std::abs(c_x) > std::abs(c_y)) ? 0 : 1; - const unsigned int free_dim = (std::abs(c_x) > std::abs(c_y)) ? 1 : 0; - - const auto constrained_dof_index = node.dof_number(_sys.number(), constrained_dim, 0); - const auto free_dof_index = node.dof_number(_sys.number(), free_dim, 0); - DofConstraintRow constraint_row; - constraint_row[free_dof_index] = -xy_coefs[free_dim] / xy_coefs[constrained_dim]; - const auto constrained_value = -c / xy_coefs[constrained_dim]; - _sys.get_dof_map().add_constraint_row( constrained_dof_index, constraint_row, constrained_value, true); + // TODO: what if z is not the inactive dimension in 2D!?!? + const auto reference_normal = dist_vecs[0].cross(Point(0., 0., 1.)); + this->constrain_node_to_plane(node, reference_normal); } // 3D: If the current node and all boundary neighbor nodes lie on the same plane, @@ -195,44 +170,7 @@ void VariationalSmootherConstraint::constrain() continue; } - // else, determine equation of plane: c_x * x + c_y * y + c_z * z + c = 0 - const Real c_x = reference_normal(0); - const Real c_y = reference_normal(1); - const Real c_z = reference_normal(2); - const Real c = -(c_x * node(0) + c_y * node(1) + c_z * node(2)); - - const std::vector xyz_coefs{c_x, c_y, c_z}; - - // Find the dimension with the largest nonzero magnitude coefficient - auto it = std::max_element(xyz_coefs.begin(), xyz_coefs.end(), - [](double a, double b) { - return std::abs(a) < std::abs(b); - }); - const unsigned int constrained_dim = std::distance(xyz_coefs.begin(), it); - - //std::cout << "Node " << node.id() << " (" << node(0) << ", " << node(1) << ", " << node(2) - // << ") lies on the plane " << std::endl - // << c_x << " * x + " << c_y << " * y + " << c_z << " * z + " << c << " = 0" << std::endl; - - - //const std::vector dim_names{"x", "y", "z"}; - //std::cout << "Constraining " << dim_names[constrained_dim] << " = "; - - DofConstraintRow constraint_row; - auto constrained_value = -c / xyz_coefs[constrained_dim]; - for (const auto free_dim : index_range(xyz_coefs)) - { - 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]; - //std::cout << constraint_row[free_dof_index] << " * " << dim_names[free_dim] << " + "; - } - - //std::cout << constrained_value << std::endl; - 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, constrained_value, true); - + this->constrain_node_to_plane(node, reference_normal); } // if not same line/plane, then node is either part of a curved surface or @@ -300,4 +238,45 @@ void VariationalSmootherConstraint::fix_node(const Node & node) } } +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 + unsigned int constrained_dim; + 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); +} + } // namespace libMesh From eb7459ca7f843db3faa0b16e2f27a4b321839666 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Sun, 22 Jun 2025 17:12:43 -0600 Subject: [PATCH 03/38] Added method to constrain nodes to lines in 3D. --- .../systems/variational_smoother_constraint.h | 9 ++ src/systems/variational_smoother_constraint.C | 148 +++++++++++------- 2 files changed, 104 insertions(+), 53 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 96ca19a7da..d50c74882f 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -55,6 +55,15 @@ class VariationalSmootherConstraint : public System::Constraint */ 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); + public: /* diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index a9557221b1..19c2309ccc 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -62,12 +62,10 @@ void VariationalSmootherConstraint::constrain() // Determine whether the current node is colinear (coplanar) with its boundary // neighbors Start by computing the vectors from the current node to each boundary // neighbor node - //std::cout << "Node " << node.id() << ":" << std::endl; std::vector dist_vecs; for (const auto & neighbor : neighbors) { dist_vecs.push_back((*neighbor) - node); - //std::cout << " Neighbor " << neighbor->id() << ", dist_vec = " << dist_vecs.back() << std::endl; } // 2D: If the current node and all (two) boundary neighbor nodes lie on the same line, @@ -129,48 +127,54 @@ void VariationalSmootherConstraint::constrain() const Point reference_cross_prod = dist_vecs[0].cross(dist_vecs[vec_index]); const Point reference_normal = reference_cross_prod / reference_cross_prod.norm(); - + + // Does the node lie within a 2D surface? (Not on the edge) bool node_is_coplanar = true; + + // Does the node lie on the intersection of two 2D surfaces (i.e., a line)? + // If so, and the node is not located at the intersection of three 2D surfaces + // (i.e., a single point or vertex of the mesh), Then some of the dist_vecs + // will be parallel. + + // Each entry will be a vector from dist_vecs that has a corresponding + // (anti)parallel vector, also from dist_vecs + std::vector parallel_pairs; + for (const auto ii : index_range(dist_vecs)) { const Point vec_ii_normalized = dist_vecs[ii] / dist_vecs[ii].norm(); for (const auto jj : make_range(ii + 1, dist_vecs.size())) { - // No need to compute the cross product for this case, as it is by - // definition equal to the reference normal computed above. - // Also check for dist_vecs that are (anti)parallel, and skip, - // as their cross product will be zero. const Point vec_jj_normalized = dist_vecs[jj] / dist_vecs[jj].norm(); const bool is_parallel = vec_ii_normalized.relative_fuzzy_equals(vec_jj_normalized); const bool is_antiparallel = vec_ii_normalized.relative_fuzzy_equals( -vec_jj_normalized); - if ((ii == 0 and jj == vec_index) || (is_parallel || is_antiparallel)) + if (is_parallel || is_antiparallel) + { + parallel_pairs.push_back(vec_ii_normalized); + // Don't bother computing the cross product of parallel vector below, + // it will be zero and cannot be used to define a normal vector. continue; + } const Point cross_prod = dist_vecs[ii].cross(dist_vecs[jj]); const Point normal = cross_prod / cross_prod.norm(); - // node is not coplanar with its boundary neighbors and is thus immovable + // node is not coplanar with its boundary neighbors if (!(reference_normal.relative_fuzzy_equals(normal) || reference_normal.relative_fuzzy_equals(-normal))) - { node_is_coplanar = false; - break; - } } } - // TODO: Need to add check for sliding edge node - - if (!node_is_coplanar) - { + if (node_is_coplanar) + this->constrain_node_to_plane(node, reference_normal); + else if (parallel_pairs.size()) + this->constrain_node_to_line(node, parallel_pairs[0]); + else this->fix_node(node); - continue; - } - - this->constrain_node_to_plane(node, reference_normal); } // if not same line/plane, then node is either part of a curved surface or @@ -240,43 +244,81 @@ void VariationalSmootherConstraint::fix_node(const Node & node) 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 - unsigned int constrained_dim; - 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 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 + unsigned int constrained_dim; + 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; - } - } + 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; + 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 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 paralle 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; - 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); + 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); + } } } // namespace libMesh From d76ad06a6e7093c1ede7cf66f5dbf323f55d7747 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Mon, 23 Jun 2025 17:06:39 -0600 Subject: [PATCH 04/38] documentation --- src/systems/variational_smoother_constraint.C | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 19c2309ccc..16525b8f74 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -59,9 +59,9 @@ void VariationalSmootherConstraint::constrain() neighbors.end() ); - // Determine whether the current node is colinear (coplanar) with its boundary - // neighbors Start by computing the vectors from the current node to each boundary - // neighbor node + // Determine whether the current node is colinear (2D) or coplanar 3D with + // its boundary neighbors. Start by computing the vectors from the current + // node to each boundary neighbor node std::vector dist_vecs; for (const auto & neighbor : neighbors) { @@ -89,7 +89,17 @@ void VariationalSmootherConstraint::constrain() continue; } - // TODO: what if z is not the inactive dimension in 2D!?!? + // TODO: what if z is not the inactive dimension in 2D? + // Would this even happen!?!? + // + // Yes, yes, we are using a function called "constrain_node_to_plane" to + // constrain a node to a line in a 2D mesh. However, the line + // c_x * x + c_y * y + c = 0 is equivalent to the plane + // c_x * x + c_y * y + 0 * z + c = 0, so the same logic applies here. + // Since all dist_vecs reside in the xy plane, and are parallel to the line + // we are constraining to, crossing one of the dist_vecs with the unit + // vector in the z direction should give us a vector normal to the + // constraining line. This reference normal vector also resides in the xy plane. const auto reference_normal = dist_vecs[0].cross(Point(0., 0., 1.)); this->constrain_node_to_plane(node, reference_normal); } From 1262cf669ea2266bd96628542eaa059bc5243643 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 27 Jun 2025 21:30:18 -0600 Subject: [PATCH 05/38] Added logic where only nodes that share the same bid with neighbors are considered for sliding boundary constraints. This helps with adjacent corners nodes in triangular elements. --- .../systems/variational_smoother_constraint.h | 14 +++ src/systems/variational_smoother_constraint.C | 106 ++++++++++++++++-- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index d50c74882f..f85d67bd5f 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -64,6 +64,20 @@ class VariationalSmootherConstraint : public System::Constraint */ 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); + public: /* diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 16525b8f74..d6bc005038 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -18,6 +18,7 @@ // Local Includes #include "libmesh/variational_smoother_constraint.h" #include "libmesh/mesh_tools.h" +#include "libmesh/boundary_info.h" namespace libMesh { @@ -40,6 +41,8 @@ void VariationalSmootherConstraint::constrain() std::unordered_map> nodes_to_elem_map; MeshTools::build_nodes_to_elem_map(mesh, nodes_to_elem_map); + const auto & boundary_info = mesh.get_boundary_info(); + const auto boundary_node_ids = MeshTools::find_boundary_nodes (mesh); for (const auto & bid : boundary_node_ids) { @@ -49,13 +52,37 @@ void VariationalSmootherConstraint::constrain() 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(); + // Remove any neighbors that are not boundary nodes OR boundary neighbor nodes + // that don't share a boundary id with node + auto remove_neighbor = [&boundary_node_ids, &node, &nodes_to_elem_map, &boundary_info] + (const Node * neigh) -> bool + { + const bool is_neighbor_boundary_node = boundary_node_ids.find(neigh->id()) != boundary_node_ids.end(); + + // 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[node.id()]; + const auto & elems_containing_neigh = nodes_to_elem_map[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()) + { + common_elem = neigh_elem; + break; } - ), + + libmesh_assert(common_elem != nullptr); + + // Now, determine whether node and neigh share a common boundary id + const bool nodes_have_common_bid = nodes_share_boundary_id( + node, *neigh, *common_elem, boundary_info); + + // remove if neighbor is not boundary node or nodes don't share a common bid + return (is_neighbor_boundary_node && nodes_have_common_bid) ? false : true; + }; + + neighbors.erase( + std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), neighbors.end() ); @@ -141,10 +168,10 @@ void VariationalSmootherConstraint::constrain() // Does the node lie within a 2D surface? (Not on the edge) bool node_is_coplanar = true; - // Does the node lie on the intersection of two 2D surfaces (i.e., a line)? - // If so, and the node is not located at the intersection of three 2D surfaces - // (i.e., a single point or vertex of the mesh), Then some of the dist_vecs - // will be parallel. + // Does the node lie on the intersection of two 2D surfaces (i.e., a + // line)? If so, and the node is not located at the intersection of three + // 2D surfaces (i.e., a single point or vertex of the mesh), then some of + // the dist_vecs will be parallel. // Each entry will be a vector from dist_vecs that has a corresponding // (anti)parallel vector, also from dist_vecs @@ -228,6 +255,7 @@ void VariationalSmootherConstraint::constrain() node.id()) == already_constrained_node_ids.end() ) { + // TODO Allow nodes to slide along subdomain boundary this->fix_node(node); already_constrained_node_ids.insert(node.id()); } @@ -331,4 +359,62 @@ void VariationalSmootherConstraint::constrain_node_to_line(const Node & node, co } } +// 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; +} + } // namespace libMesh From 405e516ade1551bef59afd7747b9d2352dfbfc1c Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 27 Jun 2025 21:30:58 -0600 Subject: [PATCH 06/38] Added equilateral triangle as the target element for TRI3s. --- include/systems/variational_smoother_system.h | 12 ++- src/systems/variational_smoother_system.C | 97 +++++++++++++++++-- 2 files changed, 98 insertions(+), 11 deletions(-) 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/systems/variational_smoother_system.C b/src/systems/variational_smoother_system.C index 8b30d5cf11..313753bdb1 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,16 @@ 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 +109,90 @@ 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 +200,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 +338,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 From 11331fbd1607e80bfa11654055455ca6f579ff0b Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Sat, 28 Jun 2025 21:03:04 -0600 Subject: [PATCH 07/38] Cleaned up constrints, added relative_fuzzy_equals to multiple subdomain tests to account for barely-moved subdomain boundary nodes that coincide with the mesh boundary. --- .../systems/variational_smoother_constraint.h | 9 + src/systems/variational_smoother_constraint.C | 283 +++++++++--------- tests/mesh/mesh_smoother_test.C | 3 +- 3 files changed, 152 insertions(+), 143 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index f85d67bd5f..5ad1b91349 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -40,6 +40,15 @@ class VariationalSmootherConstraint : public System::Constraint /// Whether nodes on subdomain boundaries are subject to change via smoothing const bool _preserve_subdomain_boundaries; + /* + * Identifies and imposes the appropriate constraints on a node. + * @param node The node to constrain. + * @param neighbors Vector of neighbors to use to identify the constraint to + * impose. + */ + void impose_constraints(const Node &node, + const std::vector neighbors); + /* * Constrain (i.e., fix) a node to not move during mesh smoothing. * @param node Node to fix. diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index d6bc005038..d1debd4c24 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -34,8 +34,7 @@ VariationalSmootherConstraint::~VariationalSmootherConstraint() = default; void VariationalSmootherConstraint::constrain() { - const auto & mesh = _sys.get_mesh(); - const auto dim = mesh.mesh_dimension(); + const auto &mesh = _sys.get_mesh(); // Only compute the node to elem map once std::unordered_map> nodes_to_elem_map; @@ -86,146 +85,7 @@ void VariationalSmootherConstraint::constrain() neighbors.end() ); - // Determine whether the current node is colinear (2D) or coplanar 3D with - // its boundary neighbors. Start by computing the vectors from the current - // node to each boundary neighbor node - std::vector dist_vecs; - for (const auto & neighbor : neighbors) - { - dist_vecs.push_back((*neighbor) - node); - } - - // 2D: If the current node and all (two) boundary neighbor nodes lie on the same line, - // the magnitude of the dot product of the distance vectors will be equal - // to the product of the magnitudes of the vectors. This is because the distance - // vectors lie on the same line, so the cos(theta) term in the dot product - // evaluates to -1 or 1. - if (dim == 2) - { - // Physically, the boundary of a 2D mesh is a 1D curve. By the - // definition of a "neighbor", it is only possible for a node - // to have 2 neighbors on the boundary. - libmesh_assert_equal_to(dist_vecs.size(), dim); - const Real dot_product = dist_vecs[0] * dist_vecs[1]; - const Real norm_product = dist_vecs[0].norm() * dist_vecs[1].norm(); - - // node is not colinear with its boundary neighbors and is thus immovable - if (!relative_fuzzy_equals(std::abs(dot_product), norm_product)) - { - this->fix_node(node); - continue; - } - - // TODO: what if z is not the inactive dimension in 2D? - // Would this even happen!?!? - // - // Yes, yes, we are using a function called "constrain_node_to_plane" to - // constrain a node to a line in a 2D mesh. However, the line - // c_x * x + c_y * y + c = 0 is equivalent to the plane - // c_x * x + c_y * y + 0 * z + c = 0, so the same logic applies here. - // Since all dist_vecs reside in the xy plane, and are parallel to the line - // we are constraining to, crossing one of the dist_vecs with the unit - // vector in the z direction should give us a vector normal to the - // constraining line. This reference normal vector also resides in the xy plane. - const auto reference_normal = dist_vecs[0].cross(Point(0., 0., 1.)); - this->constrain_node_to_plane(node, reference_normal); - } - - // 3D: If the current node and all boundary neighbor nodes lie on the same plane, - // all the distance vectors from the current node to the boundary nodes will be - // orthogonal to the plane normal. We can obtain a reference normal by normalizing - // the cross product between two of the distance vectors. If the normalized cross - // products of all other combinations (excluding self combinations) match this - // reference normal, then the current node is coplanar with all of its boundary nodes. - else if (dim == 3) - { - // We should have at least 2 distance vectors to compute a normal with in 3D - libmesh_assert_greater_equal(dist_vecs.size(), 2); - - // Compute the reference normal by taking the cross product of two vectors in - // dist_vecs. We will use dist_vecs[0] as the first vector and the next available - // vector in dist_vecs that is not (anti)parallel to dist_vecs[0]. Without this - // check we may end up with a zero vector for the reference vector. - unsigned int vec_index; - const Point vec_0_normalized = dist_vecs[0] / dist_vecs[0].norm(); - for (const auto ii : make_range(size_t(1), dist_vecs.size())) - { - // (anti)parallel check - const bool is_parallel = - vec_0_normalized.relative_fuzzy_equals(dist_vecs[ii] / dist_vecs[ii].norm()); - const bool is_antiparallel = - vec_0_normalized.relative_fuzzy_equals(-dist_vecs[ii] / dist_vecs[ii].norm()); - if (!(is_parallel || is_antiparallel)) - { - vec_index = ii; - break; - } - } - - const Point reference_cross_prod = dist_vecs[0].cross(dist_vecs[vec_index]); - const Point reference_normal = reference_cross_prod / reference_cross_prod.norm(); - - // Does the node lie within a 2D surface? (Not on the edge) - bool node_is_coplanar = true; - - // Does the node lie on the intersection of two 2D surfaces (i.e., a - // line)? If so, and the node is not located at the intersection of three - // 2D surfaces (i.e., a single point or vertex of the mesh), then some of - // the dist_vecs will be parallel. - - // Each entry will be a vector from dist_vecs that has a corresponding - // (anti)parallel vector, also from dist_vecs - std::vector parallel_pairs; - - for (const auto ii : index_range(dist_vecs)) - { - const Point vec_ii_normalized = dist_vecs[ii] / dist_vecs[ii].norm(); - for (const auto jj : make_range(ii + 1, dist_vecs.size())) - { - const Point vec_jj_normalized = dist_vecs[jj] / dist_vecs[jj].norm(); - const bool is_parallel = - vec_ii_normalized.relative_fuzzy_equals(vec_jj_normalized); - const bool is_antiparallel = vec_ii_normalized.relative_fuzzy_equals( - -vec_jj_normalized); - - if (is_parallel || is_antiparallel) - { - parallel_pairs.push_back(vec_ii_normalized); - // Don't bother computing the cross product of parallel vector below, - // it will be zero and cannot be used to define a normal vector. - continue; - } - - const Point cross_prod = dist_vecs[ii].cross(dist_vecs[jj]); - const Point normal = cross_prod / cross_prod.norm(); - - // node is not coplanar with its boundary neighbors - if (!(reference_normal.relative_fuzzy_equals(normal) || - reference_normal.relative_fuzzy_equals(-normal))) - node_is_coplanar = false; - } - } - - if (node_is_coplanar) - this->constrain_node_to_plane(node, reference_normal); - else if (parallel_pairs.size()) - this->constrain_node_to_line(node, parallel_pairs[0]); - else - this->fix_node(node); - } - - // 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. - - // 1D - else - this->fix_node(node); + this->impose_constraints(node, neighbors); }// end bid @@ -256,6 +116,8 @@ void VariationalSmootherConstraint::constrain() ) { // TODO Allow nodes to slide along subdomain boundary + // Should define methods to identify lines/places from a list of + // neighbors, use it here this->fix_node(node); already_constrained_node_ids.insert(node.id()); } @@ -264,7 +126,144 @@ void VariationalSmootherConstraint::constrain() }// for side }// for elem } +} + +void VariationalSmootherConstraint::impose_constraints( + const Node &node, const std::vector neighbors) { + const auto &mesh = _sys.get_mesh(); + const auto dim = mesh.mesh_dimension(); + + // Determine whether the current node is colinear (2D) or coplanar 3D with + // its boundary neighbors. Start by computing the vectors from the current + // node to each boundary neighbor node + std::vector dist_vecs; + for (const auto &neighbor : neighbors) + dist_vecs.push_back((*neighbor) - node); + + // 2D: If the current node and all (two) boundary neighbor nodes lie on the + // same line, the magnitude of the dot product of the distance vectors will be + // equal to the product of the magnitudes of the vectors. This is because the + // distance vectors lie on the same line, so the cos(theta) term in the dot + // product evaluates to -1 or 1. + if (dim == 2) { + // Physically, the boundary of a 2D mesh is a 1D curve. By the + // definition of a "neighbor", it is only possible for a node + // to have 2 neighbors on the boundary. + libmesh_assert_equal_to(dist_vecs.size(), dim); + const Real dot_product = dist_vecs[0] * dist_vecs[1]; + const Real norm_product = dist_vecs[0].norm() * dist_vecs[1].norm(); + + // node is not colinear with its boundary neighbors and is thus immovable + if (!relative_fuzzy_equals(std::abs(dot_product), norm_product)) + this->fix_node(node); + + else { + // Yes, yes, we are using a function called "constrain_node_to_plane" to + // constrain a node to a line in a 2D mesh. However, the line + // c_x * x + c_y * y + c = 0 is equivalent to the plane + // c_x * x + c_y * y + 0 * z + c = 0, so the same logic applies here. + // Since all dist_vecs reside in the xy plane, and are parallel to the + // line we are constraining to, crossing one of the dist_vecs with the + // unit vector in the z direction should give us a vector normal to the + // constraining line. This reference normal vector also resides in the xy + // plane. + // + // TODO: what if z is not the inactive dimension in 2D? + // Would this even happen!?!? + const auto reference_normal = dist_vecs[0].cross(Point(0., 0., 1.)); + this->constrain_node_to_plane(node, reference_normal); + } + } + + // 3D: If the current node and all boundary neighbor nodes lie on the same + // plane, all the distance vectors from the current node to the boundary nodes + // will be orthogonal to the plane normal. We can obtain a reference normal by + // normalizing the cross product between two of the distance vectors. If the + // normalized cross products of all other combinations (excluding self + // combinations) match this reference normal, then the current node is + // coplanar with all of its boundary nodes and should be constrained to the + // 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, 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. + else if (dim == 3) { + // We should have at least 2 distance vectors to compute a normal with in 3D + libmesh_assert_greater_equal(dist_vecs.size(), 2); + + // Compute the reference normal by taking the cross product of two vectors + // in dist_vecs. We will use dist_vecs[0] as the first vector and the next + // available vector in dist_vecs that is not (anti)parallel to dist_vecs[0]. + // Without this check we may end up with a zero vector for the reference + // vector. + unsigned int vec_index; + const Point vec_0_normalized = dist_vecs[0] / dist_vecs[0].norm(); + for (const auto ii : make_range(size_t(1), dist_vecs.size())) { + // (anti)parallel check + const bool is_parallel = vec_0_normalized.relative_fuzzy_equals( + dist_vecs[ii] / dist_vecs[ii].norm()); + const bool is_antiparallel = vec_0_normalized.relative_fuzzy_equals( + -dist_vecs[ii] / dist_vecs[ii].norm()); + if (!(is_parallel || is_antiparallel)) { + vec_index = ii; + break; + } + } + + const Point reference_cross_prod = dist_vecs[0].cross(dist_vecs[vec_index]); + const Point reference_normal = + reference_cross_prod / reference_cross_prod.norm(); + + // Does the node lie within a 2D surface? (Not on the edge) + bool node_is_coplanar = true; + + // Does the node lie on the intersection of two 2D surfaces (i.e., a + // line)? If so, and the node is not located at the intersection of three + // 2D surfaces (i.e., a single point or vertex of the mesh), then some of + // the dist_vecs will be parallel. + + // Each entry will be a vector from dist_vecs that has a corresponding + // (anti)parallel vector, also from dist_vecs + std::vector parallel_pairs; + + for (const auto ii : index_range(dist_vecs)) { + const Point vec_ii_normalized = dist_vecs[ii] / dist_vecs[ii].norm(); + for (const auto jj : make_range(ii + 1, dist_vecs.size())) { + const Point vec_jj_normalized = dist_vecs[jj] / dist_vecs[jj].norm(); + const bool is_parallel = + vec_ii_normalized.relative_fuzzy_equals(vec_jj_normalized); + const bool is_antiparallel = + vec_ii_normalized.relative_fuzzy_equals(-vec_jj_normalized); + + if (is_parallel || is_antiparallel) { + parallel_pairs.push_back(vec_ii_normalized); + // Don't bother computing the cross product of parallel vector below, + // it will be zero and cannot be used to define a normal vector. + continue; + } + + const Point cross_prod = dist_vecs[ii].cross(dist_vecs[jj]); + const Point normal = cross_prod / cross_prod.norm(); + + // node is not coplanar with its boundary neighbors + if (!(reference_normal.relative_fuzzy_equals(normal) || + reference_normal.relative_fuzzy_equals(-normal))) + node_is_coplanar = false; + } + } + + if (node_is_coplanar) + this->constrain_node_to_plane(node, reference_normal); + else if (parallel_pairs.size()) + this->constrain_node_to_line(node, parallel_pairs[0]); + else + this->fix_node(node); + } + // 1D + else + this->fix_node(node); } void VariationalSmootherConstraint::fix_node(const Node & node) diff --git a/tests/mesh/mesh_smoother_test.C b/tests/mesh/mesh_smoother_test.C index 301b641147..2568ec1db6 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -221,7 +221,8 @@ public: (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 return true; From a9f92bb0d0a6fd72549ec0a13ded049b5bc1a610 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Sun, 29 Jun 2025 19:34:09 -0600 Subject: [PATCH 08/38] Nodes on internal subdomain boundaries are now allowed to slide. --- src/systems/variational_smoother_constraint.C | 108 +++++++++++++++--- 1 file changed, 89 insertions(+), 19 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index d1debd4c24..c809d03bfe 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -95,34 +95,104 @@ void VariationalSmootherConstraint::constrain() auto already_constrained_node_ids = boundary_node_ids; for (const auto * elem : mesh.active_element_ptr_range()) { - const auto & subdomain_id = elem->subdomain_id(); + 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 & neighbor_subdomain_id = neighbor->subdomain_id(); - if (subdomain_id != neighbor_subdomain_id) + const auto & sub_id2 = neighbor->subdomain_id(); + if (sub_id1 == sub_id2) + continue; + + // elem and neighbor are in difference subdomains, and share nodes + // that need to be constrained + for (const auto local_node_id : elem->nodes_on_side(side)) { - 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 constrained this node + if ( + std::find(already_constrained_node_ids.begin(), + already_constrained_node_ids.end(), + node.id()) == already_constrained_node_ids.end() + ) { - 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() - ) + + // 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 on the subdomain boundary + auto remove_neighbor = [&node, &nodes_to_elem_map, &sub_id1, &sub_id2] + (const Node * neigh) -> bool { - // TODO Allow nodes to slide along subdomain boundary - // Should define methods to identify lines/places from a list of - // neighbors, use it here - this->fix_node(node); - already_constrained_node_ids.insert(node.id()); - } - }//for local_node_id - } + // Determine whether the neighbor is on the subdomain boundary + // First, find the common element that both node and neigh belong to + const auto & elems_containing_node = nodes_to_elem_map[node.id()]; + const auto & elems_containing_neigh = nodes_to_elem_map[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()) + { + common_elem = neigh_elem; + break; + } + } + + libmesh_assert(common_elem != nullptr); + const auto common_sub_id = common_elem->subdomain_id(); + libmesh_assert(common_sub_id == sub_id1 || common_sub_id == sub_id2); + + // Define this allias for convenience + const auto & other_sub_id = (common_sub_id == sub_id1) ? sub_id2: sub_id1; + + // Now, determine whether node and neigh are on a side coincident + // with the interval boundary + unsigned int matched_side; + 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) + { + matched_side = common_side; + break; + } + } + + // Does matched_side, containing both node and neigh, lie on the + // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) + // and sub_id2 (= other sub_id or common_sub_id)? + 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 == other_sub_id; + + // We return wheather neigh should be removed + return !is_matched_side_on_subdomain_boundary; + + }; + + neighbors.erase( + std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), + neighbors.end() + ); + + + this->impose_constraints(node, neighbors); + already_constrained_node_ids.insert(node.id()); + } + }//for local_node_id + }// for side }// for elem } From 3e1173075c01e924aaca65e0a461871a822b171c Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Mon, 30 Jun 2025 14:52:44 -0600 Subject: [PATCH 09/38] Fixed bug in constraints where the correct common element and matched sides are sometimes not found. --- src/systems/variational_smoother_constraint.C | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index c809d03bfe..b072d53aee 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -63,19 +63,16 @@ void VariationalSmootherConstraint::constrain() const auto & elems_containing_node = nodes_to_elem_map[node.id()]; const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; const Elem * common_elem = nullptr; + bool nodes_have_common_bid = false; 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()) { common_elem = neigh_elem; - break; + // Keep this in the loop because there can be multiple common elements + // Now, determine whether node and neigh share a common boundary id + nodes_have_common_bid = nodes_share_boundary_id(node, *neigh, *common_elem, boundary_info) || nodes_have_common_bid; } - libmesh_assert(common_elem != nullptr); - - // Now, determine whether node and neigh share a common boundary id - const bool nodes_have_common_bid = nodes_share_boundary_id( - node, *neigh, *common_elem, boundary_info); - // remove if neighbor is not boundary node or nodes don't share a common bid return (is_neighbor_boundary_node && nodes_have_common_bid) ? false : true; }; @@ -151,7 +148,6 @@ void VariationalSmootherConstraint::constrain() // Now, determine whether node and neigh are on a side coincident // with the interval boundary - unsigned int matched_side; for (const auto common_side : common_elem->side_index_range()) { bool node_found_on_side = false; @@ -166,20 +162,21 @@ void VariationalSmootherConstraint::constrain() if (node_found_on_side && neigh_found_on_side) { - matched_side = common_side; - break; + const auto matched_side = common_side; + // There could be multiple matched sides, so keep this next part + // inside the loop + // + // Does matched_side, containing both node and neigh, lie on the + // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) + // and sub_id2 (= other sub_id or common_sub_id)? + 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 == other_sub_id; + if (is_matched_side_on_subdomain_boundary) + return false; // Don't remove the neighbor node } } - // Does matched_side, containing both node and neigh, lie on the - // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) - // and sub_id2 (= other sub_id or common_sub_id)? - 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 == other_sub_id; - - // We return wheather neigh should be removed - return !is_matched_side_on_subdomain_boundary; - + return true; // Remove the neighbor node }; neighbors.erase( From ed5b413a3a8301ea230a7f5795997e277435be62 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 2 Jul 2025 16:07:31 -0600 Subject: [PATCH 10/38] Eliminated a level of indentation. --- src/systems/variational_smoother_constraint.C | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index b072d53aee..721cab9e74 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -46,6 +46,7 @@ void VariationalSmootherConstraint::constrain() 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; @@ -112,82 +113,81 @@ void VariationalSmootherConstraint::constrain() if ( std::find(already_constrained_node_ids.begin(), already_constrained_node_ids.end(), - node.id()) == already_constrained_node_ids.end() + node.id()) != already_constrained_node_ids.end() ) - { + continue; - // 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); + // 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 on the subdomain boundary - auto remove_neighbor = [&node, &nodes_to_elem_map, &sub_id1, &sub_id2] - (const Node * neigh) -> bool + // Remove any neighbors that are not on the subdomain boundary + auto remove_neighbor = [&node, &nodes_to_elem_map, &sub_id1, &sub_id2] + (const Node * neigh) -> bool + { + // Determine whether the neighbor is on the subdomain boundary + // First, find the common element that both node and neigh belong to + const auto & elems_containing_node = nodes_to_elem_map[node.id()]; + const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; + const Elem * common_elem = nullptr; + for (const auto * neigh_elem : elems_containing_neigh) { - // Determine whether the neighbor is on the subdomain boundary - // First, find the common element that both node and neigh belong to - const auto & elems_containing_node = nodes_to_elem_map[node.id()]; - const auto & elems_containing_neigh = nodes_to_elem_map[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()) { - if (std::find(elems_containing_node.begin(), elems_containing_node.end(), neigh_elem) != elems_containing_node.end()) - { - common_elem = neigh_elem; - break; - } + common_elem = neigh_elem; + break; } + } + + libmesh_assert(common_elem != nullptr); + const auto common_sub_id = common_elem->subdomain_id(); + libmesh_assert(common_sub_id == sub_id1 || common_sub_id == sub_id2); - libmesh_assert(common_elem != nullptr); - const auto common_sub_id = common_elem->subdomain_id(); - libmesh_assert(common_sub_id == sub_id1 || common_sub_id == sub_id2); + // Define this allias for convenience + const auto & other_sub_id = (common_sub_id == sub_id1) ? sub_id2: sub_id1; - // Define this allias for convenience - const auto & other_sub_id = (common_sub_id == sub_id1) ? sub_id2: sub_id1; + // Now, determine whether node and neigh are on a side coincident + // with the interval 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; + } - // Now, determine whether node and neigh are on a side coincident - // with the interval boundary - for (const auto common_side : common_elem->side_index_range()) + if (node_found_on_side && neigh_found_on_side) { - 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) - { - const auto matched_side = common_side; - // There could be multiple matched sides, so keep this next part - // inside the loop - // - // Does matched_side, containing both node and neigh, lie on the - // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) - // and sub_id2 (= other sub_id or common_sub_id)? - 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 == other_sub_id; - if (is_matched_side_on_subdomain_boundary) - return false; // Don't remove the neighbor node - } + const auto matched_side = common_side; + // There could be multiple matched sides, so keep this next part + // inside the loop + // + // Does matched_side, containing both node and neigh, lie on the + // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) + // and sub_id2 (= other sub_id or common_sub_id)? + 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 == other_sub_id; + if (is_matched_side_on_subdomain_boundary) + return false; // Don't remove the neighbor node } + } - return true; // Remove the neighbor node - }; + return true; // Remove the neighbor node + }; - neighbors.erase( - std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), - neighbors.end() - ); + neighbors.erase( + std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), + neighbors.end() + ); + this->impose_constraints(node, neighbors); + already_constrained_node_ids.insert(node.id()); - this->impose_constraints(node, neighbors); - already_constrained_node_ids.insert(node.id()); - } }//for local_node_id }// for side From 0ea66cb88d5384ba2ad65d83b7aa4fd2a3fd2df2 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 2 Jul 2025 16:29:46 -0600 Subject: [PATCH 11/38] Moved subdomain constraints ahead of boundary constraints. Result looks better, but both constaints should be conmbined for strict enforcement. --- src/systems/variational_smoother_constraint.C | 109 ++++++++++-------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 721cab9e74..9e47a49471 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -40,57 +40,12 @@ void VariationalSmootherConstraint::constrain() std::unordered_map> nodes_to_elem_map; MeshTools::build_nodes_to_elem_map(mesh, nodes_to_elem_map); - const auto & boundary_info = mesh.get_boundary_info(); - - const auto boundary_node_ids = MeshTools::find_boundary_nodes (mesh); - 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 OR boundary neighbor nodes - // that don't share a boundary id with node - auto remove_neighbor = [&boundary_node_ids, &node, &nodes_to_elem_map, &boundary_info] - (const Node * neigh) -> bool - { - const bool is_neighbor_boundary_node = boundary_node_ids.find(neigh->id()) != boundary_node_ids.end(); - - // 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[node.id()]; - const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; - const Elem * common_elem = nullptr; - bool nodes_have_common_bid = false; - 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()) - { - common_elem = neigh_elem; - // Keep this in the loop because there can be multiple common elements - // Now, determine whether node and neigh share a common boundary id - nodes_have_common_bid = nodes_share_boundary_id(node, *neigh, *common_elem, boundary_info) || nodes_have_common_bid; - } - - // remove if neighbor is not boundary node or nodes don't share a common bid - return (is_neighbor_boundary_node && nodes_have_common_bid) ? false : true; - }; - - neighbors.erase( - std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), - neighbors.end() - ); - - this->impose_constraints(node, neighbors); - - }// end bid - - // Constrain subdomain boundary nodes, if requested + // Constrain subdomain boundary nodes, if requested. We do this before + // constraining true boundary nodes because subdomain boundary constraints + // are more strict. + std::unordered_set already_constrained_node_ids; if (_preserve_subdomain_boundaries) { - auto already_constrained_node_ids = boundary_node_ids; for (const auto * elem : mesh.active_element_ptr_range()) { const auto & sub_id1 = elem->subdomain_id(); @@ -161,7 +116,7 @@ void VariationalSmootherConstraint::constrain() neigh_found_on_side = true; } - if (node_found_on_side && neigh_found_on_side) + if (node_found_on_side && neigh_found_on_side && common_elem->neighbor_ptr(common_side)) { const auto matched_side = common_side; // There could be multiple matched sides, so keep this next part @@ -193,6 +148,60 @@ void VariationalSmootherConstraint::constrain() }// for side }// for elem } + + const auto & boundary_info = mesh.get_boundary_info(); + + const auto boundary_node_ids = MeshTools::find_boundary_nodes (mesh); + for (const auto & bid : boundary_node_ids) + { + if ( + std::find(already_constrained_node_ids.begin(), + already_constrained_node_ids.end(), + bid) != already_constrained_node_ids.end() + ) + continue; + + 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 OR boundary neighbor nodes + // that don't share a boundary id with node + auto remove_neighbor = [&boundary_node_ids, &node, &nodes_to_elem_map, &boundary_info] + (const Node * neigh) -> bool + { + const bool is_neighbor_boundary_node = boundary_node_ids.find(neigh->id()) != boundary_node_ids.end(); + + // 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[node.id()]; + const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; + const Elem * common_elem = nullptr; + bool nodes_have_common_bid = false; + 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()) + { + common_elem = neigh_elem; + // Keep this in the loop because there can be multiple common elements + // Now, determine whether node and neigh share a common boundary id + nodes_have_common_bid = nodes_share_boundary_id(node, *neigh, *common_elem, boundary_info) || nodes_have_common_bid; + } + + // remove if neighbor is not boundary node or nodes don't share a common bid + return (is_neighbor_boundary_node && nodes_have_common_bid) ? false : true; + }; + + neighbors.erase( + std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), + neighbors.end() + ); + + this->impose_constraints(node, neighbors); + + }// end bid } void VariationalSmootherConstraint::impose_constraints( From 1c3b0bfe8e3fe3564dbc041415128ee17a6d0df6 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Tue, 8 Jul 2025 16:27:08 -0600 Subject: [PATCH 12/38] Saving current changes that almost fix constraint issues, but not quite. --- .../systems/variational_smoother_constraint.h | 16 ++ src/systems/variational_smoother_constraint.C | 268 ++++++++++-------- 2 files changed, 173 insertions(+), 111 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 5ad1b91349..8e2582e186 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -87,6 +87,22 @@ class VariationalSmootherConstraint : public System::Constraint const Elem & containing_elem, const BoundaryInfo & boundary_info); + static void filter_neighbors_for_subdomain_constraint( + const Node & node, + std::vector & neighbors, + const subdomain_id_type sub_id1, + const subdomain_id_type sub_id2, + std::unordered_map> & nodes_to_elem_map + ); + + static void filter_neighbors_for_boundary_constraint( + const Node & node, + std::vector & neighbors, + std::unordered_map> & nodes_to_elem_map, + const std::unordered_set & boundary_node_ids, + const BoundaryInfo & boundary_info + ); + public: /* diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 9e47a49471..d13e50f817 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -40,10 +40,12 @@ void VariationalSmootherConstraint::constrain() std::unordered_map> nodes_to_elem_map; MeshTools::build_nodes_to_elem_map(mesh, nodes_to_elem_map); - // Constrain subdomain boundary nodes, if requested. We do this before - // constraining true boundary nodes because subdomain boundary constraints - // are more strict. - std::unordered_set already_constrained_node_ids; + 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()) @@ -64,12 +66,8 @@ void VariationalSmootherConstraint::constrain() 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 constrained this node - if ( - std::find(already_constrained_node_ids.begin(), - already_constrained_node_ids.end(), - node.id()) != already_constrained_node_ids.end() - ) + // Make sure we haven't already processed this node + if (subdomain_boundary_map.count(node.id())) continue; // Find all the nodal neighbors... that is the nodes directly connected @@ -78,70 +76,19 @@ void VariationalSmootherConstraint::constrain() MeshTools::find_nodal_neighbors(mesh, node, nodes_to_elem_map, neighbors); // Remove any neighbors that are not on the subdomain boundary - auto remove_neighbor = [&node, &nodes_to_elem_map, &sub_id1, &sub_id2] - (const Node * neigh) -> bool - { - // Determine whether the neighbor is on the subdomain boundary - // First, find the common element that both node and neigh belong to - const auto & elems_containing_node = nodes_to_elem_map[node.id()]; - const auto & elems_containing_neigh = nodes_to_elem_map[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()) - { - common_elem = neigh_elem; - break; - } - } - - libmesh_assert(common_elem != nullptr); - const auto common_sub_id = common_elem->subdomain_id(); - libmesh_assert(common_sub_id == sub_id1 || common_sub_id == sub_id2); - - // Define this allias for convenience - const auto & other_sub_id = (common_sub_id == sub_id1) ? sub_id2: sub_id1; - - // Now, determine whether node and neigh are on a side coincident - // with the interval 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)) - { - const auto matched_side = common_side; - // There could be multiple matched sides, so keep this next part - // inside the loop - // - // Does matched_side, containing both node and neigh, lie on the - // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) - // and sub_id2 (= other sub_id or common_sub_id)? - 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 == other_sub_id; - if (is_matched_side_on_subdomain_boundary) - return false; // Don't remove the neighbor node - } - } - - return true; // Remove the neighbor node - }; - - neighbors.erase( - std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), - neighbors.end() - ); + VariationalSmootherConstraint::filter_neighbors_for_subdomain_constraint( + node, neighbors, sub_id1, sub_id2, nodes_to_elem_map); + + // 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_constraints(node, neighbors); + + // This subdomain boundary node lies on an external boundary, save it + // for later to combine with the external boundary constraint + else + subdomain_boundary_map[node.id()] = neighbors; - this->impose_constraints(node, neighbors); - already_constrained_node_ids.insert(node.id()); }//for local_node_id @@ -149,18 +96,10 @@ void VariationalSmootherConstraint::constrain() }// for elem } - const auto & boundary_info = mesh.get_boundary_info(); - const auto boundary_node_ids = MeshTools::find_boundary_nodes (mesh); + // Loop through boundary nodes and impose constraints for (const auto & bid : boundary_node_ids) { - if ( - std::find(already_constrained_node_ids.begin(), - already_constrained_node_ids.end(), - bid) != already_constrained_node_ids.end() - ) - continue; - const auto & node = mesh.node_ref(bid); // Find all the nodal neighbors... that is the nodes directly connected @@ -170,34 +109,19 @@ void VariationalSmootherConstraint::constrain() // Remove any neighbors that are not boundary nodes OR boundary neighbor nodes // that don't share a boundary id with node - auto remove_neighbor = [&boundary_node_ids, &node, &nodes_to_elem_map, &boundary_info] - (const Node * neigh) -> bool - { - const bool is_neighbor_boundary_node = boundary_node_ids.find(neigh->id()) != boundary_node_ids.end(); - - // 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[node.id()]; - const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; - const Elem * common_elem = nullptr; - bool nodes_have_common_bid = false; - 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()) - { - common_elem = neigh_elem; - // Keep this in the loop because there can be multiple common elements - // Now, determine whether node and neigh share a common boundary id - nodes_have_common_bid = nodes_share_boundary_id(node, *neigh, *common_elem, boundary_info) || nodes_have_common_bid; - } + VariationalSmootherConstraint::filter_neighbors_for_boundary_constraint( + node, neighbors, nodes_to_elem_map, boundary_node_ids, boundary_info); - // remove if neighbor is not boundary node or nodes don't share a common bid - return (is_neighbor_boundary_node && nodes_have_common_bid) ? false : true; - }; - - neighbors.erase( - std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), - neighbors.end() - ); + // Check for the case where this boundary node is also part of a subdomain id boundary + const auto it = subdomain_boundary_map.find(bid); + if (it != subdomain_boundary_map.end()) + { + const auto & subdomain_neighbors = it->second; + // Combine current neighbors with subdomain boundary neighbors + for (const auto & neighbor : subdomain_neighbors) + if (std::find(neighbors.begin(), neighbors.end(), neighbor) == neighbors.end()) + neighbors.push_back(neighbor); + } this->impose_constraints(node, neighbors); @@ -302,6 +226,7 @@ void VariationalSmootherConstraint::impose_constraints( // Each entry will be a vector from dist_vecs that has a corresponding // (anti)parallel vector, also from dist_vecs std::vector parallel_pairs; + bool all_pairs_parallel = true; for (const auto ii : index_range(dist_vecs)) { const Point vec_ii_normalized = dist_vecs[ii] / dist_vecs[ii].norm(); @@ -314,6 +239,10 @@ void VariationalSmootherConstraint::impose_constraints( if (is_parallel || is_antiparallel) { parallel_pairs.push_back(vec_ii_normalized); + all_pairs_parallel &= ( + vec_ii_normalized.relative_fuzzy_equals(parallel_pairs[0]) || + vec_ii_normalized.relative_fuzzy_equals(-parallel_pairs[0]) + ); // Don't bother computing the cross product of parallel vector below, // it will be zero and cannot be used to define a normal vector. continue; @@ -329,9 +258,13 @@ void VariationalSmootherConstraint::impose_constraints( } } + if (relative_fuzzy_equals(node(0), -29., 0.01) && absolute_fuzzy_equals(node(1), 0., 0.01) && relative_fuzzy_equals(node(2), 1., 0.01)) + std::cout << "Node " << node.id() << std::endl; + if (node_is_coplanar) this->constrain_node_to_plane(node, reference_normal); - else if (parallel_pairs.size()) + + else if (parallel_pairs.size() && all_pairs_parallel) this->constrain_node_to_line(node, parallel_pairs[0]); else this->fix_node(node); @@ -400,7 +333,7 @@ void VariationalSmootherConstraint::constrain_node_to_line(const Node & node, co { const auto dim = _sys.get_mesh().mesh_dimension(); - // We will free the dimension most paralle to line_vec to keep the + // 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(), @@ -492,4 +425,117 @@ bool VariationalSmootherConstraint::nodes_share_boundary_id( return nodes_share_bid; } +void VariationalSmootherConstraint::filter_neighbors_for_subdomain_constraint( + const Node & node, + std::vector & neighbors, + const subdomain_id_type sub_id1, + const subdomain_id_type sub_id2, + std::unordered_map> & nodes_to_elem_map) +{ + + // Remove any neighbors that are not on the subdomain boundary + auto remove_neighbor = [&node, &nodes_to_elem_map, &sub_id1, &sub_id2] + (const Node * neigh) -> bool + { + // 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[node.id()]; + const auto & elems_containing_neigh = nodes_to_elem_map[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()) + common_elem = neigh_elem; + else + continue; + + const auto common_sub_id = common_elem->subdomain_id(); + libmesh_assert(common_sub_id == sub_id1 || common_sub_id == sub_id2); + + // Define this allias for convenience + const auto & other_sub_id = (common_sub_id == sub_id1) ? sub_id2: sub_id1; + + // Now, determine whether node and neigh are on a side coincident + // with the interval 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)) + { + const auto matched_side = common_side; + // There could be multiple matched sides, so keep this next part + // inside the loop + // + // Does matched_side, containing both node and neigh, lie on the + // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) + // and sub_id2 (= other sub_id or common_sub_id)? + 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 == other_sub_id; + if (is_matched_side_on_subdomain_boundary) + return false; // Don't remove the neighbor node + } + }// for common_side + + }// for neigh_elem + + return true; // Remove the neighbor node + }; + + neighbors.erase( + std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), + neighbors.end() + ); + +} + +void VariationalSmootherConstraint::filter_neighbors_for_boundary_constraint( + const Node & node, + std::vector & neighbors, + std::unordered_map> & nodes_to_elem_map, + const std::unordered_set & boundary_node_ids, + const BoundaryInfo & boundary_info) +{ + + // Remove any neighbors that are not boundary nodes OR boundary neighbor nodes + // that don't share a boundary id with node + auto remove_neighbor = [&boundary_node_ids, &node, &nodes_to_elem_map, &boundary_info] + (const Node * neigh) -> bool + { + const bool is_neighbor_boundary_node = boundary_node_ids.find(neigh->id()) != boundary_node_ids.end(); + + // 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[node.id()]; + const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; + const Elem * common_elem = nullptr; + bool nodes_have_common_bid = false; + 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()) + { + common_elem = neigh_elem; + // Keep this in the loop because there can be multiple common elements + // Now, determine whether node and neigh share a common boundary id + nodes_have_common_bid = VariationalSmootherConstraint::nodes_share_boundary_id(node, *neigh, *common_elem, boundary_info) || nodes_have_common_bid; + } + + // remove if neighbor is not boundary node or nodes don't share a common bid + return (is_neighbor_boundary_node && nodes_have_common_bid) ? false : true; + }; + + neighbors.erase( + std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), + neighbors.end() + ); + +} + } // namespace libMesh From c6e42e726b4ee1de71cb737ef49d5d3d5d261c12 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Tue, 15 Jul 2025 14:55:38 -0600 Subject: [PATCH 13/38] Refactored variational smoother constraints. Implemented structs to represent Point, Line, and Plane constraints and logic to combine (intersect) them. This is more robust than the previous version and gives the correct constraints for nodes that are both subdomain boundary nodes and external boundary nodes. Ref #4082 --- .../systems/variational_smoother_constraint.h | 204 ++++- src/systems/variational_smoother_constraint.C | 700 ++++++++++++------ 2 files changed, 641 insertions(+), 263 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 8e2582e186..d41c477953 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -24,6 +24,138 @@ namespace libMesh { + +struct PointConstraint; +struct LineConstraint; +struct PlaneConstraint; + +/// Type used to store a constraint that may be a PlaneConstraint, +/// LineConstraint, or PointConstraint +using ConstraintVariant = + std::variant; + +/// Represents a fixed point constraint. +struct PointConstraint { + Point location; + + PointConstraint() = default; + + /// Constructor + /// @parm p The point defining the constraint. + PointConstraint(const Point &p); + + bool operator<(const PointConstraint &other) const; + + bool operator==(const PointConstraint &other) const; + + /// 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. + ConstraintVariant intersect(const ConstraintVariant &other) const; +}; + +/// Represents a line constraint defined by a base point and direction vector. +struct LineConstraint { + Point r0; + Point dir; + + LineConstraint() = default; + + /// Constructor + /// @parm p A point on the constraining line. + /// @param d the direction of the constraining line. + LineConstraint(const Point &p, const Point &d); + + bool operator<(const LineConstraint &other) const; + + 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. + ConstraintVariant intersect(const ConstraintVariant &other) const; +}; + +/// Represents a plane constraint defined by a point and normal vector. +struct PlaneConstraint { + Point point; + Point normal; + + PlaneConstraint() = default; + + /// Constructor + /// @parm p A point on the constraining plane. + /// @param n the direction normal to the constraining plane. + PlaneConstraint(const Point &p, const Point &n); + + bool operator<(const PlaneConstraint &other) const; + + 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. + ConstraintVariant intersect(const ConstraintVariant &other) const; +}; + +/// 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) { + return std::visit( + [](const auto &lhs, const auto &rhs) -> ConstraintVariant { + return lhs.intersect(rhs); + }, + a, b); +} + /* * Constraint class for the VariationalMeshSmoother. * @@ -40,15 +172,6 @@ class VariationalSmootherConstraint : public System::Constraint /// Whether nodes on subdomain boundaries are subject to change via smoothing const bool _preserve_subdomain_boundaries; - /* - * Identifies and imposes the appropriate constraints on a node. - * @param node The node to constrain. - * @param neighbors Vector of neighbors to use to identify the constraint to - * impose. - */ - void impose_constraints(const Node &node, - const std::vector neighbors); - /* * Constrain (i.e., fix) a node to not move during mesh smoothing. * @param node Node to fix. @@ -87,21 +210,56 @@ class VariationalSmootherConstraint : public System::Constraint const Elem & containing_elem, const BoundaryInfo & boundary_info); - static void filter_neighbors_for_subdomain_constraint( - const Node & node, - std::vector & neighbors, - const subdomain_id_type sub_id1, + /// 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_id1 The subdomain id of the block on one side of the subdomain + /// boundary. + /// @param sub_id2 The subdomain id of the block on the other 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_id1, const subdomain_id_type sub_id2, - std::unordered_map> & nodes_to_elem_map - ); - - static void filter_neighbors_for_boundary_constraint( - const Node & node, - std::vector & neighbors, - std::unordered_map> & nodes_to_elem_map, - const std::unordered_set & boundary_node_ids, - const BoundaryInfo & boundary_info - ); + 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. + void impose_constraint(const Node &node, const ConstraintVariant &constraint); public: diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index d13e50f817..a25b14a1a9 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -23,6 +23,248 @@ namespace libMesh { +PointConstraint::PointConstraint(const Point &p) : location(p) {} + +bool PointConstraint::operator<(const PointConstraint &other) const { + return location < other.location; +} + +bool PointConstraint::operator==(const PointConstraint &other) const { + return location == other.location; +} + +ConstraintVariant +PointConstraint::intersect(const ConstraintVariant &other) const { + return std::visit( + [&](auto &&o) -> ConstraintVariant { + if constexpr (std::is_same_v, + PointConstraint>) { + libmesh_error_msg_if(!(this->location == o.location), + "Points do not match."); + return *this; + } else { + libmesh_error_msg_if(!o.contains_point(*this), + "Point is not on the constraint."); + return *this; + } + }, + other); +} + +LineConstraint::LineConstraint(const Point &p, const Point &d) { + r0 = p; + libmesh_error_msg_if( + d.norm() < TOLERANCE, + "Can't define a line with zero magnitude direction vector."); + // Flip direction vector if necessary so it points in the positive x/y/z + // direction This helps to eliminate duplicate lines + 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(d(dim_id), 0.)) { + canonical(dim_id) = 1.; + break; + } + + const auto dot_prod = d * canonical; + libmesh_assert(!absolute_fuzzy_equals(dot_prod, 0.)); + dir = (dot_prod > 0) ? d.unit() : -d.unit(); +} + +bool LineConstraint::operator<(const LineConstraint &other) const { + if (!(dir.absolute_fuzzy_equals(other.dir, TOLERANCE))) + return dir < other.dir; + return (dir * r0) < (other.dir * other.r0) - TOLERANCE; +} + +bool LineConstraint::operator==(const LineConstraint &other) const { + if (!(dir.absolute_fuzzy_equals(other.dir, TOLERANCE))) + return false; + return this->contains_point(other.r0); +} + +bool LineConstraint::contains_point(const PointConstraint &p) const { + // If the point lies on the line, then the vector p - r0 is parallel to the + // line In that case, the cross product of p - r0 with the line's direction + // will be zero. + return dir.cross(p.location - r0).norm() < TOLERANCE; +} + +bool LineConstraint::is_parallel(const LineConstraint &l) const { + return dir.absolute_fuzzy_equals(l.dir, TOLERANCE); +} + +bool LineConstraint::is_parallel(const PlaneConstraint &p) const { + return dir * p.normal < TOLERANCE; +} + +ConstraintVariant +LineConstraint::intersect(const ConstraintVariant &other) const { + return std::visit( + [&](auto &&o) -> ConstraintVariant { + using T = std::decay_t; + if constexpr (std::is_same_v) { + if (*this == o) + return *this; + libmesh_error_msg_if(this->is_parallel(o), + "Lines are parallel and do not intersect."); + + // 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.r0 - r0; + const Point cross_d1_d2 = dir.cross(o.dir); + const Real cross_dot = (delta.cross(o.dir)) * cross_d1_d2; + const Real denom = cross_d1_d2.norm_sq(); + + const Real t = cross_dot / denom; + const Point intersection = r0 + t * dir; + + // Verify that intersection lies on both lines + libmesh_error_msg_if(o.dir.cross(intersection - o.r0).norm() > + TOLERANCE, + "Lines do not intersect at a single point."); + + return PointConstraint{intersection}; + } else if constexpr (std::is_same_v) { + return o.intersect(*this); + } else if constexpr (std::is_same_v) { + libmesh_error_msg_if(!this->contains_point(o), + "Point is not on the line."); + return o; + } else + libmesh_error_msg("Unsupported constraint type in Line::intersect."); + }, + other); +} + +PlaneConstraint::PlaneConstraint(const Point &p, const Point &n) { + point = p; + libmesh_error_msg_if( + n.norm() < TOLERANCE, + "Can't define a plane with zero magnitude direction vector."); + // Flip normal vector if necessary so it points in the positive x/y/z + // direction This helps to eliminate duplicate points + 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(n(dim_id), 0.)) { + canonical(dim_id) = 1.; + break; + } + + const auto dot_prod = n * canonical; + libmesh_assert(!absolute_fuzzy_equals(dot_prod, 0.)); + normal = (dot_prod > 0) ? n.unit() : -n.unit(); +} + +bool PlaneConstraint::operator<(const PlaneConstraint &other) const { + if (!(normal.absolute_fuzzy_equals(other.normal, TOLERANCE))) + return normal < other.normal; + return (normal * point) < (other.normal * other.point) - TOLERANCE; +} + +bool PlaneConstraint::operator==(const PlaneConstraint &other) const { + if (!(normal.absolute_fuzzy_equals(other.normal, TOLERANCE))) + return false; + return this->contains_point(other.point); +} + +bool PlaneConstraint::is_parallel(const PlaneConstraint &p) const { + return normal.absolute_fuzzy_equals(p.normal, TOLERANCE); +} + +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.location - point) * normal; + return std::abs(dist) < TOLERANCE; +} + +bool PlaneConstraint::contains_line(const LineConstraint &l) const { + const bool base_on_plane = this->contains_point(PointConstraint(l.r0)); + const bool dir_orthogonal = std::abs(normal * l.dir) < TOLERANCE; + return base_on_plane && dir_orthogonal; +} + +ConstraintVariant +PlaneConstraint::intersect(const ConstraintVariant &other) const { + return std::visit( + [&](auto &&o) -> ConstraintVariant { + using T = std::decay_t; + if constexpr (std::is_same_v) { + // If planes are identical, return one of them + if (*this == o) + return *this; + libmesh_error_msg_if(this->is_parallel(o), + "Planes are parallel and do not intersect."); + + // 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() > TOLERANCE); + const Point w = this->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) > TOLERANCE); + + 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; + libmesh_error_msg_if( + this->is_parallel(o), + "Line is parallel and does not intersect the plane."); + + // Solve for t in the parametric equation: + // p(t) = r0 + t·d + // such that this point also satisfies the plane equation: + // n · (p(t) - p0) = 0 + // which leads to: + // t = (n · (p0 - r0)) / (n · d) + + const Real denom = normal * o.dir; + libmesh_assert(std::abs(denom) > TOLERANCE); + const Real t = (normal * (point - o.r0)) / denom; + return PointConstraint{o.r0 + t * o.dir}; + } else if constexpr (std::is_same_v) { + libmesh_error_msg_if(!this->contains_point(o), + "Point is not on the plane."); + return o; + } else + libmesh_error_msg("Unsupported constraint type in Plane::intersect."); + }, + other); +} + VariationalSmootherConstraint::VariationalSmootherConstraint(System & sys, const bool & preserve_subdomain_boundaries) : Constraint(), @@ -35,6 +277,7 @@ VariationalSmootherConstraint::~VariationalSmootherConstraint() = default; void VariationalSmootherConstraint::constrain() { 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; @@ -45,7 +288,7 @@ void VariationalSmootherConstraint::constrain() const auto boundary_node_ids = MeshTools::find_boundary_nodes(mesh); // Identify/constrain subdomain boundary nodes, if requested - std::unordered_map> subdomain_boundary_map; + std::unordered_map subdomain_boundary_map; if (_preserve_subdomain_boundaries) { for (const auto * elem : mesh.active_element_ptr_range()) @@ -70,25 +313,24 @@ void VariationalSmootherConstraint::constrain() if (subdomain_boundary_map.count(node.id())) continue; - // 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); + // Get the relevant nodal neighbors for the subdomain constraint + const auto side_grouped_boundary_neighbors = + get_neighbors_for_subdomain_constraint( + mesh, node, sub_id1, sub_id2, nodes_to_elem_map); - // Remove any neighbors that are not on the subdomain boundary - VariationalSmootherConstraint::filter_neighbors_for_subdomain_constraint( - node, neighbors, sub_id1, sub_id2, 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_constraints(node, neighbors); + this->impose_constraint(node, subdomain_constraint); // This subdomain boundary node lies on an external boundary, save it // for later to combine with the external boundary constraint else - subdomain_boundary_map[node.id()] = neighbors; - + subdomain_boundary_map[node.id()] = subdomain_constraint; }//for local_node_id @@ -102,177 +344,29 @@ void VariationalSmootherConstraint::constrain() { 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); + // 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); - // Remove any neighbors that are not boundary nodes OR boundary neighbor nodes - // that don't share a boundary id with node - VariationalSmootherConstraint::filter_neighbors_for_boundary_constraint( - node, neighbors, nodes_to_elem_map, boundary_node_ids, boundary_info); + // 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 const auto it = subdomain_boundary_map.find(bid); if (it != subdomain_boundary_map.end()) { - const auto & subdomain_neighbors = it->second; - // Combine current neighbors with subdomain boundary neighbors - for (const auto & neighbor : subdomain_neighbors) - if (std::find(neighbors.begin(), neighbors.end(), neighbor) == neighbors.end()) - neighbors.push_back(neighbor); - } - - this->impose_constraints(node, neighbors); - - }// end bid -} - -void VariationalSmootherConstraint::impose_constraints( - const Node &node, const std::vector neighbors) { - const auto &mesh = _sys.get_mesh(); - const auto dim = mesh.mesh_dimension(); - - // Determine whether the current node is colinear (2D) or coplanar 3D with - // its boundary neighbors. Start by computing the vectors from the current - // node to each boundary neighbor node - std::vector dist_vecs; - for (const auto &neighbor : neighbors) - dist_vecs.push_back((*neighbor) - node); - - // 2D: If the current node and all (two) boundary neighbor nodes lie on the - // same line, the magnitude of the dot product of the distance vectors will be - // equal to the product of the magnitudes of the vectors. This is because the - // distance vectors lie on the same line, so the cos(theta) term in the dot - // product evaluates to -1 or 1. - if (dim == 2) { - // Physically, the boundary of a 2D mesh is a 1D curve. By the - // definition of a "neighbor", it is only possible for a node - // to have 2 neighbors on the boundary. - libmesh_assert_equal_to(dist_vecs.size(), dim); - const Real dot_product = dist_vecs[0] * dist_vecs[1]; - const Real norm_product = dist_vecs[0].norm() * dist_vecs[1].norm(); - - // node is not colinear with its boundary neighbors and is thus immovable - if (!relative_fuzzy_equals(std::abs(dot_product), norm_product)) - this->fix_node(node); - - else { - // Yes, yes, we are using a function called "constrain_node_to_plane" to - // constrain a node to a line in a 2D mesh. However, the line - // c_x * x + c_y * y + c = 0 is equivalent to the plane - // c_x * x + c_y * y + 0 * z + c = 0, so the same logic applies here. - // Since all dist_vecs reside in the xy plane, and are parallel to the - // line we are constraining to, crossing one of the dist_vecs with the - // unit vector in the z direction should give us a vector normal to the - // constraining line. This reference normal vector also resides in the xy - // plane. - // - // TODO: what if z is not the inactive dimension in 2D? - // Would this even happen!?!? - const auto reference_normal = dist_vecs[0].cross(Point(0., 0., 1.)); - this->constrain_node_to_plane(node, reference_normal); - } - } - - // 3D: If the current node and all boundary neighbor nodes lie on the same - // plane, all the distance vectors from the current node to the boundary nodes - // will be orthogonal to the plane normal. We can obtain a reference normal by - // normalizing the cross product between two of the distance vectors. If the - // normalized cross products of all other combinations (excluding self - // combinations) match this reference normal, then the current node is - // coplanar with all of its boundary nodes and should be constrained to the - // 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, 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. - else if (dim == 3) { - // We should have at least 2 distance vectors to compute a normal with in 3D - libmesh_assert_greater_equal(dist_vecs.size(), 2); - - // Compute the reference normal by taking the cross product of two vectors - // in dist_vecs. We will use dist_vecs[0] as the first vector and the next - // available vector in dist_vecs that is not (anti)parallel to dist_vecs[0]. - // Without this check we may end up with a zero vector for the reference - // vector. - unsigned int vec_index; - const Point vec_0_normalized = dist_vecs[0] / dist_vecs[0].norm(); - for (const auto ii : make_range(size_t(1), dist_vecs.size())) { - // (anti)parallel check - const bool is_parallel = vec_0_normalized.relative_fuzzy_equals( - dist_vecs[ii] / dist_vecs[ii].norm()); - const bool is_antiparallel = vec_0_normalized.relative_fuzzy_equals( - -dist_vecs[ii] / dist_vecs[ii].norm()); - if (!(is_parallel || is_antiparallel)) { - vec_index = ii; - break; - } - } - - const Point reference_cross_prod = dist_vecs[0].cross(dist_vecs[vec_index]); - const Point reference_normal = - reference_cross_prod / reference_cross_prod.norm(); - - // Does the node lie within a 2D surface? (Not on the edge) - bool node_is_coplanar = true; - - // Does the node lie on the intersection of two 2D surfaces (i.e., a - // line)? If so, and the node is not located at the intersection of three - // 2D surfaces (i.e., a single point or vertex of the mesh), then some of - // the dist_vecs will be parallel. - - // Each entry will be a vector from dist_vecs that has a corresponding - // (anti)parallel vector, also from dist_vecs - std::vector parallel_pairs; - bool all_pairs_parallel = true; - - for (const auto ii : index_range(dist_vecs)) { - const Point vec_ii_normalized = dist_vecs[ii] / dist_vecs[ii].norm(); - for (const auto jj : make_range(ii + 1, dist_vecs.size())) { - const Point vec_jj_normalized = dist_vecs[jj] / dist_vecs[jj].norm(); - const bool is_parallel = - vec_ii_normalized.relative_fuzzy_equals(vec_jj_normalized); - const bool is_antiparallel = - vec_ii_normalized.relative_fuzzy_equals(-vec_jj_normalized); - - if (is_parallel || is_antiparallel) { - parallel_pairs.push_back(vec_ii_normalized); - all_pairs_parallel &= ( - vec_ii_normalized.relative_fuzzy_equals(parallel_pairs[0]) || - vec_ii_normalized.relative_fuzzy_equals(-parallel_pairs[0]) - ); - // Don't bother computing the cross product of parallel vector below, - // it will be zero and cannot be used to define a normal vector. - continue; - } - - const Point cross_prod = dist_vecs[ii].cross(dist_vecs[jj]); - const Point normal = cross_prod / cross_prod.norm(); - - // node is not coplanar with its boundary neighbors - if (!(reference_normal.relative_fuzzy_equals(normal) || - reference_normal.relative_fuzzy_equals(-normal))) - node_is_coplanar = false; - } - } - - if (relative_fuzzy_equals(node(0), -29., 0.01) && absolute_fuzzy_equals(node(1), 0., 0.01) && relative_fuzzy_equals(node(2), 1., 0.01)) - std::cout << "Node " << node.id() << std::endl; - - if (node_is_coplanar) - this->constrain_node_to_plane(node, reference_normal); - - else if (parallel_pairs.size() && all_pairs_parallel) - this->constrain_node_to_line(node, parallel_pairs[0]); - else - this->fix_node(node); - } - - // 1D - else - this->fix_node(node); + const auto &subdomain_constraint = it->second; + // Combine current boundary constraint with previously determined + // subdomain_constraint + const auto combined_constraint = + intersect_constraints(subdomain_constraint, boundary_constraint); + this->impose_constraint(node, combined_constraint); + } else + this->impose_constraint(node, boundary_constraint); + + } // end bid } void VariationalSmootherConstraint::fix_node(const Node & node) @@ -425,22 +519,27 @@ bool VariationalSmootherConstraint::nodes_share_boundary_id( return nodes_share_bid; } -void VariationalSmootherConstraint::filter_neighbors_for_subdomain_constraint( - const Node & node, - std::vector & neighbors, - const subdomain_id_type sub_id1, - const subdomain_id_type sub_id2, - std::unordered_map> & nodes_to_elem_map) -{ +std::set> +VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( + const MeshBase &mesh, const Node &node, const subdomain_id_type sub_id1, + const subdomain_id_type sub_id2, + const std::unordered_map> + &nodes_to_elem_map) { - // Remove any neighbors that are not on the subdomain boundary - auto remove_neighbor = [&node, &nodes_to_elem_map, &sub_id1, &sub_id2] - (const Node * neigh) -> bool - { + // 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[node.id()]; - const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; + 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) { @@ -456,7 +555,7 @@ void VariationalSmootherConstraint::filter_neighbors_for_subdomain_constraint( const auto & other_sub_id = (common_sub_id == sub_id1) ? sub_id2: sub_id1; // Now, determine whether node and neigh are on a side coincident - // with the interval boundary + // with the subdomain boundary for (const auto common_side : common_elem->side_index_range()) { bool node_found_on_side = false; @@ -469,73 +568,194 @@ void VariationalSmootherConstraint::filter_neighbors_for_subdomain_constraint( neigh_found_on_side = true; } - if (node_found_on_side && neigh_found_on_side && common_elem->neighbor_ptr(common_side)) - { - const auto matched_side = common_side; - // There could be multiple matched sides, so keep this next part - // inside the loop - // - // Does matched_side, containing both node and neigh, lie on the - // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) - // and sub_id2 (= other sub_id or common_sub_id)? - 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 == other_sub_id; - if (is_matched_side_on_subdomain_boundary) - return false; // Don't remove the neighbor node + 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 + // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) + // and sub_id2 (= other sub_id or common_sub_id)? + 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 == other_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 true; // Remove the neighbor node - }; + return side_grouped_boundary_neighbors; +} - neighbors.erase( - std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), - neighbors.end() - ); +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); -void VariationalSmootherConstraint::filter_neighbors_for_boundary_constraint( - const Node & node, - std::vector & neighbors, - std::unordered_map> & nodes_to_elem_map, - const std::unordered_set & boundary_node_ids, - const BoundaryInfo & boundary_info) -{ + // Each constituent set corresponds to neighbors sharing a face on the + // boundary + std::set> side_grouped_boundary_neighbors; - // Remove any neighbors that are not boundary nodes OR boundary neighbor nodes - // that don't share a boundary id with node - auto remove_neighbor = [&boundary_node_ids, &node, &nodes_to_elem_map, &boundary_info] - (const Node * neigh) -> bool - { + 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[node.id()]; - const auto & elems_containing_neigh = nodes_to_elem_map[neigh->id()]; - const Elem * common_elem = nullptr; - bool nodes_have_common_bid = false; - 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()) - { - common_elem = neigh_elem; - // Keep this in the loop because there can be multiple common elements - // Now, determine whether node and neigh share a common boundary id - nodes_have_common_bid = VariationalSmootherConstraint::nodes_share_boundary_id(node, *neigh, *common_elem, boundary_info) || nodes_have_common_bid; + 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()); + libmesh_assert_greater_equal(neighbors.size(), 1); + + // 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(size_t(1), neighbors.size())) { + 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)) { + all_colinear = false; + break; } + } + if (all_colinear) + return LineConstraint{node, ref_dir}; - // remove if neighbor is not boundary node or nodes don't share a common bid - return (is_neighbor_boundary_node && nodes_have_common_bid) ? false : true; - }; + return PointConstraint{node}; + } - neighbors.erase( - std::remove_if(neighbors.begin(), neighbors.end(), remove_neighbor), - neighbors.end() - ); + // 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)) { + 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); + } + } + } + + // 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); + + return current; +} + +// Applies the computed constraint (PointConstraint, LineConstraint, or +// PlaneConstraint) to a node. +void VariationalSmootherConstraint::impose_constraint( + const Node &node, const ConstraintVariant &constraint) { + if (std::holds_alternative(constraint)) + fix_node(node); + else if (std::holds_alternative(constraint)) + constrain_node_to_line(node, std::get(constraint).dir); + 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 From 4c07077b7170ceb4c83aa146fdfd135c3f7a001b Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 16 Jul 2025 12:19:47 -0600 Subject: [PATCH 14/38] Fixed bug causing 'node already constrained' error. --- src/systems/variational_smoother_constraint.C | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index a25b14a1a9..b199b3a358 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -327,10 +327,12 @@ void VariationalSmootherConstraint::constrain() if (boundary_node_ids.find(node.id()) == boundary_node_ids.end()) this->impose_constraint(node, subdomain_constraint); - // This subdomain boundary node lies on an external boundary, save it - // for later to combine with the external boundary constraint - else - subdomain_boundary_map[node.id()] = 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 From 702e58dbdc04078743e4d24372e5e9c2ffefca6d Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 08:06:50 -0600 Subject: [PATCH 15/38] Generalized DistortSquare, now distort sliding boundary nodes. Replaced DistortSquare class with the DistortHypercube class, which generalizes dimension and distorts boundary nodes within boundaries instead of leaving them unchanged. The helper also checks all applicable node dimensions instead of receiving the dimension to check as a parameter. Renamed the `center_distortion_is` helper function to `distortion_is` and updated to check sliding boundary nodes for distortion. --- tests/mesh/mesh_smoother_test.C | 268 ++++++++++++++++++-------------- 1 file changed, 154 insertions(+), 114 deletions(-) diff --git a/tests/mesh/mesh_smoother_test.C b/tests/mesh/mesh_smoother_test.C index 2568ec1db6..7e9df67da9 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -1,15 +1,16 @@ -#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" @@ -18,27 +19,64 @@ namespace { using namespace libMesh; -class DistortSquare : public FunctionBase -{ - std::unique_ptr> clone () const override - { return std::make_unique(); } +// Distortion function supporting 1D, 2D, and 3D +class DistortHyperCube : public FunctionBase { +public: + DistortHyperCube(const unsigned int dim) : _dim(dim) {} - Real operator() (const Point &, - const Real = 0.) override - { libmesh_not_implemented(); } // scalar-only API +private: + std::unique_ptr> clone() const override { + return std::make_unique(_dim); + } - // Skew inward based on a cubic function - void operator() (const Point & p, - const Real, - DenseVector & output) - { + Real operator()(const Point &, const Real = 0.) override { + libmesh_not_implemented(); + } + + void operator()(const Point &p, const Real, + DenseVector &output) override { 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)); - output(2) = 0; + 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 @@ -121,20 +159,30 @@ public: { LOG_UNIT_TEST; - unsigned int n_elems_per_side = 4; + 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."); 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); + 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) { // Increment the subdomain id on the right half by 1 - for (auto * elem : mesh.active_element_ptr_range()) + for (auto *elem : mesh.active_element_ptr_range()) if (elem->vertex_average()(0) > 0.5) ++elem->subdomain_id(); @@ -156,95 +204,92 @@ public: } } - const auto & boundary_info = mesh.get_boundary_info(); - // 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; - - // 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. - - // Get boundary ids associated with the node - std::vector boundary_ids; - boundary_info.boundary_ids(&node, boundary_ids); - - switch (boundary_ids.size()) - { - // 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; - 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; - } + 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. + */ + + 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; - 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))); + 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_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 (relative_fuzzy_equals( + Point(node), subdomain_boundary_node_id_to_point[node.id()])); + else + // node is not a subdomain boundary node, just return true return true; - } - return false; - break; - default: - libmesh_error_msg("Node has unsupported number of boundary ids = " << boundary_ids.size()); - } - }; - - // 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 it = subdomain_boundary_node_id_to_point.find(node.id()); - if (it != subdomain_boundary_node_id_to_point.end()) - 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 - return true; - }; - - // Make sure our DistortSquare transformation has distorted the mesh - for (auto node : mesh.node_ptr_range()) - { - CPPUNIT_ASSERT(center_distortion_is(*node, 0, true)); - CPPUNIT_ASSERT(center_distortion_is(*node, 1, 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 + const bool is_variational_smoother_type = + (dynamic_cast(&smoother) != nullptr); + if (type == TRI3 && is_variational_smoother_type) { + 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. @@ -265,14 +310,9 @@ public: 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)); } } From 15cf24a7622da8c4eb58f22aa88c44714373cb50 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 08:53:56 -0600 Subject: [PATCH 16/38] Increased the complexity of the subdomain boundary test. --- tests/mesh/mesh_smoother_test.C | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/mesh/mesh_smoother_test.C b/tests/mesh/mesh_smoother_test.C index 7e9df67da9..d5cdd7006b 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -181,10 +181,14 @@ public: 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(); + // 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 From c6e37e47ad28fca6e79a120812a8649bf0c53176 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 12:08:12 -0600 Subject: [PATCH 17/38] Added fallback to fixed node constraint in event that constrints do not intersect. --- src/systems/variational_smoother_constraint.C | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index b199b3a358..2024ef66b9 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -362,9 +362,19 @@ void VariationalSmootherConstraint::constrain() const auto &subdomain_constraint = it->second; // Combine current boundary constraint with previously determined // subdomain_constraint - const auto combined_constraint = - intersect_constraints(subdomain_constraint, boundary_constraint); - this->impose_constraint(node, combined_constraint); + try + { + const auto combined_constraint = + intersect_constraints(subdomain_constraint, boundary_constraint); + this->impose_constraint(node, combined_constraint); + } + catch (const std::exception & e) + { + // This will catch cases where constraints have no intersection + // Fall back to fixed node constraint + this->impose_constraint(node, PointConstraint(node)); + } + } else this->impose_constraint(node, boundary_constraint); @@ -741,7 +751,19 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( auto it = valid_planes.begin(); ConstraintVariant current = *it++; for (; it != valid_planes.end(); ++it) - current = intersect_constraints(current, *it); + { + try + { + current = intersect_constraints(current, *it); + } + catch (const std::exception & e) + { + // This will catch cases where constraints have no intersection + // Fall back to fixed node constraint + current = PointConstraint(node); + break; + } + } return current; } From 6c5303c498f2310bc31dc3e1593c713613e63a42 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 13:04:46 -0600 Subject: [PATCH 18/38] Updated identification of subdomain boundary node neighbors. Instead of looking for the boundary between sub_id2 and sub_id2, we now look for the boundary between sub_id1 and "not sub_id1". This better handles cases where multiple faces of an element have neighbors of multiple different subdomain ids. --- .../systems/variational_smoother_constraint.h | 7 ++-- src/systems/variational_smoother_constraint.C | 32 +++++++++++-------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index d41c477953..af09b95338 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -213,10 +213,8 @@ class VariationalSmootherConstraint : public System::Constraint /// 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_id1 The subdomain id of the block on one side of the subdomain + /// @param sub_id The subdomain id of the block on one side of the subdomain /// boundary. - /// @param sub_id2 The subdomain id of the block on the other 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 @@ -224,8 +222,7 @@ class VariationalSmootherConstraint : public System::Constraint /// in this set. static std::set> get_neighbors_for_subdomain_constraint( - const MeshBase &mesh, const Node &node, const subdomain_id_type sub_id1, - const subdomain_id_type sub_id2, + const MeshBase &mesh, const Node &node, const subdomain_id_type sub_id, const std::unordered_map> &nodes_to_elem_map); diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 2024ef66b9..18faa882ce 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -316,7 +316,7 @@ void VariationalSmootherConstraint::constrain() // Get the relevant nodal neighbors for the subdomain constraint const auto side_grouped_boundary_neighbors = get_neighbors_for_subdomain_constraint( - mesh, node, sub_id1, sub_id2, nodes_to_elem_map); + mesh, node, sub_id1, nodes_to_elem_map); // Determine which constraint should be imposed const auto subdomain_constraint = @@ -533,8 +533,7 @@ bool VariationalSmootherConstraint::nodes_share_boundary_id( std::set> VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( - const MeshBase &mesh, const Node &node, const subdomain_id_type sub_id1, - const subdomain_id_type sub_id2, + const MeshBase &mesh, const Node &node, const subdomain_id_type sub_id, const std::unordered_map> &nodes_to_elem_map) { @@ -555,17 +554,16 @@ VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( 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()) + 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; - const auto common_sub_id = common_elem->subdomain_id(); - libmesh_assert(common_sub_id == sub_id1 || common_sub_id == sub_id2); - - // Define this allias for convenience - const auto & other_sub_id = (common_sub_id == sub_id1) ? sub_id2: sub_id1; - // 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()) @@ -589,12 +587,12 @@ VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( // inside the common_side loop // // Does matched_side, containing both node and neigh, lie on the - // subdomain boundary between sub_id1 (= common_sub_id or other_sub_id) - // and sub_id2 (= other sub_id or common_sub_id)? + // 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 == other_sub_id; + 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); @@ -612,6 +610,10 @@ VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( }// for neigh_elem } + libmesh_assert_msg(!side_grouped_boundary_neighbors.empty(), + "No boundary neighbors found for node " << node << " on the subdomain " + << "boundary for subdomain " << sub_id); + return side_grouped_boundary_neighbors; } @@ -685,6 +687,10 @@ VariationalSmootherConstraint::get_neighbors_for_boundary_constraint( } } } + + libmesh_assert_msg(!side_grouped_boundary_neighbors.empty(), + "No boundary neighbors found for node " << node << " on the external boundary"); + return side_grouped_boundary_neighbors; } From 86eee47b628c21966a60fbaf2a58b328a2c828f6 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 13:08:32 -0600 Subject: [PATCH 19/38] Added 1D and 3D tests for the VariationalMeshSmoother. --- tests/mesh/mesh_smoother_test.C | 68 +++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/tests/mesh/mesh_smoother_test.C b/tests/mesh/mesh_smoother_test.C index d5cdd7006b..116005cb2e 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -142,9 +142,14 @@ public: CPPUNIT_TEST( testLaplaceQuad ); CPPUNIT_TEST( testLaplaceTri ); #if defined(LIBMESH_ENABLE_VSMOOTHER) && defined(LIBMESH_HAVE_SOLVER) - CPPUNIT_TEST( testVariationalQuad ); - CPPUNIT_TEST( testVariationalTri ); + 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 @@ -170,8 +175,24 @@ public: libmesh_error_msg_if(n_elems_per_side % 2 != 1, "n_elems_per_side should be odd."); - MeshTools::Generation::build_square(mesh, n_elems_per_side, n_elems_per_side, - 0.,1.,0.,1., type); + switch (dim) { + case 1: + MeshTools::Generation::build_line(mesh, n_elems_per_side, 0., 1., type); + break; + case 2: + MeshTools::Generation::build_square( + mesh, n_elems_per_side, n_elems_per_side, 0., 1., 0., 1., type); + break; + + 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; + + default: + libmesh_error_msg("Unsupported dimension " << dim); + } // Move it around so we have something that needs smoothing DistortHyperCube dh(dim); @@ -340,6 +361,20 @@ public: #ifdef LIBMESH_ENABLE_VSMOOTHER + void testVariationalEdge() { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testSmoother(mesh, variational, EDGE2); + } + + void testVariationalEdgeMultipleSubdomains() { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testSmoother(mesh, variational, EDGE2, true); + } + void testVariationalQuad() { ReplicatedMesh mesh(*TestCommWorld); @@ -348,6 +383,12 @@ public: testSmoother(mesh, variational, QUAD4); } + void testVariationalQuadMultipleSubdomains() { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testSmoother(mesh, variational, QUAD4, true); + } void testVariationalTri() { @@ -357,12 +398,25 @@ public: testSmoother(mesh, variational, TRI3); } - void testVariationalQuadMultipleSubdomains() - { + void testVariationalTriMultipleSubdomains() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, QUAD4, true); + testSmoother(mesh, variational, TRI3, true); + } + + void testVariationalHex() { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testSmoother(mesh, variational, HEX8); + } + + void testVariationalHexMultipleSubdomains() { + ReplicatedMesh mesh(*TestCommWorld); + VariationalMeshSmoother variational(mesh); + + testSmoother(mesh, variational, HEX8, true); } #endif // LIBMESH_ENABLE_VSMOOTHER }; From c9973d1ee841d56d3b522d8d2d5cbfa6c172d922 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 14:22:08 -0600 Subject: [PATCH 20/38] Include variant. --- include/systems/variational_smoother_constraint.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index af09b95338..f83f989756 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -22,6 +22,9 @@ #include "libmesh/system.h" #include "libmesh/dof_map.h" +// C++ includes +#include + namespace libMesh { From db4b69d3d8de3b2ea00c14c0445780722b4c881c Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 15:39:09 -0600 Subject: [PATCH 21/38] Removed some assertions that don't apply in 1D. --- src/systems/variational_smoother_constraint.C | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 18faa882ce..a6f5c7ba98 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -304,7 +304,7 @@ void VariationalSmootherConstraint::constrain() if (sub_id1 == sub_id2) continue; - // elem and neighbor are in difference subdomains, and share nodes + // 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)) { @@ -610,10 +610,6 @@ VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( }// for neigh_elem } - libmesh_assert_msg(!side_grouped_boundary_neighbors.empty(), - "No boundary neighbors found for node " << node << " on the subdomain " - << "boundary for subdomain " << sub_id); - return side_grouped_boundary_neighbors; } @@ -688,9 +684,6 @@ VariationalSmootherConstraint::get_neighbors_for_boundary_constraint( } } - libmesh_assert_msg(!side_grouped_boundary_neighbors.empty(), - "No boundary neighbors found for node " << node << " on the external boundary"); - return side_grouped_boundary_neighbors; } @@ -704,7 +697,6 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( std::vector neighbors; for (const auto &side : side_grouped_boundary_neighbors) neighbors.insert(neighbors.end(), side.begin(), side.end()); - libmesh_assert_greater_equal(neighbors.size(), 1); // Constrain the node to it's current location if (dim == 1 || neighbors.size() == 1) From 92200b50323e32f0a8fbfefbc919ac236fde875d Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 15:52:32 -0600 Subject: [PATCH 22/38] Addressed maybe-uninitialized error. --- src/systems/variational_smoother_constraint.C | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index a6f5c7ba98..19e8040799 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -404,7 +404,14 @@ void VariationalSmootherConstraint::constrain_node_to_plane(const Node & node, c // 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 - unsigned int constrained_dim; + + // 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)) { From 7aeaa2b4be0b8acd720b34ab166abfb8f9cf9f83 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 17 Jul 2025 17:07:42 -0600 Subject: [PATCH 23/38] Separated testSmoother function into testVariationalSmoother and testLaplaceSmoother functions. --- tests/mesh/mesh_smoother_test.C | 138 +++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 29 deletions(-) diff --git a/tests/mesh/mesh_smoother_test.C b/tests/mesh/mesh_smoother_test.C index 116005cb2e..eb0a9a18d9 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -19,7 +19,30 @@ 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(); + } + + 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) { + 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)); + 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) {} @@ -160,8 +183,73 @@ 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; + + 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); + + // Move it around so we have something that needs smoothing + DistortSquare ds; + MeshTools::Modification::redistribute(mesh, ds); + + // 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 (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)); + } + + // 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(); + + // 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)); + } + } + + void testVariationalSmoother(ReplicatedMesh &mesh, MeshSmoother &smoother, + const ElemType type, + const bool multiple_subdomains = false) { LOG_UNIT_TEST; const auto dim = ReferenceElem::get(type).dim(); @@ -308,28 +396,21 @@ public: // 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) { + if (type == TRI3) { 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(); - // 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. for (auto node : mesh.node_ptr_range()) @@ -341,13 +422,12 @@ public: } } - void testLaplaceQuad() { ReplicatedMesh mesh(*TestCommWorld); LaplaceMeshSmoother laplace(mesh); - testSmoother(mesh, laplace, QUAD4); + testLaplaceSmoother(mesh, laplace, QUAD4); } @@ -356,7 +436,7 @@ public: ReplicatedMesh mesh(*TestCommWorld); LaplaceMeshSmoother laplace(mesh); - testSmoother(mesh, laplace, TRI3); + testLaplaceSmoother(mesh, laplace, TRI3); } @@ -365,14 +445,14 @@ public: ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, EDGE2); + testVariationalSmoother(mesh, variational, EDGE2); } void testVariationalEdgeMultipleSubdomains() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, EDGE2, true); + testVariationalSmoother(mesh, variational, EDGE2, true); } void testVariationalQuad() @@ -380,14 +460,14 @@ public: ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, QUAD4); + testVariationalSmoother(mesh, variational, QUAD4); } void testVariationalQuadMultipleSubdomains() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, QUAD4, true); + testVariationalSmoother(mesh, variational, QUAD4, true); } void testVariationalTri() @@ -395,28 +475,28 @@ public: ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, TRI3); + testVariationalSmoother(mesh, variational, TRI3); } void testVariationalTriMultipleSubdomains() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, TRI3, true); + testVariationalSmoother(mesh, variational, TRI3, true); } void testVariationalHex() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, HEX8); + testVariationalSmoother(mesh, variational, HEX8); } void testVariationalHexMultipleSubdomains() { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); - testSmoother(mesh, variational, HEX8, true); + testVariationalSmoother(mesh, variational, HEX8, true); } #endif // LIBMESH_ENABLE_VSMOOTHER }; From d4e397b4f3ae18953d8c0f5dbd8ccf579117736b Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 18 Jul 2025 07:46:01 -0600 Subject: [PATCH 24/38] Swaped doxygen syntax from '///' to '/***/' --- .../systems/variational_smoother_constraint.h | 243 +++++++++++------- 1 file changed, 144 insertions(+), 99 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index f83f989756..b6e6d3d122 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -32,124 +32,159 @@ struct PointConstraint; struct LineConstraint; struct PlaneConstraint; -/// Type used to store a constraint that may be a PlaneConstraint, -/// LineConstraint, or PointConstraint +/** + * Type used to store a constraint that may be a PlaneConstraint, + * LineConstraint, or PointConstraint + */ using ConstraintVariant = std::variant; -/// Represents a fixed point constraint. +/** + * Represents a fixed point constraint. + */ struct PointConstraint { Point location; - PointConstraint() = default; - /// Constructor - /// @parm p The point defining the constraint. + /** + * Constructor + * @param p The point defining the constraint. + */ PointConstraint(const Point &p); bool operator<(const PointConstraint &other) const; bool operator==(const PointConstraint &other) const; - /// 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. + /** + * 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. + */ ConstraintVariant intersect(const ConstraintVariant &other) const; }; -/// Represents a line constraint defined by a base point and direction vector. +/** + * Represents a line constraint defined by a base point and direction vector. + */ struct LineConstraint { Point r0; Point dir; LineConstraint() = default; - /// Constructor - /// @parm p A point on the constraining line. - /// @param d the direction of the constraining line. + /** + * Constructor + * @param p A point on the constraining line. + * @param d the direction of the constraining line. + */ LineConstraint(const Point &p, const Point &d); bool operator<(const LineConstraint &other) const; 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. + /** + * 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. + /** + * 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. + /** + * 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. + /** + * 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. + */ ConstraintVariant intersect(const ConstraintVariant &other) const; }; -/// Represents a plane constraint defined by a point and normal vector. +/** + * Represents a plane constraint defined by a point and normal vector. + */ struct PlaneConstraint { Point point; Point normal; PlaneConstraint() = default; - /// Constructor - /// @parm p A point on the constraining plane. - /// @param n the direction normal to the constraining plane. + /** + * Constructor + * @param p A point on the constraining plane. + * @param n the direction normal to the constraining plane. + */ PlaneConstraint(const Point &p, const Point &n); bool operator<(const PlaneConstraint &other) const; 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. + /** + * 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. + /** + * 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. + /** + * 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. + /** + * 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. + /** + * 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. + */ ConstraintVariant intersect(const ConstraintVariant &other) const; }; -/// 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. +/** + * 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) { return std::visit( @@ -159,12 +194,12 @@ inline ConstraintVariant intersect_constraints(const ConstraintVariant &a, 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 { @@ -172,16 +207,18 @@ 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 (i.e., fix) a node to not move during mesh smoothing. * @param node Node to fix. */ void fix_node(const Node & node); - /* + /** * 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. @@ -190,7 +227,7 @@ class VariationalSmootherConstraint : public System::Constraint */ 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. @@ -199,7 +236,7 @@ class VariationalSmootherConstraint : public System::Constraint */ 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. @@ -213,32 +250,36 @@ class VariationalSmootherConstraint : public System::Constraint 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. + /** + * 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. + /** + * 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, @@ -246,27 +287,31 @@ class VariationalSmootherConstraint : public System::Constraint 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. + /** + * 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. + /** + * 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. + */ 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); From b32de64ca846291de33e399ca82ed4115737338f Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 18 Jul 2025 08:02:35 -0600 Subject: [PATCH 25/38] Added documentation for Point/Line/PlaneConstraint operators < and ==. --- .../systems/variational_smoother_constraint.h | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index b6e6d3d122..4d2cc7ae3f 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -52,8 +52,20 @@ struct PointConstraint { */ PointConstraint(const Point &p); + /** + * 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. + * @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; /** @@ -83,8 +95,21 @@ struct LineConstraint { */ LineConstraint(const Point &p, const Point &d); + /** + * 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; /** @@ -135,8 +160,21 @@ struct PlaneConstraint { */ PlaneConstraint(const Point &p, const Point &n); + /** + * 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; /** From 3533633952ea83320fe3d00afc324422d50e238f Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 18 Jul 2025 09:25:58 -0600 Subject: [PATCH 26/38] Document the throwing of error in *Constraint structs. --- include/systems/variational_smoother_constraint.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 4d2cc7ae3f..3a74f9597d 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -75,6 +75,8 @@ struct PointConstraint { * @param other The constraint to intersect with. * @return The most specific ConstraintVariant that satisfies both * constraints. + * @throw libMesh::LogicError If the constraints are incompatible and cannot + * intersect. */ ConstraintVariant intersect(const ConstraintVariant &other) const; }; @@ -140,6 +142,8 @@ struct LineConstraint { * @param other The constraint to intersect with. * @return The most specific ConstraintVariant that satisfies both * constraints. + * @throw libMesh::LogicError If the constraints are incompatible and cannot + * intersect. */ ConstraintVariant intersect(const ConstraintVariant &other) const; }; @@ -212,6 +216,8 @@ struct PlaneConstraint { * @param other The constraint to intersect with. * @return The most specific ConstraintVariant that satisfies both * constraints. + * @throw libMesh::LogicError If the constraints are incompatible and cannot + * intersect. */ ConstraintVariant intersect(const ConstraintVariant &other) const; }; From 899e7431bf35276072b31c2ca65b70b91c7cef53 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 18 Jul 2025 10:17:51 -0600 Subject: [PATCH 27/38] Made *Constraint struct operator< symmetric and compatible with operator==. --- src/systems/variational_smoother_constraint.C | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 19e8040799..75cc540226 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -26,11 +26,14 @@ namespace libMesh PointConstraint::PointConstraint(const Point &p) : location(p) {} bool PointConstraint::operator<(const PointConstraint &other) const { + if (*this == other) + return false; + return location < other.location; } bool PointConstraint::operator==(const PointConstraint &other) const { - return location == other.location; + return location.absolute_fuzzy_equals(other.location, TOLERANCE); } ConstraintVariant @@ -72,9 +75,12 @@ LineConstraint::LineConstraint(const Point &p, const Point &d) { } bool LineConstraint::operator<(const LineConstraint &other) const { + if (*this == other) + return false; + if (!(dir.absolute_fuzzy_equals(other.dir, TOLERANCE))) return dir < other.dir; - return (dir * r0) < (other.dir * other.r0) - TOLERANCE; + return (dir * r0) < (other.dir * other.r0); } bool LineConstraint::operator==(const LineConstraint &other) const { @@ -163,9 +169,12 @@ PlaneConstraint::PlaneConstraint(const Point &p, const Point &n) { } bool PlaneConstraint::operator<(const PlaneConstraint &other) const { + if (*this == other) + return false; + if (!(normal.absolute_fuzzy_equals(other.normal, TOLERANCE))) return normal < other.normal; - return (normal * point) < (other.normal * other.point) - TOLERANCE; + return (normal * point) < (other.normal * other.point); } bool PlaneConstraint::operator==(const PlaneConstraint &other) const { From 072061009d2988766fbe46de6b9163a7c479b393 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 18 Jul 2025 10:30:26 -0600 Subject: [PATCH 28/38] Changed try/catch to libmesh_try/libmesh_catch. --- src/systems/variational_smoother_constraint.C | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 75cc540226..207434cdc3 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -371,14 +371,12 @@ void VariationalSmootherConstraint::constrain() const auto &subdomain_constraint = it->second; // Combine current boundary constraint with previously determined // subdomain_constraint - try - { + libmesh_try { const auto combined_constraint = intersect_constraints(subdomain_constraint, boundary_constraint); this->impose_constraint(node, combined_constraint); } - catch (const std::exception & e) - { + libmesh_catch(const std::exception &e) { // This will catch cases where constraints have no intersection // Fall back to fixed node constraint this->impose_constraint(node, PointConstraint(node)); @@ -766,12 +764,8 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( ConstraintVariant current = *it++; for (; it != valid_planes.end(); ++it) { - try - { - current = intersect_constraints(current, *it); - } - catch (const std::exception & e) - { + libmesh_try { current = intersect_constraints(current, *it); } + libmesh_catch(const std::exception &e) { // This will catch cases where constraints have no intersection // Fall back to fixed node constraint current = PointConstraint(node); From 4153e1e2e6dd1c2aa2d778545c598b12847a7ee5 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 18 Jul 2025 15:10:20 -0600 Subject: [PATCH 29/38] Changed Point/Line/PlaneConstraint structs to classes, made attributes private. --- .../systems/variational_smoother_constraint.h | 84 +++++++--- src/systems/variational_smoother_constraint.C | 154 +++++++++--------- 2 files changed, 140 insertions(+), 98 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 3a74f9597d..16ea2075f3 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -28,9 +28,9 @@ namespace libMesh { -struct PointConstraint; -struct LineConstraint; -struct PlaneConstraint; +class PointConstraint; +class LineConstraint; +class PlaneConstraint; /** * Type used to store a constraint that may be a PlaneConstraint, @@ -42,15 +42,16 @@ using ConstraintVariant = /** * Represents a fixed point constraint. */ -struct PointConstraint { - Point location; +class PointConstraint { + +public: PointConstraint() = default; /** * Constructor - * @param p The point defining the constraint. + * @param point The point defining the constraint. */ - PointConstraint(const Point &p); + PointConstraint(const Point &point); /** * Comparison operator for ordering PointConstraint objects. @@ -79,23 +80,33 @@ struct PointConstraint { * intersect. */ ConstraintVariant intersect(const ConstraintVariant &other) const; + + /** + * Const getter for the _point attribute + */ + const Point &point() const { return _point; } + +private: + // Life is easier if we don't make this const + /** + * Location of constraint + */ + Point _point; }; /** * Represents a line constraint defined by a base point and direction vector. */ -struct LineConstraint { - Point r0; - Point dir; - +class LineConstraint { +public: LineConstraint() = default; /** * Constructor - * @param p A point on the constraining line. - * @param d the direction of the constraining line. + * @param point A point on the constraining line. + * @param direction the direction of the constraining line. */ - LineConstraint(const Point &p, const Point &d); + LineConstraint(const Point &point, const Point &direction); /** * Comparison operator for ordering LineConstraint objects. @@ -146,23 +157,39 @@ struct LineConstraint { * intersect. */ 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; } + +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; }; /** * Represents a plane constraint defined by a point and normal vector. */ -struct PlaneConstraint { - Point point; - Point normal; +class PlaneConstraint { +public: PlaneConstraint() = default; /** * Constructor - * @param p A point on the constraining plane. - * @param n the direction normal to the constraining plane. + * @param point A point on the constraining plane. + * @param normal the direction normal to the constraining plane. */ - PlaneConstraint(const Point &p, const Point &n); + PlaneConstraint(const Point &point, const Point &normal); /** * Comparison operator for ordering PlaneConstraint objects. @@ -220,6 +247,23 @@ struct PlaneConstraint { * intersect. */ 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; } + +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; }; /** diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 207434cdc3..6c7b93d5de 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -23,17 +23,39 @@ namespace libMesh { -PointConstraint::PointConstraint(const Point &p) : location(p) {} +// 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) : _point(point) {} bool PointConstraint::operator<(const PointConstraint &other) const { if (*this == other) return false; - return location < other.location; + return _point < other.point(); } bool PointConstraint::operator==(const PointConstraint &other) const { - return location.absolute_fuzzy_equals(other.location, TOLERANCE); + return _point.absolute_fuzzy_equals(other.point(), TOLERANCE); } ConstraintVariant @@ -42,7 +64,7 @@ PointConstraint::intersect(const ConstraintVariant &other) const { [&](auto &&o) -> ConstraintVariant { if constexpr (std::is_same_v, PointConstraint>) { - libmesh_error_msg_if(!(this->location == o.location), + libmesh_error_msg_if(!(this->_point == o.point()), "Points do not match."); return *this; } else { @@ -54,54 +76,41 @@ PointConstraint::intersect(const ConstraintVariant &other) const { other); } -LineConstraint::LineConstraint(const Point &p, const Point &d) { - r0 = p; +LineConstraint::LineConstraint(const Point &point, const Point &direction) + : _point(point), _direction(get_positive_vector(direction)) { libmesh_error_msg_if( - d.norm() < TOLERANCE, + _direction.norm() < TOLERANCE, "Can't define a line with zero magnitude direction vector."); - // Flip direction vector if necessary so it points in the positive x/y/z - // direction This helps to eliminate duplicate lines - 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(d(dim_id), 0.)) { - canonical(dim_id) = 1.; - break; - } - - const auto dot_prod = d * canonical; - libmesh_assert(!absolute_fuzzy_equals(dot_prod, 0.)); - dir = (dot_prod > 0) ? d.unit() : -d.unit(); } bool LineConstraint::operator<(const LineConstraint &other) const { if (*this == other) return false; - if (!(dir.absolute_fuzzy_equals(other.dir, TOLERANCE))) - return dir < other.dir; - return (dir * r0) < (other.dir * other.r0); + if (!(_direction.absolute_fuzzy_equals(other.direction(), TOLERANCE))) + return _direction < other.direction(); + return (_direction * _point) < (other.direction() * other.point()); } bool LineConstraint::operator==(const LineConstraint &other) const { - if (!(dir.absolute_fuzzy_equals(other.dir, TOLERANCE))) + if (!(_direction.absolute_fuzzy_equals(other.direction(), TOLERANCE))) return false; - return this->contains_point(other.r0); + 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 - r0 is parallel to the - // line In that case, the cross product of p - r0 with the line's direction + // 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 dir.cross(p.location - r0).norm() < TOLERANCE; + return _direction.cross(p.point() - _point).norm() < TOLERANCE; } bool LineConstraint::is_parallel(const LineConstraint &l) const { - return dir.absolute_fuzzy_equals(l.dir, TOLERANCE); + return _direction.absolute_fuzzy_equals(l.direction(), TOLERANCE); } bool LineConstraint::is_parallel(const PlaneConstraint &p) const { - return dir * p.normal < TOLERANCE; + return _direction * p.normal() < TOLERANCE; } ConstraintVariant @@ -122,18 +131,18 @@ LineConstraint::intersect(const ConstraintVariant &other) const { // ((p2 - p1) × d2) · (d1 × d2) = t · |d1 × d2|² // ⇒ t = ((delta × d2) · (d1 × d2)) / |d1 × d2|² - const Point delta = o.r0 - r0; - const Point cross_d1_d2 = dir.cross(o.dir); - const Real cross_dot = (delta.cross(o.dir)) * cross_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 = r0 + t * dir; + const Point intersection = _point + t * _direction; // Verify that intersection lies on both lines - libmesh_error_msg_if(o.dir.cross(intersection - o.r0).norm() > - TOLERANCE, - "Lines do not intersect at a single point."); + libmesh_error_msg_if( + o.direction().cross(intersection - o.point()).norm() > TOLERANCE, + "Lines do not intersect at a single point."); return PointConstraint{intersection}; } else if constexpr (std::is_same_v) { @@ -148,43 +157,30 @@ LineConstraint::intersect(const ConstraintVariant &other) const { other); } -PlaneConstraint::PlaneConstraint(const Point &p, const Point &n) { - point = p; +PlaneConstraint::PlaneConstraint(const Point &point, const Point &normal) + : _point(point), _normal(get_positive_vector(normal)) { libmesh_error_msg_if( - n.norm() < TOLERANCE, + _normal.norm() < TOLERANCE, "Can't define a plane with zero magnitude direction vector."); - // Flip normal vector if necessary so it points in the positive x/y/z - // direction This helps to eliminate duplicate points - 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(n(dim_id), 0.)) { - canonical(dim_id) = 1.; - break; - } - - const auto dot_prod = n * canonical; - libmesh_assert(!absolute_fuzzy_equals(dot_prod, 0.)); - normal = (dot_prod > 0) ? n.unit() : -n.unit(); } bool PlaneConstraint::operator<(const PlaneConstraint &other) const { if (*this == other) return false; - if (!(normal.absolute_fuzzy_equals(other.normal, TOLERANCE))) - return normal < other.normal; - return (normal * point) < (other.normal * other.point); + if (!(_normal.absolute_fuzzy_equals(other.normal(), TOLERANCE))) + 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, TOLERANCE))) + if (!(_normal.absolute_fuzzy_equals(other.normal(), TOLERANCE))) return false; - return this->contains_point(other.point); + return this->contains_point(other.point()); } bool PlaneConstraint::is_parallel(const PlaneConstraint &p) const { - return normal.absolute_fuzzy_equals(p.normal, TOLERANCE); + return _normal.absolute_fuzzy_equals(p.normal(), TOLERANCE); } bool PlaneConstraint::is_parallel(const LineConstraint &l) const { @@ -193,13 +189,13 @@ bool PlaneConstraint::is_parallel(const LineConstraint &l) const { bool PlaneConstraint::contains_point(const PointConstraint &p) const { // distance between the point and the plane - const Real dist = (p.location - point) * normal; + const Real dist = (p.point() - _point) * _normal; return std::abs(dist) < TOLERANCE; } bool PlaneConstraint::contains_line(const LineConstraint &l) const { - const bool base_on_plane = this->contains_point(PointConstraint(l.r0)); - const bool dir_orthogonal = std::abs(normal * l.dir) < TOLERANCE; + const bool base_on_plane = this->contains_point(PointConstraint(l.point())); + const bool dir_orthogonal = std::abs(_normal * l.direction()) < TOLERANCE; return base_on_plane && dir_orthogonal; } @@ -227,23 +223,23 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { // [-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 + const Point dir = this->_normal.cross( + o.normal()); // direction of line of intersection libmesh_assert(dir.norm() > TOLERANCE); - const Point w = this->point - o.point; + 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 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) > TOLERANCE); const Real s = -(n1_dot_n2 * n2_dot_w - n2_dot_n2 * n1_dot_w) / denom; - const Point p0 = point + s * normal; + const Point p0 = _point + s * _normal; return LineConstraint{p0, dir}; } else if constexpr (std::is_same_v) { @@ -254,16 +250,16 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { "Line is parallel and does not intersect the plane."); // Solve for t in the parametric equation: - // p(t) = r0 + t·d + // 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 - r0)) / (n · d) + // t = (n · (p0 - point)) / (n · d) - const Real denom = normal * o.dir; + const Real denom = _normal * o.direction(); libmesh_assert(std::abs(denom) > TOLERANCE); - const Real t = (normal * (point - o.r0)) / denom; - return PointConstraint{o.r0 + t * o.dir}; + const Real t = (_normal * (_point - o.point())) / denom; + return PointConstraint{o.point() + t * o.direction()}; } else if constexpr (std::is_same_v) { libmesh_error_msg_if(!this->contains_point(o), "Point is not on the plane."); @@ -783,9 +779,11 @@ void VariationalSmootherConstraint::impose_constraint( if (std::holds_alternative(constraint)) fix_node(node); else if (std::holds_alternative(constraint)) - constrain_node_to_line(node, std::get(constraint).dir); + constrain_node_to_line(node, + std::get(constraint).direction()); else if (std::holds_alternative(constraint)) - constrain_node_to_plane(node, std::get(constraint).normal); + constrain_node_to_plane(node, + std::get(constraint).normal()); else libmesh_assert_msg(false, "Unknown constraint type."); } From cf0805972a64a74ae1da080d5659184df9dc024a Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Fri, 18 Jul 2025 15:58:22 -0600 Subject: [PATCH 30/38] Added _tol attibute to Point/Line/Plane constraints. --- .../systems/variational_smoother_constraint.h | 60 ++++++++++++++++--- src/systems/variational_smoother_constraint.C | 47 ++++++++------- 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 16ea2075f3..262c507cc9 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -50,14 +50,16 @@ class PointConstraint { /** * Constructor * @param point The point defining the constraint. + * @param tol The tolerance to use for numerical comparisons. */ - PointConstraint(const Point &point); + 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; @@ -86,12 +88,22 @@ class PointConstraint { */ 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; }; /** @@ -105,8 +117,10 @@ class LineConstraint { * 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); + LineConstraint(const Point &point, const Point &direction, + const Real &tol = TOLERANCE); /** * Comparison operator for ordering LineConstraint objects. @@ -168,12 +182,27 @@ class LineConstraint { */ 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 + /** + * A point on the constraining line + */ Point _point; - /// Direction of the constraining line + + /** + * Direction of the constraining line + */ Point _direction; + + /** + * Tolerance to use for numerical comparisons + */ + Real _tol; }; /** @@ -188,8 +217,10 @@ class PlaneConstraint { * 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); + PlaneConstraint(const Point &point, const Point &normal, + const Real &tol = TOLERANCE); /** * Comparison operator for ordering PlaneConstraint objects. @@ -258,12 +289,27 @@ class PlaneConstraint { */ 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 + /** + * A point on the constraining plane + */ Point _point; - /// The direction normal to the constraining plane + + /** + * The direction normal to the constraining plane + */ Point _normal; + + /** + * Tolerance to use for numerical comparisons + */ + Real _tol; }; /** diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 6c7b93d5de..509896ad18 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -45,7 +45,8 @@ auto get_positive_vector = [](const Point &vec) -> Point { return (dot_prod > 0) ? vec.unit() : -vec.unit(); }; -PointConstraint::PointConstraint(const Point &point) : _point(point) {} +PointConstraint::PointConstraint(const Point &point, const Real &tol) + : _point(point), _tol(tol) {} bool PointConstraint::operator<(const PointConstraint &other) const { if (*this == other) @@ -55,7 +56,7 @@ bool PointConstraint::operator<(const PointConstraint &other) const { } bool PointConstraint::operator==(const PointConstraint &other) const { - return _point.absolute_fuzzy_equals(other.point(), TOLERANCE); + return _point.absolute_fuzzy_equals(other.point(), _tol); } ConstraintVariant @@ -76,10 +77,11 @@ PointConstraint::intersect(const ConstraintVariant &other) const { other); } -LineConstraint::LineConstraint(const Point &point, const Point &direction) - : _point(point), _direction(get_positive_vector(direction)) { +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() < TOLERANCE, + _direction.norm() < _tol, "Can't define a line with zero magnitude direction vector."); } @@ -87,13 +89,13 @@ bool LineConstraint::operator<(const LineConstraint &other) const { if (*this == other) return false; - if (!(_direction.absolute_fuzzy_equals(other.direction(), TOLERANCE))) + 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(), TOLERANCE))) + if (!(_direction.absolute_fuzzy_equals(other.direction(), _tol))) return false; return this->contains_point(other.point()); } @@ -102,15 +104,15 @@ 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() < TOLERANCE; + return _direction.cross(p.point() - _point).norm() < _tol; } bool LineConstraint::is_parallel(const LineConstraint &l) const { - return _direction.absolute_fuzzy_equals(l.direction(), TOLERANCE); + return _direction.absolute_fuzzy_equals(l.direction(), _tol); } bool LineConstraint::is_parallel(const PlaneConstraint &p) const { - return _direction * p.normal() < TOLERANCE; + return _direction * p.normal() < _tol; } ConstraintVariant @@ -141,7 +143,7 @@ LineConstraint::intersect(const ConstraintVariant &other) const { // Verify that intersection lies on both lines libmesh_error_msg_if( - o.direction().cross(intersection - o.point()).norm() > TOLERANCE, + o.direction().cross(intersection - o.point()).norm() > _tol, "Lines do not intersect at a single point."); return PointConstraint{intersection}; @@ -157,10 +159,11 @@ LineConstraint::intersect(const ConstraintVariant &other) const { other); } -PlaneConstraint::PlaneConstraint(const Point &point, const Point &normal) - : _point(point), _normal(get_positive_vector(normal)) { +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() < TOLERANCE, + _normal.norm() < _tol, "Can't define a plane with zero magnitude direction vector."); } @@ -168,19 +171,19 @@ bool PlaneConstraint::operator<(const PlaneConstraint &other) const { if (*this == other) return false; - if (!(_normal.absolute_fuzzy_equals(other.normal(), TOLERANCE))) + 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(), TOLERANCE))) + 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(), TOLERANCE); + return _normal.absolute_fuzzy_equals(p.normal(), _tol); } bool PlaneConstraint::is_parallel(const LineConstraint &l) const { @@ -190,12 +193,12 @@ bool PlaneConstraint::is_parallel(const LineConstraint &l) const { 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) < TOLERANCE; + 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()) < TOLERANCE; + const bool dir_orthogonal = std::abs(_normal * l.direction()) < _tol; return base_on_plane && dir_orthogonal; } @@ -225,7 +228,7 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { const Point dir = this->_normal.cross( o.normal()); // direction of line of intersection - libmesh_assert(dir.norm() > TOLERANCE); + libmesh_assert(dir.norm() > _tol); const Point w = _point - o.point(); // Dot product terms used in 2x2 system @@ -236,7 +239,7 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { 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) > TOLERANCE); + 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; @@ -257,7 +260,7 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { // t = (n · (p0 - point)) / (n · d) const Real denom = _normal * o.direction(); - libmesh_assert(std::abs(denom) > TOLERANCE); + 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) { From e6a353fb7ab8d031a7093808169b2bd29a9590ee Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 23 Jul 2025 09:39:47 -0600 Subject: [PATCH 31/38] Silenced variational smoother solver. --- src/mesh/mesh_smoother_vsmoother.C | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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(); From bca5f982713d1e79836c79cff83325dc0d0d8310 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 23 Jul 2025 09:43:25 -0600 Subject: [PATCH 32/38] Added dummy InvalidConstraint class to eliminate dependent on thrown exceptions. --- .../systems/variational_smoother_constraint.h | 61 ++++++++++--- src/systems/variational_smoother_constraint.C | 88 +++++++++++-------- 2 files changed, 100 insertions(+), 49 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 262c507cc9..80f9e14247 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -31,13 +31,14 @@ namespace libMesh class PointConstraint; class LineConstraint; class PlaneConstraint; +class InvalidConstraint; /** * Type used to store a constraint that may be a PlaneConstraint, * LineConstraint, or PointConstraint */ -using ConstraintVariant = - std::variant; +using ConstraintVariant = std::variant; /** * Represents a fixed point constraint. @@ -71,15 +72,21 @@ class PointConstraint { */ 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. - * @throw libMesh::LogicError If the constraints are incompatible and cannot - * intersect. + * constraints. constraints. If no intersection exists, return an + * InvalidConstraint. */ ConstraintVariant intersect(const ConstraintVariant &other) const; @@ -166,9 +173,8 @@ class LineConstraint { * PointConstraint. * @param other The constraint to intersect with. * @return The most specific ConstraintVariant that satisfies both - * constraints. - * @throw libMesh::LogicError If the constraints are incompatible and cannot - * intersect. + * constraints. constraints. If no intersection exists, return an + * InvalidConstraint. */ ConstraintVariant intersect(const ConstraintVariant &other) const; @@ -273,9 +279,8 @@ class PlaneConstraint { * PointConstraint. * @param other The constraint to intersect with. * @return The most specific ConstraintVariant that satisfies both - * constraints. - * @throw libMesh::LogicError If the constraints are incompatible and cannot - * intersect. + * constraints. constraints. If no intersection exists, return an + * InvalidConstraint. */ ConstraintVariant intersect(const ConstraintVariant &other) const; @@ -312,6 +317,39 @@ class PlaneConstraint { Real _tol; }; +/** + * Represents an invalid constraint (i.e., when the two constraints don't + * intersect) + */ +class InvalidConstraint { + +public: + InvalidConstraint() = default; + + /** + * 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 = + std::string("We should never get here! The InvalidConstraint object ") + + std::string( + "should be detected and replaced with a valid ConstraintVariant ") + + std::string("prior to calling any class methods."); +}; + /** * Dispatch intersection between two constraint variants. * Resolves to the appropriate method based on the type of the first operand. @@ -437,6 +475,7 @@ class VariationalSmootherConstraint : public System::Constraint * 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); diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index 509896ad18..b134646092 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -63,16 +63,11 @@ ConstraintVariant PointConstraint::intersect(const ConstraintVariant &other) const { return std::visit( [&](auto &&o) -> ConstraintVariant { - if constexpr (std::is_same_v, - PointConstraint>) { - libmesh_error_msg_if(!(this->_point == o.point()), - "Points do not match."); - return *this; - } else { - libmesh_error_msg_if(!o.contains_point(*this), - "Point is not on the constraint."); - return *this; - } + if (!o.contains_point(*this)) + // Point is not on the constraint + return InvalidConstraint(); + + return *this; }, other); } @@ -123,8 +118,10 @@ LineConstraint::intersect(const ConstraintVariant &other) const { if constexpr (std::is_same_v) { if (*this == o) return *this; - libmesh_error_msg_if(this->is_parallel(o), - "Lines are parallel and do not intersect."); + + 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 @@ -142,17 +139,22 @@ LineConstraint::intersect(const ConstraintVariant &other) const { const Point intersection = _point + t * _direction; // Verify that intersection lies on both lines - libmesh_error_msg_if( - o.direction().cross(intersection - o.point()).norm() > _tol, - "Lines do not intersect at a single point."); + 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) { - libmesh_error_msg_if(!this->contains_point(o), - "Point is not on the line."); + 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."); }, @@ -211,8 +213,10 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { // If planes are identical, return one of them if (*this == o) return *this; - libmesh_error_msg_if(this->is_parallel(o), - "Planes are parallel and do not intersect."); + + 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: @@ -248,9 +252,10 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { } else if constexpr (std::is_same_v) { if (this->contains_line(o)) return o; - libmesh_error_msg_if( - this->is_parallel(o), - "Line is parallel and does not intersect the plane."); + + 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 @@ -264,8 +269,10 @@ PlaneConstraint::intersect(const ConstraintVariant &other) const { const Real t = (_normal * (_point - o.point())) / denom; return PointConstraint{o.point() + t * o.direction()}; } else if constexpr (std::is_same_v) { - libmesh_error_msg_if(!this->contains_point(o), - "Point is not on the plane."); + 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."); @@ -370,16 +377,15 @@ void VariationalSmootherConstraint::constrain() const auto &subdomain_constraint = it->second; // Combine current boundary constraint with previously determined // subdomain_constraint - libmesh_try { - const auto combined_constraint = - intersect_constraints(subdomain_constraint, boundary_constraint); - this->impose_constraint(node, combined_constraint); - } - libmesh_catch(const std::exception &e) { - // This will catch cases where constraints have no intersection - // Fall back to fixed node constraint - this->impose_constraint(node, PointConstraint(node)); - } + 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); } else this->impose_constraint(node, boundary_constraint); @@ -763,10 +769,11 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( ConstraintVariant current = *it++; for (; it != valid_planes.end(); ++it) { - libmesh_try { current = intersect_constraints(current, *it); } - libmesh_catch(const std::exception &e) { - // This will catch cases where constraints have no intersection - // Fall back to fixed node constraint + current = intersect_constraints(current, *it); + + // This will catch cases where constraints have no intersection + // Fall back to fixed node constraint + if (std::holds_alternative(current)) { current = PointConstraint(node); break; } @@ -779,6 +786,10 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( // PlaneConstraint) to a node. void VariationalSmootherConstraint::impose_constraint( const Node &node, const ConstraintVariant &constraint) { + + 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)) @@ -787,6 +798,7 @@ void VariationalSmootherConstraint::impose_constraint( else if (std::holds_alternative(constraint)) constrain_node_to_plane(node, std::get(constraint).normal()); + else libmesh_assert_msg(false, "Unknown constraint type."); } From 73e927c61199b2b3a7ad4ef8671d25b8ece32cfe Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 23 Jul 2025 09:56:54 -0600 Subject: [PATCH 33/38] Added explanatory comment. --- src/systems/variational_smoother_constraint.C | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index b134646092..a5a4b72a30 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -772,6 +772,7 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( 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); From 57c7c4eac333536406330b01ece6ca30e0c650b6 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 23 Jul 2025 15:28:57 -0600 Subject: [PATCH 34/38] clang-format on variational_smoother_constraint.C --- src/systems/variational_smoother_constraint.C | 954 +++++++++--------- 1 file changed, 493 insertions(+), 461 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index a5a4b72a30..a81e0e0ba0 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -24,20 +24,20 @@ namespace libMesh { // 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."); +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; - } + 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.)); @@ -45,24 +45,27 @@ auto get_positive_vector = [](const Point &vec) -> Point { return (dot_prod > 0) ? vec.unit() : -vec.unit(); }; -PointConstraint::PointConstraint(const Point &point, const Real &tol) - : _point(point), _tol(tol) {} +PointConstraint::PointConstraint(const Point & point, const Real & tol) : _point(point), _tol(tol) +{ +} -bool PointConstraint::operator<(const PointConstraint &other) const { +bool PointConstraint::operator<(const PointConstraint & other) const +{ if (*this == other) return false; return _point < other.point(); } -bool PointConstraint::operator==(const PointConstraint &other) const { +bool PointConstraint::operator==(const PointConstraint & other) const +{ return _point.absolute_fuzzy_equals(other.point(), _tol); } -ConstraintVariant -PointConstraint::intersect(const ConstraintVariant &other) const { +ConstraintVariant PointConstraint::intersect(const ConstraintVariant & other) const +{ return std::visit( - [&](auto &&o) -> ConstraintVariant { + [&](auto && o) -> ConstraintVariant { if (!o.contains_point(*this)) // Point is not on the constraint return InvalidConstraint(); @@ -72,15 +75,15 @@ PointConstraint::intersect(const ConstraintVariant &other) const { 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."); +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 { +bool LineConstraint::operator<(const LineConstraint & other) const +{ if (*this == other) return false; @@ -89,87 +92,94 @@ bool LineConstraint::operator<(const LineConstraint &other) const { return (_direction * _point) < (other.direction() * other.point()); } -bool LineConstraint::operator==(const LineConstraint &other) const { +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 { +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 { +bool LineConstraint::is_parallel(const LineConstraint & l) const +{ return _direction.absolute_fuzzy_equals(l.direction(), _tol); } -bool LineConstraint::is_parallel(const PlaneConstraint &p) const { +bool LineConstraint::is_parallel(const PlaneConstraint & p) const +{ return _direction * p.normal() < _tol; } -ConstraintVariant -LineConstraint::intersect(const ConstraintVariant &other) const { +ConstraintVariant LineConstraint::intersect(const ConstraintVariant & other) const +{ return std::visit( - [&](auto &&o) -> ConstraintVariant { + [&](auto && o) -> ConstraintVariant { 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; + 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(); - } else + 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."); +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 { +bool PlaneConstraint::operator<(const PlaneConstraint & other) const +{ if (*this == other) return false; @@ -178,120 +188,126 @@ bool PlaneConstraint::operator<(const PlaneConstraint &other) const { return (_normal * _point) < (other.normal() * other.point()); } -bool PlaneConstraint::operator==(const PlaneConstraint &other) const { +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 { +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::is_parallel(const LineConstraint & l) const { return l.is_parallel(*this); } -bool PlaneConstraint::contains_point(const PointConstraint &p) const { +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 { +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 { +ConstraintVariant PlaneConstraint::intersect(const ConstraintVariant & other) const +{ return std::visit( - [&](auto &&o) -> ConstraintVariant { + [&](auto && o) -> ConstraintVariant { 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 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(); - 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 + 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( + System & sys, const bool & preserve_subdomain_boundaries) + : Constraint(), _sys(sys), _preserve_subdomain_boundaries(preserve_subdomain_boundaries) +{ +} VariationalSmootherConstraint::~VariationalSmootherConstraint() = default; void VariationalSmootherConstraint::constrain() { - const 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 @@ -305,108 +321,108 @@ void VariationalSmootherConstraint::constrain() // 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)) + for (const auto * elem : mesh.active_element_ptr_range()) { - 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 - } - + 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); - - // 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 - const auto it = subdomain_boundary_map.find(bid); - if (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); + const auto & node = mesh.node_ref(bid); - // 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); + // 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); - this->impose_constraint(node, combined_constraint); + // Determine which constraint should be imposed + const auto boundary_constraint = + determine_constraint(node, dim, side_grouped_boundary_neighbors); - } else - this->impose_constraint(node, boundary_constraint); + // Check for the case where this boundary node is also part of a subdomain id boundary + const auto it = subdomain_boundary_map.find(bid); + if (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); + } + else + this->impose_constraint(node, boundary_constraint); - } // end bid + } // end bid } void VariationalSmootherConstraint::fix_node(const Node & node) { 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); - } + { + 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) +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 @@ -426,45 +442,46 @@ void VariationalSmootherConstraint::constrain_node_to_plane(const Node & node, c 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; + 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; + { + 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 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); + _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) +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); - }); + 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); @@ -480,16 +497,18 @@ void VariationalSmootherConstraint::constrain_node_to_line(const Node & node, co 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); - } + { + 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. @@ -519,11 +538,10 @@ void VariationalSmootherConstraint::constrain_node_to_line(const Node & node, co // 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 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; @@ -532,29 +550,32 @@ bool VariationalSmootherConstraint::nodes_share_boundary_id( 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; + // 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) { + 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 @@ -565,80 +586,80 @@ VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint( // 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) + for (const auto * neigh : neighbors) { - 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)) + // 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 (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 + 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; - }// for neigh_elem - } + // 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) { + 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 @@ -649,116 +670,127 @@ VariationalSmootherConstraint::get_neighbors_for_boundary_constraint( // 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) + 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; - 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)) + // 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; - - 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); + 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; + } } - continue; - } } - } return side_grouped_boundary_neighbors; } ConstraintVariant VariationalSmootherConstraint::determine_constraint( - const Node &node, const unsigned int dim, - const std::set> &side_grouped_boundary_neighbors) { + 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) + for (const auto & side : side_grouped_boundary_neighbors) neighbors.insert(neighbors.end(), side.begin(), side.end()); // 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(size_t(1), neighbors.size())) { - 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)) { - all_colinear = false; - break; - } - } - if (all_colinear) - return LineConstraint{node, ref_dir}; + 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(size_t(1), neighbors.size())) + { + 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)) + { + all_colinear = false; + break; + } + } + if (all_colinear) + return LineConstraint{node, ref_dir}; - return PointConstraint{node}; - } + 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)) { - 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 (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)) + { + 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); + } + } } - } // Fall back to point constraint if (valid_planes.empty()) @@ -768,25 +800,27 @@ ConstraintVariant VariationalSmootherConstraint::determine_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; + { + 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; } // Applies the computed constraint (PointConstraint, LineConstraint, or // PlaneConstraint) to a node. -void VariationalSmootherConstraint::impose_constraint( - const Node &node, const ConstraintVariant &constraint) { +void VariationalSmootherConstraint::impose_constraint(const Node & node, + const ConstraintVariant & constraint) +{ libmesh_assert_msg(!std::holds_alternative(constraint), "Cannot impose constraint using InvalidConstraint."); @@ -794,11 +828,9 @@ void VariationalSmootherConstraint::impose_constraint( if (std::holds_alternative(constraint)) fix_node(node); else if (std::holds_alternative(constraint)) - constrain_node_to_line(node, - std::get(constraint).direction()); + constrain_node_to_line(node, std::get(constraint).direction()); else if (std::holds_alternative(constraint)) - constrain_node_to_plane(node, - std::get(constraint).normal()); + constrain_node_to_plane(node, std::get(constraint).normal()); else libmesh_assert_msg(false, "Unknown constraint type."); From e2d026a2920ae08b199082b934409349190ae6ee Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Wed, 23 Jul 2025 15:39:27 -0600 Subject: [PATCH 35/38] clang-format on mesh_smoother_test.C --- tests/mesh/mesh_smoother_test.C | 479 +++++++++++++++++--------------- 1 file changed, 255 insertions(+), 224 deletions(-) diff --git a/tests/mesh/mesh_smoother_test.C b/tests/mesh/mesh_smoother_test.C index eb0a9a18d9..fd7bfb6809 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -16,22 +16,27 @@ #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 { +class DistortSquare : public FunctionBase +{ + std::unique_ptr> clone() const override + { return std::make_unique(); } - Real operator()(const Point &, const Real = 0.) override { + 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; @@ -43,60 +48,66 @@ class DistortSquare : public FunctionBase { // Distortion function supporting 1D, 2D, and 3D // Use for VariationalMeshSmoother -class DistortHyperCube : public FunctionBase { +class DistortHyperCube : public FunctionBase +{ public: DistortHyperCube(const unsigned int dim) : _dim(dim) {} private: - std::unique_ptr> clone() const override { + std::unique_ptr> clone() const override + { return std::make_unique(_dim); } - Real operator()(const Point &, const Real = 0.) override { - libmesh_not_implemented(); - } + Real operator()(const Point &, const Real = 0.) override { libmesh_not_implemented(); } - void operator()(const Point &p, const Real, - DenseVector &output) override { + 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; + 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; - } + 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 + for (unsigned int i = 0; i < _dim; ++i) { - 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 + 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 } - } - output(i) = p(i) + (std::pow(xi, 3) - xi) * modulation; - } else { - output(i) = p(i); // dimension on boundary remains unchanged } - } } const unsigned int _dim; @@ -104,19 +115,20 @@ private: 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); @@ -127,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.); @@ -159,21 +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(testVariationalEdge); CPPUNIT_TEST(testVariationalEdgeMultipleSubdomains); CPPUNIT_TEST(testVariationalQuad); - CPPUNIT_TEST( testVariationalQuadMultipleSubdomains ); + CPPUNIT_TEST(testVariationalQuadMultipleSubdomains); CPPUNIT_TEST(testVariationalTri); CPPUNIT_TEST(testVariationalTriMultipleSubdomains); CPPUNIT_TEST(testVariationalHex); CPPUNIT_TEST(testVariationalHexMultipleSubdomains); -# endif // LIBMESH_ENABLE_VSMOOTHER +#endif // LIBMESH_ENABLE_VSMOOTHER #endif CPPUNIT_TEST_SUITE_END(); @@ -183,56 +196,57 @@ public: void tearDown() {} - void testLaplaceSmoother(ReplicatedMesh &mesh, MeshSmoother &smoother, - ElemType type) { + void testLaplaceSmoother(ReplicatedMesh & mesh, MeshSmoother & smoother, ElemType type) + { LOG_UNIT_TEST; 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); // 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; - } + 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 (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))); + 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; - } + return true; + } - // If we're at the center we're fine - if (std::abs(r - 0.5) < TOLERANCE * TOLERANCE) - 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); - }; + 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)); - } + for (auto node : mesh.node_ptr_range()) + { + CPPUNIT_ASSERT(center_distortion_is(*node, 0, true)); + CPPUNIT_ASSERT(center_distortion_is(*node, 1, true)); + } // 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 @@ -241,15 +255,18 @@ public: smoother.smooth(); // 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)); - } + 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)); + } } - void testVariationalSmoother(ReplicatedMesh &mesh, MeshSmoother &smoother, + void testVariationalSmoother(ReplicatedMesh & mesh, + MeshSmoother & smoother, const ElemType type, - const bool multiple_subdomains = false) { + const bool multiple_subdomains = false) + { LOG_UNIT_TEST; const auto dim = ReferenceElem::get(type).dim(); @@ -260,27 +277,35 @@ public: // 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 (dim) { - case 1: - MeshTools::Generation::build_line(mesh, n_elems_per_side, 0., 1., type); - break; - case 2: - MeshTools::Generation::build_square( - mesh, n_elems_per_side, n_elems_per_side, 0., 1., 0., 1., type); - break; - - 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; - - default: - libmesh_error_msg("Unsupported dimension " << dim); - } + libmesh_error_msg_if(n_elems_per_side % 2 != 1, "n_elems_per_side should be odd."); + + switch (dim) + { + case 1: + MeshTools::Generation::build_line(mesh, n_elems_per_side, 0., 1., type); + break; + case 2: + MeshTools::Generation::build_square( + mesh, n_elems_per_side, n_elems_per_side, 0., 1., 0., 1., type); + break; + + 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; + + default: + libmesh_error_msg("Unsupported dimension " << dim); + } // Move it around so we have something that needs smoothing DistortHyperCube dh(dim); @@ -289,68 +314,70 @@ public: // 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; + { + // 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; + } - 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])); - } + // 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])); + } } // 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. - */ - - size_t num_zero_or_one = 0; - - bool distorted = false; - for (const auto d : make_range(dim)) { + 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. + */ + + 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); @@ -364,55 +391,56 @@ public: 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.)); + 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_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 (relative_fuzzy_equals( - Point(node), subdomain_boundary_node_id_to_point[node.id()])); - else - // node is not a subdomain boundary node, just return true - return true; - }; + CPPUNIT_ASSERT_GREATEREQUAL(dim - num_dofs, num_zero_or_one); - // Make sure our DistortSquare transformation has distorted the mesh - for (auto node : mesh.node_ptr_range()) - CPPUNIT_ASSERT(distortion_is(*node, true)); + // 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_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 (relative_fuzzy_equals(Point(node), subdomain_boundary_node_id_to_point[node.id()])); + else + // node is not a subdomain boundary node, just return true + return 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) { + // 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) + { SquareToParallelogram stp; MeshTools::Modification::redistribute(mesh, stp); } - 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) { + // 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) @@ -430,7 +458,6 @@ public: testLaplaceSmoother(mesh, laplace, QUAD4); } - void testLaplaceTri() { ReplicatedMesh mesh(*TestCommWorld); @@ -439,16 +466,17 @@ public: testLaplaceSmoother(mesh, laplace, TRI3); } - #ifdef LIBMESH_ENABLE_VSMOOTHER - void testVariationalEdge() { + void testVariationalEdge() + { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); testVariationalSmoother(mesh, variational, EDGE2); } - void testVariationalEdgeMultipleSubdomains() { + void testVariationalEdgeMultipleSubdomains() + { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); @@ -463,7 +491,8 @@ public: testVariationalSmoother(mesh, variational, QUAD4); } - void testVariationalQuadMultipleSubdomains() { + void testVariationalQuadMultipleSubdomains() + { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); @@ -478,21 +507,24 @@ public: testVariationalSmoother(mesh, variational, TRI3); } - void testVariationalTriMultipleSubdomains() { + void testVariationalTriMultipleSubdomains() + { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); testVariationalSmoother(mesh, variational, TRI3, true); } - void testVariationalHex() { + void testVariationalHex() + { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); testVariationalSmoother(mesh, variational, HEX8); } - void testVariationalHexMultipleSubdomains() { + void testVariationalHexMultipleSubdomains() + { ReplicatedMesh mesh(*TestCommWorld); VariationalMeshSmoother variational(mesh); @@ -501,5 +533,4 @@ public: #endif // LIBMESH_ENABLE_VSMOOTHER }; - -CPPUNIT_TEST_SUITE_REGISTRATION( MeshSmootherTest ); +CPPUNIT_TEST_SUITE_REGISTRATION(MeshSmootherTest); From ea104cedc36e071fa1b06a91b7af1a1222e8dd29 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 24 Jul 2025 10:22:33 -0600 Subject: [PATCH 36/38] Addressed John's review. --- .../systems/variational_smoother_constraint.h | 35 ++- src/systems/variational_smoother_constraint.C | 265 +++++++++--------- src/systems/variational_smoother_system.C | 30 +- tests/mesh/mesh_smoother_test.C | 2 +- 4 files changed, 178 insertions(+), 154 deletions(-) diff --git a/include/systems/variational_smoother_constraint.h b/include/systems/variational_smoother_constraint.h index 80f9e14247..2bcfc02afd 100644 --- a/include/systems/variational_smoother_constraint.h +++ b/include/systems/variational_smoother_constraint.h @@ -28,6 +28,7 @@ namespace libMesh { +// Forward declarations class PointConstraint; class LineConstraint; class PlaneConstraint; @@ -35,7 +36,9 @@ class InvalidConstraint; /** * Type used to store a constraint that may be a PlaneConstraint, - * LineConstraint, or PointConstraint + * 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; @@ -43,7 +46,8 @@ using ConstraintVariant = std::variant ConstraintVariant { return lhs.intersect(rhs); diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index a81e0e0ba0..f168b8e7b8 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -64,6 +64,7 @@ bool PointConstraint::operator==(const PointConstraint & other) const 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)) @@ -119,52 +120,55 @@ bool LineConstraint::is_parallel(const PlaneConstraint & p) const 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}; - } + { + 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); - } + { + return o.intersect(*this); + } else if constexpr (std::is_same_v) - { - if (!this->contains_point(o)) - // Point is not on the line - return InvalidConstraint(); + { + if (!this->contains_point(o)) + // Point is not on the line + return InvalidConstraint(); - return o; - } + return o; + } else libmesh_error_msg("Unsupported constraint type in Line::intersect."); }, @@ -218,79 +222,82 @@ bool PlaneConstraint::contains_line(const LineConstraint & l) const 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}; - } + { + // 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()}; - } + { + 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(); + { + if (!this->contains_point(o)) + // Point is not on the plane + return InvalidConstraint(); - return o; - } + return o; + } else libmesh_error_msg("Unsupported constraint type in Plane::intersect."); }, @@ -385,22 +392,20 @@ void VariationalSmootherConstraint::constrain() determine_constraint(node, dim, side_grouped_boundary_neighbors); // Check for the case where this boundary node is also part of a subdomain id boundary - const auto it = subdomain_boundary_map.find(bid); - if (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); - } + 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); + } else this->impose_constraint(node, boundary_constraint); @@ -754,17 +759,17 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( // 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(size_t(1), neighbors.size())) + for (const auto i : make_range(std::size_t(1), neighbors.size())) + { + 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 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)) - { - all_colinear = false; - break; - } + all_colinear = false; + break; } + } if (all_colinear) return LineConstraint{node, ref_dir}; diff --git a/src/systems/variational_smoother_system.C b/src/systems/variational_smoother_system.C index 313753bdb1..4b774d8a83 100644 --- a/src/systems/variational_smoother_system.C +++ b/src/systems/variational_smoother_system.C @@ -93,7 +93,8 @@ void VariationalSmootherSystem::init_data () this->prepare_for_smoothing(); } -void VariationalSmootherSystem::prepare_for_smoothing() { +void VariationalSmootherSystem::prepare_for_smoothing() +{ std::unique_ptr con = this->build_context(); FEMContext & femcontext = cast_ref(*con); this->init_context(femcontext); @@ -117,18 +118,18 @@ void VariationalSmootherSystem::prepare_for_smoothing() { femcontext.elem_fe_reinit(); // Add target element info, if applicable - if (_target_inverse_jacobians.find(elem->type()) == - _target_inverse_jacobians.end()) { + 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 & 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 & 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 @@ -137,8 +138,10 @@ void VariationalSmootherSystem::prepare_for_smoothing() { target_elem_inverse_jacobian_dets[elem->type()] = std::vector(nq_points, 1.0); - switch (elem->type()) { - case TRI3: { + switch (elem->type()) + { + case TRI3: + { // Build target element: an equilateral triangle Tri3 target_elem; @@ -166,7 +169,8 @@ void VariationalSmootherSystem::prepare_for_smoothing() { // will keep things general for now _target_inverse_jacobians[target_elem.type()] = std::vector(nq_points); - for (const auto qp : make_range(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) @@ -608,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 @@ -625,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 fd7bfb6809..6b6df42818 100644 --- a/tests/mesh/mesh_smoother_test.C +++ b/tests/mesh/mesh_smoother_test.C @@ -373,7 +373,7 @@ public: * dim - num_dofs coordinantes should be 0 or 1. */ - size_t num_zero_or_one = 0; + std::size_t num_zero_or_one = 0; bool distorted = false; for (const auto d : make_range(dim)) From 505bb55397cf398cd11e3ec60c7fe3ae24ae5c5a Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 24 Jul 2025 10:23:21 -0600 Subject: [PATCH 37/38] indented contents of switch statement. --- src/systems/variational_smoother_system.C | 86 +++++++++++------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/systems/variational_smoother_system.C b/src/systems/variational_smoother_system.C index 4b774d8a83..ab3c7cec48 100644 --- a/src/systems/variational_smoother_system.C +++ b/src/systems/variational_smoother_system.C @@ -140,52 +140,52 @@ void VariationalSmootherSystem::prepare_for_smoothing() 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)) + case TRI3: { - 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(); - } + // 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(); - break; - } + _target_inverse_jacobians[target_elem.type()][qp] = H_inv; + target_elem_inverse_jacobian_dets[target_elem.type()][qp] = + H_inv.det(); + } - default: - break; + break; + } + + default: + break; } } From f8c37d4283c928ecde65e2768595827312201e55 Mon Sep 17 00:00:00 2001 From: Patrick Behne Date: Thu, 24 Jul 2025 15:33:43 -0600 Subject: [PATCH 38/38] reindeneted some unindents. --- src/systems/variational_smoother_constraint.C | 250 +++++++++--------- src/systems/variational_smoother_system.C | 88 +++--- 2 files changed, 172 insertions(+), 166 deletions(-) diff --git a/src/systems/variational_smoother_constraint.C b/src/systems/variational_smoother_constraint.C index f168b8e7b8..781b2a786b 100644 --- a/src/systems/variational_smoother_constraint.C +++ b/src/systems/variational_smoother_constraint.C @@ -127,48 +127,49 @@ ConstraintVariant LineConstraint::intersect(const ConstraintVariant & other) con // 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}; - } + { + 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(); + { + if (!this->contains_point(o)) + // Point is not on the line + return InvalidConstraint(); + + return o; + } - return o; - } else libmesh_error_msg("Unsupported constraint type in Line::intersect."); }, @@ -229,75 +230,78 @@ ConstraintVariant PlaneConstraint::intersect(const ConstraintVariant & other) co // 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}; - } + { + // 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->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()}; + } - 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(); + { + if (!this->contains_point(o)) + // Point is not on the plane + return InvalidConstraint(); + + return o; + } - return o; - } else libmesh_error_msg("Unsupported constraint type in Plane::intersect."); }, @@ -393,19 +397,20 @@ void VariationalSmootherConstraint::constrain() // 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); + { + 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 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); + } - this->impose_constraint(node, combined_constraint); - } else this->impose_constraint(node, boundary_constraint); @@ -760,16 +765,17 @@ ConstraintVariant VariationalSmootherConstraint::determine_constraint( bool all_colinear = true; const Point ref_dir = (*neighbors[0] - node).unit(); for (const auto i : make_range(std::size_t(1), neighbors.size())) - { - 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)) { - all_colinear = false; - break; + 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)) + { + all_colinear = false; + break; + } } - } + if (all_colinear) return LineConstraint{node, ref_dir}; diff --git a/src/systems/variational_smoother_system.C b/src/systems/variational_smoother_system.C index ab3c7cec48..700ac091e4 100644 --- a/src/systems/variational_smoother_system.C +++ b/src/systems/variational_smoother_system.C @@ -139,54 +139,54 @@ void VariationalSmootherSystem::prepare_for_smoothing() 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(); + 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(); - } + _target_inverse_jacobians[target_elem.type()][qp] = H_inv; + target_elem_inverse_jacobian_dets[target_elem.type()][qp] = + H_inv.det(); + } - break; - } + break; + } - default: - break; - } + default: + break; + } } Real elem_integrated_det_J(0.); 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