From b4ae2fd221ad9c3ea05ff6b68d1435b8945c9785 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Wed, 28 Jan 2026 09:44:58 -0600 Subject: [PATCH 01/18] check_non_remote optional in find_neighbors We're going to need an extra find_neighbors() call after mesh redistribution to handle a corner case that unpacking neighbor information doesn't, so let's make that call as cheap as possible. --- include/mesh/mesh_base.h | 9 ++++++++- include/mesh/unstructured_mesh.h | 3 ++- src/mesh/unstructured_mesh.C | 8 ++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/include/mesh/mesh_base.h b/include/mesh/mesh_base.h index cf454353b2f..098bc2cf959 100644 --- a/include/mesh/mesh_base.h +++ b/include/mesh/mesh_base.h @@ -986,10 +986,17 @@ class MeshBase : public ParallelObject * * If \p assert_valid is left as true, then in dbg mode extensive * consistency checking is performed before returning. + * + * If \p check_non_remote is set to false, then only sides which + * currently have remote neighbors are checked for possible local + * neighbors. This is intended to handle a corner case where + * ancestor neighbors are redistributed to a processor only by other + * processors who do not see that neighbor link. */ virtual void find_neighbors (const bool reset_remote_elements = false, const bool reset_current_list = true, - const bool assert_valid = true) = 0; + const bool assert_valid = true, + const bool check_non_remote = true) = 0; /** * Removes any orphaned nodes, nodes not connected to any elements. diff --git a/include/mesh/unstructured_mesh.h b/include/mesh/unstructured_mesh.h index 8d838a67e14..c931534192f 100644 --- a/include/mesh/unstructured_mesh.h +++ b/include/mesh/unstructured_mesh.h @@ -295,7 +295,8 @@ class UnstructuredMesh : public MeshBase */ virtual void find_neighbors (const bool reset_remote_elements = false, const bool reset_current_list = true, - const bool assert_valid = true) override; + const bool assert_valid = true, + const bool check_non_remote = true) override; #ifdef LIBMESH_ENABLE_AMR /** diff --git a/src/mesh/unstructured_mesh.C b/src/mesh/unstructured_mesh.C index 5cff72f1097..0aa577c860c 100644 --- a/src/mesh/unstructured_mesh.C +++ b/src/mesh/unstructured_mesh.C @@ -956,7 +956,8 @@ UnstructuredMesh::~UnstructuredMesh () void UnstructuredMesh::find_neighbors (const bool reset_remote_elements, const bool reset_current_list, - const bool assert_valid) + const bool assert_valid, + const bool check_non_remote) { // We might actually want to run this on an empty mesh // (e.g. the boundary mesh for a nonexistent bcid!) @@ -1003,7 +1004,10 @@ void UnstructuredMesh::find_neighbors (const bool reset_remote_elements, // If we haven't yet found a neighbor on this side, try. // Even if we think our neighbor is remote, that // information may be out of date. - if (element->neighbor_ptr(ms) == nullptr || + // + // If we're only checking remote neighbors, after a + // redistribution, then we'll skip the non-remote ones + if ((element->neighbor_ptr(ms) == nullptr && check_non_remote) || element->neighbor_ptr(ms) == remote_elem) { // Get the key for the side of this element. Use the From 8e443ec9fc8a6489d515fa4bdd4f9976932e2ba8 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Wed, 28 Jan 2026 11:12:25 -0600 Subject: [PATCH 02/18] Check for non-remoted neighbors postredistribution There's a corner case where a processor can receive 2 ancestor elements that are neighbors, but from source processors each of which only sees one of the two semilocally. This is necessary to fix that. --- src/mesh/distributed_mesh.C | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/mesh/distributed_mesh.C b/src/mesh/distributed_mesh.C index 6087bcada66..53cc4e000e5 100644 --- a/src/mesh/distributed_mesh.C +++ b/src/mesh/distributed_mesh.C @@ -1055,9 +1055,13 @@ void DistributedMesh::redistribute () this->update_parallel_id_counts(); - // We ought to still have valid neighbor links; we communicate - // them for newly-redistributed elements - // this->find_neighbors(); + // We communicate valid neighbor links for newly-redistributed + // elements, but we may still have remote_elem links that should + // be corrected but whose correction wasn't communicated. + this->find_neighbors(/*reset_remote_elements*/ false, + /*reset_current_list*/ false /*non-default*/, + /*assert_valid*/ false, + /*check_non_remote*/ false /*non-default!*/); // Is this necessary? If we are called from prepare_for_use(), this will be called // anyway... but users can always call partition directly, in which case we do need From 1e3d21924b1020078a61e3daa68da1a96c7a3e94 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Mon, 22 Jan 2024 10:44:24 -0600 Subject: [PATCH 03/18] Assert our cache is still good --- src/mesh/mesh_base.C | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index 926ca478d72..a0e106e6e6a 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -605,6 +605,8 @@ void MeshBase::change_elemset_id(elemset_id_type old_id, elemset_id_type new_id) unsigned int MeshBase::spatial_dimension () const { + libmesh_assert(_preparation.has_cached_elem_data); + return cast_int(_spatial_dimension); } From 2d49f68290b23d155ff7edab85ca763c69da40aa Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Tue, 2 Dec 2025 22:11:11 -0600 Subject: [PATCH 04/18] Test partially-prepared status validity too --- src/mesh/mesh_tools.C | 63 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/mesh/mesh_tools.C b/src/mesh/mesh_tools.C index 84495843d3c..a2415f2ee8a 100644 --- a/src/mesh/mesh_tools.C +++ b/src/mesh/mesh_tools.C @@ -1257,29 +1257,74 @@ bool valid_is_prepared (const MeshBase & mesh) { LOG_SCOPE("valid_is_prepared()", "MeshTools"); - if (!mesh.is_prepared()) + const MeshBase::Preparation prep = mesh.preparation(); + + // If the mesh doesn't think *anything* has been prepared, we have + // nothing to check. + if (prep == MeshBase::Preparation()) return true; + // If the mesh thinks it's partitioned, check. These are counts, + // not caches, so it's a real check. + if (prep.is_partitioned) + if (mesh.n_unpartitioned_elem() || + mesh.n_unpartitioned_nodes()) + return false; + + // If the mesh thinks it's prepared in some way, *re*-preparing in + // that way shouldn't change a clone of it, as long as we disallow + // repartitioning or renumbering or remote element removal. std::unique_ptr mesh_clone = mesh.clone(); // Try preparing (without allowing repartitioning or renumbering, to // avoid false assertion failures) - bool old_allow_renumbering = mesh_clone->allow_renumbering(); + const bool old_allow_renumbering = mesh_clone->allow_renumbering(); + const bool old_allow_remote_element_removal = + mesh_clone->allow_remote_element_removal(); + const bool old_skip_partitioning = mesh_clone->skip_partitioning(); mesh_clone->allow_renumbering(false); - - bool old_skip_partitioning = mesh_clone->skip_partitioning(); + mesh_clone->allow_remote_element_removal(false); mesh_clone->skip_partitioning(true); - bool old_allow_remote_element_removal = mesh_clone->allow_remote_element_removal(); - mesh_clone->allow_remote_element_removal(false); + // If the mesh thinks it's already completely prepared, test that + if (prep) + mesh_clone->prepare_for_use(); + // If the mesh thinks it's somewhat prepared, test each way it + // thinks so. + else + { + if (prep.has_synched_id_counts) + mesh_clone->update_parallel_id_counts(); + + if (prep.has_neighbor_ptrs) + mesh_clone->find_neighbors(); - // Call prepare_for_use() on the clone - mesh_clone->prepare_for_use(); + if (prep.has_cached_elem_data) + mesh_clone->cache_elem_data(); + + if (prep.has_interior_parent_ptrs) + mesh_clone->detect_interior_parents(); + + if (old_allow_remote_element_removal && + prep.has_removed_remote_elements) + mesh_clone->delete_remote_elements(); + + if (prep.has_removed_orphaned_nodes) + mesh_clone->remove_orphaned_nodes(); + + if (prep.has_boundary_id_sets) + mesh_clone->get_boundary_info().regenerate_id_sets(); + + // I don't know how we'll tell if this changes anything, but + // we'll do it for completeness + if (prep.has_reinit_ghosting_functors) + mesh_clone->reinit_ghosting_functors(); + } // Restore original flag values mesh_clone->allow_renumbering(old_allow_renumbering); - mesh_clone->skip_partitioning(old_skip_partitioning); mesh_clone->allow_remote_element_removal(old_allow_remote_element_removal); + mesh_clone->skip_partitioning(old_skip_partitioning); // Check whether the original and clone compare equal return (mesh == *mesh_clone); From 9f95bef78d424eb9ca26ff884c2496cb4a21b43b Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Thu, 4 Dec 2025 20:56:43 -0600 Subject: [PATCH 05/18] More testing of preparation() validity in dbg mode --- src/mesh/mesh_base.C | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index a0e106e6e6a..fb59d29d547 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -881,10 +881,14 @@ void MeshBase::complete_preparation() libmesh_assert(this->comm().verify(this->is_serial())); +#ifdef DEBUG // If we don't go into this method with valid constraint rows, we're // only going to be able to make that worse. -#ifdef DEBUG MeshTools::libmesh_assert_valid_constraint_rows(*this); + + // If this mesh thinks it's already partially prepared, then in + // optimized builds we'll trust it, but in debug builds we'll check. + const bool was_partly_prepared = (_preparation == Preparation()); #endif // A distributed mesh may have processors with no elements (or @@ -1016,6 +1020,10 @@ void MeshBase::complete_preparation() #endif #ifdef DEBUG + // The if() here avoids both unnecessary work *and* stack overflow + if (was_partly_prepared) + libmesh_assert(MeshTools::valid_is_prepared(*this)); + MeshTools::libmesh_assert_valid_boundary_ids(*this); #ifdef LIBMESH_ENABLE_UNIQUE_ID MeshTools::libmesh_assert_valid_unique_ids(*this); From 46583d19476b624484ed1485868984e1b436a2ac Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Tue, 9 Dec 2025 21:46:30 -0600 Subject: [PATCH 06/18] Added MeshBase::assert_equal_to It's quite annoying to me to have to hunt down *why* a valid_is_prepared() call fails, which means it's got to be utterly intractable to too many users. So, instead of just asserting the final boolean results of an operator==, let's provide some way to get assertion failures that deliver more information about what wasn't equal. --- include/mesh/distributed_mesh.h | 2 +- include/mesh/mesh_base.h | 18 +++- include/mesh/replicated_mesh.h | 2 +- src/mesh/distributed_mesh.C | 46 ++++----- src/mesh/mesh_base.C | 162 +++++++++++++++++++------------- src/mesh/replicated_mesh.C | 20 ++-- 6 files changed, 151 insertions(+), 99 deletions(-) diff --git a/include/mesh/distributed_mesh.h b/include/mesh/distributed_mesh.h index ec1febb473a..cec39aeb4b5 100644 --- a/include/mesh/distributed_mesh.h +++ b/include/mesh/distributed_mesh.h @@ -111,7 +111,7 @@ class DistributedMesh : public UnstructuredMesh * Shim to allow operator == (&) to behave like a virtual function * without having to be one. */ - virtual bool subclass_locally_equals (const MeshBase & other_mesh) const override; + virtual std::string_view subclass_first_difference_from (const MeshBase & other_mesh_base) const override; /** * Virtual copy-constructor, creates a copy of this mesh diff --git a/include/mesh/mesh_base.h b/include/mesh/mesh_base.h index 098bc2cf959..b12147a3c31 100644 --- a/include/mesh/mesh_base.h +++ b/include/mesh/mesh_base.h @@ -130,7 +130,7 @@ class MeshBase : public ParallelObject * ids). * * Though this method is non-virtual, its implementation calls the - * virtual function \p subclass_locally_equals() to test for + * virtual function \p subclass_first_difference_from() to test for * equality of subclass-specific data as well. */ bool operator== (const MeshBase & other_mesh) const; @@ -140,6 +140,14 @@ class MeshBase : public ParallelObject return !(*this == other_mesh); } + /** + * This behaves like libmesh_assert(*this == other_mesh), but gives + * a more useful accounting of the first difference found, if the + * assertion fails. + */ + void assert_equal_to (const MeshBase & other_mesh, + std::string_view failure_context) const; + /** * This behaves the same as operator==, but only for the local and * ghosted aspects of the mesh; i.e. operator== is true iff local @@ -2157,7 +2165,7 @@ class MeshBase : public ParallelObject * Shim to allow operator == (&) to behave like a virtual function * without having to be one. */ - virtual bool subclass_locally_equals (const MeshBase & other_mesh) const = 0; + virtual std::string_view subclass_first_difference_from (const MeshBase & other_mesh) const = 0; /** * Tests for equality of all elements and nodes in the mesh. Helper @@ -2165,6 +2173,12 @@ class MeshBase : public ParallelObject */ bool nodes_and_elements_equal(const MeshBase & other_mesh) const; + /** + * + */ + std::string_view first_difference_from(const MeshBase & other_mesh) const; + + /** * \returns A writable reference to the number of partitions. */ diff --git a/include/mesh/replicated_mesh.h b/include/mesh/replicated_mesh.h index 6cb5e60a96b..fbd8d4d44ba 100644 --- a/include/mesh/replicated_mesh.h +++ b/include/mesh/replicated_mesh.h @@ -97,7 +97,7 @@ class ReplicatedMesh : public UnstructuredMesh * Shim to allow operator == (&) to behave like a virtual function * without having to be one. */ - virtual bool subclass_locally_equals (const MeshBase & other_mesh) const override; + virtual std::string_view subclass_first_difference_from (const MeshBase & other_mesh_base) const override; /** * Virtual copy-constructor, creates a copy of this mesh diff --git a/src/mesh/distributed_mesh.C b/src/mesh/distributed_mesh.C index 53cc4e000e5..1ce4daf62d2 100644 --- a/src/mesh/distributed_mesh.C +++ b/src/mesh/distributed_mesh.C @@ -95,48 +95,52 @@ MeshBase & DistributedMesh::assign(MeshBase && other_mesh) return *this; } -bool DistributedMesh::subclass_locally_equals(const MeshBase & other_mesh_base) const +std::string_view DistributedMesh::subclass_first_difference_from (const MeshBase & other_mesh_base) const { const DistributedMesh * dist_mesh_ptr = dynamic_cast(&other_mesh_base); if (!dist_mesh_ptr) - return false; + return "DistributedMesh class"; const DistributedMesh & other_mesh = *dist_mesh_ptr; - if (_is_serial != other_mesh._is_serial || - _is_serial_on_proc_0 != other_mesh._is_serial_on_proc_0 || - _deleted_coarse_elements != other_mesh._deleted_coarse_elements || - _n_nodes != other_mesh._n_nodes || - _n_elem != other_mesh._n_elem || - _max_node_id != other_mesh._max_node_id || - _max_elem_id != other_mesh._max_elem_id || - // We expect these things to change in a prepare_for_use(); - // they're conceptually "mutable"... +#define CHECK_MEMBER(member_name) \ + if (member_name != other_mesh.member_name) \ + return #member_name; + + CHECK_MEMBER(_is_serial); + CHECK_MEMBER(_is_serial_on_proc_0); + CHECK_MEMBER(_deleted_coarse_elements); + CHECK_MEMBER(_n_nodes); + CHECK_MEMBER(_n_elem); + CHECK_MEMBER(_max_node_id); + CHECK_MEMBER(_max_elem_id); + // We expect these things to change in a prepare_for_use(); + // they're conceptually "mutable"... /* - _next_free_local_node_id != other_mesh._next_free_local_node_id || - _next_free_local_elem_id != other_mesh._next_free_local_elem_id || - _next_free_unpartitioned_node_id != other_mesh._next_free_unpartitioned_node_id || - _next_free_unpartitioned_elem_id != other_mesh._next_free_unpartitioned_elem_id || + CHECK_MEMBER(_next_free_local_node_id); + CHECK_MEMBER(_next_free_local_elem_id); + CHECK_MEMBER(_next_free_unpartitioned_node_id); + CHECK_MEMBER(_next_free_unpartitioned_elem_id); #ifdef LIBMESH_ENABLE_UNIQUE_ID - _next_unpartitioned_unique_id != other_mesh._next_unpartitioned_unique_id || + CHECK_MEMBER(_next_unpartitioned_unique_id); #endif */ - !this->nodes_and_elements_equal(other_mesh)) - return false; + if (!this->nodes_and_elements_equal(other_mesh)) + return "nodes and/or elements"; if (_extra_ghost_elems.size() != other_mesh._extra_ghost_elems.size()) - return false; + return "_extra_ghost_elems size"; for (auto & elem : _extra_ghost_elems) { libmesh_assert(this->query_elem_ptr(elem->id()) == elem); const Elem * other_elem = other_mesh.query_elem_ptr(elem->id()); if (!other_elem || !other_mesh._extra_ghost_elems.count(const_cast(other_elem))) - return false; + return "_extra_ghost_elems entry"; } - return true; + return ""; } DistributedMesh::~DistributedMesh () diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index fb59d29d547..f7c697c9651 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -278,7 +278,57 @@ bool MeshBase::operator== (const MeshBase & other_mesh) const } +void MeshBase::assert_equal_to (const MeshBase & other_mesh, + std::string_view failure_context) const +{ +#ifndef NDEBUG + LOG_SCOPE("operator==()", "MeshBase"); + + std::string_view local_diff = first_difference_from(other_mesh); + + bool diff_found = !local_diff.empty(); + this->comm().max(diff_found); + + if (diff_found) + { + // Construct a user-friendly message to throw on pid 0 + std::set unique_diffs; + if (!local_diff.empty()) + unique_diffs.insert(std::string(local_diff)); + this->comm().set_union(unique_diffs); + + if (!this->processor_id()) + { + std::string error_msg {failure_context}; + error_msg += "\nMeshes failed asserted equality in at least these aspects:\n"; + for (auto & diff : unique_diffs) + { + error_msg += diff; + error_msg += '\n'; + } + libmesh_assert_msg(!diff_found, error_msg); + } + + // We're not going to throw on other processors because we don't + // want to accidentally preempt pid 0's error message. We're + // not even going to exit on other processors because for all we + // know user code is going to catch that error and sync up with + // us later. + } +#else + libmesh_ignore(other_mesh, failure_context); +#endif // NDEBUG +} + + bool MeshBase::locally_equals (const MeshBase & other_mesh) const +{ + const std::string_view diff = first_difference_from(other_mesh); + return diff.empty(); +} + + +std::string_view MeshBase::first_difference_from(const MeshBase & other_mesh) const { // Check whether (almost) everything in the base is equal // @@ -286,21 +336,19 @@ bool MeshBase::locally_equals (const MeshBase & other_mesh) const // change in a DistributedMesh prepare_for_use(); it's conceptually // "mutable". // - // We use separate if statements instead of logical operators here, - // to make it easy to see the failing condition when using a - // debugger to figure out why a MeshTools::valid_is_prepared(mesh) - // is failing. - if (_n_parts != other_mesh._n_parts) - return false; - if (_default_mapping_type != other_mesh._default_mapping_type) - return false; - if (_default_mapping_data != other_mesh._default_mapping_data) - return false; - if (_preparation != other_mesh._preparation) - return false; - if (_count_lower_dim_elems_in_point_locator != - other_mesh._count_lower_dim_elems_in_point_locator) - return false; + // We use separate tests here and return strings for each test, + // to make it easy to see the failing condition a + // MeshTools::valid_is_prepared(mesh) is failing. + +#define CHECK_MEMBER(member_name) \ + if (member_name != other_mesh.member_name) \ + return #member_name; + + CHECK_MEMBER(_n_parts); + CHECK_MEMBER(_default_mapping_type); + CHECK_MEMBER(_default_mapping_data); + CHECK_MEMBER(_preparation); + CHECK_MEMBER(_count_lower_dim_elems_in_point_locator); // We should either both have our own interior parents or both not; // but if we both don't then we can't really assert anything else @@ -308,61 +356,43 @@ bool MeshBase::locally_equals (const MeshBase & other_mesh) const // pointing at two different copies of "the same" interior mesh. if ((_interior_mesh == this) != (other_mesh._interior_mesh == &other_mesh)) - return false; + return "_interior_mesh"; + + CHECK_MEMBER(_skip_noncritical_partitioning); + CHECK_MEMBER(_skip_all_partitioning); + CHECK_MEMBER(_skip_renumber_nodes_and_elements); + CHECK_MEMBER(_skip_find_neighbors); + CHECK_MEMBER(_skip_detect_interior_parents); + CHECK_MEMBER(_allow_remote_element_removal); + CHECK_MEMBER(_allow_node_and_elem_unique_id_overlap); + CHECK_MEMBER(_spatial_dimension); + CHECK_MEMBER(_point_locator_close_to_point_tol); + CHECK_MEMBER(_block_id_to_name); + CHECK_MEMBER(_elem_dims); + CHECK_MEMBER(_elem_default_orders); + CHECK_MEMBER(_supported_nodal_order); + CHECK_MEMBER(_mesh_subdomains); + CHECK_MEMBER(_all_elemset_ids); + CHECK_MEMBER(_elem_integer_names); + CHECK_MEMBER(_elem_integer_default_values); + CHECK_MEMBER(_node_integer_names); + CHECK_MEMBER(_node_integer_default_values); - if (_skip_noncritical_partitioning != other_mesh._skip_noncritical_partitioning) - return false; - if (_skip_all_partitioning != other_mesh._skip_all_partitioning) - return false; - if (_skip_renumber_nodes_and_elements != other_mesh._skip_renumber_nodes_and_elements) - return false; - if (_skip_find_neighbors != other_mesh._skip_find_neighbors) - return false; - if (_skip_detect_interior_parents != other_mesh._skip_detect_interior_parents) - return false; - if (_allow_remote_element_removal != other_mesh._allow_remote_element_removal) - return false; - if (_allow_node_and_elem_unique_id_overlap != other_mesh._allow_node_and_elem_unique_id_overlap) - return false; - if (_spatial_dimension != other_mesh._spatial_dimension) - return false; - if (_point_locator_close_to_point_tol != other_mesh._point_locator_close_to_point_tol) - return false; - if (_block_id_to_name != other_mesh._block_id_to_name) - return false; - if (_elem_dims != other_mesh._elem_dims) - return false; - if (_elem_default_orders != other_mesh._elem_default_orders) - return false; - if (_supported_nodal_order != other_mesh._supported_nodal_order) - return false; - if (_mesh_subdomains != other_mesh._mesh_subdomains) - return false; - if (_all_elemset_ids != other_mesh._all_elemset_ids) - return false; - if (_elem_integer_names != other_mesh._elem_integer_names) - return false; - if (_elem_integer_default_values != other_mesh._elem_integer_default_values) - return false; - if (_node_integer_names != other_mesh._node_integer_names) - return false; - if (_node_integer_default_values != other_mesh._node_integer_default_values) - return false; if (static_cast(_default_ghosting) != static_cast(other_mesh._default_ghosting)) - return false; + return "_default_ghosting"; if (static_cast(_partitioner) != static_cast(other_mesh._partitioner)) - return false; + return "_partitioner"; if (*boundary_info != *other_mesh.boundary_info) - return false; + return "boundary_info"; // First check whether the "existence" of the two pointers differs (one present, one absent) if (static_cast(_disjoint_neighbor_boundary_pairs) != static_cast(other_mesh._disjoint_neighbor_boundary_pairs)) - return false; + return "_disjoint_neighbor_boundary_pairs existence"; // If both exist, compare the contents (Weak Test: just compare sizes like `_ghosting_functors`) if (_disjoint_neighbor_boundary_pairs && (_disjoint_neighbor_boundary_pairs->size() != other_mesh._disjoint_neighbor_boundary_pairs->size())) - return false; + return "_disjoint_neighbor_boundary_pairs size"; const constraint_rows_type & other_rows = other_mesh.get_constraint_rows(); @@ -371,15 +401,15 @@ bool MeshBase::locally_equals (const MeshBase & other_mesh) const const dof_id_type node_id = node->id(); const Node * other_node = other_mesh.query_node_ptr(node_id); if (!other_node) - return false; + return "_constraint_rows node presence"; auto it = other_rows.find(other_node); if (it == other_rows.end()) - return false; + return "_constraint_rows row presence"; const auto & other_row = it->second; if (row.size() != other_row.size()) - return false; + return "_constraint_rows row size"; for (auto i : index_range(row)) { @@ -392,14 +422,14 @@ bool MeshBase::locally_equals (const MeshBase & other_mesh) const elem_pair.second != other_elem_pair.second || coef != other_coef) - return false; + return "_constraint_rows row entry"; } } for (const auto & [elemset_code, elemset_ptr] : this->_elemset_codes) if (const auto it = other_mesh._elemset_codes.find(elemset_code); it == other_mesh._elemset_codes.end() || *elemset_ptr != *it->second) - return false; + return "_elemset_codes"; // FIXME: we have no good way to compare ghosting functors, since // they're in a vector of pointers, and we have no way *at all* @@ -408,13 +438,13 @@ bool MeshBase::locally_equals (const MeshBase & other_mesh) const // we have the same number, is all. if (_ghosting_functors.size() != other_mesh._ghosting_functors.size()) - return false; + return "_ghosting_functors size"; // Same deal for partitioners. We tested that we both have one or // both don't, but are they equivalent? Let's guess "yes". // Now let the subclasses decide whether everything else is equal - return this->subclass_locally_equals(other_mesh); + return this->subclass_first_difference_from(other_mesh); } diff --git a/src/mesh/replicated_mesh.C b/src/mesh/replicated_mesh.C index 23fdc4f9684..aff6e3e9ad8 100644 --- a/src/mesh/replicated_mesh.C +++ b/src/mesh/replicated_mesh.C @@ -59,23 +59,27 @@ ReplicatedMesh::ReplicatedMesh (const Parallel::Communicator & comm_in, } -bool ReplicatedMesh::subclass_locally_equals(const MeshBase & other_mesh_base) const +std::string_view ReplicatedMesh::subclass_first_difference_from (const MeshBase & other_mesh_base) const { const ReplicatedMesh * rep_mesh_ptr = dynamic_cast(&other_mesh_base); if (!rep_mesh_ptr) - return false; + return "ReplicatedMesh class"; const ReplicatedMesh & other_mesh = *rep_mesh_ptr; - if (_n_nodes != other_mesh._n_nodes || - _n_elem != other_mesh._n_elem || +#define CHECK_MEMBER(member_name) \ + if (member_name != other_mesh.member_name) \ + return #member_name; + + CHECK_MEMBER(_n_nodes); + CHECK_MEMBER(_n_elem); #ifdef LIBMESH_ENABLE_UNIQUE_ID - _next_unique_id != other_mesh._next_unique_id || + CHECK_MEMBER(_next_unique_id); #endif - !this->nodes_and_elements_equal(other_mesh)) - return false; + if (!this->nodes_and_elements_equal(other_mesh)) + return "nodes and/or elements"; - return true; + return ""; } From 7c4bcae67beaa28386e4d20ead9f5698d7e46fa0 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Tue, 9 Dec 2025 21:50:23 -0600 Subject: [PATCH 07/18] Add MeshTools::libmesh_assert_valid_is_prepared() This should give a more informative assertion failure message when needed. --- include/mesh/mesh_tools.h | 21 ++++-- src/mesh/mesh_tools.C | 137 +++++++++++++++++++++++++------------- 2 files changed, 108 insertions(+), 50 deletions(-) diff --git a/include/mesh/mesh_tools.h b/include/mesh/mesh_tools.h index 50b4c6dd07c..632341f7f41 100644 --- a/include/mesh/mesh_tools.h +++ b/include/mesh/mesh_tools.h @@ -394,11 +394,15 @@ void clear_spline_nodes(MeshBase &); /** * A function for testing whether a mesh's cached is_prepared() setting - * is not a false positive. If the mesh is marked as not prepared, or - * if preparing the already-partitioned mesh (without any - * repartitioning or renumbering) does not change it, then we return - * true. If the mesh believes it is prepared but prepare_for_use() - * would change it, we return false. + * is not a false positive, and that its preparation() values are not + * false positives. If the mesh is marked as completely not prepared, or + * if preparing a already-prepared mesh (without any repartitioning or + * renumbering) or preparing after a complete_preparation() (likewise) + * does not change the mesh, then we return true. If the mesh believes it + * is prepared but prepare_for_use() would change it, or if the mesh + * believes it is partially prepared in a certain way but + * complete_preparation() does not completely prepare it, we return + * false. */ bool valid_is_prepared (const MeshBase & mesh); @@ -412,6 +416,13 @@ bool valid_is_prepared (const MeshBase & mesh); #ifndef NDEBUG +/** + * Like libmesh_assert(MeshTools::valid_is_prepared(mesh)), but + * provides more information on preparation incompleteness in case of + * an error. + */ +void libmesh_assert_valid_is_prepared (const MeshBase & mesh); + /** * A function for testing that all DofObjects within a mesh * have the same n_systems count diff --git a/src/mesh/mesh_tools.C b/src/mesh/mesh_tools.C index a2415f2ee8a..f9f3c8cac87 100644 --- a/src/mesh/mesh_tools.C +++ b/src/mesh/mesh_tools.C @@ -51,7 +51,7 @@ // ------------------------------------------------------------ -// anonymous namespace for helper classes +// anonymous namespace for helper classes and subroutines namespace { using namespace libMesh; @@ -406,6 +406,69 @@ void find_nodal_neighbors_helper(const dof_id_type global_id, neighbors.assign(neighbor_set.begin(), neighbor_set.end()); } + + +std::unique_ptr reprepared_mesh_clone (const MeshBase & mesh) +{ + const MeshBase::Preparation prep = mesh.preparation(); + + // If the mesh thinks it's prepared in some way, *re*-preparing in + // that way shouldn't change a clone of it, as long as we disallow + // repartitioning or renumbering or remote element removal. + std::unique_ptr mesh_clone = mesh.clone(); + + const bool old_allow_renumbering = mesh_clone->allow_renumbering(); + const bool old_allow_remote_element_removal = + mesh_clone->allow_remote_element_removal(); + const bool old_skip_partitioning = mesh_clone->skip_partitioning(); + mesh_clone->allow_renumbering(false); + mesh_clone->allow_remote_element_removal(false); + mesh_clone->skip_partitioning(true); + + // If the mesh thinks it's already completely prepared, test that + if (prep) + mesh_clone->prepare_for_use(); + // If the mesh thinks it's somewhat prepared, test each way it + // thinks so. + else + { + if (prep.has_synched_id_counts) + mesh_clone->update_parallel_id_counts(); + + if (prep.has_neighbor_ptrs) + mesh_clone->find_neighbors(); + + if (prep.has_cached_elem_data) + mesh_clone->cache_elem_data(); + + if (prep.has_interior_parent_ptrs) + mesh_clone->detect_interior_parents(); + + if (old_allow_remote_element_removal && + prep.has_removed_remote_elements) + mesh_clone->delete_remote_elements(); + + if (prep.has_removed_orphaned_nodes) + mesh_clone->remove_orphaned_nodes(); + + if (prep.has_boundary_id_sets) + mesh_clone->get_boundary_info().regenerate_id_sets(); + + // I don't know how we'll tell if this changes anything, but + // we'll do it for completeness + if (prep.has_reinit_ghosting_functors) + mesh_clone->reinit_ghosting_functors(); + } + + // Restore original flag values + mesh_clone->allow_renumbering(old_allow_renumbering); + mesh_clone->allow_remote_element_removal(old_allow_remote_element_removal); + mesh_clone->skip_partitioning(old_skip_partitioning); + + return mesh_clone; +} + + } @@ -1274,65 +1337,49 @@ bool valid_is_prepared (const MeshBase & mesh) // If the mesh thinks it's prepared in some way, *re*-preparing in // that way shouldn't change a clone of it, as long as we disallow // repartitioning or renumbering or remote element removal. - std::unique_ptr mesh_clone = mesh.clone(); + std::unique_ptr mesh_clone = reprepared_mesh_clone(mesh); - // Try preparing (without allowing repartitioning or renumbering, to - // avoid false assertion failures) - const bool old_allow_renumbering = mesh_clone->allow_renumbering(); - const bool old_allow_remote_element_removal = - mesh_clone->allow_remote_element_removal(); - const bool old_skip_partitioning = mesh_clone->skip_partitioning(); - mesh_clone->allow_renumbering(false); - mesh_clone->allow_remote_element_removal(false); - mesh_clone->skip_partitioning(true); + // Check whether the original and clone compare equal + return (mesh == *mesh_clone); +} - // If the mesh thinks it's already completely prepared, test that - if (prep) - mesh_clone->prepare_for_use(); - // If the mesh thinks it's somewhat prepared, test each way it - // thinks so. - else - { - if (prep.has_synched_id_counts) - mesh_clone->update_parallel_id_counts(); - if (prep.has_neighbor_ptrs) - mesh_clone->find_neighbors(); - if (prep.has_cached_elem_data) - mesh_clone->cache_elem_data(); +#ifndef NDEBUG - if (prep.has_interior_parent_ptrs) - mesh_clone->detect_interior_parents(); - if (old_allow_remote_element_removal && - prep.has_removed_remote_elements) - mesh_clone->delete_remote_elements(); +void libmesh_assert_valid_is_prepared (const MeshBase & mesh) +{ + LOG_SCOPE("libmesh_assert_valid_is_prepared()", "MeshTools"); - if (prep.has_removed_orphaned_nodes) - mesh_clone->remove_orphaned_nodes(); + const MeshBase::Preparation prep = mesh.preparation(); - if (prep.has_boundary_id_sets) - mesh_clone->get_boundary_info().regenerate_id_sets(); + // If the mesh doesn't think *anything* has been prepared, we have + // nothing to check. + if (prep == MeshBase::Preparation()) + return; - // I don't know how we'll tell if this changes anything, but - // we'll do it for completeness - if (prep.has_reinit_ghosting_functors) - mesh_clone->reinit_ghosting_functors(); - } + // If the mesh thinks it's partitioned, check. These are counts, + // not caches, so it's a real check. + if (prep.is_partitioned) + libmesh_assert_msg + (!mesh.n_unpartitioned_elem() && !mesh.n_unpartitioned_nodes(), + "Mesh preparation().is_partitioned does not reflect mesh data."); - // Restore original flag values - mesh_clone->allow_renumbering(old_allow_renumbering); - mesh_clone->allow_remote_element_removal(old_allow_remote_element_removal); - mesh_clone->skip_partitioning(old_skip_partitioning); + // If the mesh thinks it's prepared in some way, *re*-preparing in + // that way shouldn't change a clone of it, as long as we disallow + // repartitioning or renumbering or remote element removal. + std::unique_ptr mesh_clone = reprepared_mesh_clone(mesh); - // Check whether the original and clone compare equal - return (mesh == *mesh_clone); + mesh.assert_equal_to + (*mesh_clone, + "Mesh data does not match mesh preparation().\n" + "Efficiently-prepared mesh != exhaustively-prepared clone."); } -#ifndef NDEBUG + void libmesh_assert_equal_n_systems (const MeshBase & mesh) { From b167807af041fea7ea5c67716008833c4a103961 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Tue, 9 Dec 2025 21:53:36 -0600 Subject: [PATCH 08/18] Enable !NDEBUG libmesh_assert_X as libmesh_assert When NDEBUG isn't defined, these should be no-ops, not unavailable, so we don't have to wrap them in ifdefs elsewhere. --- include/mesh/mesh_tools.h | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/include/mesh/mesh_tools.h b/include/mesh/mesh_tools.h index 632341f7f41..c1a6d3002cc 100644 --- a/include/mesh/mesh_tools.h +++ b/include/mesh/mesh_tools.h @@ -409,7 +409,7 @@ bool valid_is_prepared (const MeshBase & mesh); ///@{ /** - * The following functions, only available in builds with NDEBUG + * The following functions, no-ops except in builds with NDEBUG * undefined, are for asserting internal consistency that we hope * should never be broken in opt */ @@ -655,6 +655,39 @@ namespace Private { void globally_renumber_nodes_and_elements (MeshBase &); } // end namespace Private + +// Declare inline no-ops for assertions with NDEBUG defined +#ifdef NDEBUG + +inline void libmesh_assert_valid_is_prepared (const MeshBase &) {} + +inline void libmesh_assert_equal_n_systems (const MeshBase &) {} + +inline void libmesh_assert_old_dof_objects (const MeshBase &) {} + +inline void libmesh_assert_valid_node_pointers (const MeshBase &) {} + +inline void libmesh_assert_valid_remote_elems (const MeshBase &) {} + +inline void libmesh_assert_valid_elem_ids (const MeshBase &) {} + +inline void libmesh_assert_valid_amr_elem_ids (const MeshBase &) {} + +inline void libmesh_assert_valid_amr_interior_parents (const MeshBase &) {} + +inline void libmesh_assert_contiguous_dof_ids (const MeshBase &, unsigned int) {} + +template +inline void libmesh_assert_topology_consistent_procids (const MeshBase &) {} + +inline void libmesh_assert_canonical_node_procids (const MeshBase &) {} + +inline void libmesh_assert_valid_refinement_tree (const MeshBase &) {} + +#endif // NDEBUG + + + } // end namespace MeshTools } // namespace libMesh From 1a98301e00161e346c24520964601841f128c656 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Wed, 3 Dec 2025 13:17:40 -0600 Subject: [PATCH 09/18] Assert that we only assemble on prepared meshes We're trying to allow meshes to keep track of "half-prepared" status, for the sake of efficiency in large chains or webs of mesh generators, but at the very least user assembly kernels should be able to rely on a mesh being prepared for use - they're what we mean by "use"!. --- src/systems/fem_system.C | 16 ++++++++++++++++ src/systems/nonlinear_implicit_system.C | 6 ++++++ src/systems/system.C | 16 ++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/systems/fem_system.C b/src/systems/fem_system.C index 2807229c95f..b1aa79aa347 100644 --- a/src/systems/fem_system.C +++ b/src/systems/fem_system.C @@ -25,6 +25,7 @@ #include "libmesh/fem_system.h" #include "libmesh/libmesh_logging.h" #include "libmesh/mesh_base.h" +#include "libmesh/mesh_tools.h" #include "libmesh/numeric_vector.h" #include "libmesh/parallel_algebra.h" #include "libmesh/parallel_ghost_sync.h" @@ -893,6 +894,11 @@ void FEMSystem::assembly (bool get_residual, bool get_jacobian, const MeshBase & mesh = this->get_mesh(); + libmesh_assert(mesh.is_prepared()); +#ifdef DEBUG + libmesh_assert(MeshTools::valid_is_prepared(mesh)); +#endif + if (print_solution_norms) { this->solution->close(); @@ -1143,6 +1149,11 @@ void FEMSystem::assemble_qoi (const QoISet & qoi_indices) const MeshBase & mesh = this->get_mesh(); + libmesh_assert(mesh.is_prepared()); +#ifdef DEBUG + libmesh_assert(MeshTools::valid_is_prepared(mesh)); +#endif + this->update(); const unsigned int Nq = this->n_qois(); @@ -1176,6 +1187,11 @@ void FEMSystem::assemble_qoi_derivative (const QoISet & qoi_indices, const MeshBase & mesh = this->get_mesh(); + libmesh_assert(mesh.is_prepared()); +#ifdef DEBUG + libmesh_assert(MeshTools::valid_is_prepared(mesh)); +#endif + this->update(); // The quantity of interest derivative assembly accumulates on diff --git a/src/systems/nonlinear_implicit_system.C b/src/systems/nonlinear_implicit_system.C index f4f764e307c..f04e30806ee 100644 --- a/src/systems/nonlinear_implicit_system.C +++ b/src/systems/nonlinear_implicit_system.C @@ -22,6 +22,7 @@ #include "libmesh/diff_solver.h" #include "libmesh/equation_systems.h" #include "libmesh/libmesh_logging.h" +#include "libmesh/mesh_tools.h" #include "libmesh/nonlinear_solver.h" #include "libmesh/sparse_matrix.h" #include "libmesh/static_condensation.h" @@ -251,6 +252,11 @@ void NonlinearImplicitSystem::assembly(bool get_residual, bool /*apply_heterogeneous_constraints*/, bool /*apply_no_constraints*/) { + libmesh_assert(this->get_mesh().is_prepared()); +#ifdef DEBUG + libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); +#endif + // Get current_local_solution in sync this->update(); diff --git a/src/systems/system.C b/src/systems/system.C index 730ea329455..976b1b461eb 100644 --- a/src/systems/system.C +++ b/src/systems/system.C @@ -23,6 +23,7 @@ #include "libmesh/int_range.h" #include "libmesh/libmesh_logging.h" #include "libmesh/mesh_base.h" +#include "libmesh/mesh_tools.h" #include "libmesh/numeric_vector.h" #include "libmesh/parameter_vector.h" #include "libmesh/point.h" // For point_value @@ -556,6 +557,11 @@ void System::assemble () // Log how long the user's assembly code takes LOG_SCOPE("assemble()", "System"); + libmesh_assert(this->get_mesh().is_prepared()); +#ifdef DEBUG + libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); +#endif + // Call the user-specified assembly function this->user_assembly(); } @@ -567,6 +573,11 @@ void System::assemble_qoi (const QoISet & qoi_indices) // Log how long the user's assembly code takes LOG_SCOPE("assemble_qoi()", "System"); + libmesh_assert(this->get_mesh().is_prepared()); +#ifdef DEBUG + libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); +#endif + // Call the user-specified quantity of interest function this->user_QOI(qoi_indices); } @@ -580,6 +591,11 @@ void System::assemble_qoi_derivative(const QoISet & qoi_indices, // Log how long the user's assembly code takes LOG_SCOPE("assemble_qoi_derivative()", "System"); + libmesh_assert(this->get_mesh().is_prepared()); +#ifdef DEBUG + libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); +#endif + // Call the user-specified quantity of interest function this->user_QOI_derivative(qoi_indices, include_liftfunc, apply_constraints); From d77907a22447f857d36d20dc423d675394a6f96a Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Wed, 10 Dec 2025 13:26:37 -0600 Subject: [PATCH 10/18] Use libmesh_assert_valid_is_prepared() This should be much better for debugging cases where the assert fails. --- src/mesh/mesh_base.C | 4 ++-- src/mesh/mesh_tet_interface.C | 2 +- src/systems/fem_system.C | 6 +++--- src/systems/nonlinear_implicit_system.C | 2 +- src/systems/system.C | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index f7c697c9651..10468e6eba9 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -338,7 +338,7 @@ std::string_view MeshBase::first_difference_from(const MeshBase & other_mesh) co // // We use separate tests here and return strings for each test, // to make it easy to see the failing condition a - // MeshTools::valid_is_prepared(mesh) is failing. + // MeshTools::libmesh_valid_is_prepared(mesh) is failing. #define CHECK_MEMBER(member_name) \ if (member_name != other_mesh.member_name) \ @@ -1052,7 +1052,7 @@ void MeshBase::complete_preparation() #ifdef DEBUG // The if() here avoids both unnecessary work *and* stack overflow if (was_partly_prepared) - libmesh_assert(MeshTools::valid_is_prepared(*this)); + MeshTools::libmesh_assert_valid_is_prepared(*this); MeshTools::libmesh_assert_valid_boundary_ids(*this); #ifdef LIBMESH_ENABLE_UNIQUE_ID diff --git a/src/mesh/mesh_tet_interface.C b/src/mesh/mesh_tet_interface.C index f457e66c90f..6de7b7d37f3 100644 --- a/src/mesh/mesh_tet_interface.C +++ b/src/mesh/mesh_tet_interface.C @@ -135,7 +135,7 @@ BoundingBox MeshTetInterface::volume_to_surface_mesh(UnstructuredMesh & mesh) { // If we've been handed an unprepared mesh then we need to be made // aware of that and fix that; we're relying on neighbor pointers. - libmesh_assert(MeshTools::valid_is_prepared(mesh)); + MeshTools::libmesh_assert_valid_is_prepared(mesh); if (!mesh.is_prepared()) mesh.prepare_for_use(); diff --git a/src/systems/fem_system.C b/src/systems/fem_system.C index b1aa79aa347..97f96780b87 100644 --- a/src/systems/fem_system.C +++ b/src/systems/fem_system.C @@ -896,7 +896,7 @@ void FEMSystem::assembly (bool get_residual, bool get_jacobian, libmesh_assert(mesh.is_prepared()); #ifdef DEBUG - libmesh_assert(MeshTools::valid_is_prepared(mesh)); + MeshTools::libmesh_assert_valid_is_prepared(mesh); #endif if (print_solution_norms) @@ -1151,7 +1151,7 @@ void FEMSystem::assemble_qoi (const QoISet & qoi_indices) libmesh_assert(mesh.is_prepared()); #ifdef DEBUG - libmesh_assert(MeshTools::valid_is_prepared(mesh)); + MeshTools::libmesh_assert_valid_is_prepared(mesh); #endif this->update(); @@ -1189,7 +1189,7 @@ void FEMSystem::assemble_qoi_derivative (const QoISet & qoi_indices, libmesh_assert(mesh.is_prepared()); #ifdef DEBUG - libmesh_assert(MeshTools::valid_is_prepared(mesh)); + MeshTools::libmesh_assert_valid_is_prepared(mesh); #endif this->update(); diff --git a/src/systems/nonlinear_implicit_system.C b/src/systems/nonlinear_implicit_system.C index f04e30806ee..5f27cab4316 100644 --- a/src/systems/nonlinear_implicit_system.C +++ b/src/systems/nonlinear_implicit_system.C @@ -254,7 +254,7 @@ void NonlinearImplicitSystem::assembly(bool get_residual, { libmesh_assert(this->get_mesh().is_prepared()); #ifdef DEBUG - libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); + MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif // Get current_local_solution in sync diff --git a/src/systems/system.C b/src/systems/system.C index 976b1b461eb..a8a3cd7d023 100644 --- a/src/systems/system.C +++ b/src/systems/system.C @@ -559,7 +559,7 @@ void System::assemble () libmesh_assert(this->get_mesh().is_prepared()); #ifdef DEBUG - libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); + MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif // Call the user-specified assembly function @@ -575,7 +575,7 @@ void System::assemble_qoi (const QoISet & qoi_indices) libmesh_assert(this->get_mesh().is_prepared()); #ifdef DEBUG - libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); + MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif // Call the user-specified quantity of interest function @@ -593,7 +593,7 @@ void System::assemble_qoi_derivative(const QoISet & qoi_indices, libmesh_assert(this->get_mesh().is_prepared()); #ifdef DEBUG - libmesh_assert(MeshTools::valid_is_prepared(this->get_mesh())); + MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif // Call the user-specified quantity of interest function From 2db9af1a5df2f5f737d8755487e916d42d657d8f Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Wed, 10 Dec 2025 23:24:54 -0600 Subject: [PATCH 11/18] Only enforce preparation with --disable-deprecated This is actually a big behavior change, considering it led to failed assertions in a half dozen of our own examples. Let's give users some time to get up to speed. --- src/systems/fem_system.C | 6 +++--- src/systems/nonlinear_implicit_system.C | 2 +- src/systems/system.C | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/systems/fem_system.C b/src/systems/fem_system.C index 97f96780b87..2cf48e3ac54 100644 --- a/src/systems/fem_system.C +++ b/src/systems/fem_system.C @@ -895,7 +895,7 @@ void FEMSystem::assembly (bool get_residual, bool get_jacobian, const MeshBase & mesh = this->get_mesh(); libmesh_assert(mesh.is_prepared()); -#ifdef DEBUG +#if defined(DEBUG) && !defined(LIBMESH_ENABLE_DEPRECATED) MeshTools::libmesh_assert_valid_is_prepared(mesh); #endif @@ -1150,7 +1150,7 @@ void FEMSystem::assemble_qoi (const QoISet & qoi_indices) const MeshBase & mesh = this->get_mesh(); libmesh_assert(mesh.is_prepared()); -#ifdef DEBUG +#if defined(DEBUG) && !defined(LIBMESH_ENABLE_DEPRECATED) MeshTools::libmesh_assert_valid_is_prepared(mesh); #endif @@ -1188,7 +1188,7 @@ void FEMSystem::assemble_qoi_derivative (const QoISet & qoi_indices, const MeshBase & mesh = this->get_mesh(); libmesh_assert(mesh.is_prepared()); -#ifdef DEBUG +#if defined(DEBUG) && !defined(LIBMESH_ENABLE_DEPRECATED) MeshTools::libmesh_assert_valid_is_prepared(mesh); #endif diff --git a/src/systems/nonlinear_implicit_system.C b/src/systems/nonlinear_implicit_system.C index 5f27cab4316..0d41502b9af 100644 --- a/src/systems/nonlinear_implicit_system.C +++ b/src/systems/nonlinear_implicit_system.C @@ -253,7 +253,7 @@ void NonlinearImplicitSystem::assembly(bool get_residual, bool /*apply_no_constraints*/) { libmesh_assert(this->get_mesh().is_prepared()); -#ifdef DEBUG +#if defined(DEBUG) && !defined(LIBMESH_ENABLE_DEPRECATED) MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif diff --git a/src/systems/system.C b/src/systems/system.C index a8a3cd7d023..6ce737c0ffc 100644 --- a/src/systems/system.C +++ b/src/systems/system.C @@ -558,7 +558,7 @@ void System::assemble () LOG_SCOPE("assemble()", "System"); libmesh_assert(this->get_mesh().is_prepared()); -#ifdef DEBUG +#if defined(DEBUG) && !defined(LIBMESH_ENABLE_DEPRECATED) MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif @@ -574,7 +574,7 @@ void System::assemble_qoi (const QoISet & qoi_indices) LOG_SCOPE("assemble_qoi()", "System"); libmesh_assert(this->get_mesh().is_prepared()); -#ifdef DEBUG +#if defined(DEBUG) && !defined(LIBMESH_ENABLE_DEPRECATED) MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif @@ -592,7 +592,7 @@ void System::assemble_qoi_derivative(const QoISet & qoi_indices, LOG_SCOPE("assemble_qoi_derivative()", "System"); libmesh_assert(this->get_mesh().is_prepared()); -#ifdef DEBUG +#if defined(DEBUG) && !defined(LIBMESH_ENABLE_DEPRECATED) MeshTools::libmesh_assert_valid_is_prepared(this->get_mesh()); #endif From 72a2dab189e38b48c52910dc47fd73ac61e7b949 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Thu, 18 Dec 2025 14:25:09 -0600 Subject: [PATCH 12/18] Preparation::libmesh_assert_consistent() I haven't actually seen this fail yet but it does seem like the sort of thing we should be checking. --- include/mesh/mesh_base.h | 3 +++ src/mesh/mesh_base.C | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/include/mesh/mesh_base.h b/include/mesh/mesh_base.h index b12147a3c31..d6f64c705ca 100644 --- a/include/mesh/mesh_base.h +++ b/include/mesh/mesh_base.h @@ -2115,6 +2115,9 @@ class MeshBase : public ParallelObject bool operator== (const Preparation & other) const; bool operator!= (const Preparation & other) const; + // Assert that a Preparation object is identical across processors + void libmesh_assert_consistent (const Parallel::Communicator & libmesh_dbg_var(comm)); + bool is_partitioned; bool has_synched_id_counts; bool has_neighbor_ptrs; diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index 10468e6eba9..70b71753960 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -2972,6 +2972,21 @@ MeshBase::Preparation::operator!= (const Preparation & other) const } +void +MeshBase::Preparation::libmesh_assert_consistent (const Parallel::Communicator & libmesh_dbg_var(comm)) +{ + libmesh_assert(comm.verify(is_partitioned)); + libmesh_assert(comm.verify(has_synched_id_counts)); + libmesh_assert(comm.verify(has_neighbor_ptrs)); + libmesh_assert(comm.verify(has_cached_elem_data)); + libmesh_assert(comm.verify(has_interior_parent_ptrs)); + libmesh_assert(comm.verify(has_removed_remote_elements)); + libmesh_assert(comm.verify(has_removed_orphaned_nodes)); + libmesh_assert(comm.verify(has_reinit_ghosting_functors)); + libmesh_assert(comm.verify(has_boundary_id_sets)); +} + + // Explicit instantiations for our template function template LIBMESH_EXPORT void MeshBase::copy_constraint_rows(const SparseMatrix & constraint_operator, From b441e57e552cf037a6a452d2c8182427c5ebdcdb Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Thu, 18 Dec 2025 14:27:12 -0600 Subject: [PATCH 13/18] Test for parallel-consistent preparation --- src/mesh/mesh_base.C | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index 70b71753960..e5ec202eb47 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -911,6 +911,8 @@ void MeshBase::complete_preparation() libmesh_assert(this->comm().verify(this->is_serial())); + _preparation.libmesh_assert_consistent(this->comm()); + #ifdef DEBUG // If we don't go into this method with valid constraint rows, we're // only going to be able to make that worse. From d6b93f6b8a5939bef6483afa409da38356ef6561 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Fri, 27 Feb 2026 09:13:16 -0600 Subject: [PATCH 14/18] More fine-grained copy_nodes_and_elements flagging --- src/mesh/unstructured_mesh.C | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/mesh/unstructured_mesh.C b/src/mesh/unstructured_mesh.C index 0aa577c860c..ef8b5ab1122 100644 --- a/src/mesh/unstructured_mesh.C +++ b/src/mesh/unstructured_mesh.C @@ -910,7 +910,7 @@ void UnstructuredMesh::copy_nodes_and_elements(const MeshBase & other_mesh, const bool skipped_partitioning = this->skip_partitioning(); this->skip_partitioning(true); - const bool was_prepared = this->is_prepared(); + const Preparation old_preparation = this->preparation(); this->prepare_for_use(); //But in the long term, don't change our policies. @@ -923,9 +923,19 @@ void UnstructuredMesh::copy_nodes_and_elements(const MeshBase & other_mesh, // That prepare_for_use() call marked us as prepared, but we // specifically avoided some important preparation, so we might not // actually be prepared now. - if (skip_find_neighbors || - !was_prepared || !other_mesh.is_prepared()) - this->unset_is_prepared(); + if (skip_find_neighbors) + this->unset_has_neighbor_ptrs(); + + const Preparation other_preparation = other_mesh.preparation(); + if (!old_preparation.is_partitioned || + !other_preparation.is_partitioned) + this->unset_is_partitioned(); + if (!old_preparation.has_removed_orphaned_nodes || + !other_preparation.has_removed_orphaned_nodes) + this->unset_has_removed_orphaned_nodes(); + if (!old_preparation.has_removed_remote_elements || + !other_preparation.has_removed_remote_elements) + this->unset_has_removed_remote_elements(); } // In general we've just invalidated just about everything, and we'd From 13f1f479e6349e14fa71175fba6b079523b9d3b9 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Fri, 27 Feb 2026 09:15:05 -0600 Subject: [PATCH 15/18] Assert caches are prepared before allowing queries --- include/mesh/mesh_base.h | 54 ++++++++++++++++++++++++++++++++++------ src/mesh/mesh_base.C | 7 ------ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/include/mesh/mesh_base.h b/include/mesh/mesh_base.h index d6f64c705ca..ddc4c27841d 100644 --- a/include/mesh/mesh_base.h +++ b/include/mesh/mesh_base.h @@ -435,23 +435,20 @@ class MeshBase : public ParallelObject * \returns A const reference to a std::set of element dimensions * present in the mesh. */ - const std::set & elem_dimensions() const - { return _elem_dims; } + const std::set & elem_dimensions() const; /** * \returns A const reference to a std::set of element default * orders present in the mesh. */ - const std::set & elem_default_orders() const - { return _elem_default_orders; } + const std::set & elem_default_orders() const; /** * \returns The smallest supported_nodal_order() of any element * present in the mesh, which is thus the maximum supported nodal * order on the mesh as a whole. */ - Order supported_nodal_order() const - { return _supported_nodal_order; } + Order supported_nodal_order() const; /** * Most of the time you should not need to call this, as the element @@ -2069,8 +2066,7 @@ class MeshBase : public ParallelObject * should contain all the subdomain ids across processors. Relies on the mesh * being prepared */ - const std::set & get_mesh_subdomains() const - { libmesh_assert(this->is_prepared()); return _mesh_subdomains; } + const std::set & get_mesh_subdomains() const; #ifdef LIBMESH_ENABLE_PERIODIC /** @@ -2649,6 +2645,48 @@ MeshBase::const_node_iterator : MeshBase::const_node_filter_iter }; +// ------------------------------------------------------------ +// Elem class member functions +inline +const std::set & MeshBase::elem_dimensions() const +{ + libmesh_assert(_preparation.has_cached_elem_data); + return _elem_dims; +} + + +inline +const std::set & MeshBase::elem_default_orders() const +{ + libmesh_assert(_preparation.has_cached_elem_data); + return _elem_default_orders; +} + + +inline +Order MeshBase::supported_nodal_order() const +{ + libmesh_assert(_preparation.has_cached_elem_data); + return _supported_nodal_order; +} + + +inline +const std::set & MeshBase::get_mesh_subdomains() const +{ + libmesh_assert(_preparation.has_cached_elem_data); + return _mesh_subdomains; +} + + +inline +unsigned int MeshBase::spatial_dimension () const +{ + libmesh_assert(_preparation.has_cached_elem_data); + + return cast_int(_spatial_dimension); +} + template inline unsigned int MeshBase::add_elem_datum(const std::string & name, diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index e5ec202eb47..6984d10b094 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -633,13 +633,6 @@ void MeshBase::change_elemset_id(elemset_id_type old_id, elemset_id_type new_id) _all_elemset_ids.insert(new_id); } -unsigned int MeshBase::spatial_dimension () const -{ - libmesh_assert(_preparation.has_cached_elem_data); - - return cast_int(_spatial_dimension); -} - void MeshBase::set_spatial_dimension(unsigned char d) From 0d00d2348e27c42d594343640bc4d7803f1897ad Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Fri, 27 Feb 2026 17:12:47 -0600 Subject: [PATCH 16/18] Fix PerfLog name --- src/mesh/mesh_base.C | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/mesh_base.C b/src/mesh/mesh_base.C index 6984d10b094..226de9e8584 100644 --- a/src/mesh/mesh_base.C +++ b/src/mesh/mesh_base.C @@ -282,7 +282,7 @@ void MeshBase::assert_equal_to (const MeshBase & other_mesh, std::string_view failure_context) const { #ifndef NDEBUG - LOG_SCOPE("operator==()", "MeshBase"); + LOG_SCOPE("assert_equal_to()", "MeshBase"); std::string_view local_diff = first_difference_from(other_mesh); From 7ca2abdc4a705309710a0d230ac0cf9055e39f55 Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Mon, 13 Apr 2026 21:28:32 -0500 Subject: [PATCH 17/18] Fix detect_interior_parents w/o cached elem dims --- src/mesh/mesh_tools.C | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/mesh_tools.C b/src/mesh/mesh_tools.C index f9f3c8cac87..f4c7549e558 100644 --- a/src/mesh/mesh_tools.C +++ b/src/mesh/mesh_tools.C @@ -441,7 +441,8 @@ std::unique_ptr reprepared_mesh_clone (const MeshBase & mesh) if (prep.has_cached_elem_data) mesh_clone->cache_elem_data(); - if (prep.has_interior_parent_ptrs) + if (mesh.allow_detect_interior_parents() && + prep.has_interior_parent_ptrs) mesh_clone->detect_interior_parents(); if (old_allow_remote_element_removal && From 113001abe40290e9342ecf198a38dbc5dc19e51f Mon Sep 17 00:00:00 2001 From: Roy Stogner Date: Tue, 14 Apr 2026 15:55:58 -0500 Subject: [PATCH 18/18] System/ES init can prepare unprepared meshes This should be good for usability, especially because DofMap::distribute_dofs() is asserting that we have a prepared mesh but I just had some MeshModification:: methods mark their targets as unprepared. --- src/systems/equation_systems.C | 8 ++++++++ src/systems/system.C | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/systems/equation_systems.C b/src/systems/equation_systems.C index 557017a377c..0ba83fc74cd 100644 --- a/src/systems/equation_systems.C +++ b/src/systems/equation_systems.C @@ -105,6 +105,14 @@ void EquationSystems::reinit_mesh () libmesh_assert_not_equal_to (n_sys, 0); + // Our DofMaps are going to expect a prepared mesh later, but our + // older codes might have used some utilities that mark a mesh + // unprepared without re-preparing it. At *this* point hopefully + // everybody's done changing the mesh and we can make sure it's + // prepared. + if (!_mesh.is_prepared()) + _mesh.complete_preparation(); + // Tell all the \p DofObject entities how many systems // there are. for (auto & node : _mesh.node_ptr_range()) diff --git a/src/systems/system.C b/src/systems/system.C index 6ce737c0ffc..488cce2f0df 100644 --- a/src/systems/system.C +++ b/src/systems/system.C @@ -211,6 +211,11 @@ void System::init_data () MeshBase & mesh = this->get_mesh(); + // Hopefully the user or the EquationSystems prepared the mesh, but + // if not then we'd better do it now. + if (!mesh.is_prepared()) + mesh.complete_preparation(); + // Distribute the degrees of freedom on the mesh auto total_dofs = _dof_map->distribute_dofs (mesh);