You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ja...@apache.org on 2026/09/09 18:27:15 UTC
(sedona-db) branch main updated: test(integration): raster-geometry predicate parity (#1237)
This is an automated email from the ASF dual-hosted git repository.
james-willis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new 45983896b test(integration): raster-geometry predicate parity (#1237)
45983896b is described below
commit 45983896b4152313513b84762c5d0fee68228f7d
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Wed Sep 9 11:27:10 2026 -0700
test(integration): raster-geometry predicate parity (#1237)
---
integration/spark-parity/test_rs_contains.py | 96 ++++++++++++++
integration/spark-parity/test_rs_intersects.py | 177 +++++++++++++++++++++++++
integration/spark-parity/test_rs_within.py | 132 ++++++++++++++++++
3 files changed, 405 insertions(+)
diff --git a/integration/spark-parity/test_rs_contains.py b/integration/spark-parity/test_rs_contains.py
new file mode 100644
index 000000000..b847853ce
--- /dev/null
+++ b/integration/spark-parity/test_rs_contains.py
@@ -0,0 +1,96 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""SedonaDB vs Sedona Spark parity for RS_Contains.
+
+Raster-geometry predicates: the roi travels as `ST_GeomFromWKT` (the
+suite's established input spelling) and the boolean result compares raw.
+The geometries are placed against the standard grid (x in [100, 114],
+y in [482, 500]).
+"""
+
+import pytest
+
+from sedonadb.raster_testing import write_random_geotiff
+from sedonadb.testing import SedonaDB, compare
+from sedonadb.testing_spark import SedonaSpark
+
+INSIDE = "POLYGON((105 490, 108 490, 108 493, 105 493, 105 490))"
+OVERLAPPING = "POLYGON((90 490, 105 490, 105 493, 90 493, 90 490))"
+DISJOINT = "POLYGON((300 300, 310 300, 310 310, 300 310, 300 300))"
+COVERING = "POLYGON((90 470, 120 470, 120 510, 90 510, 90 470))"
+
+
+def test_rs_contains(tmp_path):
+ """True only when the raster's extent contains the whole geometry."""
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_random_raster_view("ct_src", tmp_path / "ct_src.tif")
+ for wkt, expected in ((INSIDE, True), (OVERLAPPING, False), (DISJOINT, False)):
+ sql = f"SELECT RS_Contains(rast, ST_GeomFromWKT('{wkt}')) FROM ct_src"
+ compare(sql, sedona, spark, expected=expected)
+
+
+def test_rs_contains_cross_crs(tmp_path):
+ """An EPSG:4326 band raster (latitudes 60-84) contains an EPSG:3413 point
+ that transforms to latitude 66.33 in both engines."""
+ path = tmp_path / "band.tif"
+ write_random_geotiff(
+ path,
+ "uint8",
+ bands=1,
+ height=12,
+ width=36,
+ bbox=(-180.0, 60.0, 180.0, 84.0),
+ crs="EPSG:4326",
+ )
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_raster_view("ct_band_src", path)
+ sql = (
+ "SELECT RS_Contains(rast, ST_SetSRID(ST_GeomFromWKT("
+ "'POINT(0 -2600000)'), 3413)) FROM ct_band_src"
+ )
+ compare(sql, sedona, spark, expected=True)
+
+
+@pytest.mark.xfail(
+ reason="Sedona Spark reprojects the raster footprint with corner vertices "
+ "only, so a point truly inside the polar square — (90, 85) lands at "
+ "(383228, 383228) — reads not-contained; SedonaDB densifies the footprint "
+ "edges and matches the truth (apache/sedona#3323)"
+)
+def test_rs_contains_polar_raster(tmp_path):
+ """The EPSG:3413 pole square contains an EPSG:4326 point that transforms
+ to well inside it, in both engines."""
+ path = tmp_path / "polar.tif"
+ write_random_geotiff(
+ path,
+ "uint8",
+ bands=1,
+ height=20,
+ width=20,
+ bbox=(-1000000.0, -1000000.0, 1000000.0, 1000000.0),
+ crs="EPSG:3413",
+ )
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_raster_view("ct_polar_src", path)
+ sql = (
+ "SELECT RS_Contains(rast, ST_SetSRID(ST_GeomFromWKT("
+ "'POINT(90 85)'), 4326)) FROM ct_polar_src"
+ )
+ compare(sql, sedona, spark)
diff --git a/integration/spark-parity/test_rs_intersects.py b/integration/spark-parity/test_rs_intersects.py
new file mode 100644
index 000000000..c733d0ead
--- /dev/null
+++ b/integration/spark-parity/test_rs_intersects.py
@@ -0,0 +1,177 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""SedonaDB vs Sedona Spark parity for RS_Intersects.
+
+Raster-geometry predicates: the roi travels as `ST_GeomFromWKT` (the
+suite's established input spelling) and the boolean result compares raw.
+The geometries are placed against the standard grid (x in [100, 114],
+y in [482, 500]).
+"""
+
+import pytest
+
+from sedonadb.raster_testing import write_random_geotiff
+from sedonadb.testing import SedonaDB, compare
+from sedonadb.testing_spark import SedonaSpark
+
+INSIDE = "POLYGON((105 490, 108 490, 108 493, 105 493, 105 490))"
+OVERLAPPING = "POLYGON((90 490, 105 490, 105 493, 90 493, 90 490))"
+DISJOINT = "POLYGON((300 300, 310 300, 310 310, 300 310, 300 300))"
+COVERING = "POLYGON((90 470, 120 470, 120 510, 90 510, 90 470))"
+
+
+def test_rs_intersects(tmp_path):
+ """True for contained and overlapping geometries, false for disjoint."""
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_random_raster_view("ix_src", tmp_path / "ix_src.tif")
+ for wkt, expected in ((INSIDE, True), (OVERLAPPING, True), (DISJOINT, False)):
+ sql = f"SELECT RS_Intersects(rast, ST_GeomFromWKT('{wkt}')) FROM ix_src"
+ compare(sql, sedona, spark, expected=expected)
+
+
+# Cross-CRS fixtures: a 2000 km EPSG:3413 square on the north pole (its true
+# EPSG:4326 footprint is a cap whose boundary latitude swings between ~77.3 at
+# the corners and ~81.0 at the edge midpoints), and an EPSG:4326 band from
+# latitude 60 to 84. Query points state their coordinates in both CRSs so the
+# expected answer is checkable against the raster's native rectangle by hand.
+POLAR_BBOX = (-1000000.0, -1000000.0, 1000000.0, 1000000.0)
+BAND_BBOX = (-180.0, 60.0, 180.0, 84.0)
+
+
+def _polar_engines(tmp_path):
+ path = tmp_path / "polar.tif"
+ write_random_geotiff(
+ path, "uint8", bands=1, height=20, width=20, bbox=POLAR_BBOX, crs="EPSG:3413"
+ )
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_raster_view("polar_src", path)
+ return sedona, spark
+
+
+def _band_engines(tmp_path):
+ path = tmp_path / "band.tif"
+ write_random_geotiff(
+ path, "uint8", bands=1, height=12, width=36, bbox=BAND_BBOX, crs="EPSG:4326"
+ )
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_raster_view("band_src", path)
+ return sedona, spark
+
+
+@pytest.mark.parametrize(
+ "wkt,expected",
+ [
+ # (0, 60) is (2349829, -2349829) in EPSG:3413 — far outside the square.
+ pytest.param("POINT(0 60)", False, id="far-south"),
+ # (135, 78.5) is (0, 1249794) — outside the top edge by 250 km.
+ pytest.param("POINT(135 78.5)", False, id="past-top-edge"),
+ ],
+)
+def test_rs_intersects_polar_raster_from_latlon(wkt, expected, tmp_path):
+ """EPSG:4326 points against the polar raster: truly-outside points agree
+ (the truly-inside ones are the densification xfail below)."""
+ sedona, spark = _polar_engines(tmp_path)
+ sql = (
+ "SELECT RS_Intersects(rast, ST_SetSRID(ST_GeomFromWKT("
+ f"'{wkt}'), 4326)) FROM polar_src"
+ )
+ compare(sql, sedona, spark, expected=expected)
+
+
+@pytest.mark.parametrize(
+ "wkt,expected",
+ [
+ # (0, -2600000) is (lon -45, lat 66.33) — inside the band.
+ pytest.param("POINT(0 -2600000)", True, id="inside"),
+ # (0, -3350000) is (lon -45, lat 59.77) — just south of the band.
+ pytest.param("POINT(0 -3350000)", False, id="south-of-band"),
+ # (0, 0) is the pole itself (lat 90) — north of the band.
+ pytest.param("POINT(0 0)", False, id="pole"),
+ ],
+)
+def test_rs_intersects_latlon_band_from_polar(wkt, expected, tmp_path):
+ """EPSG:3413 points against the EPSG:4326 band raster agree with the exact
+ transformed-latitude truth in both engines."""
+ sedona, spark = _band_engines(tmp_path)
+ sql = (
+ "SELECT RS_Intersects(rast, ST_SetSRID(ST_GeomFromWKT("
+ f"'{wkt}'), 3413)) FROM band_src"
+ )
+ compare(sql, sedona, spark, expected=expected)
+
+
+@pytest.mark.parametrize(
+ "wkt",
+ [
+ # (45, 82) is (867972, 0) in EPSG:3413 — on the +x axis, 87% of the
+ # half-width, nowhere near a curved-edge subtlety.
+ pytest.param("POINT(45 82)", id="on-x-axis"),
+ # The next three sit above every corner's latitude, in the cap the
+ # corner chords cut off entirely.
+ pytest.param("POINT(180 85)", id="lon-180"),
+ pytest.param("POINT(0 85)", id="lon-0"),
+ pytest.param("POINT(90 85)", id="lon-90"),
+ ],
+)
+@pytest.mark.xfail(
+ reason="Sedona Spark reprojects the raster footprint with corner vertices "
+ "only, so the lon/lat chords between corners miss most of the polar cap "
+ "and truly-inside points read false; SedonaDB densifies each footprint "
+ "edge before reprojecting and matches the exact point-in-square truth "
+ "(apache/sedona#3323)"
+)
+def test_rs_intersects_polar_raster_densified_footprint(wkt, tmp_path):
+ """Points that transform to well inside the polar square intersect it in
+ both engines."""
+ sedona, spark = _polar_engines(tmp_path)
+ sql = (
+ "SELECT RS_Intersects(rast, ST_SetSRID(ST_GeomFromWKT("
+ f"'{wkt}'), 4326)) FROM polar_src"
+ )
+ compare(sql, sedona, spark)
+
+
+def test_rs_intersects_antimeridian_raster(tmp_path):
+ """An EPSG:3413 square straddling the longitude-180 direction intersects
+ a two-lobe region written on both sides of the antimeridian — the flat
+ lon/lat spelling both engines evaluate correctly (contrast RS_Within,
+ where the same footprint turns inside out)."""
+ path = tmp_path / "am.tif"
+ write_random_geotiff(
+ path,
+ "uint8",
+ bands=1,
+ height=10,
+ width=10,
+ bbox=(-1330942.0, 730942.0, -730942.0, 1330942.0),
+ crs="EPSG:3413",
+ )
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_raster_view("ix_am_src", path)
+ lobes = (
+ "MULTIPOLYGON(((160 70, 180 70, 180 84, 160 84, 160 70)), "
+ "((-180 70, -160 70, -160 84, -180 84, -180 70)))"
+ )
+ sql = (
+ "SELECT RS_Intersects(rast, ST_SetSRID(ST_GeomFromWKT("
+ f"'{lobes}'), 4326)) FROM ix_am_src"
+ )
+ compare(sql, sedona, spark, expected=True)
diff --git a/integration/spark-parity/test_rs_within.py b/integration/spark-parity/test_rs_within.py
new file mode 100644
index 000000000..dadfd5d2c
--- /dev/null
+++ b/integration/spark-parity/test_rs_within.py
@@ -0,0 +1,132 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""SedonaDB vs Sedona Spark parity for RS_Within.
+
+Raster-geometry predicates: the roi travels as `ST_GeomFromWKT` (the
+suite's established input spelling) and the boolean result compares raw.
+The geometries are placed against the standard grid (x in [100, 114],
+y in [482, 500]).
+"""
+
+import pytest
+
+from sedonadb.raster_testing import write_random_geotiff
+from sedonadb.testing import SedonaDB, compare
+from sedonadb.testing_spark import SedonaSpark
+
+INSIDE = "POLYGON((105 490, 108 490, 108 493, 105 493, 105 490))"
+OVERLAPPING = "POLYGON((90 490, 105 490, 105 493, 90 493, 90 490))"
+DISJOINT = "POLYGON((300 300, 310 300, 310 310, 300 310, 300 300))"
+COVERING = "POLYGON((90 470, 120 470, 120 510, 90 510, 90 470))"
+
+
+def test_rs_within(tmp_path):
+ """True only when the geometry covers the raster's whole extent."""
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_random_raster_view("wi_src", tmp_path / "wi_src.tif")
+ for wkt, expected in ((COVERING, True), (INSIDE, False), (DISJOINT, False)):
+ sql = f"SELECT RS_Within(rast, ST_GeomFromWKT('{wkt}')) FROM wi_src"
+ compare(sql, sedona, spark, expected=expected)
+
+
+def _polar_engines(tmp_path):
+ path = tmp_path / "polar.tif"
+ write_random_geotiff(
+ path,
+ "uint8",
+ bands=1,
+ height=20,
+ width=20,
+ bbox=(-1000000.0, -1000000.0, 1000000.0, 1000000.0),
+ crs="EPSG:3413",
+ )
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_raster_view("wi_polar_src", path)
+ return sedona, spark
+
+
+def test_rs_within_polar_raster_not_within(tmp_path):
+ """The pole square's footprint dips to latitude ~77.3 at its corners, so
+ it is not within a cap polygon starting at latitude 80 — both engines
+ agree across the CRS boundary."""
+ sedona, spark = _polar_engines(tmp_path)
+ sql = (
+ "SELECT RS_Within(rast, ST_SetSRID(ST_GeomFromWKT("
+ "'POLYGON((-180 80, 180 80, 180 90, -180 90, -180 80))'), 4326)) "
+ "FROM wi_polar_src"
+ )
+ compare(sql, sedona, spark, expected=False)
+
+
+@pytest.mark.xfail(
+ reason="a footprint whose interior wraps the pole turns inside out when "
+ "reprojected: the pole maps to the degenerate latitude-90 edge of "
+ "lon/lat space, so BOTH engines are wrong — SedonaDB answers false "
+ "(apache/sedona-db#1240) and Sedona Spark throws (apache/sedona#3323) "
+ "where the spherical truth is true; the anchor states that truth"
+)
+def test_rs_within_polar_raster_pole_spanning(tmp_path):
+ """The pole square is within the latitude-75 cap (its footprint stays
+ above latitude 77) — a case where the raster interior's pole becomes an
+ edge of the geometry's CRS. Anchored to the spherical truth so the test
+ proves both engines wrong rather than merely recording their mismatch."""
+ sedona, spark = _polar_engines(tmp_path)
+ sql = (
+ "SELECT RS_Within(rast, ST_SetSRID(ST_GeomFromWKT("
+ "'POLYGON((-180 75, 180 75, 180 90, -180 90, -180 75))'), 4326)) "
+ "FROM wi_polar_src"
+ )
+ compare(sql, sedona, spark, expected=True)
+
+
+@pytest.mark.xfail(
+ reason="SedonaDB reprojects the densified footprint into the geometry's "
+ "CRS, and a footprint straddling the antimeridian turns inside out there "
+ "(longitudes jump across the ±180 seam), so it answers false for a "
+ "truly-within raster (apache/sedona-db#1240); Sedona Spark transforms "
+ "the geometry into the raster's CRS instead and answers true — the "
+ "suite's first case where SedonaDB is the wrong engine"
+)
+def test_rs_within_antimeridian_raster(tmp_path):
+ """An EPSG:3413 square centred on the longitude-180 direction (corner
+ longitudes ±163.8 and ±180, latitudes 72.7-80.5) is within a two-lobe
+ region covering longitudes 160..180 and -180..-160 at latitudes 70-84 —
+ every footprint point lies in a lobe."""
+ path = tmp_path / "am.tif"
+ write_random_geotiff(
+ path,
+ "uint8",
+ bands=1,
+ height=10,
+ width=10,
+ bbox=(-1330942.0, 730942.0, -730942.0, 1330942.0),
+ crs="EPSG:3413",
+ )
+ sedona, spark = SedonaDB(), SedonaSpark()
+ for eng in (sedona, spark):
+ eng.create_raster_view("wi_am_src", path)
+ lobes = (
+ "MULTIPOLYGON(((160 70, 180 70, 180 84, 160 84, 160 70)), "
+ "((-180 70, -160 70, -160 84, -180 84, -180 70)))"
+ )
+ sql = (
+ "SELECT RS_Within(rast, ST_SetSRID(ST_GeomFromWKT("
+ f"'{lobes}'), 4326)) FROM wi_am_src"
+ )
+ compare(sql, sedona, spark, expected=True)