This is an automated email from the ASF dual-hosted git repository.
paleolimbot 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 a3dcc123 feat(rust/sedona-raster-functions): Add RS_RasterToWorldCoord
(#383)
a3dcc123 is described below
commit a3dcc12365ac10ad52ab8f79c57159cffb42991a
Author: jp <[email protected]>
AuthorDate: Tue Dec 2 08:31:40 2025 -0800
feat(rust/sedona-raster-functions): Add RS_RasterToWorldCoord (#383)
---
.../benches/native-raster-functions.rs | 32 +++-
rust/sedona-raster-functions/src/lib.rs | 1 +
rust/sedona-raster-functions/src/register.rs | 2 +
.../src/rs_worldcoordinate.rs | 205 +++++++++++++++++++++
rust/sedona-raster/src/affine_transformation.rs | 91 +++++++++
rust/sedona-raster/src/lib.rs | 1 +
rust/sedona-testing/src/benchmark_util.rs | 32 ++++
7 files changed, 355 insertions(+), 9 deletions(-)
diff --git a/rust/sedona-raster-functions/benches/native-raster-functions.rs
b/rust/sedona-raster-functions/benches/native-raster-functions.rs
index e29761d5..ca7a1f49 100644
--- a/rust/sedona-raster-functions/benches/native-raster-functions.rs
+++ b/rust/sedona-raster-functions/benches/native-raster-functions.rs
@@ -15,19 +15,33 @@
// specific language governing permissions and limitations
// under the License.
use criterion::{criterion_group, criterion_main, Criterion};
-use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*};
+use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*,
BenchmarkArgs};
fn criterion_benchmark(c: &mut Criterion) {
let f = sedona_raster_functions::register::default_function_set();
- benchmark::scalar(c, &f, "native", "rs_height", Raster(64, 64));
- benchmark::scalar(c, &f, "native", "rs_scalex", Raster(64, 64));
- benchmark::scalar(c, &f, "native", "rs_scaley", Raster(64, 64));
- benchmark::scalar(c, &f, "native", "rs_skewx", Raster(64, 64));
- benchmark::scalar(c, &f, "native", "rs_skewy", Raster(64, 64));
- benchmark::scalar(c, &f, "native", "rs_upperleftx", Raster(64, 64));
- benchmark::scalar(c, &f, "native", "rs_upperlefty", Raster(64, 64));
- benchmark::scalar(c, &f, "native", "rs_width", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_height", Raster(64, 64));
+ benchmark::scalar(
+ c,
+ &f,
+ "native-raster",
+ "rs_rastertoworldcoordx",
+ BenchmarkArgs::ArrayScalarScalar(Raster(64, 64), Int32(0, 63),
Int32(0, 63)),
+ );
+ benchmark::scalar(
+ c,
+ &f,
+ "native-raster",
+ "rs_rastertoworldcoordy",
+ BenchmarkArgs::ArrayScalarScalar(Raster(64, 64), Int32(0, 63),
Int32(0, 63)),
+ );
+ benchmark::scalar(c, &f, "native-raster", "rs_scalex", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_scaley", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_skewx", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_skewy", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_upperleftx", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_upperlefty", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_width", Raster(64, 64));
}
criterion_group!(benches, criterion_benchmark);
diff --git a/rust/sedona-raster-functions/src/lib.rs
b/rust/sedona-raster-functions/src/lib.rs
index c678a571..c6c96a7a 100644
--- a/rust/sedona-raster-functions/src/lib.rs
+++ b/rust/sedona-raster-functions/src/lib.rs
@@ -20,3 +20,4 @@ pub mod register;
pub mod rs_example;
pub mod rs_geotransform;
pub mod rs_size;
+pub mod rs_worldcoordinate;
diff --git a/rust/sedona-raster-functions/src/register.rs
b/rust/sedona-raster-functions/src/register.rs
index 9fee06ea..c850e365 100644
--- a/rust/sedona-raster-functions/src/register.rs
+++ b/rust/sedona-raster-functions/src/register.rs
@@ -47,6 +47,8 @@ pub fn default_function_set() -> FunctionSet {
crate::rs_geotransform::rs_upperlefty_udf,
crate::rs_size::rs_height_udf,
crate::rs_size::rs_width_udf,
+ crate::rs_worldcoordinate::rs_rastertoworldcoordx_udf,
+ crate::rs_worldcoordinate::rs_rastertoworldcoordy_udf,
);
register_aggregate_udfs!(function_set,);
diff --git a/rust/sedona-raster-functions/src/rs_worldcoordinate.rs
b/rust/sedona-raster-functions/src/rs_worldcoordinate.rs
new file mode 100644
index 00000000..09e90896
--- /dev/null
+++ b/rust/sedona-raster-functions/src/rs_worldcoordinate.rs
@@ -0,0 +1,205 @@
+// 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.
+use std::{sync::Arc, vec};
+
+use crate::executor::RasterExecutor;
+use arrow_array::builder::Float64Builder;
+use arrow_schema::DataType;
+use datafusion_common::{error::Result, exec_err, ScalarValue};
+use datafusion_expr::{
+ scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation,
Volatility,
+};
+use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_raster::affine_transformation::to_world_coordinate;
+use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+
+/// RS_RasterToWorldCoordY() scalar UDF implementation
+///
+/// Converts pixel coordinates to world Y coordinate
+pub fn rs_rastertoworldcoordy_udf() -> SedonaScalarUDF {
+ SedonaScalarUDF::new(
+ "rs_rastertoworldcoordy",
+ vec![Arc::new(RsCoordinateMapper { coord: Coord::Y })],
+ Volatility::Immutable,
+ Some(rs_rastertoworldcoordy_doc()),
+ )
+}
+
+/// RS_RasterToWorldCoordX() scalar UDF documentation
+///
+/// Converts pixel coordinates to world X coordinate
+pub fn rs_rastertoworldcoordx_udf() -> SedonaScalarUDF {
+ SedonaScalarUDF::new(
+ "rs_rastertoworldcoordx",
+ vec![Arc::new(RsCoordinateMapper { coord: Coord::X })],
+ Volatility::Immutable,
+ Some(rs_rastertoworldcoordx_doc()),
+ )
+}
+
+fn rs_rastertoworldcoordy_doc() -> Documentation {
+ Documentation::builder(
+ DOC_SECTION_OTHER,
+ "Returns the upper left Y coordinate of the given row and column of
the given raster geometric units of the geo-referenced raster. If any out of
bounds values are given, the Y coordinate of the assumed point considering
existing raster pixel size and skew values will be returned.".to_string(),
+ "RS_RasterToWorldCoordY(raster: Raster)".to_string(),
+ )
+ .with_argument("raster", "Raster: Input raster")
+ .with_argument("x", "Integer: Column x into the raster")
+ .with_argument("y", "Integer: Row y into the raster")
+ .with_sql_example("SELECT RS_RasterToWorldCoordY(RS_Example(), 0,
0)".to_string())
+ .build()
+}
+
+fn rs_rastertoworldcoordx_doc() -> Documentation {
+ Documentation::builder(
+ DOC_SECTION_OTHER,
+ "Returns the upper left X coordinate of the given row and column of
the given raster geometric units of the geo-referenced raster. If any out of
bounds values are given, the X coordinate of the assumed point considering
existing raster pixel size and skew values will be returned.".to_string(),
+ "RS_RasterToWorldCoordX(raster: Raster)".to_string(),
+ )
+ .with_argument("raster", "Raster: Input raster")
+ .with_argument("x", "Integer: Column x into the raster")
+ .with_argument("y", "Integer: Row y into the raster")
+ .with_sql_example("SELECT RS_RasterToWorldCoordX(RS_Example(), 0,
0)".to_string())
+ .build()
+}
+
+#[derive(Debug, Clone)]
+enum Coord {
+ X,
+ Y,
+}
+
+#[derive(Debug)]
+struct RsCoordinateMapper {
+ coord: Coord,
+}
+
+impl SedonaScalarKernel for RsCoordinateMapper {
+ fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+ let matcher = ArgMatcher::new(
+ vec![
+ ArgMatcher::is_raster(),
+ ArgMatcher::is_integer(),
+ ArgMatcher::is_integer(),
+ ],
+ SedonaType::Arrow(DataType::Float64),
+ );
+
+ matcher.match_args(args)
+ }
+
+ fn invoke_batch(
+ &self,
+ arg_types: &[SedonaType],
+ args: &[ColumnarValue],
+ ) -> Result<ColumnarValue> {
+ let executor = RasterExecutor::new(arg_types, args);
+ let mut builder =
Float64Builder::with_capacity(executor.num_iterations());
+
+ let (x_opt, y_opt) = get_scalar_coord(&args[1], &args[2])?;
+
+ executor.execute_raster_void(|_i, raster_opt| {
+ match (raster_opt, x_opt, y_opt) {
+ (Some(raster), Some(x), Some(y)) => {
+ let (world_x, world_y) = to_world_coordinate(&raster, x,
y);
+ match self.coord {
+ Coord::X => builder.append_value(world_x),
+ Coord::Y => builder.append_value(world_y),
+ };
+ }
+ (_, _, _) => builder.append_null(),
+ }
+ Ok(())
+ })?;
+
+ executor.finish(Arc::new(builder.finish()))
+ }
+}
+
+fn extract_int_scalar(arg: &ColumnarValue) -> Result<Option<i64>> {
+ match arg {
+ ColumnarValue::Scalar(scalar) => {
+ let i64_val = scalar.cast_to(&DataType::Int64)?;
+ match i64_val {
+ ScalarValue::Int64(Some(v)) => Ok(Some(v)),
+ _ => Ok(None),
+ }
+ }
+ _ => exec_err!("Expected scalar integer argument for coordinate"),
+ }
+}
+
+fn get_scalar_coord(
+ x_arg: &ColumnarValue,
+ y_arg: &ColumnarValue,
+) -> Result<(Option<i64>, Option<i64>)> {
+ let x_opt = extract_int_scalar(x_arg)?;
+ let y_opt = extract_int_scalar(y_arg)?;
+ Ok((x_opt, y_opt))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use datafusion_expr::ScalarUDF;
+ use rstest::rstest;
+ use sedona_schema::datatypes::RASTER;
+ use sedona_testing::compare::assert_array_equal;
+ use sedona_testing::rasters::generate_test_rasters;
+ use sedona_testing::testers::ScalarUdfTester;
+
+ #[test]
+ fn udf_docs() {
+ let udf: ScalarUDF = rs_rastertoworldcoordy_udf().into();
+ assert_eq!(udf.name(), "rs_rastertoworldcoordy");
+ assert!(udf.documentation().is_some());
+
+ let udf: ScalarUDF = rs_rastertoworldcoordx_udf().into();
+ assert_eq!(udf.name(), "rs_rastertoworldcoordx");
+ assert!(udf.documentation().is_some());
+ }
+
+ #[rstest]
+ fn udf_invoke(#[values(Coord::Y, Coord::X)] coord: Coord) {
+ let udf = match coord {
+ Coord::X => rs_rastertoworldcoordx_udf(),
+ Coord::Y => rs_rastertoworldcoordy_udf(),
+ };
+ let tester = ScalarUdfTester::new(
+ udf.into(),
+ vec![
+ RASTER,
+ SedonaType::Arrow(DataType::Int32),
+ SedonaType::Arrow(DataType::Int32),
+ ],
+ );
+
+ let rasters = generate_test_rasters(3, Some(1)).unwrap();
+ // At 0,0 expect the upper left corner of the test values
+ let expected_values = match coord {
+ Coord::X => vec![Some(1.0), None, Some(3.0)],
+ Coord::Y => vec![Some(2.0), None, Some(4.0)],
+ };
+ let expected: Arc<dyn arrow_array::Array> =
+ Arc::new(arrow_array::Float64Array::from(expected_values));
+
+ let result = tester
+ .invoke_array_scalar_scalar(Arc::new(rasters), 0_i32, 0_i32)
+ .unwrap();
+ assert_array_equal(&result, &expected);
+ }
+}
diff --git a/rust/sedona-raster/src/affine_transformation.rs
b/rust/sedona-raster/src/affine_transformation.rs
new file mode 100644
index 00000000..b846c49c
--- /dev/null
+++ b/rust/sedona-raster/src/affine_transformation.rs
@@ -0,0 +1,91 @@
+// 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.
+
+use crate::traits::RasterRef;
+
+/// Performs an affine transformation on the provided x and y coordinates
based on the geotransform
+/// data in the raster.
+///
+/// # Arguments
+/// * `raster` - Reference to the raster containing metadata
+/// * `x` - X coordinate in pixel space (column)
+/// * `y` - Y coordinate in pixel space (row)
+#[inline]
+pub fn to_world_coordinate(raster: &dyn RasterRef, x: i64, y: i64) -> (f64,
f64) {
+ let metadata = raster.metadata();
+ let x_f64 = x as f64;
+ let y_f64 = y as f64;
+
+ let world_x = metadata.upper_left_x() + x_f64 * metadata.scale_x() + y_f64
* metadata.skew_x();
+ let world_y = metadata.upper_left_y() + x_f64 * metadata.skew_y() + y_f64
* metadata.scale_y();
+
+ (world_x, world_y)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::{MetadataRef, RasterMetadata};
+
+ struct TestRaster {
+ metadata: RasterMetadata,
+ }
+
+ impl RasterRef for TestRaster {
+ fn metadata(&self) -> &dyn MetadataRef {
+ &self.metadata
+ }
+ fn crs(&self) -> Option<&str> {
+ None
+ }
+ fn bands(&self) -> &dyn crate::traits::BandsRef {
+ unimplemented!()
+ }
+ }
+
+ #[test]
+ fn test_to_world_coordinate_basic() {
+ // Test case with rotation/skew
+ let raster = TestRaster {
+ metadata: RasterMetadata {
+ width: 10,
+ height: 20,
+ upperleft_x: 100.0,
+ upperleft_y: 200.0,
+ scale_x: 1.0,
+ scale_y: -2.0,
+ skew_x: 0.25,
+ skew_y: 0.5,
+ },
+ };
+
+ let (wx, wy) = to_world_coordinate(&raster, 0, 0);
+ assert_eq!((wx, wy), (100.0, 200.0));
+
+ let (wx, wy) = to_world_coordinate(&raster, 5, 10);
+ assert_eq!((wx, wy), (107.5, 182.5));
+
+ let (wx, wy) = to_world_coordinate(&raster, 9, 19);
+ assert_eq!((wx, wy), (113.75, 166.5));
+
+ let (wx, wy) = to_world_coordinate(&raster, 1, 0);
+ assert_eq!((wx, wy), (101.0, 200.5));
+
+ let (wx, wy) = to_world_coordinate(&raster, 0, 1);
+ assert_eq!((wx, wy), (100.25, 198.0));
+ }
+}
diff --git a/rust/sedona-raster/src/lib.rs b/rust/sedona-raster/src/lib.rs
index 89a00ba1..de2e9b2e 100644
--- a/rust/sedona-raster/src/lib.rs
+++ b/rust/sedona-raster/src/lib.rs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+pub mod affine_transformation;
pub mod array;
pub mod builder;
pub mod traits;
diff --git a/rust/sedona-testing/src/benchmark_util.rs
b/rust/sedona-testing/src/benchmark_util.rs
index 00f3c981..ba86db9c 100644
--- a/rust/sedona-testing/src/benchmark_util.rs
+++ b/rust/sedona-testing/src/benchmark_util.rs
@@ -282,6 +282,8 @@ pub enum BenchmarkArgSpec {
Int64(i64, i64),
/// Randomly generated floating point input with a given range of values
Float64(f64, f64),
+ /// Randomly generated integer input with a given range of values
+ Int32(i32, i32),
/// A transformation of any of the above based on a [ScalarUDF] accepting
/// a single argument
Transformed(Box<BenchmarkArgSpec>, ScalarUDF),
@@ -303,6 +305,7 @@ impl Debug for BenchmarkArgSpec {
Self::MultiPoint(arg0) =>
f.debug_tuple("MultiPoint").field(arg0).finish(),
Self::Int64(arg0, arg1) =>
f.debug_tuple("Int64").field(arg0).field(arg1).finish(),
Self::Float64(arg0, arg1) =>
f.debug_tuple("Float64").field(arg0).field(arg1).finish(),
+ Self::Int32(arg0, arg1) =>
f.debug_tuple("Int32").field(arg0).field(arg1).finish(),
Self::Transformed(inner, t) => write!(f, "{}({:?})", t.name(),
inner),
Self::String(s) => write!(f, "String({s})"),
Self::Raster(w, h) =>
f.debug_tuple("Raster").field(w).field(h).finish(),
@@ -321,6 +324,7 @@ impl BenchmarkArgSpec {
| BenchmarkArgSpec::MultiPoint(_) => WKB_GEOMETRY,
BenchmarkArgSpec::Int64(_, _) =>
SedonaType::Arrow(DataType::Int64),
BenchmarkArgSpec::Float64(_, _) =>
SedonaType::Arrow(DataType::Float64),
+ BenchmarkArgSpec::Int32(_, _) =>
SedonaType::Arrow(DataType::Int32),
BenchmarkArgSpec::Transformed(inner, t) => {
let tester = ScalarUdfTester::new(t.clone(),
vec![inner.sedona_type()]);
tester.return_type().unwrap()
@@ -418,6 +422,17 @@ impl BenchmarkArgSpec {
})
.collect()
}
+ BenchmarkArgSpec::Int32(lo, hi) => {
+ let mut rng = self.rng(i);
+ let dist = Uniform::new(lo, hi);
+ (0..num_batches)
+ .map(|_| -> Result<ArrayRef> {
+ let int32_array: arrow_array::Int32Array =
+ (0..rows_per_batch).map(|_|
rng.sample(dist)).collect();
+ Ok(Arc::new(int32_array))
+ })
+ .collect()
+ }
BenchmarkArgSpec::Transformed(inner, t) => {
let inner_type = inner.sedona_type();
let inner_arrays = inner.build_arrays(i, num_batches,
rows_per_batch)?;
@@ -696,6 +711,23 @@ mod test {
}
}
+ #[test]
+ fn arg_spec_int() {
+ let spec = BenchmarkArgSpec::Int32(1, 10);
+ assert_eq!(spec.sedona_type(), SedonaType::Arrow(DataType::Int32));
+ let arrays = spec.build_arrays(0, 2, ROWS_PER_BATCH).unwrap();
+ assert_eq!(arrays.len(), 2);
+ // Make sure this is deterministic
+ assert_eq!(spec.build_arrays(0, 2, ROWS_PER_BATCH).unwrap(), arrays);
+ // Make sure we generate different arrays for different argument
numbers
+ assert_ne!(spec.build_arrays(1, 2, ROWS_PER_BATCH).unwrap(), arrays);
+ for array in arrays {
+ assert_eq!(array.data_type(), &DataType::Int32);
+ assert_eq!(array.len(), ROWS_PER_BATCH);
+ assert_eq!(array.null_count(), 0);
+ }
+ }
+
#[test]
fn arg_spec_transformed() {
let udf = SimpleScalarUDF::new(