diff --git a/README.md b/README.md index 4193373dd..3fbb9b86c 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,7 @@ gitGraph ## Using cuSpatial **CUDA/GPU requirements** - CUDA 11.2+ with a [compatible, supported driver](https://docs.nvidia.com/datacenter/tesla/drivers/#cuda-drivers) -- Linux native: Pascal architecture or newer ([Compute Capability >=6.0](https://developer.nvidia.com/cuda-gpus)) -- WSL2: Volta architecture or newer ([Compute Capability >=7.0](https://developer.nvidia.com/cuda-gpus)) +- Volta architecture or newer ([Compute Capability >=7.0](https://developer.nvidia.com/cuda-gpus)) ### Quick start: Docker Use the [RAPIDS Release Selector](https://docs.rapids.ai/install#selector), selecting `Docker` as the installation method. All RAPIDS Docker images contain cuSpatial. @@ -113,7 +112,7 @@ An example command from the Release Selector: docker run --gpus all --pull always --rm -it \ --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ -p 8888:8888 -p 8787:8787 -p 8786:8786 \ - nvcr.io/nvidia/rapidsai/notebooks:24.12-cuda11.8-py3.10 + nvcr.io/nvidia/rapidsai/notebooks:24.12-cuda11.8-py3.12 ``` ### Install with Conda @@ -121,7 +120,7 @@ docker run --gpus all --pull always --rm -it \ To install via conda: > **Note** cuSpatial is supported only on Linux or [through WSL](https://rapids.ai/wsl2.html), and with Python versions 3.10, 3.11, and 3.12. -cuSpatial can be installed with conda (miniconda, or the full Anaconda distribution) from the rapidsai channel: +cuSpatial can be installed with conda from the rapidsai channel: ```shell conda install -c rapidsai -c conda-forge -c nvidia \ diff --git a/python/cuspatial/cuspatial/testing/helpers.py b/python/cuspatial/cuspatial/testing/helpers.py new file mode 100644 index 000000000..a03ab2d57 --- /dev/null +++ b/python/cuspatial/cuspatial/testing/helpers.py @@ -0,0 +1,15 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +from itertools import chain + +from shapely import get_coordinates + + +def geometry_to_coords(geom, geom_types): + points_list = geom[geom.apply(lambda x: isinstance(x, geom_types))] + # flatten multigeometries, then geometries, then coordinates + points = list(chain(points_list.apply(get_coordinates))) + coords_list = list(chain(*points)) + xy = list(chain(*coords_list)) + x = xy[::2] + y = xy[1::2] + return xy, x, y diff --git a/python/cuspatial/cuspatial/tests/conftest.py b/python/cuspatial/cuspatial/tests/conftest.py index b71b08128..a8ecb11ec 100644 --- a/python/cuspatial/cuspatial/tests/conftest.py +++ b/python/cuspatial/cuspatial/tests/conftest.py @@ -19,48 +19,46 @@ @pytest.fixture def gs(): g0 = Point(-1, 0) - g1 = MultiPoint(((1, 2), (3, 4))) - g2 = MultiPoint(((5, 6), (7, 8))) - g3 = Point(9, 10) + g1 = Point(9, 10) + g2 = MultiPoint(((1, 2), (3, 4))) + g3 = MultiPoint(((5, 6), (7, 8))) g4 = LineString(((11, 12), (13, 14))) g5 = MultiLineString((((15, 16), (17, 18)), ((19, 20), (21, 22)))) g6 = MultiLineString((((23, 24), (25, 26)), ((27, 28), (29, 30)))) g7 = LineString(((31, 32), (33, 34))) g8 = Polygon( - ((35, 36), (37, 38), (39, 40), (41, 42)), + ((35, 36), (38, 36), (41, 39), (41, 42)), ) - # TODO: g9, g10, g11 are invalid - # https://github.com/libgeos/geos/issues/1177 g9 = MultiPolygon( [ ( - ((43, 44), (45, 46), (47, 48)), - [((49, 50), (51, 52), (53, 54))], + ((43, 44), (48, 44), (47, 48)), + [((45, 45), (46, 46), (47, 45))], ), ( - ((55, 56), (57, 58), (59, 60)), - [((61, 62), (63, 64), (65, 66))], + ((55, 56), (60, 56), (59, 60)), + [((57, 57), (58, 58), (59, 57))], ), ] ) g10 = MultiPolygon( [ ( - ((67, 68), (69, 70), (71, 72)), - [((73, 74), (75, 76), (77, 78))], + ((67, 68), (72, 68), (71, 72)), + [((69, 69), (70, 70), (71, 69))], ), ( - ((79, 80), (81, 82), (83, 84)), + ((79, 80), (90, 82), (83, 90)), [ - ((85, 86), (87, 88), (89, 90)), - ((91, 92), (93, 94), (95, 96)), + ((80, 81), (82, 84), (84, 82)), + ((85, 85), (88, 82), (86, 82)), ], ), ] ) g11 = Polygon( ((97, 98), (99, 101), (102, 103), (101, 108)), - [((106, 107), (108, 109), (110, 111), (113, 108))], + [((99, 102), (100, 103), (101, 103), (100, 102))], ) gs = gpd.GeoSeries([g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, g10, g11]) return gs @@ -72,8 +70,10 @@ def gpdf(gs): random_col = int_col np.random.shuffle(random_col) str_col = [str(x) for x in int_col] - key_col = np.repeat(np.arange(4), len(int_col) // 4) + key_col = np.repeat(np.arange(4), (len(int_col) // 4) + 1) + key_col = key_col[: len(int_col)] np.random.shuffle(key_col) + result = gpd.GeoDataFrame( { "geometry": gs, @@ -87,65 +87,6 @@ def gpdf(gs): return result -@pytest.fixture -def polys(): - return np.array( - ( - (35, 36), - (37, 38), - (39, 40), - (41, 42), - (35, 36), - (43, 44), - (45, 46), - (47, 48), - (43, 44), - (49, 50), - (51, 52), - (53, 54), - (49, 50), - (55, 56), - (57, 58), - (59, 60), - (55, 56), - (61, 62), - (63, 64), - (65, 66), - (61, 62), - (67, 68), - (69, 70), - (71, 72), - (67, 68), - (73, 74), - (75, 76), - (77, 78), - (73, 74), - (79, 80), - (81, 82), - (83, 84), - (79, 80), - (85, 86), - (87, 88), - (89, 90), - (85, 86), - (91, 92), - (93, 94), - (95, 96), - (91, 92), - (97, 98), - (99, 101), - (102, 103), - (101, 108), - (97, 98), - (106, 107), - (108, 109), - (110, 111), - (113, 108), - (106, 107), - ) - ) - - @pytest.fixture def gs_sorted(gs): result = pd.concat( diff --git a/python/cuspatial/cuspatial/tests/test_cudf_integration.py b/python/cuspatial/cuspatial/tests/test_cudf_integration.py index 41214e816..0e67733c0 100644 --- a/python/cuspatial/cuspatial/tests/test_cudf_integration.py +++ b/python/cuspatial/cuspatial/tests/test_cudf_integration.py @@ -2,17 +2,10 @@ import geopandas as gpd import numpy as np import pandas as pd -import pytest import cuspatial -reason = ( - "gs fixture contains invalid Polygons/MultiPolygons: " - "https://github.com/libgeos/geos/issues/1177" -) - -@pytest.mark.xfail(reason=reason) def test_sort_index_series(gs): gs.index = np.random.permutation(len(gs)) cugs = cuspatial.from_geopandas(gs) @@ -21,7 +14,6 @@ def test_sort_index_series(gs): gpd.testing.assert_geoseries_equal(got, expected) -@pytest.mark.xfail(reason=reason) def test_sort_index_dataframe(gpdf): gpdf.index = np.random.permutation(len(gpdf)) cugpdf = cuspatial.from_geopandas(gpdf) @@ -30,7 +22,6 @@ def test_sort_index_dataframe(gpdf): gpd.testing.assert_geodataframe_equal(got, expected) -@pytest.mark.xfail(reason=reason) def test_sort_values(gpdf): cugpdf = cuspatial.from_geopandas(gpdf) expected = gpdf.sort_values("random") diff --git a/python/cuspatial/cuspatial/tests/test_from_geopandas.py b/python/cuspatial/cuspatial/tests/test_from_geopandas.py index 1c33e216a..a02ab4a35 100644 --- a/python/cuspatial/cuspatial/tests/test_from_geopandas.py +++ b/python/cuspatial/cuspatial/tests/test_from_geopandas.py @@ -36,15 +36,8 @@ def test_dataframe_column_access(gs): def test_from_geoseries_complex(gs): cugs = cuspatial.from_geopandas(gs) - assert cugs.points.xy.sum() == 18 - assert cugs.lines.xy.sum() == 540 - assert cugs.multipoints.xy.sum() == 36 - assert cugs.polygons.xy.sum() == 7436 - assert cugs._column.polygons._column.base_children[0].sum() == 15 - assert ( - cugs._column.polygons._column.base_children[1].base_children[0].sum() - == 38 - ) + gs_roundtrip = cugs.to_geopandas() + gpd.testing.assert_geoseries_equal(gs_roundtrip, gs) def test_from_geopandas_point(): diff --git a/python/cuspatial/cuspatial/tests/test_geodataframe.py b/python/cuspatial/cuspatial/tests/test_geodataframe.py index f834b35e5..af8b7334e 100644 --- a/python/cuspatial/cuspatial/tests/test_geodataframe.py +++ b/python/cuspatial/cuspatial/tests/test_geodataframe.py @@ -19,6 +19,7 @@ import cudf import cuspatial +from cuspatial.testing.helpers import geometry_to_coords np.random.seed(0) @@ -114,7 +115,7 @@ def test_type_persistence(gpdf): assert type(cugpdf["geometry"]) is cuspatial.GeoSeries -def test_interleaved_point(gpdf, polys): +def test_interleaved_point(gpdf): cugpdf = cuspatial.from_geopandas(gpdf) cugs = cugpdf["geometry"] gs = gpdf["geometry"] @@ -128,7 +129,7 @@ def test_interleaved_point(gpdf, polys): ) -def test_interleaved_multipoint(gpdf, polys): +def test_interleaved_multipoint(gpdf): cugpdf = cuspatial.from_geopandas(gpdf) cugs = cugpdf["geometry"] gs = gpdf["geometry"] @@ -156,7 +157,7 @@ def test_interleaved_multipoint(gpdf, polys): ) -def test_interleaved_lines(gpdf, polys): +def test_interleaved_lines(gpdf): cugpdf = cuspatial.from_geopandas(gpdf) cugs = cugpdf["geometry"] cudf.testing.assert_series_equal( @@ -175,16 +176,19 @@ def test_interleaved_lines(gpdf, polys): ) -def test_interleaved_polygons(gpdf, polys): +def test_interleaved_polygons(gpdf): cugpdf = cuspatial.from_geopandas(gpdf) cugs = cugpdf["geometry"] + gs = gpdf["geometry"] + xy, x, y = geometry_to_coords(gs, (Polygon, MultiPolygon)) + cudf.testing.assert_series_equal( cudf.Series.from_arrow(cugs.polygons.x.to_arrow()), - cudf.Series(polys[:, 0], dtype="float64"), + cudf.Series(x, dtype="float64"), ) cudf.testing.assert_series_equal( cudf.Series.from_arrow(cugs.polygons.y.to_arrow()), - cudf.Series(polys[:, 1], dtype="float64"), + cudf.Series(y, dtype="float64"), ) diff --git a/python/cuspatial/cuspatial/tests/test_geoseries.py b/python/cuspatial/cuspatial/tests/test_geoseries.py index 56f61158e..5304d7ea6 100644 --- a/python/cuspatial/cuspatial/tests/test_geoseries.py +++ b/python/cuspatial/cuspatial/tests/test_geoseries.py @@ -23,6 +23,7 @@ from cudf.testing import assert_series_equal import cuspatial +from cuspatial.testing.helpers import geometry_to_coords np.random.seed(0) @@ -133,7 +134,7 @@ def assert_eq_geo(geo1, geo2): assert result.all() -def test_interleaved_point(gs, polys): +def test_interleaved_point(gs): cugs = cuspatial.from_geopandas(gs) pd.testing.assert_series_equal( cugs.points.x.to_pandas(), @@ -181,13 +182,16 @@ def test_interleaved_point(gs, polys): dtype="float64", ).reset_index(drop=True), ) + + xy, x, y = geometry_to_coords(gs, (MultiPolygon, Polygon)) + cudf.testing.assert_series_equal( cugs.polygons.x.reset_index(drop=True), - cudf.Series(polys[:, 0], dtype="float64").reset_index(drop=True), + cudf.Series(x, dtype="float64").reset_index(drop=True), ) cudf.testing.assert_series_equal( cugs.polygons.y.reset_index(drop=True), - cudf.Series(polys[:, 1], dtype="float64").reset_index(drop=True), + cudf.Series(y, dtype="float64").reset_index(drop=True), ) @@ -350,155 +354,29 @@ def test_size(gs, series_slice): assert len(gi) == len(cugs) -def test_geometry_point_slicing(gs): - cugs = cuspatial.from_geopandas(gs) - assert (cugs[:1].points.x == cudf.Series([-1])).all() - assert (cugs[:1].points.y == cudf.Series([0])).all() - assert (cugs[:1].points.xy == cudf.Series([-1, 0])).all() - assert (cugs[3:].points.x == cudf.Series([9])).all() - assert (cugs[3:].points.y == cudf.Series([10])).all() - assert (cugs[3:].points.xy == cudf.Series([9, 10])).all() - assert (cugs[0:4].points.x == cudf.Series([-1, 9])).all() - assert (cugs[0:4].points.y == cudf.Series([0, 10])).all() - assert (cugs[0:4].points.xy == cudf.Series([-1, 0, 9, 10])).all() - - -def test_geometry_multipoint_slicing(gs): - cugs = cuspatial.from_geopandas(gs) - assert (cugs[:2].multipoints.x == cudf.Series([1, 3])).all() - assert (cugs[:2].multipoints.y == cudf.Series([2, 4])).all() - assert (cugs[:2].multipoints.xy == cudf.Series([1, 2, 3, 4])).all() - assert (cugs[2:].multipoints.x == cudf.Series([5, 7])).all() - assert (cugs[2:].multipoints.y == cudf.Series([6, 8])).all() - assert (cugs[2:].multipoints.xy == cudf.Series([5, 6, 7, 8])).all() - assert (cugs[0:4].multipoints.x == cudf.Series([1, 3, 5, 7])).all() - assert (cugs[0:4].multipoints.y == cudf.Series([2, 4, 6, 8])).all() - assert ( - cugs[0:4].multipoints.xy == cudf.Series([1, 2, 3, 4, 5, 6, 7, 8]) - ).all() - - -def test_geometry_linestring_slicing(gs): - cugs = cuspatial.from_geopandas(gs) - assert (cugs[:5].lines.x == cudf.Series([11, 13])).all() - assert (cugs[:5].lines.y == cudf.Series([12, 14])).all() - assert (cugs[:5].lines.xy == cudf.Series([11, 12, 13, 14])).all() - assert (cugs[:6].lines.x == cudf.Series([11, 13, 15, 17, 19, 21])).all() - assert (cugs[:6].lines.y == cudf.Series([12, 14, 16, 18, 20, 22])).all() - assert ( - cugs[:6].lines.xy - == cudf.Series([11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]) - ).all() - assert (cugs[7:].lines.x == cudf.Series([31, 33])).all() - assert (cugs[7:].lines.y == cudf.Series([32, 34])).all() - assert (cugs[7:].lines.xy == cudf.Series([31, 32, 33, 34])).all() - assert (cugs[6:].lines.x == cudf.Series([23, 25, 27, 29, 31, 33])).all() - assert (cugs[6:].lines.y == cudf.Series([24, 26, 28, 30, 32, 34])).all() - assert ( - cugs[6:].lines.xy - == cudf.Series([23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]) - ).all() - - -def test_geometry_polygon_slicing(gs): - cugs = cuspatial.from_geopandas(gs) - assert (cugs[:9].polygons.x == cudf.Series([35, 37, 39, 41, 35])).all() - assert (cugs[:9].polygons.y == cudf.Series([36, 38, 40, 42, 36])).all() - assert ( - cugs[:9].polygons.xy - == cudf.Series([35, 36, 37, 38, 39, 40, 41, 42, 35, 36]) - ).all() - assert ( - cugs[:10].polygons.x - == cudf.Series( - [ - 35, - 37, - 39, - 41, - 35, - 43, - 45, - 47, - 43, - 49, - 51, - 53, - 49, - 55, - 57, - 59, - 55, - 61, - 63, - 65, - 61, - ] - ) - ).all() - assert ( - cugs[:10].polygons.y - == cudf.Series( - [ - 36, - 38, - 40, - 42, - 36, - 44, - 46, - 48, - 44, - 50, - 52, - 54, - 50, - 56, - 58, - 60, - 56, - 62, - 64, - 66, - 62, - ] - ) - ).all() - assert ( - cugs[11:].polygons.x - == cudf.Series([97, 99, 102, 101, 97, 106, 108, 110, 113, 106]) - ).all() - assert ( - cugs[11:].polygons.y - == cudf.Series([98, 101, 103, 108, 98, 107, 109, 111, 108, 107]) - ).all() - assert ( - cugs[11:].polygons.xy - == cudf.Series( - [ - 97, - 98, - 99, - 101, - 102, - 103, - 101, - 108, - 97, - 98, - 106, - 107, - 108, - 109, - 110, - 111, - 113, - 108, - 106, - 107, - ] - ) - ).all() +@pytest.mark.parametrize( + "geom_access", + [ + # Tuples: accessor, types, slice + # slices here are meant to be supersets of the range in the gs fixture + # that contains the types of geometries being accessed + # Note that cuspatial.GeoSeries provides accessors for "multipoints", + # but not for "multilinestrings" or "multipolygons" + # (inconsistent interface) + ("points", Point, slice(0, 6)), + ("multipoints", MultiPoint, slice(2, 8)), + ("lines", (LineString, MultiLineString), slice(2, 10)), + ("polygons", (Polygon, MultiPolygon), slice(6, 12)), + ], +) +def test_geometry_access_slicing(gs, geom_access): + accessor, types, slice = geom_access + xy, x, y = geometry_to_coords(gs, types) + + cugs = cuspatial.from_geopandas(gs)[slice] + assert (getattr(cugs, accessor).x == cudf.Series(x)).all() + assert (getattr(cugs, accessor).y == cudf.Series(y)).all() + assert (getattr(cugs, accessor).xy == cudf.Series(xy)).all() def test_loc(gs):