Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion vortex-geo/benches/envelope.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

//! Microbenchmark for the `vortex.geo.envelope` scalar function: per-row bounding boxes over
//! Microbenchmark for the `vortex.st.envelope` scalar function: per-row bounding boxes over
//! native geometry storage.
//!
//! Cases vary the two axes that dominate the kernel's cost profile:
Expand Down
36 changes: 36 additions & 0 deletions vortex-geo/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,18 @@ use vortex_array::dtype::extension::ExtDType;
use vortex_array::dtype::extension::ExtVTable;
use vortex_array::scalar::Scalar;
use vortex_arrow::FromArrowArray;
use vortex_buffer::BitBuffer;
use vortex_buffer::Buffer;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_error::vortex_ensure;
use vortex_error::vortex_err;
use vortex_mask::Mask;
pub use wkb::*;

use crate::extension::coordinate::box_corners;
use crate::extension::coordinate::ordinates;

/// Whether `dtype` is one of the native geometry extension types the geo kernels operate on.
pub(crate) fn is_native_geometry(dtype: &DType) -> bool {
dtype.as_extension_opt().is_some_and(|ext| {
Expand Down Expand Up @@ -159,6 +164,37 @@ pub(crate) fn flatten_row_offsets(
Ok((row_offsets, level.execute::<StructArray>(ctx)?))
}

/// Visit the XY bounds of every non-empty row in native coordinate storage.
///
/// The callback receives the row index and `[xmin, ymin, xmax, ymax]`. The returned mask marks
/// exactly the rows that own at least one coordinate. Callers combine it with the geometry
/// validity: a null row can still have placeholder coordinates in its storage, while an empty
/// (but valid) row owns none.
///
/// This walks the nested list parents once to attribute leaf coordinates to their outer geometry
/// row. It deliberately does not materialize a `geoarrow.box` array, so scalar kernels can
/// consume each row's bounds directly.
pub(crate) fn for_each_row_coordinate_bounds(
storage: ArrayRef,
ctx: &mut ExecutionCtx,
mut visit: impl FnMut(usize, [f64; 4]),
) -> VortexResult<Mask> {
let len = storage.len();
let (row_offsets, coords) = flatten_row_offsets(storage, ctx)?;
let xs = ordinates(&coords, "x", ctx)?;
let ys = ordinates(&coords, "y", ctx)?;

let non_empty = Mask::from(BitBuffer::collect_bool(len, |row| {
row_offsets[row] < row_offsets[row + 1]
}));
for (row, (&start, &end)) in row_offsets.iter().zip(&row_offsets[1..]).enumerate() {
if start != end {
visit(row, box_corners(&xs[start..end], &ys[start..end]));
}
}
Ok(non_empty)
}

/// Decode a native geometry column to `geo_types`. A non-geometry operand is an error.
pub(crate) fn geometries(
array: &ArrayRef,
Expand Down
2 changes: 2 additions & 0 deletions vortex-geo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::prune::GeoIntersectsPrune;
use crate::scalar_fn::contains::GeoContains;
use crate::scalar_fn::distance::GeoDistance;
use crate::scalar_fn::envelope::GeoEnvelope;
use crate::scalar_fn::hilbert::GeoHilbert;
use crate::scalar_fn::intersects::GeoIntersects;

pub mod aggregate_fn;
Expand Down Expand Up @@ -65,6 +66,7 @@ pub fn initialize(session: &VortexSession) {

// Register the geometry scalar functions.
session.scalar_fns().register(GeoEnvelope);
session.scalar_fns().register(GeoHilbert);
session.scalar_fns().register(GeoContains);
session.scalar_fns().register(GeoDistance);
session.scalar_fns().register(GeoIntersects);
Expand Down
2 changes: 1 addition & 1 deletion vortex-geo/src/scalar_fn/contains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl ScalarFnVTable for GeoContains {
type Options = EmptyOptions;

fn id(&self) -> ScalarFnId {
static ID: CachedId = CachedId::new("vortex.geo.contains");
static ID: CachedId = CachedId::new("vortex.st.contains");
*ID
}

Expand Down
2 changes: 1 addition & 1 deletion vortex-geo/src/scalar_fn/distance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl ScalarFnVTable for GeoDistance {
type Options = EmptyOptions;

fn id(&self) -> ScalarFnId {
static ID: CachedId = CachedId::new("vortex.geo.distance");
static ID: CachedId = CachedId::new("vortex.st.distance");
*ID
}

Expand Down
32 changes: 9 additions & 23 deletions vortex-geo/src/scalar_fn/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ use vortex_array::scalar_fn::ScalarFnId;
use vortex_array::scalar_fn::ScalarFnVTable;
use vortex_array::scalar_fn::TypedScalarFnInstance;
use vortex_array::validity::Validity;
use vortex_buffer::BitBuffer;
use vortex_buffer::BufferMut;
use vortex_error::VortexResult;
use vortex_error::vortex_err;
use vortex_mask::Mask;
use vortex_session::VortexSession;
use vortex_session::registry::CachedId;

Expand All @@ -40,9 +38,7 @@ use crate::extension::Rect;
use crate::extension::box_field_names;
use crate::extension::box_storage_dtype;
use crate::extension::coordinate::Dimension;
use crate::extension::coordinate::box_corners;
use crate::extension::coordinate::ordinates;
use crate::extension::flatten_row_offsets;
use crate::extension::for_each_row_coordinate_bounds;
use crate::extension::validate_geometry_operands;

/// `envelope`: the axis-aligned bounding box of each geometry in a native geometry operand (a column
Expand Down Expand Up @@ -78,30 +74,20 @@ fn output_box_dtype() -> VortexResult<ExtDType<Rect>> {
fn row_boxes(storage: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<(Vec<ArrayRef>, Validity)> {
let len = storage.len();
let valid = storage.validity()?.execute_mask(len, ctx)?;
let (row_offsets, coords) = flatten_row_offsets(storage, ctx)?;

// A row has a box iff it is valid and owns at least one coordinate (an empty geometry has
// no box). Two masks combined word-at-a-time: folding `valid` into the closure instead —
// per-index or via `Mask::iter` — benches 6-33% slower end-to-end.
let non_empty = Mask::from(BitBuffer::collect_bool(len, |r| {
row_offsets[r] < row_offsets[r + 1]
}));
let xs = ordinates(&coords, "x", ctx)?;
let ys = ordinates(&coords, "y", ctx)?;

// The output's four corner columns.
let mut xmins = BufferMut::zeroed(len);
let mut ymins = BufferMut::zeroed(len);
let mut xmaxs = BufferMut::zeroed(len);
let mut ymaxs = BufferMut::zeroed(len);

for (r, (&start, &end)) in row_offsets.iter().zip(&row_offsets[1..]).enumerate() {
let [xmin, ymin, xmax, ymax] = box_corners(&xs[start..end], &ys[start..end]);
xmins[r] = xmin;
ymins[r] = ymin;
xmaxs[r] = xmax;
ymaxs[r] = ymax;
}
let non_empty =
for_each_row_coordinate_bounds(storage, ctx, |row, [xmin, ymin, xmax, ymax]| {
xmins[row] = xmin;
ymins[row] = ymin;
xmaxs[row] = xmax;
ymaxs[row] = ymax;
})?;

Ok((
vec![
Expand All @@ -118,7 +104,7 @@ impl ScalarFnVTable for GeoEnvelope {
type Options = EmptyOptions;

fn id(&self) -> ScalarFnId {
static ID: CachedId = CachedId::new("vortex.geo.envelope");
static ID: CachedId = CachedId::new("vortex.st.envelope");
*ID
}

Expand Down
Loading
Loading