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() {