Skip to content

Commit 120b7c1

Browse files
committed
add new unit test for disconnected neighbor in libmesh
1 parent 9a39fa7 commit 120b7c1

File tree

8 files changed

+438
-45
lines changed

8 files changed

+438
-45
lines changed

.DS_Store

-8 KB
Binary file not shown.

include/base/periodic_boundaries.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class PeriodicBoundaries : public std::map<boundary_id_type, std::unique_ptr<Per
6969
const Elem * e,
7070
unsigned int side,
7171
unsigned int * neigh_side = nullptr,
72-
bool skip_finding_check = false) const;
72+
bool skip_found_check = false) const;
7373
};
7474

7575
} // namespace libMesh

include/geom/elem.h

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -306,23 +306,6 @@ class Elem : public ReferenceCountedObject<Elem>,
306306
*/
307307
bool topologically_equal (const Elem & rhs) const;
308308

309-
310-
/**
311-
* \returns \p true if two elements occupy the same geometric region
312-
* within a given tolerance, \p false otherwise.
313-
*
314-
* Two elements are geometrically equal if they have:
315-
* - The same dimension and number of nodes;
316-
* - Centroids that coincide within the specified tolerance;
317-
* - Node coordinates that match within the tolerance, regardless
318-
* of node IDs or ordering.
319-
*
320-
* This function compares spatial coordinates rather than connectivity,
321-
* making it suitable for detecting coincident or disconnected elements
322-
* that share identical geometry but differ in node numbering.
323-
*/
324-
bool geometrically_equal (const Elem & rhs, const Real tol = TOLERANCE) const;
325-
326309
/**
327310
* \returns A const pointer to the \f$ i^{th} \f$ neighbor of this
328311
* element, or \p nullptr if \p MeshBase::find_neighbors() has not been

src/base/periodic_boundaries.C

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const Elem * PeriodicBoundaries::neighbor(boundary_id_type boundary_id,
6161
const Elem * e,
6262
unsigned int side,
6363
unsigned int * neigh_side,
64-
bool skip_finding_check) const
64+
bool skip_found_check) const
6565
{
6666
std::unique_ptr<const Elem> neigh_side_proxy;
6767

@@ -106,7 +106,7 @@ const Elem * PeriodicBoundaries::neighbor(boundary_id_type boundary_id,
106106
}
107107
}
108108

109-
if (!skip_finding_check)
109+
if (!skip_found_check)
110110
// If we should have found a periodic neighbor but didn't then
111111
// either we're on a ghosted element with a remote periodic neighbor
112112
// or we're on a mesh with an inconsistent periodic boundary.

src/mesh/unstructured_mesh.C

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,7 @@ void UnstructuredMesh::find_neighbors (const bool reset_remote_elements,
10161016
for (const auto & [id, boundary_ptr] : *db)
10171017
{
10181018
unsigned int neigh_side;
1019-
const Elem * neigh = db->neighbor(id, *point_locator, element, ms, &neigh_side, true /*skip_finding_check*/);
1019+
const Elem * neigh = db->neighbor(id, *point_locator, element, ms, &neigh_side, true /*skip_found_check*/);
10201020
if (neigh && neigh != remote_elem && neigh != element)
10211021
{
10221022
element->set_neighbor(ms, this->elem_ptr(neigh->id()));

tests/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ unit_tests_sources = \
138138
systems/constraint_operator_test.C \
139139
systems/equation_systems_test.C \
140140
systems/periodic_bc_test.C \
141+
systems/disconnected_neighbor_test.C \
141142
systems/systems_test.C \
142143
utils/parameters_test.C \
143144
utils/point_locator_test.C \

tests/Makefile.in

Lines changed: 129 additions & 24 deletions
Large diffs are not rendered by default.
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
#include <libmesh/dirichlet_boundaries.h>
2+
#include <libmesh/dof_map.h>
3+
#include <libmesh/elem.h>
4+
#include <libmesh/equation_systems.h>
5+
#include <libmesh/exodusII_io.h>
6+
#include <libmesh/function_base.h>
7+
#include <libmesh/linear_implicit_system.h>
8+
#include <libmesh/mesh.h>
9+
#include <libmesh/mesh_generation.h>
10+
#include <libmesh/numeric_vector.h>
11+
#include <libmesh/quadrature_gauss.h>
12+
#include <libmesh/sparse_matrix.h>
13+
#include <libmesh/wrapped_function.h>
14+
15+
#include "test_comm.h"
16+
#include "libmesh_cppunit.h"
17+
18+
using namespace libMesh;
19+
20+
static const Real b = 1.0;
21+
static const Real Cgap = 1.0;
22+
23+
// Boundary IDs (counterclockwise): bottom=0, right=1, top=2, left=3
24+
static const boundary_id_type left_id = 3;
25+
static const boundary_id_type right_id = 1;
26+
static const boundary_id_type interface_left_id = 5;
27+
static const boundary_id_type interface_right_id = 6;
28+
29+
Number
30+
heat_exact (const Point & p,
31+
const Parameters &,
32+
const std::string &,
33+
const std::string &)
34+
{
35+
return (p(0) < 0.5) ? p(0) : p(0) + b;
36+
}
37+
38+
// Assemble system with temperature jump interface conditions
39+
void assemble_temperature_jump(EquationSystems &es,
40+
const std::string & /*system_name*/)
41+
{
42+
const MeshBase &mesh = es.get_mesh();
43+
LinearImplicitSystem &system = es.get_system<LinearImplicitSystem>("TempJump");
44+
const DofMap &dof_map = system.get_dof_map();
45+
FEType fe_type = dof_map.variable_type(0);
46+
47+
// FE objects for volume and face integration
48+
auto fe = FEBase::build(mesh.mesh_dimension(), fe_type);
49+
auto fe_face_L = FEBase::build(mesh.mesh_dimension(), fe_type);
50+
auto fe_face_R = FEBase::build(mesh.mesh_dimension(), fe_type);
51+
52+
// Quadrature rules
53+
QGauss qrule(mesh.mesh_dimension(), fe_type.default_quadrature_order());
54+
QGauss qface(mesh.mesh_dimension() - 1, fe_type.default_quadrature_order());
55+
fe->attach_quadrature_rule(&qrule);
56+
fe_face_L->attach_quadrature_rule(&qface);
57+
fe_face_R->attach_quadrature_rule(&qface);
58+
59+
DenseMatrix<Number> Ke;
60+
DenseVector<Number> Fe;
61+
std::vector<dof_id_type> dof_indices;
62+
63+
const BoundaryInfo &boundary = mesh.get_boundary_info();
64+
const double conductance = Cgap * b; // interface conductance
65+
66+
for (const Elem *elem : mesh.active_local_element_ptr_range())
67+
{
68+
dof_map.dof_indices(elem, dof_indices);
69+
const unsigned int n_dofs = dof_indices.size();
70+
71+
Ke.resize(n_dofs, n_dofs);
72+
Fe.resize(n_dofs);
73+
Ke.zero();
74+
Fe.zero();
75+
76+
// --- Volume contribution ---
77+
fe->reinit(elem);
78+
const auto &JxW_vol = fe->get_JxW();
79+
const auto &dphi_vol = fe->get_dphi();
80+
81+
for (unsigned int qp = 0; qp < qrule.n_points(); qp++)
82+
for (unsigned int i = 0; i < n_dofs; i++)
83+
for (unsigned int j = 0; j < n_dofs; j++)
84+
Ke(i, j) += conductance * JxW_vol[qp] * (dphi_vol[i][qp] * dphi_vol[j][qp]);
85+
86+
// --- Left-side interface ---
87+
unsigned int side = boundary.side_with_boundary_id(elem, interface_left_id);
88+
if (side != libMesh::invalid_uint)
89+
{
90+
fe_face_L->reinit(elem, side);
91+
const auto &phi_L = fe_face_L->get_phi();
92+
const auto &JxW_face = fe_face_L->get_JxW();
93+
94+
const Elem *neighbor = elem->neighbor_ptr(side);
95+
libmesh_assert(neighbor);
96+
unsigned int n_side = neighbor->which_neighbor_am_i(elem);
97+
fe_face_R->reinit(neighbor, n_side);
98+
const auto &phi_R = fe_face_R->get_phi();
99+
100+
std::vector<dof_id_type> dofs_L, dofs_R;
101+
dof_map.dof_indices(elem, dofs_L);
102+
dof_map.dof_indices(neighbor, dofs_R);
103+
104+
DenseMatrix<Number> Ke_ll(n_dofs, n_dofs);
105+
DenseMatrix<Number> Ke_lr(n_dofs, dofs_R.size());
106+
107+
for (unsigned int qp = 0; qp < qface.n_points(); qp++)
108+
{
109+
for (unsigned int i = 0; i < n_dofs; i++)
110+
for (unsigned int j = 0; j < n_dofs; j++)
111+
Ke_ll(i, j) += Cgap * phi_L[i][qp] * phi_L[j][qp] * JxW_face[qp];
112+
113+
for (unsigned int i = 0; i < n_dofs; i++)
114+
for (unsigned int j = 0; j < dofs_R.size(); j++)
115+
Ke_lr(i, j) += -Cgap * phi_L[i][qp] * phi_R[j][qp] * JxW_face[qp];
116+
}
117+
118+
Ke += Ke_ll;
119+
system.matrix->add_matrix(Ke_lr, dofs_L, dofs_R);
120+
}
121+
122+
// --- Right-side interface ---
123+
side = boundary.side_with_boundary_id(elem, interface_right_id);
124+
if (side != libMesh::invalid_uint)
125+
{
126+
fe_face_R->reinit(elem, side);
127+
const auto &phi_R = fe_face_R->get_phi();
128+
const auto &JxW_face = fe_face_R->get_JxW();
129+
130+
const Elem *neighbor = elem->neighbor_ptr(side);
131+
libmesh_assert(neighbor);
132+
unsigned int n_side = neighbor->which_neighbor_am_i(elem);
133+
fe_face_L->reinit(neighbor, n_side);
134+
const auto &phi_L = fe_face_L->get_phi();
135+
136+
std::vector<dof_id_type> dofs_R, dofs_L;
137+
dof_map.dof_indices(elem, dofs_R);
138+
dof_map.dof_indices(neighbor, dofs_L);
139+
140+
DenseMatrix<Number> Ke_rr(n_dofs, n_dofs);
141+
DenseMatrix<Number> Ke_rl(n_dofs, dofs_L.size());
142+
143+
for (unsigned int qp = 0; qp < qface.n_points(); qp++)
144+
{
145+
for (unsigned int i = 0; i < n_dofs; i++)
146+
for (unsigned int j = 0; j < n_dofs; j++)
147+
Ke_rr(i, j) += Cgap * phi_R[i][qp] * phi_R[j][qp] * JxW_face[qp];
148+
149+
for (unsigned int i = 0; i < n_dofs; i++)
150+
for (unsigned int j = 0; j < dofs_L.size(); j++)
151+
Ke_rl(i, j) += -Cgap * phi_R[i][qp] * phi_L[j][qp] * JxW_face[qp];
152+
}
153+
154+
Ke += Ke_rr;
155+
system.matrix->add_matrix(Ke_rl, dofs_R, dofs_L);
156+
}
157+
158+
// Apply constraints and add local contributions
159+
dof_map.heterogenously_constrain_element_matrix_and_vector(Ke, Fe, dof_indices);
160+
system.matrix->add_matrix(Ke, dof_indices);
161+
system.rhs->add_vector(Fe, dof_indices);
162+
}
163+
164+
system.matrix->close();
165+
system.rhs->close();
166+
}
167+
168+
class DisconnectedNeighborTest : public CppUnit::TestCase {
169+
public:
170+
LIBMESH_CPPUNIT_TEST_SUITE( DisconnectedNeighborTest );
171+
CPPUNIT_TEST( testTempJump );
172+
CPPUNIT_TEST_SUITE_END();
173+
174+
private:
175+
176+
void testTempJump()
177+
{
178+
Mesh mesh(*TestCommWorld, 2);
179+
180+
// Domain: x in (0, 1), y in (0, 1)
181+
// Split into two subdomains:
182+
// Left subdomain: 0 <= x <= 0.5
183+
// Right subdomain: 0.5 <= x <= 1
184+
//
185+
// Note: Points at x = 0.5 are duplicated (same coordinates but different node IDs)
186+
// to represent an interface or discontinuity between the two subdomains.
187+
//
188+
// Coordinates layout:
189+
//
190+
// (0,1) (0.5,1)_L (0.5,1)_R (1,1)
191+
// x--------x x--------x
192+
// | | | |
193+
// | Left | Interface | Right |
194+
// | | | |
195+
// x--------x x--------x
196+
// (0,0) (0.5,0)_L (0.5,0)_R (1,0)
197+
198+
199+
// ---- Define points ----
200+
201+
// Left subdomain nodes
202+
mesh.add_point(Point(0.0, 0.0), 0); // bottom-left corner
203+
mesh.add_point(Point(0.5, 0.0), 1); // bottom-right corner of left element (interface node)
204+
mesh.add_point(Point(0.0, 1.0), 2); // top-left corner
205+
mesh.add_point(Point(0.5, 1.0), 3); // top-right corner of left element (interface node)
206+
207+
// Right subdomain nodes (duplicated interface points)
208+
mesh.add_point(Point(0.5, 0.0), 4); // bottom-left corner of right element (same coords as node 1) (interface node)
209+
mesh.add_point(Point(1.0, 0.0), 5); // bottom-right corner
210+
mesh.add_point(Point(0.5, 1.0), 6); // top-left corner of right element (same coords as node 3) (interface node)
211+
mesh.add_point(Point(1.0, 1.0), 7); // top-right corner
212+
213+
214+
// ---- Define elements ----
215+
BoundaryInfo & boundary = mesh.get_boundary_info();
216+
// Left element (element ID = 0)
217+
{
218+
Elem * elem = mesh.add_elem(Elem::build_with_id(QUAD4, 0));
219+
elem->set_node(0, mesh.node_ptr(0)); // bottom-left (0,0)
220+
elem->set_node(1, mesh.node_ptr(1)); // bottom-right (0.5,0)
221+
elem->set_node(2, mesh.node_ptr(3)); // top-right (0.5,1)
222+
elem->set_node(3, mesh.node_ptr(2)); // top-left (0,1)
223+
boundary.add_side(elem, 3, left_id); // left boundary
224+
boundary.add_side(elem, 1, interface_left_id);
225+
boundary.sideset_name(left_id) = "left_boundary";
226+
boundary.sideset_name(interface_left_id) = "interface_left";
227+
}
228+
229+
// Right element (element ID = 1)
230+
{
231+
Elem * elem = mesh.add_elem(Elem::build_with_id(QUAD4, 1));
232+
elem->set_node(0, mesh.node_ptr(4)); // bottom-left (0.5,0)_R
233+
elem->set_node(1, mesh.node_ptr(5)); // bottom-right (1,0)
234+
elem->set_node(2, mesh.node_ptr(7)); // top-right (1,1)
235+
elem->set_node(3, mesh.node_ptr(6)); // top-left (0.5,1)_R
236+
boundary.add_side(elem, 1, right_id); // right boundary
237+
boundary.add_side(elem, 3, interface_right_id);
238+
boundary.sideset_name(right_id) = "right_boundary";
239+
boundary.sideset_name(interface_right_id) = "interface_right";
240+
}
241+
242+
// This is the key testing step: inform libMesh about the disconnected boundaries
243+
// And, in `prepare_for_use()`, libMesh will set up the disconnected neighbor relationships.
244+
mesh.add_disconnected_boundaries(interface_left_id, interface_right_id);
245+
246+
// libMesh shouldn't renumber, or our based-on-initial-id
247+
// assertions later may fail.
248+
mesh.allow_renumbering(false);
249+
250+
mesh.prepare_for_use();
251+
252+
auto elem_left = mesh.elem_ptr(0);
253+
auto elem_right = mesh.elem_ptr(1);
254+
255+
LIBMESH_ASSERT_NUMBERS_EQUAL(elem_left->neighbor_ptr(1)->id(), elem_right->id(), 1e-15);
256+
LIBMESH_ASSERT_NUMBERS_EQUAL(elem_right->neighbor_ptr(3)->id(), elem_left->id(), 1e-15);
257+
258+
EquationSystems es(mesh);
259+
LinearImplicitSystem & sys =
260+
es.add_system<LinearImplicitSystem>("TempJump");
261+
262+
const unsigned int u_var = sys.add_variable("u", FEType(FIRST, LAGRANGE));
263+
264+
std::set<boundary_id_type> left_bdy { left_id };
265+
std::set<boundary_id_type> right_bdy { right_id };
266+
std::vector<unsigned int> vars (1, u_var);
267+
268+
auto left_fn = [](const Point &, const Parameters &, const std::string &, const std::string &) { return 0.0; };
269+
auto right_fn = [](const Point &, const Parameters &, const std::string &, const std::string &) { return 1.0 + b; };
270+
271+
WrappedFunction<Number> left_val(sys, left_fn);
272+
WrappedFunction<Number> right_val(sys, right_fn);
273+
274+
DirichletBoundary bc_left (left_bdy, vars, left_val);
275+
DirichletBoundary bc_right(right_bdy, vars, right_val);
276+
277+
sys.get_dof_map().add_dirichlet_boundary(bc_left);
278+
sys.get_dof_map().add_dirichlet_boundary(bc_right);
279+
280+
sys.attach_assemble_function(assemble_temperature_jump);
281+
282+
// Ensure the DofMap creates sparsity entries between neighboring elements.
283+
// Without this, PETSc may report "New nonzero at (a,b) caused a malloc."
284+
sys.get_dof_map().set_implicit_neighbor_dofs(true);
285+
286+
es.init();
287+
sys.solve();
288+
289+
ExodusII_IO(mesh).write_equation_systems("temperature_jump.e", es);
290+
291+
Parameters params;
292+
293+
for (Real x=0.; x<=1.; x+=0.05)
294+
for (Real y=0.; y<=1.; y+=0.05)
295+
{
296+
Point p(x,y);
297+
const Number exact = heat_exact(p,params,"","");
298+
const Number approx = sys.point_value(0,p);
299+
LIBMESH_ASSERT_NUMBERS_EQUAL(exact, approx, 1e-2);
300+
}
301+
}
302+
};
303+
304+
CPPUNIT_TEST_SUITE_REGISTRATION( DisconnectedNeighborTest );

0 commit comments

Comments
 (0)