Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve python exposure of determine_point_ownership #3344

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cpp/dolfinx/fem/interpolate.h
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,12 @@ geometry::PointOwnershipData<T> create_interpolation_data(
x[3 * i + j] = coords[i + j * num_points];

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

/// @brief Interpolate a finite element Function defined on a mesh to a
Expand Down
20 changes: 8 additions & 12 deletions cpp/dolfinx/geometry/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -663,40 +663,36 @@ graph::AdjacencyList<std::int32_t> compute_colliding_cells(
/// @param[in] mesh The mesh
/// @param[in] points Points to check for collision (`shape=(num_points,
/// 3)`). Storage is row-major.
/// @param[in] cells Cells to check for ownership
jhale marked this conversation as resolved.
Show resolved Hide resolved
/// @param[in] padding Amount of absolute padding of bounding boxes of the mesh.
/// Each bounding box of the mesh is padded with this amount, to increase
/// the number of candidates, avoiding rounding errors in determining the owner
/// of a point if the point is on the surface of a cell in the mesh.
/// @return Tuple `(src_owner, dest_owner, dest_points, dest_cells)`,
/// where src_owner is a list of ranks corresponding to the input
/// points. dest_owner is a list of ranks corresponding to dest_points,
/// the points that this process owns. dest_cells contains the
/// @return Point ownership data with fields `src_owner`, `dest_owner`, `dest_points`, `dest_cells`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this documentation should be moved out to the struct itself.

/// where `src_owner` is a list of ranks corresponding to the input
/// points. `dest_owner` is a list of ranks corresponding to `dest_points`,
/// the points that this process owns. `dest_cells` contains the
/// corresponding cell for each entry in dest_points.
///
/// @note `dest_owner` is sorted
/// @note Returns -1 if no colliding process is found
/// @note `src_owner` is -1 if no colliding process is found
/// @note dest_points is flattened row-major, shape `(dest_owner.size(),
/// 3)`
/// @note Only looks through cells owned by the process
/// @note A large padding value can increase the runtime of the function by
/// orders of magnitude, because for non-colliding cells
/// one has to determine the closest cell among all processes with an
/// intersecting bounding box, which is an expensive operation to perform.
template <std::floating_point T>
PointOwnershipData<T> determine_point_ownership(const mesh::Mesh<T>& mesh,
std::span<const T> points,
T padding)
std::span<const std::int32_t> cells,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This argument could be converted to std::optional - can be done on later patch.

T padding = 0.0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this argument is critical to the behaviour of the function it is best to leave it as a non-default. Then user's need to engage with it, rather than not realise it exists.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

padding is forwarded to BoundingBoxTree constructor where it is also default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ordinary-slim Please make it non-default argument in both bounding box tree and here.

{
MPI_Comm comm = mesh.comm();

// Create a global bounding-box tree to find candidate processes with
// cells that could collide with the points
const int tdim = mesh.topology()->dim();
auto cell_map = mesh.topology()->index_map(tdim);
const std::int32_t num_cells = cell_map->size_local();
// NOTE: Should we send the cells in as input?
std::vector<std::int32_t> cells(num_cells, 0);
std::iota(cells.begin(), cells.end(), 0);
BoundingBoxTree bb(mesh, tdim, cells, padding);
BoundingBoxTree global_bbtree = bb.create_global_tree(comm);

Expand Down
5 changes: 3 additions & 2 deletions python/dolfinx/fem/bcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from dolfinx.fem.function import Constant, Function

import numpy as np
import numpy.typing as npt

import dolfinx
from dolfinx import cpp as _cpp
Expand Down Expand Up @@ -57,7 +58,7 @@ def locate_dofs_geometrical(
def locate_dofs_topological(
V: typing.Union[dolfinx.fem.FunctionSpace, typing.Iterable[dolfinx.fem.FunctionSpace]],
entity_dim: int,
entities: numpy.typing.NDArray[np.int32],
entities: npt.NDArray[np.int32],
remote: bool = True,
) -> np.ndarray:
"""Locate degrees-of-freedom belonging to mesh entities topologically.
Expand Down Expand Up @@ -135,7 +136,7 @@ def function_space(self) -> dolfinx.fem.FunctionSpace:

def dirichletbc(
value: typing.Union[Function, Constant, np.ndarray],
dofs: numpy.typing.NDArray[np.int32],
dofs: npt.NDArray[np.int32],
V: typing.Optional[dolfinx.fem.FunctionSpace] = None,
) -> DirichletBC:
"""Create a representation of Dirichlet boundary condition which
Expand Down
2 changes: 1 addition & 1 deletion python/dolfinx/fem/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def integral_types(self):

def get_integration_domains(
integral_type: IntegralType,
subdomain: typing.Optional[typing.Union[MeshTags, list[tuple[int, np.ndarray]]]],
subdomain: typing.Optional[typing.Union[MeshTags, list[tuple[int, npt.NDArray[np.int32]]]]],
subdomain_ids: list[int],
) -> list[tuple[int, np.ndarray]]:
"""Get integration domains from subdomain data.
Expand Down
10 changes: 5 additions & 5 deletions python/dolfinx/fem/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class Expression:
def __init__(
self,
e: ufl.core.expr.Expr,
X: np.ndarray,
X: typing.Union[npt.NDArray[np.float32], npt.NDArray[np.float64]],
comm: typing.Optional[_MPI.Comm] = None,
form_compiler_options: typing.Optional[dict] = None,
jit_options: typing.Optional[dict] = None,
Expand Down Expand Up @@ -196,7 +196,7 @@ def _create_expression(dtype):
def eval(
self,
mesh: Mesh,
entities: np.ndarray,
entities: npt.NDArray[np.int32],
values: typing.Optional[np.ndarray] = None,
) -> np.ndarray:
"""Evaluate Expression on entities.
Expand Down Expand Up @@ -412,8 +412,8 @@ def interpolate_nonmatching(
def interpolate(
self,
u0: typing.Union[typing.Callable, Expression, Function],
cells0: typing.Optional[np.ndarray] = None,
cells1: typing.Optional[np.ndarray] = None,
cells0: typing.Optional[npt.NDArray[np.int32]] = None,
cells1: typing.Optional[npt.NDArray[np.int32]] = None,
) -> None:
"""Interpolate an expression.

Expand Down Expand Up @@ -582,7 +582,7 @@ def _create_dolfinx_element(
comm: _MPI.Intracomm,
cell_type: _cpp.mesh.CellType,
ufl_e: ufl.FiniteElementBase,
dtype: np.dtype,
dtype: npt.DTypeLike,
) -> typing.Union[_cpp.fem.FiniteElement_float32, _cpp.fem.FiniteElement_float64]:
"""Create a DOLFINx element from a basix.ufl element."""
if np.issubdtype(dtype, np.float32):
Expand Down
43 changes: 43 additions & 0 deletions python/dolfinx/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,46 @@ def compute_distance_gjk(

"""
return _cpp.geometry.compute_distance_gjk(p, q)


def determine_point_ownership(
mesh: Mesh,
points: npt.NDArray[np.floating],
cells: typing.Optional[npt.NDArray[np.int32]] = None,
padding: float = 0.0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this argument is critical to the behaviour of the function it is best to leave it as a non-default. Then user's need to engage with it, rather than not realise it exists.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am bit reluctant to do this change since if cells is left as default then the argument order mesh, points, padding, cells feels a bit awkward to me. Moreover bb_tree also has a default value for padding.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jorgensd @garth-wells What do you think about this padding argument? My view is that as it's critical to the operation of the bounding box and we cannot provide a sensible default, it's better not to have it hidden as a default parameter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a crucial parameter. We have already rewritten the API for non-matching interpolation since 0.7, so I think it is fine to make it required.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ordinary-slim Please make it non-default argument in both bounding box tree and here.

) -> PointOwnershipData:
"""Build point ownership data for a mesh-points pair.

First, potential collisions are found by computing intersections
between the bounding boxes of the cells and the set of points.
Then, actual containment pairs are determined using the GJK algorithm.

Args:
mesh: The mesh
points: Points to check for collision (``shape=(num_points, gdim)``)
cells: Cells to check for ownership
If ``None`` then all cells are considered.
padding: Amount of absolute padding of bounding boxes of the mesh.
Each bounding box of the mesh is padded with this amount, to increase
the number of candidates, avoiding rounding errors in determining the owner
of a point if the point is on the surface of a cell in the mesh.

Returns:
Point ownership data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the fields of PointOwnershipData documented in the Python wrapped class?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def src_owner(self) -> npt.NDArray[np.int32]:
"""Ranks owning each point sent into ownership determination for current process"""
return self._cpp_object.src_owner
def dest_owner(self) -> npt.NDArray[np.int32]:
"""Ranks that sent `dest_points` to current process"""
return self._cpp_object.dest_owners
def dest_points(self) -> npt.NDArray[np.floating]:
"""Points owned by current rank"""
return self._cpp_object.dest_points
def dest_cells(self) -> npt.NDArray[np.int32]:
"""Cell indices (local to process) where each entry of `dest_points` is located"""
return self._cpp_object.dest_cells

The doc is the same as on the c++ side.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks for pointing it out to me.

The C++ documentation for the return argument describing PointOwnershipData looks over-specified given the struct is now properly documented.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed most of it.


Note:
`dest_owner` is sorted
jorgensd marked this conversation as resolved.
Show resolved Hide resolved
jorgensd marked this conversation as resolved.
Show resolved Hide resolved

`src_owner` is -1 if no colliding process is found
jorgensd marked this conversation as resolved.
Show resolved Hide resolved

A large padding value will increase the run-time of the code by orders
of magnitude. General advice is to use a padding on the scale of the
cell size.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove new line?

"""
if cells is None:
map = mesh.topology.index_map(mesh.topology.dim)
cells = np.arange(map.size_local, dtype=np.int32)
return PointOwnershipData(
_cpp.geometry.determine_point_ownership(mesh._cpp_object, points, cells, padding)
)
8 changes: 7 additions & 1 deletion python/dolfinx/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

from __future__ import annotations

import typing

import numpy as np
import numpy.typing as npt

from dolfinx import cpp as _cpp
from dolfinx.cpp.graph import partitioner
Expand All @@ -31,7 +34,10 @@
__all__ = ["adjacencylist", "partitioner"]


def adjacencylist(data: np.ndarray, offsets=None):
def adjacencylist(
data: typing.Union[npt.NDArray[np.int32], npt.NDArray[np.int64]],
offsets: typing.Optional[npt.NDArray[np.int32]] = None,
):
"""Create an AdjacencyList for int32 or int64 datasets.

Args:
Expand Down
12 changes: 7 additions & 5 deletions python/dolfinx/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def compute_midpoints(mesh: Mesh, dim: int, entities: npt.NDArray[np.int32]):
return _cpp.mesh.compute_midpoints(mesh._cpp_object, dim, entities)


def locate_entities(mesh: Mesh, dim: int, marker: typing.Callable) -> np.ndarray:
def locate_entities(mesh: Mesh, dim: int, marker: typing.Callable) -> npt.NDArray[np.int32]:
"""Compute mesh entities satisfying a geometric marking function.

Args:
Expand All @@ -239,7 +239,9 @@ def locate_entities(mesh: Mesh, dim: int, marker: typing.Callable) -> np.ndarray
return _cpp.mesh.locate_entities(mesh._cpp_object, dim, marker)


def locate_entities_boundary(mesh: Mesh, dim: int, marker: typing.Callable) -> np.ndarray:
def locate_entities_boundary(
mesh: Mesh, dim: int, marker: typing.Callable
) -> npt.NDArray[np.int32]:
"""Compute mesh entities that are connected to an owned boundary
facet and satisfy a geometric marking function.

Expand Down Expand Up @@ -313,7 +315,7 @@ def transfer_meshtag(


def refine(
mesh: Mesh, edges: typing.Optional[np.ndarray] = None, redistribute: bool = True
mesh: Mesh, edges: typing.Optional[npt.NDArray[np.int32]] = None, redistribute: bool = True
) -> Mesh:
"""Refine a mesh.

Expand All @@ -337,7 +339,7 @@ def refine(

def refine_interval(
mesh: Mesh,
cells: typing.Optional[np.ndarray] = None,
cells: typing.Optional[npt.NDArray[np.int32]] = None,
redistribute: bool = True,
ghost_mode: GhostMode = GhostMode.shared_facet,
) -> tuple[Mesh, npt.NDArray[np.int32]]:
Expand Down Expand Up @@ -368,7 +370,7 @@ def refine_interval(

def refine_plaza(
mesh: Mesh,
edges: typing.Optional[np.ndarray] = None,
edges: typing.Optional[npt.NDArray[np.int32]] = None,
redistribute: bool = True,
option: RefinementOption = RefinementOption.none,
) -> tuple[Mesh, npt.NDArray[np.int32], npt.NDArray[np.int32]]:
Expand Down
9 changes: 7 additions & 2 deletions python/dolfinx/wrappers/geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,18 @@ void declare_bbtree(nb::module_& m, std::string type)
nb::arg("mesh"), nb::arg("dim"), nb::arg("indices"), nb::arg("points"));
m.def("determine_point_ownership",
[](const dolfinx::mesh::Mesh<T>& mesh,
nb::ndarray<const T, nb::c_contig> points, const T padding)
nb::ndarray<const T, nb::c_contig> points,
nb::ndarray<const std::int32_t, nb::ndim<1>, nb::c_contig> cells,
const T padding)
{
const std::size_t p_s0 = points.ndim() == 1 ? 1 : points.shape(0);
std::span<const T> _p(points.data(), 3 * p_s0);
return dolfinx::geometry::determine_point_ownership<T>(mesh, _p,
std::span(cells.data(), cells.size()),
padding);
});
},
nb::arg("mesh"), nb::arg("points"), nb::arg("cells"), nb::arg("padding"),
"Compute point ownership data for mesh-points pair.");

std::string pod_pyclass_name = "PointOwnershipData_" + type;
nb::class_<dolfinx::geometry::PointOwnershipData<T>>(m,
Expand Down
Loading
Loading