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/07/17 20:24:15 UTC

(sedona-db) branch main updated: test(rust/sedona-raster-functions): assert raster results with the raster_spec helpers (#1056)

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 3dce881ad test(rust/sedona-raster-functions): assert raster results with the raster_spec helpers (#1056)
3dce881ad is described below

commit 3dce881adcc49f19da2a89cfe97b239c12a4e938
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Fri Jul 17 13:24:10 2026 -0700

    test(rust/sedona-raster-functions): assert raster results with the raster_spec helpers (#1056)
---
 .../tests/functions/test_raster_functions.py       |  18 +--
 rust/sedona-raster-functions/src/rs_dim_band.rs    |  10 +-
 rust/sedona-raster-functions/src/rs_example.rs     |  32 ++---
 rust/sedona-raster-functions/src/rs_setsrid.rs     | 137 +++++++++------------
 rust/sedona-raster-functions/src/rs_slice.rs       |   6 +-
 rust/sedona-testing/src/raster_spec.rs             |  38 ++++++
 rust/sedona-testing/src/rasters.rs                 |  41 ++++++
 7 files changed, 157 insertions(+), 125 deletions(-)

diff --git a/python/sedonadb/tests/functions/test_raster_functions.py b/python/sedonadb/tests/functions/test_raster_functions.py
index 631e68450..80b5db680 100644
--- a/python/sedonadb/tests/functions/test_raster_functions.py
+++ b/python/sedonadb/tests/functions/test_raster_functions.py
@@ -18,6 +18,10 @@
 import numpy as np
 import pandas as pd
 import pytest
+from rasterio.features import rasterize
+from rasterio.io import MemoryFile
+from rasterio.transform import Affine
+from shapely import wkt
 
 from sedonadb.testing import SedonaDB
 from sedonadb.raster import Raster
@@ -151,10 +155,6 @@ def test_rs_value_matches_rasterio(con):
     ambiguity at exact boundaries) and a batch of random interior points.
     """
 
-    pytest.importorskip("rasterio")
-    from rasterio.io import MemoryFile
-    from rasterio.transform import Affine
-
     rng = np.random.default_rng(42)
     height, width = 7, 5
     data = rng.random((height, width)) * 1000.0
@@ -360,11 +360,6 @@ def _rs_as_raster_sql(
 def test_rs_as_raster_matches_rasterio(
     con, sedona_testing, all_touched, use_geometry_extent
 ):
-    pytest.importorskip("rasterio")
-    from rasterio.features import rasterize
-    from rasterio.transform import Affine
-    from shapely import wkt
-
     path = sedona_testing / "data/raster/test4.tiff"
     transform = (0.0, 1.0, 0.0, 10.0, 0.0, -1.0)
 
@@ -400,11 +395,6 @@ def test_rs_as_raster_matches_rasterio(
 
 
 def test_rs_as_raster_all_touched_changes_pixels(con, sedona_testing):
-    pytest.importorskip("rasterio")
-    from rasterio.features import rasterize
-    from rasterio.transform import Affine
-    from shapely import wkt
-
     path = sedona_testing / "data/raster/test4.tiff"
     transform = (0.0, 1.0, 0.0, 10.0, 0.0, -1.0)
     geom = "POLYGON((1.9 8.9, 1.9 6.1, 3.1 6.1, 3.1 8.9, 1.9 8.9))"
diff --git a/rust/sedona-raster-functions/src/rs_dim_band.rs b/rust/sedona-raster-functions/src/rs_dim_band.rs
index e1470a6f7..92977fcb2 100644
--- a/rust/sedona-raster-functions/src/rs_dim_band.rs
+++ b/rust/sedona-raster-functions/src/rs_dim_band.rs
@@ -400,9 +400,8 @@ mod tests {
             .invoke_array_scalar(Arc::new(rasters), "time")
             .unwrap();
 
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        assert!(raster_array.is_null(0));
+        // A null input raster passes through as a null output raster.
+        assert_rasters_equal(&result, &[None]);
     }
 
     #[test]
@@ -577,8 +576,7 @@ mod tests {
             .invoke_array_scalar(Arc::new(rasters), "time")
             .unwrap();
 
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        assert!(raster_array.is_null(0));
+        // A null input raster passes through as a null output raster.
+        assert_rasters_equal(&result, &[None]);
     }
 }
diff --git a/rust/sedona-raster-functions/src/rs_example.rs b/rust/sedona-raster-functions/src/rs_example.rs
index 604755839..ac8dfc8bc 100644
--- a/rust/sedona-raster-functions/src/rs_example.rs
+++ b/rust/sedona-raster-functions/src/rs_example.rs
@@ -98,10 +98,8 @@ impl SedonaScalarKernel for RsExample {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use datafusion_common::ScalarValue;
     use datafusion_expr::ScalarUDF;
-    use sedona_raster::array::RasterStructArray;
-    use sedona_raster::traits::RasterRef;
+    use sedona_testing::raster_spec::{assert_raster_scalar_equals, RasterSpec};
 
     #[test]
     fn udf_size() {
@@ -116,23 +114,19 @@ mod tests {
         let arg_types = vec![];
 
         let result = kernel.invoke_batch(&arg_types, &args).unwrap();
-        if let ColumnarValue::Scalar(ScalarValue::Struct(arc_struct)) = result {
-            let raster_array = RasterStructArray::try_new(arc_struct.as_ref()).unwrap();
-
-            assert_eq!(raster_array.len(), 1);
-            let raster = raster_array.get(0).unwrap();
-            let metadata = raster.metadata();
-            assert_eq!(metadata.width(), 64);
-            assert_eq!(metadata.height(), 32);
-
-            let bands = raster.bands();
-            let band = bands.band(1).unwrap();
-            let band_metadata = band.metadata();
-            assert_eq!(band_metadata.data_type().unwrap(), BandDataType::UInt8);
-            assert_eq!(band_metadata.nodata_value(), Some(&[127u8][..]));
-            assert_eq!(band_metadata.storage_type().unwrap(), StorageType::InDb);
-        } else {
+        let ColumnarValue::Scalar(scalar) = result else {
             panic!("Expected scalar struct result");
+        };
+
+        // RS_Example builds a 64x32, 3-band UInt8 raster with origin
+        // (43.08, 79.07), scale 2, skew 1, nodata 127. Every pixel of band N
+        // is N except the top-left corner, which is set to the nodata value.
+        let mut expected = RasterSpec::d2(64, 32).transform([43.08, 2.0, 1.0, 79.07, 1.0, 2.0]);
+        for band_id in 1u8..=3 {
+            let mut band_data = vec![band_id; 64 * 32];
+            band_data[0] = 127;
+            expected = expected.band_values(&band_data).nodata(127u8);
         }
+        assert_raster_scalar_equals(&scalar, &expected);
     }
 }
diff --git a/rust/sedona-raster-functions/src/rs_setsrid.rs b/rust/sedona-raster-functions/src/rs_setsrid.rs
index 79454cd0a..fab6a766a 100644
--- a/rust/sedona-raster-functions/src/rs_setsrid.rs
+++ b/rust/sedona-raster-functions/src/rs_setsrid.rs
@@ -395,7 +395,8 @@ mod tests {
     use sedona_raster::traits::RasterRef;
     use sedona_schema::crs::deserialize_crs;
     use sedona_schema::datatypes::RASTER;
-    use sedona_testing::rasters::generate_test_rasters;
+    use sedona_testing::raster_spec::assert_rasters_equal;
+    use sedona_testing::rasters::{generate_test_raster_spec, generate_test_rasters};
     use sedona_testing::testers::ScalarUdfTester;
 
     #[test]
@@ -494,19 +495,16 @@ mod tests {
             .invoke_array_scalar(Arc::new(rasters), 3857u32)
             .unwrap();
 
-        // Verify CRS was changed to EPSG:3857
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        assert_eq!(raster_array.len(), 3);
-
-        let raster0 = raster_array.get(0).unwrap();
-        assert_eq!(raster0.crs(), Some("EPSG:3857"));
-
-        // Null raster at index 1 should remain null
-        assert!(raster_array.is_null(1));
-
-        let raster2 = raster_array.get(2).unwrap();
-        assert_eq!(raster2.crs(), Some("EPSG:3857"));
+        // Rows 0 and 2 have their CRS swapped to EPSG:3857 with all other
+        // fields preserved; the null raster at row 1 stays null.
+        assert_rasters_equal(
+            &result,
+            &[
+                Some(generate_test_raster_spec(0).crs(Some("EPSG:3857"))),
+                None,
+                Some(generate_test_raster_spec(2).crs(Some("EPSG:3857"))),
+            ],
+        );
     }
 
     #[test]
@@ -519,10 +517,11 @@ mod tests {
             .invoke_array_scalar(Arc::new(rasters), 4326u32)
             .unwrap();
 
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        let raster = raster_array.get(0).unwrap();
-        assert_eq!(raster.crs(), Some("OGC:CRS84"));
+        // SRID 4326 maps to the OGC:CRS84 authority code.
+        assert_rasters_equal(
+            &result,
+            &[Some(generate_test_raster_spec(0).crs(Some("OGC:CRS84")))],
+        );
     }
 
     #[test]
@@ -533,11 +532,8 @@ mod tests {
         let rasters = generate_test_rasters(1, None).unwrap();
         let result = tester.invoke_array_scalar(Arc::new(rasters), 0u32).unwrap();
 
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        let raster = raster_array.get(0).unwrap();
-        // CRS should be None (null) for SRID 0
-        assert_eq!(raster.crs(), None);
+        // SRID 0 clears the CRS (maps to null) while preserving the raster.
+        assert_rasters_equal(&result, &[Some(generate_test_raster_spec(0).crs(None))]);
     }
 
     #[test]
@@ -552,17 +548,16 @@ mod tests {
             .invoke_array_scalar(Arc::new(rasters), "EPSG:3857")
             .unwrap();
 
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        assert_eq!(raster_array.len(), 3);
-
-        let raster0 = raster_array.get(0).unwrap();
-        assert_eq!(raster0.crs(), Some("EPSG:3857"));
-
-        assert!(raster_array.is_null(1));
-
-        let raster2 = raster_array.get(2).unwrap();
-        assert_eq!(raster2.crs(), Some("EPSG:3857"));
+        // Rows 0 and 2 have their CRS swapped to EPSG:3857 with all other
+        // fields preserved; the null raster at row 1 stays null.
+        assert_rasters_equal(
+            &result,
+            &[
+                Some(generate_test_raster_spec(0).crs(Some("EPSG:3857"))),
+                None,
+                Some(generate_test_raster_spec(2).crs(Some("EPSG:3857"))),
+            ],
+        );
     }
 
     #[test]
@@ -595,10 +590,8 @@ mod tests {
         let rasters = generate_test_rasters(1, None).unwrap();
         let result = tester.invoke_array_scalar(Arc::new(rasters), "0").unwrap();
 
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        let raster = raster_array.get(0).unwrap();
-        assert_eq!(raster.crs(), None);
+        // CRS "0" clears the CRS (maps to null) while preserving the raster.
+        assert_rasters_equal(&result, &[Some(generate_test_raster_spec(0).crs(None))]);
     }
 
     #[test]
@@ -698,15 +691,8 @@ mod tests {
         let result = tester
             .invoke_array_scalar(Arc::new(rasters), null_srid)
             .unwrap();
-        let raster_array =
-            RasterStructArray::try_new(result.as_any().downcast_ref::<StructArray>().unwrap())
-                .unwrap();
-        for i in 0..raster_array.len() {
-            assert!(
-                raster_array.is_null(i),
-                "Expected null raster at index {i} for null SRID input"
-            );
-        }
+        // A null SRID input nulls out every raster row.
+        assert_rasters_equal(&result, &[None, None, None]);
     }
 
     #[test]
@@ -720,15 +706,8 @@ mod tests {
         let result = tester
             .invoke_array_scalar(Arc::new(rasters), null_crs)
             .unwrap();
-        let raster_array =
-            RasterStructArray::try_new(result.as_any().downcast_ref::<StructArray>().unwrap())
-                .unwrap();
-        for i in 0..raster_array.len() {
-            assert!(
-                raster_array.is_null(i),
-                "Expected null raster at index {i} for null SRID input"
-            );
-        }
+        // A null CRS input nulls out every raster row.
+        assert_rasters_equal(&result, &[None, None, None]);
     }
 
     #[test]
@@ -747,20 +726,17 @@ mod tests {
         let result = tester
             .invoke_array_array(Arc::new(rasters), srid_array)
             .unwrap();
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-
-        // Row 0: valid raster + valid SRID -> EPSG:3857
-        let raster0 = raster_array.get(0).unwrap();
-        assert_eq!(raster0.crs(), Some("EPSG:3857"));
-
-        // Row 1: null raster (from input) -> still null
-        assert!(raster_array.is_null(1));
 
-        // Row 2: valid raster + null SRID -> null raster
-        assert!(
-            raster_array.is_null(2),
-            "Expected null raster at index 2 (null SRID input)"
+        // Row 0: valid raster + valid SRID -> EPSG:3857.
+        // Row 1: null raster (from input) -> still null.
+        // Row 2: valid raster + null SRID -> null raster.
+        assert_rasters_equal(
+            &result,
+            &[
+                Some(generate_test_raster_spec(0).crs(Some("EPSG:3857"))),
+                None,
+                None,
+            ],
         );
     }
 
@@ -780,20 +756,17 @@ mod tests {
         let result = tester
             .invoke_array_array(Arc::new(rasters), crs_array)
             .unwrap();
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-
-        // Row 0: valid raster + valid CRS -> EPSG:3857
-        let raster0 = raster_array.get(0).unwrap();
-        assert_eq!(raster0.crs(), Some("EPSG:3857"));
-
-        // Row 1: null raster (from input) -> still null
-        assert!(raster_array.is_null(1));
 
-        // Row 2: valid raster + null CRS -> null raster
-        assert!(
-            raster_array.is_null(2),
-            "Expected null raster at index 2 (null CRS input)"
+        // Row 0: valid raster + valid CRS -> EPSG:3857.
+        // Row 1: null raster (from input) -> still null.
+        // Row 2: valid raster + null CRS -> null raster.
+        assert_rasters_equal(
+            &result,
+            &[
+                Some(generate_test_raster_spec(0).crs(Some("EPSG:3857"))),
+                None,
+                None,
+            ],
         );
     }
 }
diff --git a/rust/sedona-raster-functions/src/rs_slice.rs b/rust/sedona-raster-functions/src/rs_slice.rs
index 19ffafad9..bf80959b1 100644
--- a/rust/sedona-raster-functions/src/rs_slice.rs
+++ b/rust/sedona-raster-functions/src/rs_slice.rs
@@ -426,7 +426,6 @@ mod tests {
     use arrow_schema::DataType;
     use datafusion_common::ScalarValue;
     use datafusion_expr::ScalarUDF;
-    use sedona_raster::array::RasterStructArray;
     use sedona_schema::datatypes::RASTER;
     use sedona_schema::raster::BandDataType;
     use sedona_testing::raster_spec::{assert_rasters_equal, RasterSpec};
@@ -548,8 +547,7 @@ mod tests {
             .invoke_array_scalar_scalar(Arc::new(rasters), "time", 0_i64)
             .unwrap();
 
-        let result_struct = result.as_any().downcast_ref::<StructArray>().unwrap();
-        let raster_array = RasterStructArray::try_new(result_struct).unwrap();
-        assert!(raster_array.is_null(0));
+        // A null input raster passes through as a null output raster.
+        assert_rasters_equal(&result, &[None]);
     }
 }
diff --git a/rust/sedona-testing/src/raster_spec.rs b/rust/sedona-testing/src/raster_spec.rs
index 9d84b0ebb..30185bc90 100644
--- a/rust/sedona-testing/src/raster_spec.rs
+++ b/rust/sedona-testing/src/raster_spec.rs
@@ -227,6 +227,26 @@ impl RasterSpec {
         self
     }
 
+    /// Set an axis-aligned, north-up geotransform from a bounding box, with
+    /// the pixel size derived from the box and the raster's dimensions (zero
+    /// skew, origin at the top-left corner). Easier to picture than raw
+    /// coefficients when the framing is what matters. Skewed or rotated
+    /// rasters can't be expressed as a bounding box — use
+    /// [`transform`](Self::transform) for those.
+    pub fn bbox(mut self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
+        let width = self.spatial_shape[0] as f64;
+        let height = self.spatial_shape[1] as f64;
+        self.transform = [
+            min_x,
+            (max_x - min_x) / width,
+            0.0,
+            max_y,
+            0.0,
+            -(max_y - min_y) / height,
+        ];
+        self
+    }
+
     /// Add a band with the spec's default layout and sequential pixel values
     /// (0, 1, 2, … in `data_type`).
     pub fn band(self, data_type: BandDataType) -> Self {
@@ -541,6 +561,24 @@ mod tests {
         );
     }
 
+    #[test]
+    fn bbox_sets_axis_aligned_transform() {
+        // A 7x6 raster spanning x[100, 114], y[482, 500] has 2-wide, 3-tall
+        // north-up pixels with its origin at the top-left corner and no skew.
+        let raster = RasterSpec::d2(7, 6)
+            .bbox(100.0, 482.0, 114.0, 500.0)
+            .band(BandDataType::UInt8)
+            .build();
+        let array = RasterStructArray::try_new(&raster).unwrap();
+        let metadata = array.get(0).unwrap().metadata();
+        assert_eq!(metadata.upper_left_x(), 100.0);
+        assert_eq!(metadata.upper_left_y(), 500.0);
+        assert_eq!(metadata.scale_x(), 2.0);
+        assert_eq!(metadata.scale_y(), -3.0);
+        assert_eq!(metadata.skew_x(), 0.0);
+        assert_eq!(metadata.skew_y(), 0.0);
+    }
+
     #[test]
     fn nd_spatial_inference() {
         let raster = RasterSpec::nd(&["time", "y", "x"], &[3, 4, 5])
diff --git a/rust/sedona-testing/src/rasters.rs b/rust/sedona-testing/src/rasters.rs
index 78b98ec05..bdc22610d 100644
--- a/rust/sedona-testing/src/rasters.rs
+++ b/rust/sedona-testing/src/rasters.rs
@@ -23,6 +23,8 @@ use sedona_raster::traits::{BandMetadata, RasterMetadata, RasterRef};
 use sedona_schema::crs::lnglat;
 use sedona_schema::raster::{BandDataType, StorageType};
 
+use crate::raster_spec::RasterSpec;
+
 /// Describes a single in-db band used by test raster builders.
 pub struct InDbTestBand {
     pub datatype: BandDataType,
@@ -79,6 +81,29 @@ pub fn generate_test_rasters(
     Ok(builder.finish()?)
 }
 
+/// The non-null raster that [`generate_test_rasters`] produces at index `i`,
+/// as a declarative [`RasterSpec`]: identical dimensions, geotransform,
+/// sequential UInt16 pixels, lng/lat CRS, and nodata 0. Kept beside the
+/// generator so the two can't drift (a test asserts they stay identical), and
+/// so callers can use it as the expected side of assertions over
+/// `generate_test_rasters` output.
+pub fn generate_test_raster_spec(i: usize) -> RasterSpec {
+    let width = i as i64 + 1;
+    let height = i as i64 + 2;
+    let pixels: Vec<u16> = (0..(width * height) as u16).collect();
+    RasterSpec::d2(width, height)
+        .transform([
+            i as f64 + 1.0,
+            i.max(1) as f64 * 0.1,
+            i as f64 * 0.03,
+            i as f64 + 2.0,
+            i as f64 * 0.04,
+            i.max(1) as f64 * -0.2,
+        ])
+        .band_values(&pixels)
+        .nodata(0u16)
+}
+
 /// Generates a set of tiled rasters arranged in a grid
 /// - Each raster tile has specified dimensions and random pixel values
 /// - Each raster has 3 bands which can be interpreted as RGB values
@@ -533,8 +558,24 @@ pub fn assert_raster_equal(raster1: &impl RasterRef, raster2: &impl RasterRef) {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::raster_spec::assert_rasters_equal;
+    use arrow_array::ArrayRef;
     use sedona_raster::array::RasterStructArray;
     use sedona_raster::traits::RasterRef;
+    use std::sync::Arc;
+
+    #[test]
+    fn generate_test_raster_spec_matches_generator() {
+        // The declarative spec and the builder-based generator must stay in
+        // lockstep so callers can assert generator output against the spec;
+        // pin that they produce identical rasters.
+        let count = 5;
+        let actual: ArrayRef = Arc::new(generate_test_rasters(count, None).unwrap());
+        let expected: Vec<Option<RasterSpec>> = (0..count)
+            .map(|i| Some(generate_test_raster_spec(i)))
+            .collect();
+        assert_rasters_equal(&actual, &expected);
+    }
 
     #[test]
     fn test_generate_test_rasters() {