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

Fix point-on-plane detection for NumPy 1.19.0 in Linux #202

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
28 changes: 25 additions & 3 deletions polliwog/plane/_plane_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .._common.shape import check_shape_any, columnize

__all__ = [
"EPSILON_COPLANAR",
"plane_normal_from_points",
"plane_equation_from_points",
"normal_and_offset_from_plane_equations",
Expand All @@ -12,6 +13,8 @@
"mirror_point_across_plane",
]

EPSILON_COPLANAR = 1e-12


def plane_normal_from_points(points, normalize=True):
"""
Expand Down Expand Up @@ -62,20 +65,39 @@ def normal_and_offset_from_plane_equations(plane_equations):
return normal, offset


def signed_distance_to_plane(points, plane_equations):
def signed_distance_to_plane(points, plane_equations, epsilon=EPSILON_COPLANAR):
"""
Return the signed distances from each point to the corresponding plane.

For convenience, can also be called with a single point and a single
plane.

Args:
points (np.ndarray): The points of interest as `kx3`.
plane_equations (np.ndarray): The plane equations as `kx4`.
epsilon (float): Return 0 for points within this distance of the
plane. Pass `None` to skip this check.

Return:
The signed distance, or for stacked inputs, an array of signed
distances.
"""
k = check_shape_any(points, (3,), (-1, 3), name="points")
check_shape_any(
plane_equations, (4,), (-1 if k is None else k, 4), name="plane_equations"
)

normals, offsets = normal_and_offset_from_plane_equations(plane_equations)
return vg.dot(points, normals) + offsets
result = vg.dot(points, normals) + offsets

if epsilon is not None:
if np.isscalar(result):
if np.abs(result) < epsilon:
result = 0.0
else:
result[np.abs(result) < epsilon] = 0.0

return result


def translate_points_along_plane_normal(points, plane_equations, factor):
Expand All @@ -95,7 +117,7 @@ def translate_points_along_plane_normal(points, plane_equations, factor):
assert isinstance(factor, numbers.Real)

# Translate the point back to the plane along the normal.
signed_distance = signed_distance_to_plane(points, plane_equations)
signed_distance = signed_distance_to_plane(points, plane_equations, epsilon=None)
normals, _ = normal_and_offset_from_plane_equations(plane_equations)

if np.isscalar(signed_distance):
Expand Down
22 changes: 18 additions & 4 deletions polliwog/plane/_plane_object.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import vg
from ._plane_functions import (
EPSILON_COPLANAR,
mirror_point_across_plane,
plane_normal_from_points,
project_point_to_plane,
Expand Down Expand Up @@ -138,14 +139,25 @@ def flipped(self):
"""
return Plane(point_on_plane=self._r0, unit_normal=-self._n)

def sign(self, points):
def sign(self, points, epsilon=EPSILON_COPLANAR):
"""
Given an array of points, return an array with +1 for points in front
of the plane (in the direction of the normal), -1 for points behind
the plane (away from the normal), and 0 for points on the plane.

Args:
points (np.arraylike): A 3D point or a `kx3` stack of points.
epsilon (float): Return 0 for points within this distance of the
plane.

Returns:
depends:

- Given a single 3D point, the distance as a NumPy scalar.
- Given a `kx3` stack of points, an `k` array of distances.

"""
return np.sign(self.signed_distance(points))
return np.sign(self.signed_distance(points, epsilon=epsilon))

def points_in_front(self, points, inverted=False, ret_indices=False):
"""
Expand Down Expand Up @@ -203,21 +215,23 @@ def points_on_or_in_front(self, points, inverted=False, ret_indices=False):

return indices if ret_indices else points[indices]

def signed_distance(self, points):
def signed_distance(self, points, epsilon=EPSILON_COPLANAR):
"""
Returns the signed distances to the given points or the signed
distance to a single point.

Args:
points (np.arraylike): A 3D point or a `kx3` stack of points.
epsilon (float): Return 0 for points within this distance of the
plane.

Returns:
depends:

- Given a single 3D point, the distance as a NumPy scalar.
- Given a `kx3` stack of points, an `k` array of distances.
"""
return signed_distance_to_plane(points, self.equation)
return signed_distance_to_plane(points, self.equation, epsilon=epsilon)

def distance(self, points):
return np.absolute(self.signed_distance(points))
Expand Down
2 changes: 1 addition & 1 deletion polliwog/polyline/_polyline_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ def intersect_plane(self, plane, ret_edge_indices=False):
"""
# TODO: Refactor to use `..plane.intersections.intersect_segment_with_plane()`.
# Identify edges with endpoints that are not on the same side of the plane
signed_distances = plane.signed_distance(self.v)
signed_distances = plane.signed_distance(self.v, epsilon=None)
which_es = np.abs(np.sign(signed_distances)[self.e].sum(axis=1)) != 2
# For the intersecting edges, compute the distance of the endpoints to the plane
endpoint_distances = np.abs(signed_distances[self.e[which_es]])
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
numpy<1.19.0
numpy
Copy link

Choose a reason for hiding this comment

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

I think this will default to the latest published numpy. Does it make sense to tag numpy at version 1.19?

Copy link
Member Author

Choose a reason for hiding this comment

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

In applications I always pin NumPy for safety's sake, however in libraries, I usually don't bother, since issues like this one are fairly rare (once every couple years) and version mismatches will cause warnings downstream at install time.

In other words, it's less work to leave the NumPy version number loose.

When a library depends on a new feature in a dependency we can use >=, which of course does not cause warnings with later versions.

ounce>=1.1.0,<2.0
vg>=1.5.0
2 changes: 1 addition & 1 deletion requirements_doc.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Imported from code.
numpy<1.19.0
numpy
ounce>=1.1.0,<2.0
vg>=1.5.0

Expand Down