Skip to content

Commit 3398ffb

Browse files
authored
Added named_arrays.geometry.point_in_polygon() to test if a given point is inside a polygon specified by its vertices. (#147)
1 parent 00421f3 commit 3398ffb

File tree

8 files changed

+361
-2
lines changed

8 files changed

+361
-2
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
[![tests](https://github.com/sun-data/named-arrays/actions/workflows/tests.yml/badge.svg)](https://github.com/sun-data/named-arrays/actions/workflows/tests.yml)
44
[![codecov](https://codecov.io/gh/sun-data/named-arrays/graph/badge.svg?token=1GhdcsgwO0)](https://codecov.io/gh/sun-data/named-arrays)
5-
[![Ruff](https://github.com/sun-data/named-arrays/actions/workflows/ruff.yml/badge.svg?branch=main)](https://github.com/sun-data/named-arrays/actions/workflows/ruff.yml)[![Documentation Status](https://readthedocs.org/projects/named-arrays/badge/?version=latest)](https://named-arrays.readthedocs.io/en/latest/?badge=latest)
5+
[![Ruff](https://github.com/sun-data/named-arrays/actions/workflows/ruff.yml/badge.svg?branch=main)](https://github.com/sun-data/named-arrays/actions/workflows/ruff.yml)
6+
[![Documentation Status](https://readthedocs.org/projects/named-arrays/badge/?version=latest)](https://named-arrays.readthedocs.io/en/latest/?badge=latest)
67
[![PyPI version](https://badge.fury.io/py/named-arrays.svg)](https://badge.fury.io/py/named-arrays)
78

89
`named-arrays` is an implementation of a [named tensor](https://nlp.seas.harvard.edu/NamedTensor), which assigns names to each axis of an n-dimensional array such as a numpy array.

named_arrays/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from . import ndfilters
3636
from . import colorsynth
3737
from . import numexpr
38+
from . import geometry
3839
from ._core import (
3940
QuantityLike,
4041
StartT,
@@ -402,6 +403,7 @@
402403
"ndfilters",
403404
"colorsynth",
404405
"numexpr",
406+
"geometry",
405407
"QuantityLike",
406408
"StartT",
407409
"StopT",

named_arrays/_scalars/scalar_named_array_functions.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import regridding
1515
import named_arrays as na
1616
from . import scalars
17+
from ..geometry._point_in_polygon import _point_in_polygon_quantity
1718

1819
__all__ = [
1920
"ASARRAY_LIKE_FUNCTIONS",
@@ -2082,3 +2083,53 @@ def evaluate(
20822083
ndarray=result,
20832084
axes=axes,
20842085
)
2086+
2087+
2088+
@_implements(na.geometry.point_in_polygon)
2089+
def point_in_polygon(
2090+
x: na.AbstractScalarArray,
2091+
y: na.AbstractScalarArray,
2092+
vertices_x: na.AbstractScalarArray,
2093+
vertices_y: na.AbstractScalarArray,
2094+
axis: str,
2095+
) -> na.ScalarArray:
2096+
2097+
try:
2098+
x = scalars._normalize(x)
2099+
y = scalars._normalize(y)
2100+
vertices_x = scalars._normalize(vertices_x)
2101+
vertices_y = scalars._normalize(vertices_y)
2102+
except scalars.ScalarTypeError: # pragma: nocover
2103+
return NotImplemented
2104+
2105+
shape_vertices = na.shape_broadcasted(
2106+
x,
2107+
y,
2108+
vertices_x,
2109+
vertices_y,
2110+
)
2111+
2112+
shape = {a: shape_vertices[a] for a in shape_vertices if a != axis}
2113+
2114+
num_vertices = shape_vertices[axis]
2115+
shape_vertices = shape.copy()
2116+
shape_vertices[axis] = num_vertices
2117+
2118+
x = na.broadcast_to(x, shape)
2119+
y = na.broadcast_to(y, shape)
2120+
vertices_x = na.broadcast_to(vertices_x, shape_vertices)
2121+
vertices_y = na.broadcast_to(vertices_y, shape_vertices)
2122+
2123+
result = _point_in_polygon_quantity(
2124+
x=x.ndarray,
2125+
y=y.ndarray,
2126+
vertices_x=vertices_x.ndarray,
2127+
vertices_y=vertices_y.ndarray,
2128+
)
2129+
2130+
result = na.ScalarArray(
2131+
ndarray=result,
2132+
axes=tuple(shape),
2133+
)
2134+
2135+
return result

named_arrays/_scalars/uncertainties/uncertainties_named_array_functions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,3 +1343,42 @@ def evaluate(
13431343
**kwargs,
13441344
)
13451345
)
1346+
1347+
1348+
@_implements(na.geometry.point_in_polygon)
1349+
def point_in_polygon(
1350+
x: na.AbstractScalar,
1351+
y: na.AbstractScalar,
1352+
vertices_x: na.AbstractScalar,
1353+
vertices_y: na.AbstractScalar,
1354+
axis: str,
1355+
) -> na.UncertainScalarArray:
1356+
1357+
try:
1358+
x = uncertainties._normalize(x)
1359+
y = uncertainties._normalize(y)
1360+
vertices_x = uncertainties._normalize(vertices_x)
1361+
vertices_y = uncertainties._normalize(vertices_y)
1362+
except uncertainties.UncertainScalarTypeError: # pragma: nocover
1363+
return NotImplemented
1364+
1365+
result_nominal = na.geometry.point_in_polygon(
1366+
x=x.nominal,
1367+
y=y.nominal,
1368+
vertices_x=vertices_x.nominal,
1369+
vertices_y=vertices_y.nominal,
1370+
axis=axis,
1371+
)
1372+
1373+
result_distribution = na.geometry.point_in_polygon(
1374+
x=x.distribution,
1375+
y=y.distribution,
1376+
vertices_x=vertices_x.distribution,
1377+
vertices_y=vertices_y.distribution,
1378+
axis=axis,
1379+
)
1380+
1381+
return na.UncertainScalarArray(
1382+
nominal=result_nominal,
1383+
distribution=result_distribution,
1384+
)

named_arrays/geometry/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Computational geometry routines."""
2+
3+
from ._point_in_polygon import point_in_polygon
4+
5+
__all__ = [
6+
"point_in_polygon",
7+
]
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
from typing import TypeVar
2+
import numpy as np
3+
import astropy.units as u
4+
import numba
5+
import regridding
6+
import named_arrays as na
7+
8+
PointT = TypeVar("PointT", bound="float | u.Quantity | na.AbstractScalar")
9+
VertexT = TypeVar("VertexT", bound="na.AbstractScalar")
10+
11+
def point_in_polygon(
12+
x: PointT,
13+
y: PointT,
14+
vertices_x: VertexT,
15+
vertices_y: VertexT,
16+
axis: str,
17+
) -> PointT | VertexT:
18+
"""
19+
Check if a given point is inside or on the boundary of a polygon.
20+
21+
This function is a wrapper around
22+
:func:`regridding.geometry.point_is_inside_polygon`.
23+
24+
Parameters
25+
----------
26+
x
27+
The :math:`x`-coordinates of the test points.
28+
y
29+
The :math:`y`-coordinates of the test points.
30+
vertices_x
31+
The :math:`x`-coordinates of the polygon's vertices.
32+
vertices_y
33+
The :math:`y`-coordinates of the polygon's vertices.
34+
axis
35+
The logical axis representing the different vertices of the polygon.
36+
37+
Examples
38+
--------
39+
40+
Check if some random points are inside a randomly-generated polygon.
41+
42+
.. jupyter-execute::
43+
44+
import numpy as np
45+
import matplotlib.pyplot as plt
46+
import named_arrays as na
47+
48+
# Define a random polygon
49+
axis = "vertex"
50+
num_vertices = 7
51+
radius = na.random.uniform(5, 15, shape_random={axis: num_vertices})
52+
angle = na.linspace(0, 2 * np.pi, axis=axis, num=num_vertices)
53+
vertices_x = radius * np.cos(angle)
54+
vertices_y = radius * np.sin(angle)
55+
56+
# Define some random points
57+
x = na.random.uniform(-20, 20, shape_random=dict(r=1000))
58+
y = na.random.uniform(-20, 20, shape_random=dict(r=1000))
59+
60+
# Select which points are inside the polygon
61+
where = na.geometry.point_in_polygon(
62+
x=x,
63+
y=y,
64+
vertices_x=vertices_x,
65+
vertices_y=vertices_y,
66+
axis=axis,
67+
)
68+
69+
# Plot the results as a scatter plot
70+
fig, ax = plt.subplots()
71+
na.plt.fill(
72+
vertices_x,
73+
vertices_y,
74+
ax=ax,
75+
facecolor="none",
76+
edgecolor="black",
77+
)
78+
na.plt.scatter(
79+
x,
80+
y,
81+
where=where,
82+
ax=ax,
83+
);
84+
"""
85+
return na._named_array_function(
86+
func=point_in_polygon,
87+
x=x,
88+
y=y,
89+
vertices_x=vertices_x,
90+
vertices_y=vertices_y,
91+
axis=axis,
92+
)
93+
94+
95+
def _point_in_polygon_quantity(
96+
x: u.Quantity,
97+
y: u.Quantity,
98+
vertices_x: u.Quantity,
99+
vertices_y: u.Quantity,
100+
) -> np.ndarray:
101+
"""
102+
Check if a given point is inside or on the boundary of a polygon.
103+
104+
Parameters
105+
----------
106+
x
107+
The :math:`x`-coordinates of the test points.
108+
y
109+
The :math:`y`-coordinates of the test points.
110+
vertices_x
111+
The :math:`x`-coordinates of the polygon's vertices.
112+
The last axis should represent the different vertices of the polygon.
113+
vertices_y
114+
The :math:`y`-coordinates of the polygon's vertices.
115+
The last axis should represent the different vertices of the polygon.
116+
"""
117+
118+
if isinstance(x, u.Quantity):
119+
unit = x.unit
120+
y = y.to_value(unit)
121+
vertices_x = vertices_x.to_value(unit)
122+
vertices_y = vertices_y.to_value(unit)
123+
124+
shape_points = np.broadcast(x, y).shape
125+
shape_vertices = np.broadcast(vertices_x, vertices_y).shape
126+
127+
num_vertices = shape_vertices[~0]
128+
129+
shape_points = np.broadcast_shapes(shape_points, shape_vertices[:~0])
130+
shape_vertices = shape_points + (num_vertices,)
131+
132+
x = np.broadcast_to(x, shape_points)
133+
y = np.broadcast_to(y, shape_points)
134+
135+
vertices_x = np.broadcast_to(vertices_x, shape_vertices)
136+
vertices_y = np.broadcast_to(vertices_y, shape_vertices)
137+
138+
result = _point_in_polygon_numba(
139+
x=x.reshape(-1),
140+
y=y.reshape(-1),
141+
vertices_x=vertices_x.reshape(-1, num_vertices),
142+
vertices_y=vertices_y.reshape(-1, num_vertices),
143+
)
144+
145+
result = result.reshape(shape_points)
146+
147+
return result
148+
149+
@numba.njit(cache=True, parallel=True)
150+
def _point_in_polygon_numba(
151+
x: np.ndarray,
152+
y: np.ndarray,
153+
vertices_x: np.ndarray,
154+
vertices_y: np.ndarray,
155+
) -> np.ndarray: # pragma: nocover
156+
"""
157+
Numba-accelerated check if a given point is inside or on the boundary of a polygon.
158+
159+
Vectorized version of :func:`regridding.geometry.point_is_inside_polygon`.
160+
161+
Parameters
162+
----------
163+
x
164+
The :math:`x`-coordinates of the test points.
165+
Should be 1-dimensional.
166+
y
167+
The :math:`y`-coordinates of the test points.
168+
Should be 1-dimensional, with the same number of elements as `x`.
169+
vertices_x
170+
The :math:`x`-coordinates of the polygon's vertices.
171+
Should be 2-dimensional, where the first axis has the same number
172+
of elements as `x`.
173+
vertices_y
174+
The :math:`y`-coordinates of the polygon's vertices.
175+
Should be 2-dimensional, where the last axis has the same number
176+
of elements as `vertices_y`.
177+
"""
178+
179+
num_pts, num_vertices = vertices_x.shape
180+
181+
result = np.empty(num_pts, dtype=np.bool)
182+
183+
for i in numba.prange(num_pts):
184+
result[i] = regridding.geometry.point_is_inside_polygon(
185+
x=x[i],
186+
y=y[i],
187+
vertices_x=vertices_x[i],
188+
vertices_y=vertices_y[i],
189+
)
190+
191+
return result

0 commit comments

Comments
 (0)