Skip to content

Commit 5dbbc22

Browse files
feat(vortex-geo): per-row 2-D AABB compute primitive
Adds vortex_geo::bounds::aabbs_2d, decoding a native geometry column to per-row (min, max) corner pairs for row-oriented consumers, e.g. bulk-loading an in-memory R-tree in a spatial-join operator. Deliberately an internal compute primitive rather than a scalar function: no registration, no SQL name, and no columnar Rect output, since the consumer reads the boxes row by row. Empty geometries yield None; 2-D only. Columnar consumers keep the Rect extension type, and the GeometryAabb aggregate remains the whole-column box. Signed-off-by: Nemo Yu <zyu379@wisc.edu>
1 parent 928738e commit 5dbbc22

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

vortex-geo/src/bounds.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Row-shaped bounding-box computation over native geometry columns.
5+
6+
use geo::BoundingRect;
7+
use vortex_array::ArrayRef;
8+
use vortex_array::ExecutionCtx;
9+
use vortex_error::VortexResult;
10+
11+
use crate::extension::geometries;
12+
13+
/// Per-row 2-D axis-aligned bounding boxes (AABBs) of a native geometry column, as `(min, max)`
14+
/// pairs of `[x, y]` corners.
15+
///
16+
/// The per-row counterpart of the [`GeometryAabb`](crate::aggregate_fn::GeometryAabb) aggregate,
17+
/// which unions the whole column into one box. A point yields a degenerate box with `min == max`;
18+
/// a geometry with no extent (e.g. an empty polygon) yields `None`.
19+
///
20+
/// A non-geometry column is an error. Any `z`/`m` ordinates carry no bounds, this decodes to 2-D `geo_types`.
21+
pub fn aabbs_2d(
22+
array: &ArrayRef,
23+
ctx: &mut ExecutionCtx,
24+
) -> VortexResult<impl Iterator<Item = Option<([f64; 2], [f64; 2])>> + use<>> {
25+
Ok(geometries(array, ctx)?.into_iter().map(|geometry| {
26+
geometry
27+
.bounding_rect()
28+
.map(|rect| ([rect.min().x, rect.min().y], [rect.max().x, rect.max().y]))
29+
}))
30+
}
31+
32+
#[cfg(test)]
33+
mod tests {
34+
use vortex_array::IntoArray;
35+
use vortex_array::VortexSessionExecute;
36+
use vortex_array::arrays::PrimitiveArray;
37+
use vortex_error::VortexResult;
38+
39+
use super::aabbs_2d;
40+
use crate::test_harness::linestring_column;
41+
use crate::test_harness::multilinestring_column;
42+
use crate::test_harness::multipoint_column;
43+
use crate::test_harness::multipolygon_column;
44+
use crate::test_harness::point_column;
45+
use crate::test_harness::polygon_column;
46+
use crate::test_harness::rect_column;
47+
48+
/// A shorthand box from its four corners.
49+
fn bounds(xmin: f64, ymin: f64, xmax: f64, ymax: f64) -> Option<([f64; 2], [f64; 2])> {
50+
Some(([xmin, ymin], [xmax, ymax]))
51+
}
52+
53+
/// A point's box is degenerate: both corners are the point itself.
54+
#[test]
55+
fn point_box_is_degenerate() -> VortexResult<()> {
56+
let session = vortex_array::array_session();
57+
let mut ctx = session.create_execution_ctx();
58+
59+
let points = point_column(vec![1.0, 3.0], vec![2.0, 4.0])?;
60+
assert_eq!(
61+
aabbs_2d(&points, &mut ctx)?.collect::<Vec<_>>(),
62+
vec![bounds(1.0, 2.0, 1.0, 2.0), bounds(3.0, 4.0, 3.0, 4.0)]
63+
);
64+
Ok(())
65+
}
66+
67+
/// A polygon's box is its extent: the min/max over every ring vertex, one box per row.
68+
#[test]
69+
fn polygon_box_is_extent() -> VortexResult<()> {
70+
let session = vortex_array::array_session();
71+
let mut ctx = session.create_execution_ctx();
72+
73+
let polygons = polygon_column(vec![
74+
vec![vec![(0.0, 0.0), (4.0, 0.0), (2.0, 5.0)]],
75+
vec![vec![(-3.0, 7.0), (-2.0, 7.0), (-2.0, 9.0), (-3.0, 9.0)]],
76+
])?;
77+
assert_eq!(
78+
aabbs_2d(&polygons, &mut ctx)?.collect::<Vec<_>>(),
79+
vec![bounds(0.0, 0.0, 4.0, 5.0), bounds(-3.0, 7.0, -2.0, 9.0)]
80+
);
81+
Ok(())
82+
}
83+
84+
/// A `Rect` row is its own bounding box.
85+
#[test]
86+
fn rect_box_is_itself() -> VortexResult<()> {
87+
let session = vortex_array::array_session();
88+
let mut ctx = session.create_execution_ctx();
89+
90+
let rects = rect_column(vec![(0.0, 0.0, 2.0, 3.0), (-1.0, -1.0, 1.0, 1.0)])?;
91+
assert_eq!(
92+
aabbs_2d(&rects, &mut ctx)?.collect::<Vec<_>>(),
93+
vec![bounds(0.0, 0.0, 2.0, 3.0), bounds(-1.0, -1.0, 1.0, 1.0)]
94+
);
95+
Ok(())
96+
}
97+
98+
/// Every multi-vertex native type over the same vertex set yields that set's box, so the
99+
/// whole type family is covered (`Point` has its own degenerate-box test above).
100+
#[test]
101+
fn covers_every_native_geometry_type() -> VortexResult<()> {
102+
let session = vortex_array::array_session();
103+
let mut ctx = session.create_execution_ctx();
104+
105+
let vertices = vec![(1.0, 2.0), (-1.0, 5.0), (3.0, 4.0)];
106+
let columns = vec![
107+
linestring_column(vec![vertices.clone()])?,
108+
multipoint_column(vec![vertices.clone()])?,
109+
polygon_column(vec![vec![vertices.clone()]])?,
110+
multilinestring_column(vec![vec![vertices.clone()]])?,
111+
multipolygon_column(vec![vec![vec![vertices]]])?,
112+
];
113+
for column in columns {
114+
assert_eq!(
115+
aabbs_2d(&column, &mut ctx)?.collect::<Vec<_>>(),
116+
vec![bounds(-1.0, 2.0, 3.0, 5.0)],
117+
"box mismatch for {}",
118+
column.dtype()
119+
);
120+
}
121+
Ok(())
122+
}
123+
124+
/// An empty geometry (here a zero-part multipolygon) has no extent and yields `None`; other
125+
/// rows keep their boxes.
126+
#[test]
127+
fn empty_geometry_has_no_box() -> VortexResult<()> {
128+
let session = vortex_array::array_session();
129+
let mut ctx = session.create_execution_ctx();
130+
131+
let multipolygons = multipolygon_column(vec![
132+
vec![vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]]],
133+
vec![],
134+
])?;
135+
assert_eq!(
136+
aabbs_2d(&multipolygons, &mut ctx)?.collect::<Vec<_>>(),
137+
vec![bounds(0.0, 0.0, 1.0, 1.0), None]
138+
);
139+
Ok(())
140+
}
141+
142+
/// A zero-row column yields a zero-row result.
143+
#[test]
144+
fn empty_column_yields_nothing() -> VortexResult<()> {
145+
let session = vortex_array::array_session();
146+
let mut ctx = session.create_execution_ctx();
147+
148+
let points = point_column(vec![], vec![])?;
149+
assert_eq!(aabbs_2d(&points, &mut ctx)?.count(), 0);
150+
Ok(())
151+
}
152+
153+
/// A non-geometry column is an error.
154+
#[test]
155+
fn non_geometry_column_is_rejected() -> VortexResult<()> {
156+
let session = vortex_array::array_session();
157+
let mut ctx = session.create_execution_ctx();
158+
159+
let numbers = PrimitiveArray::from_iter([1.0f64, 2.0]).into_array();
160+
assert!(aabbs_2d(&numbers, &mut ctx).is_err());
161+
Ok(())
162+
}
163+
}

vortex-geo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::scalar_fn::distance::GeoDistance;
2626
use crate::scalar_fn::intersects::GeoIntersects;
2727

2828
pub mod aggregate_fn;
29+
pub mod bounds;
2930
pub mod extension;
3031
pub mod prune;
3132
pub mod scalar_fn;

0 commit comments

Comments
 (0)