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::dtype::DType;
17use vortex_array::dtype::Nullability;
18use vortex_array::dtype::extension::ExtDType;
19use vortex_array::scalar::Scalar;
20use vortex_error::VortexExpect;
21use vortex_error::VortexResult;
22use vortex_error::vortex_err;
23use vortex_session::VortexSession;
24use vortex_session::registry::CachedId;
25
26use crate::extension::GeoMetadata;
27use crate::extension::Rect;
28use crate::extension::box_storage_dtype;
29use crate::extension::coordinate::Dimension;
30use crate::extension::coordinate::box_corners;
31use crate::extension::coordinate::ordinates;
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 (!xs.is_empty()).then(|| {
82 let [xmin, ymin, xmax, ymax] = box_corners(xs, ys);
83 GeoRect::new((xmin, ymin), (xmax, ymax))
84 })
85}
86
87fn rect_from_storage(scalar: &Scalar) -> VortexResult<Option<GeoRect<f64>>> {
90 if scalar.is_null() {
91 return Ok(None);
92 }
93 let storage = scalar.as_extension().to_storage_scalar();
94 let fields = storage.as_struct();
95 let read = |name: &str| -> VortexResult<f64> {
96 f64::try_from(
97 &fields
98 .field(name)
99 .ok_or_else(|| vortex_err!("AABB missing {name}"))?,
100 )
101 };
102 Ok(Some(GeoRect::new(
103 (read("xmin")?, read("ymin")?),
104 (read("xmax")?, read("ymax")?),
105 )))
106}
107
108fn rect_to_storage(rect: GeoRect<f64>) -> Scalar {
110 let storage = Scalar::struct_(
111 aabb_storage_dtype(),
112 vec![
113 Scalar::primitive(rect.min().x, Nullability::NonNullable),
114 Scalar::primitive(rect.min().y, Nullability::NonNullable),
115 Scalar::primitive(rect.max().x, Nullability::NonNullable),
116 Scalar::primitive(rect.max().y, Nullability::NonNullable),
117 ],
118 );
119 Scalar::extension::<Rect>(GeoMetadata::default(), storage)
120}
121
122impl AggregateFnVTable for GeometryAabb {
123 type Options = EmptyOptions;
124 type Partial = AabbPartial;
125
126 fn id(&self) -> AggregateFnId {
127 static ID: CachedId = CachedId::new("vortex.geo.aabb");
128 *ID
129 }
130
131 fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
133 Ok(Some(vec![]))
134 }
135
136 fn deserialize(
137 &self,
138 _metadata: &[u8],
139 _session: &VortexSession,
140 ) -> VortexResult<Self::Options> {
141 Ok(EmptyOptions)
142 }
143
144 fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
145 is_native_geometry(input_dtype).then(aabb_dtype)
146 }
147
148 fn zone_stat_default(&self, input_dtype: &DType) -> Option<AggregateFnRef> {
149 is_native_geometry(input_dtype).then(|| self.bind(EmptyOptions))
150 }
151
152 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
153 self.return_dtype(options, input_dtype)
154 }
155
156 fn empty_partial(
157 &self,
158 _options: &Self::Options,
159 _input_dtype: &DType,
160 ) -> VortexResult<Self::Partial> {
161 Ok(AabbPartial { rect: None })
162 }
163
164 fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
165 if let Some(rect) = rect_from_storage(&other)? {
166 partial.merge(rect);
167 }
168 Ok(())
169 }
170
171 fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
172 Ok(match partial.rect {
173 Some(rect) => rect_to_storage(rect),
174 None => Scalar::null(aabb_dtype()),
175 })
176 }
177
178 fn reset(&self, partial: &mut Self::Partial) {
179 partial.rect = None;
180 }
181
182 fn is_saturated(&self, _partial: &Self::Partial) -> bool {
183 false
185 }
186
187 fn accumulate(
188 &self,
189 partial: &mut Self::Partial,
190 batch: &Columnar,
191 ctx: &mut ExecutionCtx,
192 ) -> VortexResult<()> {
193 let array = match batch {
194 Columnar::Canonical(canonical) => canonical.clone().into_array(),
195 Columnar::Constant(constant) => constant.clone().into_array(),
196 };
197 let valid = array.validity()?.execute_mask(array.len(), ctx)?;
207 let array = array.filter(valid)?;
208 let coords = flatten_coordinates(&array, ctx)?;
212 let xs = ordinates(&coords, "x", ctx)?;
213 let ys = ordinates(&coords, "y", ctx)?;
214 if let Some(rect) = aabb_of(&xs, &ys) {
215 partial.merge(rect);
216 }
217 Ok(())
218 }
219
220 fn finalize(&self, partials: ArrayRef) -> VortexResult<ArrayRef> {
221 Ok(partials)
223 }
224
225 fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
226 self.to_scalar(partial)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use geo::Rect as GeoRect;
233 use vortex_array::ArrayRef;
234 use vortex_array::VortexSessionExecute;
235 use vortex_array::aggregate_fn::Accumulator;
236 use vortex_array::aggregate_fn::AggregateFnVTable;
237 use vortex_array::aggregate_fn::DynAccumulator;
238 use vortex_array::aggregate_fn::EmptyOptions;
239 use vortex_array::aggregate_fn::session::AggregateFnSessionExt;
240 use vortex_array::dtype::DType;
241 use vortex_array::dtype::Nullability;
242 use vortex_array::dtype::PType;
243 use vortex_array::scalar::Scalar;
244 use vortex_error::VortexResult;
245
246 use super::AabbPartial;
247 use super::GeometryAabb;
248 use super::aabb_dtype;
249 use super::rect_from_storage;
250 use crate::test_harness::geo_session;
251 use crate::test_harness::linestring_column;
252 use crate::test_harness::multilinestring_column;
253 use crate::test_harness::multipoint_column;
254 use crate::test_harness::multipolygon_column;
255 use crate::test_harness::nullable_point_column;
256 use crate::test_harness::point_column;
257 use crate::test_harness::polygon_column;
258
259 fn every_native_column(vertices: &[(f64, f64)]) -> VortexResult<Vec<ArrayRef>> {
261 let (xs, ys): (Vec<f64>, Vec<f64>) = vertices.iter().copied().unzip();
262 let flat = vertices.to_vec();
263 Ok(vec![
264 point_column(xs, ys)?,
265 linestring_column(vec![flat.clone()])?,
266 multipoint_column(vec![flat.clone()])?,
267 polygon_column(vec![vec![flat.clone()]])?,
268 multilinestring_column(vec![vec![flat.clone()]])?,
269 multipolygon_column(vec![vec![vec![flat]]])?,
270 ])
271 }
272
273 #[test]
275 fn serializes_for_zone_storage() -> VortexResult<()> {
276 let session = vortex_array::array_session();
277 let metadata = GeometryAabb
278 .serialize(&EmptyOptions)?
279 .expect("GeometryAabb must be serializable to be stored as a zone statistic");
280 GeometryAabb.deserialize(&metadata, &session)?;
281 Ok(())
282 }
283
284 fn aabb(result: &Scalar) -> VortexResult<(f64, f64, f64, f64)> {
286 let rect = rect_from_storage(result)?.expect("non-null AABB");
287 Ok((rect.min().x, rect.min().y, rect.max().x, rect.max().y))
288 }
289
290 #[test]
292 fn point_aabb_across_batches() -> VortexResult<()> {
293 let session = vortex_array::array_session();
294 let mut ctx = session.create_execution_ctx();
295
296 let dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
297 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
298
299 acc.accumulate(&point_column(vec![1.0, 3.0], vec![2.0, 4.0])?, &mut ctx)?;
300 acc.accumulate(&point_column(vec![-1.0], vec![5.0])?, &mut ctx)?;
301
302 assert_eq!(aabb(&acc.finish()?)?, (-1.0, 2.0, 3.0, 5.0));
303 Ok(())
304 }
305
306 #[test]
309 fn null_rows_do_not_widen_aabb() -> VortexResult<()> {
310 let session = vortex_array::array_session();
311 let mut ctx = session.create_execution_ctx();
312
313 let column = nullable_point_column(vec![Some((5.0, 6.0)), None, Some((7.0, 8.0))])?;
315 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, column.dtype().clone())?;
316 acc.accumulate(&column, &mut ctx)?;
317
318 assert_eq!(aabb(&acc.finish()?)?, (5.0, 6.0, 7.0, 8.0));
320 Ok(())
321 }
322
323 #[test]
326 fn polygon_aabb_union_all_vertices() -> VortexResult<()> {
327 let session = vortex_array::array_session();
328 let mut ctx = session.create_execution_ctx();
329
330 let polygons = polygon_column(vec![
332 vec![vec![(0.0, 0.0), (2.0, 0.0), (2.0, 3.0), (0.0, 3.0)]],
333 vec![vec![(5.0, 5.0), (7.0, 5.0), (7.0, 8.0), (5.0, 8.0)]],
334 ])?;
335 let dtype = polygons.dtype().clone();
336 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
337 acc.accumulate(&polygons, &mut ctx)?;
338
339 assert_eq!(aabb(&acc.finish()?)?, (0.0, 0.0, 7.0, 8.0));
340 Ok(())
341 }
342
343 #[test]
346 fn aabb_covers_every_native_geometry_type() -> VortexResult<()> {
347 let session = vortex_array::array_session();
348 let mut ctx = session.create_execution_ctx();
349
350 for column in every_native_column(&[(1.0, 2.0), (-1.0, 5.0), (3.0, 4.0)])? {
351 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, column.dtype().clone())?;
352 acc.accumulate(&column, &mut ctx)?;
353 assert_eq!(
354 aabb(&acc.finish()?)?,
355 (-1.0, 2.0, 3.0, 5.0),
356 "AABB mismatch for {}",
357 column.dtype()
358 );
359 }
360 Ok(())
361 }
362
363 #[test]
366 fn multipolygon_aabb_union_all_vertices() -> VortexResult<()> {
367 let session = vortex_array::array_session();
368 let mut ctx = session.create_execution_ctx();
369
370 let multipolygons = multipolygon_column(vec![
372 vec![
373 vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]],
374 vec![vec![(4.0, 4.0), (5.0, 4.0), (5.0, 5.0), (4.0, 5.0)]],
375 ],
376 vec![vec![vec![
377 (-3.0, 7.0),
378 (-2.0, 7.0),
379 (-2.0, 9.0),
380 (-3.0, 9.0),
381 ]]],
382 ])?;
383 let dtype = multipolygons.dtype().clone();
384 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
385 acc.accumulate(&multipolygons, &mut ctx)?;
386
387 assert_eq!(aabb(&acc.finish()?)?, (-3.0, 0.0, 5.0, 9.0));
388 Ok(())
389 }
390
391 #[test]
394 fn combine_partials_unions_boxes() -> VortexResult<()> {
395 let bbox = |xmin, ymin, xmax, ymax| AabbPartial {
396 rect: Some(GeoRect::new((xmin, ymin), (xmax, ymax))),
397 };
398 let mut partial = AabbPartial { rect: None };
399 GeometryAabb.combine_partials(
400 &mut partial,
401 GeometryAabb.to_scalar(&bbox(0.0, 0.0, 1.0, 1.0))?,
402 )?;
403 GeometryAabb.combine_partials(
404 &mut partial,
405 GeometryAabb.to_scalar(&bbox(5.0, -2.0, 7.0, 3.0))?,
406 )?;
407 assert_eq!(
408 aabb(&GeometryAabb.to_scalar(&partial)?)?,
409 (0.0, -2.0, 7.0, 3.0)
410 );
411 Ok(())
412 }
413
414 #[test]
416 fn combine_partials_ignores_null() -> VortexResult<()> {
417 let mut partial = AabbPartial {
418 rect: Some(GeoRect::new((0.0, 0.0), (1.0, 1.0))),
419 };
420 GeometryAabb.combine_partials(&mut partial, Scalar::null(aabb_dtype()))?;
421 assert_eq!(
422 aabb(&GeometryAabb.to_scalar(&partial)?)?,
423 (0.0, 0.0, 1.0, 1.0)
424 );
425 Ok(())
426 }
427
428 #[test]
432 fn all_nan_coordinates_kept() -> VortexResult<()> {
433 let session = vortex_array::array_session();
434 let mut ctx = session.create_execution_ctx();
435
436 let column = point_column(vec![f64::NAN, f64::NAN], vec![f64::NAN, f64::NAN])?;
437 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, column.dtype().clone())?;
438 acc.accumulate(&column, &mut ctx)?;
439
440 let (xmin, ymin, xmax, ymax) = aabb(&acc.finish()?)?;
441 assert!(xmin <= xmax && ymin <= ymax);
442 Ok(())
443 }
444
445 #[test]
447 fn empty_group_is_null() -> VortexResult<()> {
448 let dtype = point_column(vec![0.0], vec![0.0])?.dtype().clone();
449 let mut acc = Accumulator::try_new(GeometryAabb, EmptyOptions, dtype)?;
450 assert!(acc.finish()?.is_null());
451 Ok(())
452 }
453
454 #[test]
457 fn registered_as_geometry_zone_default() -> VortexResult<()> {
458 let session = geo_session();
459
460 for column in every_native_column(&[(0.0, 0.0), (1.0, 1.0)])? {
461 assert!(
462 !session
463 .aggregate_fns()
464 .zone_stat_defaults(column.dtype())
465 .is_empty(),
466 "a geometry zone-stat default should be discovered for {}",
467 column.dtype()
468 );
469 }
470 let i32_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
471 assert!(
472 session
473 .aggregate_fns()
474 .zone_stat_defaults(&i32_dtype)
475 .is_empty(),
476 "no geometry zone-stat default should apply to numeric columns"
477 );
478 Ok(())
479 }
480}