Skip to content

Commit 434be5c

Browse files
authored
feat: Add ST_Perimeter implementation based on georust/geo and benchmarks (#76)
1 parent 9efa952 commit 434be5c

File tree

6 files changed

+174
-10
lines changed

6 files changed

+174
-10
lines changed

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmarks/test_functions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,19 @@ def queries():
148148
eng.execute_and_collect(f"SELECT ST_Length(geom1) from {table}")
149149

150150
benchmark(queries)
151+
152+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS, DuckDB])
153+
@pytest.mark.parametrize(
154+
"table",
155+
[
156+
"polygons_simple",
157+
"polygons_complex",
158+
],
159+
)
160+
def test_st_perimeter(self, benchmark, eng, table):
161+
eng = self._get_eng(eng)
162+
163+
def queries():
164+
eng.execute_and_collect(f"SELECT ST_Perimeter(geom1) from {table}")
165+
166+
benchmark(queries)

rust/sedona-geo/benches/geo-functions.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ fn criterion_benchmark(c: &mut Criterion) {
2727
benchmark::scalar(c, &f, "geo", "st_area", Polygon(10));
2828
benchmark::scalar(c, &f, "geo", "st_area", Polygon(500));
2929

30+
benchmark::scalar(c, &f, "geo", "st_perimeter", Polygon(10));
31+
benchmark::scalar(c, &f, "geo", "st_perimeter", Polygon(500));
32+
3033
benchmark::scalar(
3134
c,
3235
&f,

rust/sedona-geo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ mod st_intersection_aggr;
2424
mod st_intersects;
2525
mod st_length;
2626
mod st_line_interpolate_point;
27+
mod st_perimeter;
2728
mod st_union_aggr;
2829
pub mod to_geo;

rust/sedona-geo/src/register.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::st_union_aggr::st_union_aggr_impl;
2323
use crate::{
2424
st_area::st_area_impl, st_centroid::st_centroid_impl, st_distance::st_distance_impl,
2525
st_dwithin::st_dwithin_impl, st_intersects::st_intersects_impl, st_length::st_length_impl,
26+
st_perimeter::st_perimeter_impl,
2627
};
2728

2829
pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> {
@@ -33,6 +34,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> {
3334
("st_distance", st_distance_impl()),
3435
("st_dwithin", st_dwithin_impl()),
3536
("st_length", st_length_impl()),
37+
("st_perimeter", st_perimeter_impl()),
3638
("st_lineinterpolatepoint", st_line_interpolate_point_impl()),
3739
]
3840
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::sync::Arc;
19+
20+
use arrow_array::builder::Float64Builder;
21+
use arrow_schema::DataType;
22+
use datafusion_common::error::Result;
23+
use datafusion_expr::ColumnarValue;
24+
use geo_generic_alg::algorithm::{line_measures::Euclidean, LengthMeasurableExt};
25+
use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
26+
use sedona_functions::executor::WkbExecutor;
27+
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
28+
use wkb::reader::Wkb;
29+
30+
/// ST_Perimeter() implementation using [LengthMeasurableExt::perimeter_ext] with Euclidean metric
31+
pub fn st_perimeter_impl() -> ScalarKernelRef {
32+
Arc::new(STPerimeter {})
33+
}
34+
35+
#[derive(Debug)]
36+
struct STPerimeter {}
37+
38+
impl SedonaScalarKernel for STPerimeter {
39+
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
40+
let matcher = ArgMatcher::new(
41+
vec![ArgMatcher::is_geometry()],
42+
SedonaType::Arrow(DataType::Float64),
43+
);
44+
45+
matcher.match_args(args)
46+
}
47+
48+
fn invoke_batch(
49+
&self,
50+
arg_types: &[SedonaType],
51+
args: &[ColumnarValue],
52+
) -> Result<ColumnarValue> {
53+
let executor = WkbExecutor::new(arg_types, args);
54+
let mut builder = Float64Builder::with_capacity(executor.num_iterations());
55+
executor.execute_wkb_void(|maybe_wkb| {
56+
match maybe_wkb {
57+
Some(wkb) => {
58+
builder.append_value(invoke_scalar(&wkb)?);
59+
}
60+
_ => builder.append_null(),
61+
}
62+
63+
Ok(())
64+
})?;
65+
66+
executor.finish(Arc::new(builder.finish()))
67+
}
68+
}
69+
70+
fn invoke_scalar(wkb: &Wkb) -> Result<f64> {
71+
Ok(wkb.perimeter_ext(&Euclidean))
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use arrow_array::{create_array, ArrayRef};
77+
use datafusion_common::scalar::ScalarValue;
78+
use rstest::rstest;
79+
use sedona_functions::register::stubs::st_perimeter_udf;
80+
use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_VIEW_GEOMETRY};
81+
use sedona_testing::testers::ScalarUdfTester;
82+
83+
use super::*;
84+
85+
#[rstest]
86+
fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) {
87+
let mut udf = st_perimeter_udf();
88+
udf.add_kernel(st_perimeter_impl());
89+
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type]);
90+
91+
assert_eq!(
92+
tester.return_type().unwrap(),
93+
SedonaType::Arrow(DataType::Float64)
94+
);
95+
96+
// Test with a square polygon
97+
assert_eq!(
98+
tester
99+
.invoke_wkb_scalar(Some("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"))
100+
.unwrap(),
101+
ScalarValue::Float64(Some(4.0))
102+
);
103+
104+
let input_wkt = vec![
105+
Some("POINT(1 2)"), // Point should have 0 perimeter
106+
None,
107+
Some("LINESTRING (0 0, 3 4)"), // LineString perimeter equals length (0.0)
108+
Some("POLYGON ((0 0, 4 0, 4 3, 0 3, 0 0))"), // Rectangle perimeter: 2*(4+3) = 14
109+
Some("POLYGON ((0 0, 1 0, 0.5 1, 0 0))"), // Triangle with sides approx 1, 1.118, 1.118
110+
Some("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))"), // Two unit squares
111+
];
112+
let expected: ArrayRef = create_array!(
113+
Float64,
114+
[
115+
Some(0.0),
116+
None,
117+
Some(0.0),
118+
Some(14.0),
119+
Some(3.236_067_977_499_79), // 1 + sqrt(1.25) + sqrt(1.25)
120+
Some(8.0) // 4 + 4
121+
]
122+
);
123+
assert_eq!(&tester.invoke_wkb_array(input_wkt).unwrap(), &expected);
124+
}
125+
126+
#[test]
127+
fn test_polygon_with_hole() {
128+
let mut udf = st_perimeter_udf();
129+
udf.add_kernel(st_perimeter_impl());
130+
let tester = ScalarUdfTester::new(udf.into(), vec![WKB_GEOMETRY]);
131+
132+
// Polygon with a hole: outer ring 40, inner ring 24
133+
assert_eq!(
134+
tester
135+
.invoke_wkb_scalar(Some(
136+
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 8 2, 8 8, 2 8, 2 2))"
137+
))
138+
.unwrap(),
139+
ScalarValue::Float64(Some(64.0))
140+
);
141+
}
142+
}

0 commit comments

Comments
 (0)