idaholab/moose: framework coverage diff

Base 846a5c Head #33390 250e9c
Total Total +/- New
Rate 86.00% 85.96% -0.04% 100.00%
Hits 138820 138852 +32 168
Misses 22598 22675 +77 0
Filename Stmts Miss Cover
framework/include/base/Assembly.h +20 0 +0.61%
framework/include/constraints/AutomaticMortarGeneration.h +12 0 +100.00%
framework/src/constraints/AutomaticMortarGeneration.C +63 +78 -5.27%
framework/src/constraints/MortarConstraintBase.C +2 0 +0.21%
framework/src/relationshipmanagers/AugmentSparsityOnInterface.C +12 -1 +1.37%
TOTAL +109 +77 -0.04%
code
coverage unchanged
code
coverage increased
code
coverage decreased
+
line added or modified

framework/include/base/Assembly.h

3132  
3133  
3134  
3135 +
3136 +
3137  
3138  
3139 +
3140  
3141  
3142 +
3143 +
3144  
3145 +
3146 +
3147  
3148  
3149  
3150  
3151 +
3152 +
3153  
3154  
3155  
3156  
3157 +
3158 +
3159 +
3160  
3161 +
3162 +
3163 +
3164  
3165 +
3166 +
3167  
3168 +
3169 +
3170 +
3171  
3172  
3173  
    return;
  }

  const auto & first_dofs = residuals[0].derivatives().nude_indices();
  bool supports_match = true;
  for (const auto i : make_range(decltype(residuals.size())(1), residuals.size()))
  {
    const auto & current_dofs = residuals[i].derivatives().nude_indices();
    // MetaPhysicL stores sparse derivative indices in sorted order, so equal index arrays identify
    // rows with the same derivative support regardless of insertion order.
    if (current_dofs.size() != first_dofs.size() ||
        !std::equal(first_dofs.begin(), first_dofs.end(), current_dofs.begin()))
    {
      supports_match = false;
      break;
    }
  }

  // Keep the common-layout path whenever every row has the same derivative support.
  if (supports_match)
    _column_indices.assign(first_dofs.begin(), first_dofs.end());
  else
  {
    // Constraining the local matrix requires one column layout shared by every residual row.
    // Gather the union of the sparse AD supports, then leave entries absent from a row as zero.
    std::size_t combined_support_size = 0;
    for (const auto i : index_range(residuals))
      combined_support_size += residuals[i].derivatives().nude_indices().size();

    _column_indices.clear();
    _column_indices.reserve(combined_support_size);
    for (const auto i : index_range(residuals))
    {
      const auto & current_dofs = residuals[i].derivatives().nude_indices();
      _column_indices.insert(_column_indices.end(), current_dofs.begin(), current_dofs.end());
    }
    std::sort(_column_indices.begin(), _column_indices.end());
    _column_indices.erase(std::unique(_column_indices.begin(), _column_indices.end()),
                          _column_indices.end());
  }

  // If there's no derivatives then there is nothing to do. Moreover, if we pass zero size column
3194  
3195  
3196  
3197 +
3198 +
3199  
3200  
3201  
      // Constraints may turn entries that were absent from an original AD row into nonzero
      // contributions. Cache those entries while avoiding structural zeros introduced by the
      // union above.
      if (supports_match || _element_matrix(i, j) != 0.0)
        cacheJacobian(_row_indices[i], _column_indices[j], _element_matrix(i, j), {}, matrix_tags);
}

template <typename Residuals, typename Indices>

framework/include/constraints/AutomaticMortarGeneration.h

70  
71  
72  
73 +
74  
75  
76  
77  
78 +
79  
80  
81  
82 +
83 +
84  
85 +
86 +
87 +
88 +
89 +
90 +
91 +
92 +
93  
94  
95  
 */
template <typename Vector>
std::array<Vector, 2>
householderTangents(const Vector & normal)
{
  mooseAssert(MooseUtils::absoluteFuzzyEqual(MetaPhysicL::raw_value(normal.norm()), 1),
              "The input nodal normal should have unity norm");

  const Vector h_vector(normal(0) + 1.0, normal(1), normal(2));

  // This chart is singular at (-1, 0, 0), where a constant orthonormal basis with zero
  // derivatives is used.
  if (std::abs(MetaPhysicL::raw_value(h_vector(0))) < TOLERANCE)
    return {{Vector(0, 1, 0), Vector(0, 0, -1)}};

  const auto h = h_vector.norm();
  return {{Vector(-2.0 * h_vector(0) * h_vector(1) / (h * h),
                  1.0 - 2.0 * h_vector(1) * h_vector(1) / (h * h),
                  -2.0 * h_vector(1) * h_vector(2) / (h * h)),
           Vector(-2.0 * h_vector(0) * h_vector(2) / (h * h),
                  -2.0 * h_vector(1) * h_vector(2) / (h * h),
                  1.0 - 2.0 * h_vector(2) * h_vector(2) / (h * h))}};
}
}
}

framework/src/constraints/AutomaticMortarGeneration.C

477  
478  
479  
480 +
481  
482  
483  
  _mortar_interface_coupling.clear();
  _secondary_node_to_nodal_normal.clear();
  _secondary_node_to_hh_nodal_tangents.clear();
  _nodal_geometry_coordinate_snapshot.clear();
  _secondary_element_to_secondary_lowerd_element.clear();
  _secondary_elems_to_mortar_segments.clear();
  _secondary_ip_sub_ids.clear();
1731  
1732  
1733  
1734 +
1735 +
1736 +
1737 +
1738 +
1739  
1740  
1741 +
1742 +
1743 +
1744  
1745  
1746 +
1747 +
1748 +
1749  
1750  
1751 +
1752 +
1753  
1754  
1755  
    // Rows attached to a secondary node or either volume parent may need the complete secondary
    // face-star coupling, even when this lower-dimensional face is owned by another processor.
    std::set<processor_id_type> secondary_face_coupling_processors{
        secondary_elem->processor_id(),
        secondary_elem->interior_parent()->processor_id(),
        primary_elem->interior_parent()->processor_id()};
    for (const auto n : secondary_elem->node_index_range())
      secondary_face_coupling_processors.insert(secondary_elem->node_ptr(n)->processor_id());

    // LowerSecondary
    for (const auto processor : secondary_face_coupling_processors)
      coupling_info[processor].emplace_back(secondary_elem->id(),
                                            secondary_elem->interior_parent()->id());

    // LowerPrimary
    for (const auto processor : secondary_face_coupling_processors)
      coupling_info[processor].emplace_back(secondary_elem->id(),
                                            primary_elem->interior_parent()->id());

    // Lower-LowerDimensionalPrimary
    for (const auto processor : secondary_face_coupling_processors)
      coupling_info[processor].emplace_back(secondary_elem->id(), primary_elem->id());

    // SecondaryLower
    coupling_info[secondary_elem->interior_parent()->processor_id()].emplace_back(
1766  
1767  
1768  
1769 +
1770  
1771  
1772 +
1773  
1774 +
1775 +
1776  
1777  
1778  
    // PrimarySecondary
    coupling_info[primary_elem->interior_parent()->processor_id()].emplace_back(
        primary_elem->interior_parent()->id(), secondary_elem->interior_parent()->id());
  }

  // A face pair may produce several mortar segments. Remove repeated entries before communication.
  for (auto & [_, couplings] : coupling_info)
  {
    std::sort(couplings.begin(), couplings.end());
    couplings.erase(std::unique(couplings.begin(), couplings.end()), couplings.end());
  }

  // Push the coupling information
2156  
2157  
2158  
2159 +
2160  
2161  
2162  
  /// The _periodic flag tells us whether we want to inward vs outward facing normals
  Real sign = _periodic ? -1 : 1;

  _nodal_geometry_coordinate_snapshot.clear();

  // First loop over lower-dimensional secondary side elements and compute/save the outward normal
  // for each one. We loop over all active elements currently, but this procedure could be
2200  
2201  
2202  
2203 +
2204 +
2205 +
2206  
2207  
2208  

    // Save the coordinates used for the Real nodal geometry. The AD path later adds displacement
    // derivatives to these same values so both paths use the same geometry state.
    auto parent_side_elem = interior_parent->build_side_ptr(s);
    for (const auto & node : parent_side_elem->node_ref_range())
      _nodal_geometry_coordinate_snapshot.emplace(&node, node);

    // Reinit the face FE object on side s.
    nnx_fe_face->reinit(interior_parent, s);
2237  
2238  
2239  
2240 +
2241 +
2242  
2243  
2244  
2245  
2246 +
2247  
2248  
2249  
2250 +
2251  
2252  
2253 +
2254 +
2255 +
2256  
2257 +
2258 +
2259  
2260  
2261 +
2262 +
2263 +
2264 +
2265  
2266 +
2267  
2268  
2269  
2270  
2271  
2272 +
2273 +
2274 +
2275 +
2276  
2277  
2278 +
2279 +
2280 +
2281 +
2282  
2283 +
2284 +
2285 +
2286 +
2287  
2288 +
2289  
2290  
2291 +
2292  
2293  
2294  
    nodal_normal = nodal_normal.unit();

    _secondary_node_to_nodal_normal[_mesh.node_ptr(node_id)] = nodal_normal;
    _secondary_node_to_hh_nodal_tangents[_mesh.node_ptr(node_id)] =
        Moose::Mortar::householderTangents(nodal_normal);
  }
}

void
AutomaticMortarGeneration::computeADNodalNormals(
    const std::function<ADPoint(const Node &, const Point &)> & coordinate,
    std::unordered_map<const Node *, ADRealVectorValue> & nodal_normals) const
{
  const auto dim = _mesh.mesh_dimension();
  mooseAssert(dim == 2 || dim == 3, "AD nodal normals are only valid for 2D or 3D mortar.");

  QNodal qface(dim - 1);
  const auto & qweights = qface.get_weights();
  const Real sign = _periodic ? -1 : 1;

  std::unordered_map<const Node *, ADRealVectorValue> weighted_area_vectors;
  std::unordered_map<const Node *, Real> weighted_area_magnitudes;

  // AD propagates coordinate derivatives through the parent-side area vectors and normalization.
  for (const auto secondary_subdomain_id : _secondary_boundary_subdomain_ids)
    for (const Elem * const secondary_elem :
         as_range(_mesh.active_subdomain_elements_begin(secondary_subdomain_id),
                  _mesh.active_subdomain_elements_end(secondary_subdomain_id)))
    {
      const Elem * const interior_parent = secondary_elem->interior_parent();
      mooseAssert(interior_parent,
                  "No interior parent exists for element "
                      << secondary_elem->id()
                      << ". There may be a problem with your sideset set-up.");

      const Real element_size = secondary_elem->hmax();
      if (element_size <= 0)
        mooseError("Cannot compute an AD nodal normal for secondary mortar element ",
                   secondary_elem->id(),
                   " because it has a non-positive characteristic length.");

      const auto side = interior_parent->which_side_am_i(secondary_elem);
      FEType fe_type(secondary_elem->default_order(), LAGRANGE);
      std::unique_ptr<FEBase> fe_face(FEBase::build(dim, fe_type));
      fe_face->attach_quadrature_rule(&qface);

      const auto & face_normals = fe_face->get_normals();
      const auto & face_points = fe_face->get_xyz();
      const auto & map_dpsidxi = fe_face->get_fe_map().get_dpsidxi();
      const auto * const map_dpsideta = dim == 3 ? &fe_face->get_fe_map().get_dpsideta() : nullptr;

      fe_face->reinit(interior_parent, side);

      const auto qpoint_to_secondary_node =
          nodalQuadraturePointToSecondaryNodeMap(*secondary_elem, face_points);

      mooseAssert(face_normals.size() == face_points.size() &&
                      qweights.size() == face_points.size(),
2296  
2297  
2298  
2299 +
2300  
2301  
2302 +
2303 +
2304 +
2305 +
2306 +
2307 +
2308  
2309 +
2310  
2311  
2312 +
2313 +
2314 +
2315  
2316 +
2317 +
2318 +
2319  
2320  
2321 +
2322  
2323 +
2324 +
2325  
2326 +
2327 +
2328 +
2329  
2330 +
2331 +
2332 +
2333 +
2334  
2335  
2336 +
2337 +
2338 +
2339 +
2340 +
2341 +
2342 +
2343 +
2344  
2345  
2346  
2347  
2348  
2349  
2350 +
2351  
2352 +
2353 +
2354 +
2355 +
2356  
2357 +
2358 +
2359 +
2360  
2361 +
2362 +
2363 +
2364 +
2365 +
2366  
2367  
2368 +
2369 +
2370  
2371  
2372  
2373  
2374 +
2375 +
2376 +
2377 +
2378  
2379  
2380  

      // Use the parent-side map rather than a standalone lower-dimensional basis. This preserves
      // enriched mappings such as the TET14-to-TRI7 face.
      auto parent_side_elem = interior_parent->build_side_ptr(side);
      const unsigned int n_mapping_shape_functions =
          dim == 2
              ? FE<2, LAGRANGE>::n_dofs(parent_side_elem.get(), parent_side_elem->default_order())
              : FE<3, LAGRANGE>::n_dofs(parent_side_elem.get(), parent_side_elem->default_order());
      if (map_dpsidxi.size() != n_mapping_shape_functions ||
          (map_dpsideta && map_dpsideta->size() != n_mapping_shape_functions))
        mooseError("The parent-side geometric map for secondary mortar element ",
                   secondary_elem->id(),
                   " of type ",
                   libMesh::Utility::enum_to_string<ElemType>(secondary_elem->type()),
                   " does not have one mapping shape function per element node.");

      std::vector<ADPoint> side_points;
      side_points.reserve(n_mapping_shape_functions);
      for (const auto side_node_index : make_range(n_mapping_shape_functions))
      {
        const Node & node = parent_side_elem->node_ref(side_node_index);
        side_points.push_back(
            coordinate(node, libmesh_map_find(_nodal_geometry_coordinate_snapshot, &node)));
      }

      for (const auto qp : make_range(face_points.size()))
      {
        const auto secondary_node_index = qpoint_to_secondary_node[qp];
        const Node * const secondary_node = secondary_elem->node_ptr(secondary_node_index);

        ADRealVectorValue tangent_xi;
        ADRealVectorValue tangent_eta;
        for (const auto side_node_index : make_range(n_mapping_shape_functions))
        {
          tangent_xi.add_scaled(side_points[side_node_index], map_dpsidxi[side_node_index][qp]);
          if (dim == 3)
            tangent_eta.add_scaled(side_points[side_node_index],
                                   (*map_dpsideta)[side_node_index][qp]);
        }

        auto area_vector = dim == 2 ? ADRealVectorValue(tangent_xi(1), -tangent_xi(0), 0)
                                    : tangent_xi.cross(tangent_eta);
        const Real area = MetaPhysicL::raw_value(area_vector.norm());
        const Real degeneracy_tolerance =
            dim == 2 ? TOLERANCE * element_size : TOLERANCE * element_size * element_size;
        if (area <= degeneracy_tolerance)
          mooseError("Cannot compute an AD nodal normal on degenerate secondary mortar element ",
                     secondary_elem->id(),
                     ".");

        // qweight * area_vector is equivalent to JxW * face_normal. Fix its orientation to the
        // Real-valued face normal because the incident-face set and orientation are not
        // differentiated.
        const Real orientation =
            MetaPhysicL::raw_value(area_vector) * face_normals[qp] < 0 ? -1 : 1;

        weighted_area_vectors[secondary_node] += sign * orientation * qweights[qp] * area_vector;
        weighted_area_magnitudes[secondary_node] += std::abs(qweights[qp]) * area;
      }
    }

  nodal_normals.clear();
  nodal_normals.reserve(weighted_area_vectors.size());
  for (auto & [secondary_node, weighted_area_vector] : weighted_area_vectors)
  {
    const Real weighted_area_vector_norm = MetaPhysicL::raw_value(weighted_area_vector.norm());
    const Real weighted_area_magnitude = libmesh_map_find(weighted_area_magnitudes, secondary_node);
    if (weighted_area_vector_norm <= TOLERANCE * weighted_area_magnitude)
      mooseError("Cannot compute a normalized AD nodal normal for secondary node ",
                 secondary_node->id(),
                 " because its incident face contributions cancel to a near-zero weighted normal.");

    auto nodal_normal = weighted_area_vector.unit();
    const auto & stored_normal = libmesh_map_find(_secondary_node_to_nodal_normal, secondary_node);
    mooseAssert((MetaPhysicL::raw_value(nodal_normal) - stored_normal).norm() < 100 * TOLERANCE,
                "The stored and AD secondary nodal normals must use the same geometry state.");
    // Set the raw value from the stored contact direction while retaining the derivatives produced
    // by the equivalent AD area-vector calculation.
    for (const auto component : make_range(3u))
      nodal_normal(component).value() = stored_normal(component);
    nodal_normals.emplace(secondary_node, std::move(nodal_normal));
  }
}

// Project secondary nodes onto their corresponding primary elements for each primary/secondary

framework/src/constraints/MortarConstraintBase.C

37  
38  
39  
40 +
41 +
42  
43  
44  
                                      obj_params.get<SubdomainName>("secondary_subdomain");
                                  rm_params.set<SubdomainName>("primary_subdomain") =
                                      obj_params.get<SubdomainName>("primary_subdomain");
                                  rm_params.set<bool>("ghost_point_neighbors") =
                                      obj_params.get<bool>("ghost_point_neighbors");
                                  rm_params.set<bool>("ghost_higher_d_neighbors") =
                                      obj_params.get<bool>("ghost_higher_d_neighbors");
                                });

framework/src/relationshipmanagers/AugmentSparsityOnInterface.C

112  
113  
114  
115 +
116 +
117  
118  
119  
  for (const auto coupled_elem_id : coupled_set)
  {
    // Coupling data can precede a mesh rebuild, so IDs absent from the current mesh are ignored.
    const Elem * const coupled_elem = _mesh->query_elem_ptr(coupled_elem_id);
    if (coupled_elem && coupled_elem->processor_id() != p)
      coupled_elements.emplace(coupled_elem, _null_mat);
  }
}
129  
130  
131  
132 +
133 +
134 +
135  
136  
137  
138 +
139 +
140 +
141 +
142  
143 +
144 +
145 +
146 +
147 +
148 +
149  
150  
151 +
152  
153 +
154  
155  
156  
157 +
158 +
159 +
160  
161 +
162 +
163  
164  
165  
166 +
167 +
168  
169  
170  
171 +
172 +
173  
174  
175  
176  
177  
178  
179  
180 +
181  
182  
183 +
184 +
185  
186  
187  
188 +
189 +
190  
191  
192  
  // Node ownership can follow a higher-dimensional parent, so the query may be a secondary face,
  // its interior parent, or a paired primary parent. Start from every associated secondary face so
  // the ghosting graph contains the complete remote nodal-normal support in either row direction.
  std::vector<const Elem *> secondary_lower_elem_candidates;
  if (query_elem->subdomain_id() == secondary_subdomain_id)
    secondary_lower_elem_candidates.push_back(query_elem);

  // Coupling functors can run before mortar data is rebuilt after mesh refinement. Use the
  // relationship manager's current mesh for face topology instead of AMG-owned element pointers.
  for (const auto side : query_elem->side_index_range())
    if (const Elem * const lower_elem = _moose_mesh->getLowerDElem(query_elem, side);
        lower_elem && lower_elem->subdomain_id() == secondary_subdomain_id)
      secondary_lower_elem_candidates.push_back(lower_elem);

  const auto & mic = amg.mortarInterfaceCoupling();
  if (const auto find_it = mic.find(query_elem->id()); find_it != mic.end())
    for (const auto coupled_elem_id : find_it->second)
      if (const Elem * const coupled_elem = _mesh->query_elem_ptr(coupled_elem_id);
          coupled_elem && coupled_elem->subdomain_id() == secondary_subdomain_id)
        secondary_lower_elem_candidates.push_back(coupled_elem);

  std::set<dof_id_type> secondary_lower_elems_handled;
  for (const Elem * const candidate : secondary_lower_elem_candidates)
  {
    if (candidate->subactive())
      continue;

    // Resolve inactive ancestors to the active faces used by the current coupling graph.
    std::vector<const Elem *> active_secondary_lower_elems;
    candidate->active_family_tree(active_secondary_lower_elems);
    for (const Elem * const secondary_lower_elem : active_secondary_lower_elems)
    {
      if (secondary_lower_elem->subdomain_id() != secondary_subdomain_id ||
          !secondary_lower_elems_handled.insert(secondary_lower_elem->id()).second)
        continue;

      std::set<const Elem *> secondary_lower_elem_point_neighbors;
      secondary_lower_elem->find_point_neighbors(secondary_lower_elem_point_neighbors);
      secondary_lower_elem_point_neighbors.insert(secondary_lower_elem);

      for (const Elem * const neigh : secondary_lower_elem_point_neighbors)
      {
        if (!neigh->active() || neigh->subdomain_id() != secondary_subdomain_id)
          continue;

        // Ghosting functors return only elements not owned by the requesting processor.
        if (neigh->processor_id() != p)
          coupled_elements.emplace(neigh, _null_mat);

        // Every point-neighbor face contributes to the smoothed nodal normal, even when it has no
        // mortar segment. Its interior parent therefore contributes displacement derivatives.
        const Elem * const interior_parent = neigh->interior_parent();
        mooseAssert(interior_parent,
                    "A lower-dimensional secondary mortar element must have an interior parent.");
        if (interior_parent->processor_id() != p)
          coupled_elements.emplace(interior_parent, _null_mat);

        ghostMortarInterfaceCouplings(p, neigh, coupled_elements, amg);
      }
    }
  }
}

void