Skip to content

Commit 9420238

Browse files
committed
Wider test_determine_point_ownership coverage
`test_determine_point_ownership` tests for triangle and tetra elements.
1 parent 5e1246d commit 9420238

File tree

5 files changed

+109
-52
lines changed

5 files changed

+109
-52
lines changed

cpp/dolfinx/fem/interpolate.h

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,12 +1093,7 @@ geometry::PointOwnershipData<T> create_interpolation_data(
10931093
x[3 * i + j] = coords[i + j * num_points];
10941094

10951095
// Determine ownership of each point
1096-
const int tdim = mesh1.topology()->dim();
1097-
auto cell_map1 = mesh1.topology()->index_map(tdim);
1098-
const std::int32_t num_cells1 = cell_map1->size_local();
1099-
std::vector<std::int32_t> cells1(num_cells1, 0);
1100-
std::iota(cells1.begin(), cells1.end(), 0);
1101-
return geometry::determine_point_ownership<T>(mesh1, x, cells1, padding);
1096+
return geometry::determine_point_ownership<T>(mesh1, x, padding);
11021097
}
11031098

11041099
/// @brief Interpolate a finite element Function defined on a mesh to a

cpp/dolfinx/geometry/utils.h

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -668,11 +668,7 @@ graph::AdjacencyList<std::int32_t> compute_colliding_cells(
668668
/// Each bounding box of the mesh is padded with this amount, to increase
669669
/// the number of candidates, avoiding rounding errors in determining the owner
670670
/// of a point if the point is on the surface of a cell in the mesh.
671-
/// @return Point ownership data with fields `src_owner`, `dest_owner`, `dest_points`, `dest_cells`,
672-
/// where `src_owner` is a list of ranks corresponding to the input
673-
/// points. `dest_owner` is a list of ranks corresponding to `dest_points`,
674-
/// the points that this process owns. `dest_cells` contains the
675-
/// corresponding cell for each entry in dest_points.
671+
/// @return Point ownership data.
676672
///
677673
/// @note `dest_owner` is sorted
678674
/// @note `src_owner` is -1 if no colliding process is found
@@ -685,14 +681,22 @@ graph::AdjacencyList<std::int32_t> compute_colliding_cells(
685681
template <std::floating_point T>
686682
PointOwnershipData<T> determine_point_ownership(const mesh::Mesh<T>& mesh,
687683
std::span<const T> points,
688-
std::span<const std::int32_t> cells,
689-
T padding = 0.0)
684+
T padding,
685+
std::span<const std::int32_t> cells = {})
690686
{
691687
MPI_Comm comm = mesh.comm();
692688

689+
const int tdim = mesh.topology()->dim();
690+
691+
std::vector<std::int32_t> local_cells;
692+
if (cells.empty()) {
693+
auto cell_map = mesh.topology()->index_map(tdim);
694+
local_cells.resize(cell_map->size_local());
695+
std::iota(local_cells.begin(), local_cells.end(), 0);
696+
cells = std::span<const std::int32_t>(local_cells.data(), local_cells.size());
697+
}
693698
// Create a global bounding-box tree to find candidate processes with
694699
// cells that could collide with the points
695-
const int tdim = mesh.topology()->dim();
696700
BoundingBoxTree bb(mesh, tdim, cells, padding);
697701
BoundingBoxTree global_bbtree = bb.create_global_tree(comm);
698702

python/dolfinx/geometry.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ def compute_distance_gjk(
275275
def determine_point_ownership(
276276
mesh: Mesh,
277277
points: npt.NDArray[np.floating],
278+
padding: float,
278279
cells: typing.Optional[npt.NDArray[np.int32]] = None,
279-
padding: float = 0.0,
280280
) -> PointOwnershipData:
281281
"""Build point ownership data for a mesh-points pair.
282282
@@ -287,12 +287,12 @@ def determine_point_ownership(
287287
Args:
288288
mesh: The mesh
289289
points: Points to check for collision (``shape=(num_points, gdim)``)
290-
cells: Cells to check for ownership
291-
If ``None`` then all cells are considered.
292290
padding: Amount of absolute padding of bounding boxes of the mesh.
293291
Each bounding box of the mesh is padded with this amount, to increase
294292
the number of candidates, avoiding rounding errors in determining the owner
295293
of a point if the point is on the surface of a cell in the mesh.
294+
cells: Cells to check for ownership
295+
If ``None`` then all cells are considered.
296296
297297
Returns:
298298
Point ownership data
@@ -305,7 +305,6 @@ def determine_point_ownership(
305305
A large padding value will increase the run-time of the code by orders
306306
of magnitude. General advice is to use a padding on the scale of the
307307
cell size.
308-
309308
"""
310309
if cells is None:
311310
map = mesh.topology.index_map(mesh.topology.dim)

python/dolfinx/wrappers/geometry.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,10 @@ void declare_bbtree(nb::module_& m, std::string type)
187187
const std::size_t p_s0 = points.ndim() == 1 ? 1 : points.shape(0);
188188
std::span<const T> _p(points.data(), 3 * p_s0);
189189
return dolfinx::geometry::determine_point_ownership<T>(mesh, _p,
190-
std::span(cells.data(), cells.size()),
191-
padding);
190+
padding,
191+
std::span(cells.data(), cells.size()));
192192
},
193-
nb::arg("mesh"), nb::arg("points"), nb::arg("cells"), nb::arg("padding"),
193+
nb::arg("mesh"), nb::arg("points"), nb::arg("padding"), nb::arg("cells"),
194194
"Compute point ownership data for mesh-points pair.");
195195

196196
std::string pod_pyclass_name = "PointOwnershipData_" + type;

python/test/unit/geometry/test_bounding_box_tree.py

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
)
2525
from dolfinx.mesh import (
2626
CellType,
27+
compute_midpoints,
2728
create_box,
2829
create_unit_cube,
2930
create_unit_interval,
@@ -501,56 +502,107 @@ def test_surface_bbtree_collision(dtype):
501502

502503

503504
@pytest.mark.parametrize("dim", [2, 3])
505+
@pytest.mark.parametrize("affine", [True, False])
504506
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
505-
def test_determine_point_ownership(dim, dtype):
507+
def test_determine_point_ownership(dim, affine, dtype):
506508
"""Find point owners (ranks and cells) using bounding box trees + global communication
507509
and compare to point ownership data results."""
508510
comm = MPI.COMM_WORLD
509511
rank = comm.Get_rank()
512+
mpi_dtype = MPI.DOUBLE if dtype == np.float64 else MPI.FLOAT
510513

511514
tdim = dim
512515
num_cells_side = 4
513-
h = dtype(1.0 / num_cells_side)
514516
if tdim == 2:
515-
mesh = create_unit_square(
516-
MPI.COMM_WORLD, num_cells_side, num_cells_side, CellType.quadrilateral, dtype=dtype
517-
)
517+
ct = CellType.triangle if affine else CellType.quadrilateral
518+
mesh = create_unit_square(MPI.COMM_WORLD, num_cells_side, num_cells_side, ct, dtype=dtype)
518519
else:
520+
ct = CellType.tetrahedron if affine else CellType.hexahedron
519521
mesh = create_unit_cube(
520522
MPI.COMM_WORLD,
521523
num_cells_side,
522524
num_cells_side,
523525
num_cells_side,
524-
CellType.hexahedron,
526+
ct,
525527
dtype=dtype,
526528
)
527529
cell_map = mesh.topology.index_map(tdim)
528530

529531
tree = bb_tree(mesh, mesh.topology.dim, np.arange(cell_map.size_local))
530-
num_global_cells = num_cells_side**dim
532+
num_global_cells = num_cells_side**tdim
533+
if affine:
534+
num_global_cells *= 2 * (3 ** (tdim - 2))
535+
local_midpoints = compute_midpoints(
536+
mesh, tdim, np.arange(mesh.topology.index_map(tdim).size_local)
537+
)
538+
midpoints_per_rank = np.zeros(comm.size, dtype=np.int32)
539+
midpoints_offsets = np.zeros(comm.size, dtype=np.int32)
540+
comm.Allgather(np.array([local_midpoints.shape[0]], dtype=np.int32), midpoints_per_rank)
541+
midpoints_offsets[1:] = np.cumsum(midpoints_per_rank[:-1])
531542
all_midpoints = np.zeros((num_global_cells, 3), dtype=dtype)
532-
counter = 0
533-
Xs = [(i + 0.5) * h for i in range(num_cells_side)]
534-
Ys = Xs
535-
Zs = [0.0] if dim == 2 else Xs
536-
for x in Xs:
537-
for y in Ys:
538-
for z in Zs:
539-
all_midpoints[counter, :] = np.array([x, y, z], dtype=dtype)
540-
counter += 1
541-
# Since ghost cells are left out and the points considered are midpoints
542-
# of cells, they are only contained in a single process / cell
543-
# Moreover, the bounding boxes of the elements correspond to the elements,
544-
# hence the tree collisions are the exact collisions
543+
comm.Allgatherv(
544+
local_midpoints, [all_midpoints, midpoints_per_rank * 3, midpoints_offsets * 3, mpi_dtype]
545+
)
546+
# Find potential owner cells
545547
tree_col = compute_collisions_points(tree, all_midpoints)
548+
549+
mesh.topology.create_connectivity(tdim - 1, 0)
550+
mesh.topology.create_connectivity(0, tdim)
551+
cfc = mesh.topology.connectivity(tdim, tdim - 1)
552+
fpc = mesh.topology.connectivity(tdim - 1, 0)
553+
554+
# Narrow it down to a single owner cell
555+
def is_inside(mesh, icell, point):
556+
fdim = tdim - 1
557+
is_inside = True
558+
cpoints = mesh.geometry.x[mesh.geometry.dofmap[icell, :]] # cell points
559+
ccentroid = np.average(cpoints, axis=0) # cell centroid
560+
for ifacet in cfc.links(icell):
561+
fpoints_indices = _cpp.mesh.entities_to_geometry(
562+
mesh._cpp_object,
563+
0,
564+
fpc.links(ifacet),
565+
False,
566+
)
567+
fpoints_indices = fpoints_indices.reshape(fpoints_indices.size)
568+
fpoints = mesh.geometry.x[fpoints_indices]
569+
fcentroid = np.average(fpoints, axis=0) # facet centroid
570+
# Compute facet normal pointing to outside of owner cell
571+
normal = np.zeros(3, dtype=dtype)
572+
facet_vector1 = fpoints[1, :] - fpoints[0, :]
573+
if fdim == 1:
574+
normal[0] = -facet_vector1[1]
575+
normal[1] = +facet_vector1[0]
576+
elif fdim == 2:
577+
facet_vector2 = fpoints[2, :] - fpoints[0, :]
578+
normal = np.cross(facet_vector1, facet_vector2)
579+
else:
580+
raise ValueError("Unexpected facet dimension.")
581+
normal /= np.linalg.norm(normal)
582+
# Re-align if pointing to inside the parent cell
583+
normal = -normal if (np.dot((ccentroid - fcentroid), normal) > 0) else normal
584+
# Test the point
585+
signed_distance = np.dot((point - fcentroid), normal)
586+
if signed_distance > 1e-9:
587+
is_inside = False
588+
break
589+
return is_inside
590+
546591
processwise_owners = np.zeros(2 * num_global_cells, dtype=np.int32)
547592
owners = np.empty_like(processwise_owners)
548593
for ipoint in range(num_global_cells):
549-
cell = tree_col.links(ipoint)
550-
if len(cell) > 0:
594+
potential_owners = tree_col.links(ipoint)
595+
owner_cells = []
596+
for cell in potential_owners:
597+
if is_inside(mesh, cell, all_midpoints[ipoint, :]):
598+
owner_cells.append(cell)
599+
if owner_cells:
600+
assert len(owner_cells) == 1
551601
processwise_owners[2 * ipoint] = rank
552-
processwise_owners[2 * ipoint + 1] = cell[0]
602+
processwise_owners[2 * ipoint + 1] = owner_cells[0]
553603

604+
# Since ghost cells are left out and the points considered are midpoints
605+
# of cells, they are only contained in a single process / cell
554606
# The value at a given index is null if it doesn't correspond
555607
# to the current process.
556608
# We can sum the processwise arrays to obtain a global array
@@ -591,7 +643,7 @@ def check_po(po: PointOwnershipData, src_points, ownership_data, global_dest_own
591643
(iglobal, processor, _) = data
592644
if processor == rank:
593645
found = False
594-
point = np.array(point, dtype)
646+
point = np.array(point, dtype=dtype)
595647
for jpoint in dest_points_indices:
596648
found = np.allclose(point, dest_points[jpoint])
597649
if found:
@@ -630,26 +682,33 @@ def compute_global_owners(N, start, stop):
630682
# All cells
631683
points, start, stop = set_local_range(all_midpoints)
632684
owners = compute_global_owners(np.int64(all_midpoints.shape[0]), start, stop)
633-
all_cells = np.arange(cell_map.size_local, dtype=np.int32)
634-
po = determine_point_ownership(mesh, points, all_cells)
685+
all_cells = np.arange(cell_map.size_local, dtype=dtype)
686+
po = determine_point_ownership(mesh, points, 0.0, all_cells)
635687

636688
check_po(po, points, ownership_data, owners)
637689

638690
# Left half
639-
num_left_cells = np.rint((num_cells_side**dim) / 2).astype(np.int32)
640-
left_midpoints = all_midpoints[np.arange(num_left_cells), :]
691+
num_left_cells = np.rint(num_global_cells / 2).astype(np.int32)
692+
left_midpoints = np.zeros((num_left_cells, 3), dtype=dtype)
693+
counter = 0
694+
indices_left = []
695+
for ipoint in range(num_global_cells):
696+
if all_midpoints[ipoint, 0] <= 0.5:
697+
left_midpoints[counter] = all_midpoints[ipoint]
698+
indices_left.append(ipoint)
699+
counter += 1
641700
points, start, stop = set_local_range(left_midpoints)
642701
owners = compute_global_owners(np.int64(all_midpoints.shape[0]), start, stop)
643702
left_cells = locate_entities(mesh, tdim, lambda x: x[0] <= 0.5)
644703
left_cells = np.array(
645704
[cell for cell in left_cells if cell < cell_map.size_local], dtype=np.int32
646705
) # Filter out ghost cells
647-
lpo = determine_point_ownership(mesh, points, left_cells)
706+
lpo = determine_point_ownership(mesh, points, 0.0, left_cells)
648707

649708
left_ownership_data = {}
650-
for ipoint in range(num_left_cells):
709+
for idx, ipoint in enumerate(indices_left):
651710
left_ownership_data[tuple(all_midpoints[ipoint])] = (
652-
ipoint,
711+
idx,
653712
owner_ranks[ipoint],
654713
owner_cells[ipoint],
655714
)

0 commit comments

Comments
 (0)