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(

Reply via email to