1use geo::Rect as GeoRect;
7use vortex_array::ArrayRef;
8use vortex_array::Columnar;
9use vortex_array::ExecutionCtx;
10use vortex_array::IntoArray;
11use vortex_array::aggregate_fn::AggregateFnId;
12use vortex_array::aggregate_fn::AggregateFnRef;
13use vortex_array::aggregate_fn::AggregateFnVTable;
14use vortex_array::aggregate_fn::AggregateFnVTableExt;
15use vortex_array::aggregate_fn::EmptyOptions;
16use vortex_array::arrays::PrimitiveArray;
17use vortex_array::arrays::struct_::StructArrayExt;
18use vortex_array::dtype::DType;
19use vortex_array::dtype::Nullability;
20use vortex_array::dtype::extension::ExtDType;
21use vortex_array::scalar::Scalar;
22use vortex_error::VortexExpect;
23use vortex_error::VortexResult;
24use vortex_error::vortex_err;
25use vortex_session::VortexSession;
26use vortex_session::registry::CachedId;
27
28use crate::extension::GeoMetadata;
29use crate::extension::Rect;
30use crate::extension::box_storage_dtype;
31use crate::extension::coordinate::Dimension;
32use crate::extension::flatten_coordinates;
33use crate::extension::is_native_geometry;
34
35#[derive(Clone, Debug)]
39pub struct GeometryAabb;
40
41pub struct AabbPartial {
44 rect: Option<GeoRect<f64>>,
45}
46
47impl AabbPartial {
48 fn merge(&mut self, other: GeoRect<f64>) {
50 self.rect = Some(self.rect.map_or(other, |cur| {
51 GeoRect::new(
52 (
53 cur.min().x.min(other.min().x),
54 cur.min().y.min(other.min().y),
55 ),
56 (
57 cur.max().x.max(other.max().x),
58 cur.max().y.max(other.max().y),
59 ),
60 )
61 }));
62 }
63}
64
65fn aabb_dtype() -> DType {
67 DType::Extension(
68 ExtDType::<Rect>::try_new(GeoMetadata::default(), aabb_storage_dtype())
69 .vortex_expect("2D box storage is a valid Rect")
70 .erased(),
71 )
72}
73
74fn aabb_storage_dtype() -> DType {
76 box_storage_dtype(Dimension::Xy, Nullability::Nullable)
77}
78
79fn aabb_of(xs: &[f64], ys: &[f64]) -> Option<GeoRect<f64>> {
81 if xs.is_empty() {
82 return None;
83 }
84 let min_max = |vals: &[f64]| {
85 vals.iter()
86 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
87 (lo.min(v), hi.max(v))
88 })
89 };
90 let (xmin, xmax) = min_max(xs);
91 let (ymin, ymax) = min_max(ys);
92 Some(GeoRect::new((xmin, ymin), (xmax, ymax)))
93}
94
95fn rect_from_storage(scalar: &Scalar) -> VortexResult<Option<GeoRect<f64>>> {
98 if scalar.is_null() {
99 return Ok(None);
100 }
101 let storage = scalar.as_extension().to_storage_scalar();
102 let fields = storage.as_struct();
103 let read = |name: &str| -> VortexResult<f64> {
104 f64::try_from(
105 &fields
106 .field(name)
107 .ok_or_else(|| vortex_err!("AABB missing {name}"))?,
108 )
109 };
110 Ok(Some(GeoRect::new(
111 (read("xmin")?, read("ymin")?),
112 (read("xmax")?, read("ymax")?),
113 )))
114}
115
116fn rect_to_storage(rect: GeoRect<f64>) -> Scalar {
118 let storage = Scalar::struct_(
119 aabb_storage_dtype(),
120 vec![
121 Scalar::primitive(rect.min().x, Nullability::NonNullable),
122 Scalar::primitive(rect.min().y, Nullability::NonNullable),
123 Scalar::primitive(rect.max().x, Nullability::NonNullable),
124 Scalar::primitive(rect.max().y, Nullability::NonNullable),
125 ],
126 );
127 Scalar::extension::<Rect>(GeoMetadata::default(), storage)
128}
129
130impl AggregateFnVTable for GeometryAabb {
131 type Options = EmptyOptions;
132 type Partial = AabbPartial;
133
134 fn id(&self) -> AggregateFnId {
135 static ID: CachedId = CachedId::new("vortex.geo.aabb");
136 *ID
137 }
138
139 fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
141 Ok(Some(vec![]))
142 }
143
144 fn deserialize(
145 &self,
146 _metadata: &[u8],
147 _session: &VortexSession,
148 ) -> VortexResult<Self::Options> {
149 Ok(EmptyOptions)
150 }
151
152 fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
153 is_native_geometry(input_dtype).then(aabb_dtype)
154 }
155
156 fn zone_stat_default(&self, input_dtype: &DType) -> Option<AggregateFnRef> {
157 is_native_geometry(input_dtype).then(|| self.bind(EmptyOptions))
158 }
159
160 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
161 self.return_dtype(options, input_dtype)
162 }
163
164 fn empty_partial(
165 &self,
166 _options: &Self::Options,
167 _input_dtype: &DType,
168 ) -> VortexResult<Self::Partial> {
169 Ok(AabbPartial { rect: None })
170 }
171
172 fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
173 if let Some(rect) = rect_from_storage(&other)? {
174 partial.merge(rect);
175 }
176 Ok(())
177 }
178
179 fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
180 Ok(match partial.rect {
181 Some(rect) => rect_to_storage(rect),
182 None => Scalar::null(aabb_dtype()),
183 })
184 }
185
186 fn reset(&self, partial: &mut Self::Partial) {
187 partial.rect = None;
188 }
189
190 fn is_saturated(&self, _partial: &Self::Partial) -> bool {
191 false
193 }
194
195 fn accumulate(
196 &self,
197 partial: &mut Self::Partial,
198 batch: &Columnar,
199 ctx: &mut ExecutionCtx,
200 ) -> VortexResult<()> {
201 let array = match batch {
202 Columnar::Canonical(canonical) => canonical.clone().into_array(),
203 Columnar::Constant(constant) => constant.clone().into_array(),
204 };
205 let coords = flatten_coordinates(&array, ctx)?;
208 let xs = coords
209 .unmasked_field_by_name("x")?
210 .clone()
211 .execute::<PrimitiveArray>(ctx)?;
212 let ys = coords
213 .unmasked_field_by_name("y")?
214 .clone()
215 .execute::<PrimitiveArray>(ctx)?;
216 if let Some(rect) = aabb_of(xs.as_slice::<f64>(), ys.as_slice::<f64>()) {
217 partial.merge(rect);
218 }
219 Ok(())
220 }
221
222 fn finalize(&self, partials: ArrayRef) -> VortexResult<ArrayRef> {
223 Ok(partials)
225 }
226
227 fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
228 self.to_scalar(partial)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use geo::Rect as GeoRect;
235 use vortex_array::ArrayRef;
236 use vortex_array::VortexSessionExecute;
237 use vortex_array::aggregate_fn::Accumulator;
238 use vortex_array::aggregate_fn::AggregateFnVTable;
239 use vortex_array::aggregate_fn::DynAccumulator;
240 use vortex_array::aggregate_fn::EmptyOptions;
241 use vortex_array::aggregate_fn::session::AggregateFnSessionExt;
242 use vortex_array::dtype::DType;
243 use vortex_array::dtype::Nullability;
244 use vortex_array::dtype::PType;
245 use vortex_array::scalar::Scalar;
246 use vortex_error::VortexResult;
247
248 use super::AabbPartial;
249 use super::GeometryAabb;
250 use super::aabb_dtype;
251 use super::rect_from_storage;
252 use crate::test_harness::geo_session;
253 use crate::test_harness::linestring_column;
254 use crate::test_harness::multilinestring_column;
255 use crate::test_harness::multipoint_column;
256 use crate::test_harness::multipolygon_column;
257 use crate::test_harness::point_column;
258 use crate::test_harness::polygon_column;
259
260 fn every_native_column(vertices: &[(f64, f64)]) -> VortexResult<Vec<ArrayRef>> {
262 let (xs, ys): (Vec<f64>, Vec<f64>) = vertices.iter().copied().unzip();
263 let flat = vertices.to_vec();
264 Ok(vec![
265 point_column(xs, ys)?,
266 linestring_column(vec![flat.clone()])?,
267 multipoint_column(vec![flat.clone()])?,
268 polygon_column(vec![vec![flat.clone()]])?,
269 multilinestring_column(vec![vec![flat.clone()]])?,
270 multipolygon_column(vec![vec![vec![flat]]])?,
271 ])
272 }
273
274 #[test]
276 fn serializes_for_zone_storage() -> VortexResult<()> {
277 let session = vortex_array::array_session();
278 let metadata = GeometryAabb
279 .serialize(&EmptyOptions)?
280 .expect("GeometryAabb must be serializable to be stored as a zone statistic");
281 GeometryAabb.deserialize(&metadata, &session)?;
282 Ok(())
283 }
284
285 fn aabb(result: &Scalar) -> VortexResult<(f64, f64, f64, f64)> {
287 let rect = rect_from_storage(result)?.expect("non-null AABB");
288 Ok((rect.min().x, rect.min().y, rect.max().x, rect.max().y))
289 }
290
291 #[test]
293 fn point_aabb_across_batches() -> VortexResult<()> {
294 let session = vortex_array::array_session();
295 let mut ctx = session.create_execution_ctx();
296
297 let dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
298 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
299
300 acc.accumulate(&point_column(vec![1.0, 3.0], vec![2.0, 4.0])?, &mut ctx)?;
301 acc.accumulate(&point_column(vec![-1.0], vec![5.0])?, &mut ctx)?;
302
303 assert_eq!(aabb(&acc.finish()?)?, (-1.0, 2.0, 3.0, 5.0));
304 Ok(())
305 }
306
307 #[test]
310 fn polygon_aabb_union_all_vertices() -> VortexResult<()> {
311 let session = vortex_array::array_session();
312 let mut ctx = session.create_execution_ctx();
313
314 let polygons = polygon_column(vec![
316 vec![vec![(0.0, 0.0), (2.0, 0.0), (2.0, 3.0), (0.0, 3.0)]],
317 vec![vec![(5.0, 5.0), (7.0, 5.0), (7.0, 8.0), (5.0, 8.0)]],
318 ])?;
319 let dtype = polygons.dtype().clone();
320 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
321 acc.accumulate(&polygons, &mut ctx)?;
322
323 assert_eq!(aabb(&acc.finish()?)?, (0.0, 0.0, 7.0, 8.0));
324 Ok(())
325 }
326
327 #[test]
330 fn aabb_covers_every_native_geometry_type() -> VortexResult<()> {
331 let session = vortex_array::array_session();
332 let mut ctx = session.create_execution_ctx();
333
334 for column in every_native_column(&[(1.0, 2.0), (-1.0, 5.0), (3.0, 4.0)])? {
335 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, column.dtype().clone())?;
336 acc.accumulate(&column, &mut ctx)?;
337 assert_eq!(
338 aabb(&acc.finish()?)?,
339 (-1.0, 2.0, 3.0, 5.0),
340 "AABB mismatch for {}",
341 column.dtype()
342 );
343 }
344 Ok(())
345 }
346
347 #[test]
350 fn multipolygon_aabb_union_all_vertices() -> VortexResult<()> {
351 let session = vortex_array::array_session();
352 let mut ctx = session.create_execution_ctx();
353
354 let multipolygons = multipolygon_column(vec![
356 vec![
357 vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]],
358 vec![vec![(4.0, 4.0), (5.0, 4.0), (5.0, 5.0), (4.0, 5.0)]],
359 ],
360 vec![vec![vec![
361 (-3.0, 7.0),
362 (-2.0, 7.0),
363 (-2.0, 9.0),
364 (-3.0, 9.0),
365 ]]],
366 ])?;
367 let dtype = multipolygons.dtype().clone();
368 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
369 acc.accumulate(&multipolygons, &mut ctx)?;
370
371 assert_eq!(aabb(&acc.finish()?)?, (-3.0, 0.0, 5.0, 9.0));
372 Ok(())
373 }
374
375 #[test]
378 fn combine_partials_unions_boxes() -> VortexResult<()> {
379 let bbox = |xmin, ymin, xmax, ymax| AabbPartial {
380 rect: Some(GeoRect::new((xmin, ymin), (xmax, ymax))),
381 };
382 let mut partial = AabbPartial { rect: None };
383 GeometryAabb.combine_partials(
384 &mut partial,
385 GeometryAabb.to_scalar(&bbox(0.0, 0.0, 1.0, 1.0))?,
386 )?;
387 GeometryAabb.combine_partials(
388 &mut partial,
389 GeometryAabb.to_scalar(&bbox(5.0, -2.0, 7.0, 3.0))?,
390 )?;
391 assert_eq!(
392 aabb(&GeometryAabb.to_scalar(&partial)?)?,
393 (0.0, -2.0, 7.0, 3.0)
394 );
395 Ok(())
396 }
397
398 #[test]
400 fn combine_partials_ignores_null() -> VortexResult<()> {
401 let mut partial = AabbPartial {
402 rect: Some(GeoRect::new((0.0, 0.0), (1.0, 1.0))),
403 };
404 GeometryAabb.combine_partials(&mut partial, Scalar::null(aabb_dtype()))?;
405 assert_eq!(
406 aabb(&GeometryAabb.to_scalar(&partial)?)?,
407 (0.0, 0.0, 1.0, 1.0)
408 );
409 Ok(())
410 }
411
412 #[test]
416 fn all_nan_coordinates_kept() -> VortexResult<()> {
417 let session = vortex_array::array_session();
418 let mut ctx = session.create_execution_ctx();
419
420 let column = point_column(vec![f64::NAN, f64::NAN], vec![f64::NAN, f64::NAN])?;
421 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, column.dtype().clone())?;
422 acc.accumulate(&column, &mut ctx)?;
423
424 let (xmin, ymin, xmax, ymax) = aabb(&acc.finish()?)?;
425 assert!(xmin <= xmax && ymin <= ymax);
426 Ok(())
427 }
428
429 #[test]
431 fn empty_group_is_null() -> VortexResult<()> {
432 let dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
433 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
434 assert!(acc.finish()?.is_null());
435 Ok(())
436 }
437
438 #[test]
441 fn registered_as_geometry_zone_default() -> VortexResult<()> {
442 let session = geo_session();
443
444 for column in every_native_column(&[(0.0, 0.0), (1.0, 1.0)])? {
445 assert!(
446 !session
447 .aggregate_fns()
448 .zone_stat_defaults(column.dtype())
449 .is_empty(),
450 "a geometry zone-stat default should be discovered for {}",
451 column.dtype()
452 );
453 }
454 let i32_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
455 assert!(
456 session
457 .aggregate_fns()
458 .zone_stat_defaults(&i32_dtype)
459 .is_empty(),
460 "no geometry zone-stat default should apply to numeric columns"
461 );
462 Ok(())
463 }
464}