Skip to content

Commit

Permalink
ENH: prepared geometry as additional (cached) attribute on GeometryOb…
Browse files Browse the repository at this point in the history
  • Loading branch information
jorisvandenbossche committed Nov 11, 2020
1 parent b3c32c7 commit 34712ce
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 62 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Version 0.9 (unreleased)
``get_num_geometries``, ``get_num_interior_rings``, ``get_num_points``) now return 0
for ``None`` input values instead of -1 (#218).
* Fixed internal GEOS error code detection for ``get_dimensions`` and ``get_srid`` (#218).
* Addition of ``prepare`` function that generates a GEOS prepared geometry which is stored on
the Geometry object itself. All binary predicates (except ``equals``) make use of this (#92).


Version 0.8 (2020-09-06)
------------------------
Expand Down
45 changes: 45 additions & 0 deletions pygeos/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"multipolygons",
"geometrycollections",
"box",
"prepare",
"destroy_prepared",
]


Expand Down Expand Up @@ -170,3 +172,46 @@ def geometrycollections(geometries):
An array of geometries
"""
return lib.create_collection(geometries, GeometryType.GEOMETRYCOLLECTION)


def prepare(geometry, **kwargs):
"""Compute a prepared geometry.
A prepared geometry is a normal geometry with added information such as an
index on the line segments. This improves the performance of the following operations:
contains, contains_properly, covered_by, covers, crosses, disjoint, intersects,
overlaps, touches, and within.
Note that if a prepared geometry is modified, the newly created Geometry object is
not prepared. In that case, ``prepare`` should be called again.
This function does not recompute previously prepared geometries;
it is efficient to call this function on an array that partially contains prepared geometries.
Parameters
----------
geometry : Geometry or array_like
See also
--------
destroy_prepared
"""
return lib.prepare(geometry, **kwargs)


def destroy_prepared(geometry, **kwargs):
"""Destroy a previously prepared geometry, freeing up memory.
Note that the prepared geometry will always be cleaned up if the geometry itself
is dereferenced. This function needs only be called in very specific circumstances,
such as freeing up memory without losing the geometries, or benchmarking.
Parameters
----------
geometry : Geometry or array_like
See also
--------
prepare
"""
return lib.destroy_prepared(geometry, **kwargs)
18 changes: 18 additions & 0 deletions pygeos/predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ def crosses(a, b, **kwargs):
----------
a, b : Geometry or array_like
See also
--------
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
>>> line = Geometry("LINESTRING(0 0, 1 1)")
Expand Down Expand Up @@ -383,6 +387,7 @@ def contains(a, b, **kwargs):
See also
--------
within : ``contains(A, B) == within(B, A)``
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
Expand Down Expand Up @@ -424,6 +429,7 @@ def covered_by(a, b, **kwargs):
See also
--------
covers : ``covered_by(A, B) == covers(B, A)``
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
Expand Down Expand Up @@ -465,6 +471,7 @@ def covers(a, b, **kwargs):
See also
--------
covered_by : ``covers(A, B) == covered_by(B, A)``
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
Expand Down Expand Up @@ -509,6 +516,7 @@ def disjoint(a, b, **kwargs):
See also
--------
intersects : ``disjoint(A, B) == ~intersects(A, B)``
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
Expand Down Expand Up @@ -574,6 +582,7 @@ def intersects(a, b, **kwargs):
See also
--------
disjoint : ``intersects(A, B) == ~disjoint(A, B)``
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
Expand All @@ -599,6 +608,10 @@ def overlaps(a, b, **kwargs):
----------
a, b : Geometry or array_like
See also
--------
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
>>> line = Geometry("LINESTRING(0 0, 1 1)")
Expand All @@ -625,6 +638,10 @@ def touches(a, b, **kwargs):
----------
a, b : Geometry or array_like
See also
--------
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
>>> line = Geometry("LINESTRING(0 2, 2 0)")
Expand Down Expand Up @@ -663,6 +680,7 @@ def within(a, b, **kwargs):
See also
--------
contains : ``within(A, B) == contains(B, A)``
prepare : improve performance by preparing ``a`` (the first argument)
Examples
--------
Expand Down
128 changes: 84 additions & 44 deletions pygeos/test/test_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,15 @@ def test_linestrings_from_xyz():
assert str(actual) == "LINESTRING Z (0 2 0, 1 3 0)"


@pytest.mark.parametrize("shape", [
(2, 1, 2), # 2 linestrings of 1 2D point
(1, 1, 2), # 1 linestring of 1 2D point
(1, 2), # 1 linestring of 1 2D point (scalar)
(2, ), # 1 2D point (scalar)
])
@pytest.mark.parametrize(
"shape",
[
(2, 1, 2), # 2 linestrings of 1 2D point
(1, 1, 2), # 1 linestring of 1 2D point
(1, 2), # 1 linestring of 1 2D point (scalar)
(2,), # 1 2D point (scalar)
],
)
def test_linestrings_invalid_shape(shape):
with pytest.raises(ValueError):
pygeos.linestrings(np.ones(shape))
Expand All @@ -91,18 +94,21 @@ def test_linearrings_unclosed():
assert str(actual) == "LINEARRING (1 0, 1 1, 0 1, 0 0, 1 0)"


@pytest.mark.parametrize("shape", [
(2, 1, 2), # 2 linearrings of 1 2D point
(1, 1, 2), # 1 linearring of 1 2D point
(1, 2), # 1 linearring of 1 2D point (scalar)
(2, 2, 2), # 2 linearrings of 2 2D points
(1, 2, 2), # 1 linearring of 2 2D points
(2, 2), # 1 linearring of 2 2D points (scalar)
(2, 3, 2), # 2 linearrings of 3 2D points
(1, 3, 2), # 1 linearring of 3 2D points
(3, 2), # 1 linearring of 3 2D points (scalar)
(2, ), # 1 2D point (scalar)
])
@pytest.mark.parametrize(
"shape",
[
(2, 1, 2), # 2 linearrings of 1 2D point
(1, 1, 2), # 1 linearring of 1 2D point
(1, 2), # 1 linearring of 1 2D point (scalar)
(2, 2, 2), # 2 linearrings of 2 2D points
(1, 2, 2), # 1 linearring of 2 2D points
(2, 2), # 1 linearring of 2 2D points (scalar)
(2, 3, 2), # 2 linearrings of 3 2D points
(1, 3, 2), # 1 linearring of 3 2D points
(3, 2), # 1 linearring of 3 2D points (scalar)
(2,), # 1 2D point (scalar)
],
)
def test_linearrings_invalid_shape(shape):
coords = np.ones(shape)
with pytest.raises(ValueError):
Expand All @@ -113,6 +119,7 @@ def test_linearrings_invalid_shape(shape):
with pytest.raises(ValueError):
pygeos.linearrings(coords)


def test_polygon_from_linearring():
actual = pygeos.polygons(pygeos.linearrings(box_tpl(0, 0, 1, 1)))
assert str(actual) == "POLYGON ((1 0, 1 1, 0 1, 0 0, 1 0))"
Expand Down Expand Up @@ -163,46 +170,52 @@ def test_2_polygons_with_different_holes():
assert pygeos.area(actual).tolist() == [96.0, 24.0]


@pytest.mark.parametrize("shape", [
(2, 1, 2), # 2 linearrings of 1 2D point
(1, 1, 2), # 1 linearring of 1 2D point
(1, 2), # 1 linearring of 1 2D point (scalar)
(2, 2, 2), # 2 linearrings of 2 2D points
(1, 2, 2), # 1 linearring of 2 2D points
(2, 2), # 1 linearring of 2 2D points (scalar)
(2, 3, 2), # 2 linearrings of 3 2D points
(1, 3, 2), # 1 linearring of 3 2D points
(3, 2), # 1 linearring of 3 2D points (scalar)
(2, ), # 1 2D point (scalar)
])
@pytest.mark.parametrize(
"shape",
[
(2, 1, 2), # 2 linearrings of 1 2D point
(1, 1, 2), # 1 linearring of 1 2D point
(1, 2), # 1 linearring of 1 2D point (scalar)
(2, 2, 2), # 2 linearrings of 2 2D points
(1, 2, 2), # 1 linearring of 2 2D points
(2, 2), # 1 linearring of 2 2D points (scalar)
(2, 3, 2), # 2 linearrings of 3 2D points
(1, 3, 2), # 1 linearring of 3 2D points
(3, 2), # 1 linearring of 3 2D points (scalar)
(2,), # 1 2D point (scalar)
],
)
def test_polygons_not_enough_points_in_shell(shape):
coords = np.ones(shape)
with pytest.raises(ValueError):
pygeos.polygons(coords)

# make sure the first coordinate != second coordinate
coords[..., 1] += 1
with pytest.raises(ValueError):
pygeos.polygons(coords)


@pytest.mark.parametrize("shape", [
(2, 1, 2), # 2 linearrings of 1 2D point
(1, 1, 2), # 1 linearring of 1 2D point
(1, 2), # 1 linearring of 1 2D point (scalar)
(2, 2, 2), # 2 linearrings of 2 2D points
(1, 2, 2), # 1 linearring of 2 2D points
(2, 2), # 1 linearring of 2 2D points (scalar)
(2, 3, 2), # 2 linearrings of 3 2D points
(1, 3, 2), # 1 linearring of 3 2D points
(3, 2), # 1 linearring of 3 2D points (scalar)
(2, ), # 1 2D point (scalar)
])
@pytest.mark.parametrize(
"shape",
[
(2, 1, 2), # 2 linearrings of 1 2D point
(1, 1, 2), # 1 linearring of 1 2D point
(1, 2), # 1 linearring of 1 2D point (scalar)
(2, 2, 2), # 2 linearrings of 2 2D points
(1, 2, 2), # 1 linearring of 2 2D points
(2, 2), # 1 linearring of 2 2D points (scalar)
(2, 3, 2), # 2 linearrings of 3 2D points
(1, 3, 2), # 1 linearring of 3 2D points
(3, 2), # 1 linearring of 3 2D points (scalar)
(2,), # 1 2D point (scalar)
],
)
def test_polygons_not_enough_points_in_holes(shape):
coords = np.ones(shape)
with pytest.raises(ValueError):
pygeos.polygons(np.ones((1, 4, 2)), coords)

# make sure the first coordinate != second coordinate
coords[..., 1] += 1
with pytest.raises(ValueError):
Expand Down Expand Up @@ -320,6 +333,33 @@ def test_subclasses(with_point_in_registry):
assert point.x == 1


def test_prepare():
arr = np.array([pygeos.points(1, 1), None, pygeos.box(0, 0, 1, 1)])
assert arr[0]._ptr_prepared == 0
assert arr[2]._ptr_prepared == 0
pygeos.prepare(arr)
assert arr[0]._ptr_prepared != 0
assert arr[1] is None
assert arr[2]._ptr_prepared != 0

# preparing again actually does nothing
original = arr[0]._ptr_prepared
pygeos.prepare(arr)
assert arr[0]._ptr_prepared == original


def test_destroy_prepared():
arr = np.array([pygeos.points(1, 1), None, pygeos.box(0, 0, 1, 1)])
pygeos.prepare(arr)
assert arr[0]._ptr_prepared != 0
assert arr[2]._ptr_prepared != 0
pygeos.destroy_prepared(arr)
assert arr[0]._ptr_prepared == 0
assert arr[1] is None
assert arr[2]._ptr_prepared == 0
pygeos.destroy_prepared(arr) # does not error


def test_subclass_is_geometry(with_point_in_registry):
assert pygeos.is_geometry(Point("POINT (1 1)"))

Expand Down
13 changes: 13 additions & 0 deletions pygeos/test/test_predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
pygeos.equals_exact,
)

BINARY_PREPARED_PREDICATES = tuple(
set(BINARY_PREDICATES) - {pygeos.equals, pygeos.equals_exact}
)


@pytest.mark.parametrize("geometry", all_types)
@pytest.mark.parametrize("func", UNARY_PREDICATES)
Expand Down Expand Up @@ -122,3 +126,12 @@ def test_relate_none(g1, g2):
])
def test_is_ccw(geom, expected):
assert pygeos.is_ccw(geom) == expected


@pytest.mark.parametrize("a", all_types)
@pytest.mark.parametrize("func", BINARY_PREPARED_PREDICATES)
def test_binary_prepared(a, func):
actual = func(a, point)
pygeos.lib.prepare(a)
result = func(a, point)
assert actual == result
10 changes: 10 additions & 0 deletions src/fast_loop_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@
* npy_intp *steps
*/


/** (ip1) -> () */
#define NO_OUTPUT_LOOP\
char *ip1 = args[0];\
npy_intp is1 = steps[0];\
npy_intp n = dimensions[0];\
npy_intp i;\
for(i = 0; i < n; i++, ip1 += is1)


/** (ip1) -> (op1) */
#define UNARY_LOOP \
char *ip1 = args[0], *op1 = args[1]; \
Expand Down

0 comments on commit 34712ce

Please sign in to comment.