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
4 changes: 4 additions & 0 deletions vortex-spatial/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,9 @@ harness = false
name = "distance"
harness = false

[[bench]]
name = "make_line"
harness = false

[lints]
workspace = true
122 changes: 122 additions & 0 deletions vortex-spatial/benches/make_line.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

//! Microbenchmarks for native `ST_MakeLine`.
//!
//! The cases cover the normal paired-column operation, a broadcast point constant, and strict
//! null propagation. They execute the result to its canonical representation so the benchmark
//! includes construction of the two-vertex line storage.
//!
//! `ROWS` keeps each case near the roughly 1 ms iteration budget recommended for CodSpeed.
//!
//! Run with `cargo bench -p vortex-spatial --bench make_line`.

#![expect(clippy::unwrap_used)]

use std::sync::LazyLock;

use divan::Bencher;
use divan::counter::ItemsCount;
use mimalloc::MiMalloc;
use vortex_array::ArrayRef;
use vortex_array::Canonical;
use vortex_array::ExecutionCtx;
use vortex_array::IntoArray;
use vortex_array::VortexSessionExecute;
use vortex_array::arrays::ConstantArray;
use vortex_session::VortexSession;
use vortex_spatial::scalar_fn::make_line::SpatialMakeLine;
use vortex_spatial::test_harness::nullable_point_column;
use vortex_spatial::test_harness::point_column;
use vortex_spatial::test_harness::spatial_session;

// Scalar function execution allocates its output inside the timed region, so use the vendored
// allocator instead of measuring glibc differences between CodSpeed runner images.
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

static SESSION: LazyLock<VortexSession> = LazyLock::new(spatial_session);

const ROWS: usize = 512;

fn main() {
divan::main();
}

/// Deterministic pseudo-random value in `[0, 1)`.
fn unit(i: usize) -> f64 {
((i.wrapping_mul(2_654_435_761) >> 8) % 10_000) as f64 / 10_000.0
}

fn points(offset: usize) -> ArrayRef {
let xs = (0..ROWS)
.map(|i| 300.0 * unit(i + offset) - 150.0)
.collect();
let ys = (0..ROWS)
.map(|i| 300.0 * unit(i + offset + 1) - 150.0)
.collect();
point_column(xs, ys).unwrap()
}

fn nullable_points(offset: usize, null_every: usize) -> ArrayRef {
nullable_point_column(
(0..ROWS)
.map(|i| {
(!i.is_multiple_of(null_every)).then(|| {
(
300.0 * unit(i + offset) - 150.0,
300.0 * unit(i + offset + 1) - 150.0,
)
})
})
.collect(),
)
.unwrap()
}

fn point_constant(ctx: &mut ExecutionCtx) -> ArrayRef {
let scalar = point_column(vec![0.0], vec![0.0])
.unwrap()
.execute_scalar(0, ctx)
.unwrap();
ConstantArray::new(scalar, ROWS).into_array()
}

fn make_lines(starts: &ArrayRef, ends: &ArrayRef, ctx: &mut ExecutionCtx) -> ArrayRef {
SpatialMakeLine::try_new_array(starts.clone(), ends.clone())
.unwrap()
.into_array()
.execute::<Canonical>(ctx)
.unwrap()
.into_array()
}

#[divan::bench]
fn column_x_column(bencher: Bencher) {
let starts = points(0);
let ends = points(97);
let mut ctx = SESSION.create_execution_ctx();
bencher
.counter(ItemsCount::new(ROWS))
.bench_local(|| make_lines(&starts, &ends, &mut ctx));
}

#[divan::bench]
fn column_x_constant(bencher: Bencher) {
let starts = points(0);
let mut ctx = SESSION.create_execution_ctx();
let end = point_constant(&mut ctx);
bencher
.counter(ItemsCount::new(ROWS))
.bench_local(|| make_lines(&starts, &end, &mut ctx));
}

#[divan::bench]
fn nullable_columns(bencher: Bencher) {
let starts = nullable_points(0, 8);
let ends = nullable_points(97, 11);
let mut ctx = SESSION.create_execution_ctx();
bencher
.counter(ItemsCount::new(ROWS))
.bench_local(|| make_lines(&starts, &ends, &mut ctx));
}
15 changes: 15 additions & 0 deletions vortex-spatial/src/extension/coordinate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ impl Dimension {
Dimension::Xyzm => &["x", "y", "z", "m"],
}
}

/// Promote two coordinate dimensions to the smallest dimension that represents both.
///
/// Missing `z`/`m` ordinates are materialized as zero when values are converted to this
/// dimension, matching DuckDB Spatial's `ST_MakeLine` promotion.
pub(crate) fn promote(self, other: Self) -> Self {
match (self, other) {
(Self::Xyzm, _) | (_, Self::Xyzm) | (Self::Xyz, Self::Xym) | (Self::Xym, Self::Xyz) => {
Self::Xyzm
}
(Self::Xyz, _) | (_, Self::Xyz) => Self::Xyz,
(Self::Xym, _) | (_, Self::Xym) => Self::Xym,
(Self::Xy, Self::Xy) => Self::Xy,
}
}
}

impl From<GeoArrowDimension> for Dimension {
Expand Down
80 changes: 80 additions & 0 deletions vortex-spatial/src/extension/linestring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@ use prost::Message;
use vortex_array::ArrayRef;
use vortex_array::ExecutionCtx;
use vortex_array::IntoArray;
use vortex_array::arrays::ConstantArray;
use vortex_array::arrays::ExtensionArray;
use vortex_array::arrays::InterleaveArray;
use vortex_array::arrays::ListArray;
use vortex_array::arrays::PrimitiveArray;
use vortex_array::arrays::StructArray;
use vortex_array::arrays::extension::ExtensionArrayExt;
use vortex_array::arrays::struct_::StructArrayExt;
use vortex_array::dtype::DType;
use vortex_array::dtype::FieldNames;
use vortex_array::dtype::Nullability;
use vortex_array::dtype::extension::ExtDType;
use vortex_array::dtype::extension::ExtId;
use vortex_array::dtype::extension::ExtVTable;
use vortex_array::scalar::ScalarValue;
use vortex_array::validity::Validity;
use vortex_arrow::ArrowExport;
use vortex_arrow::ArrowExportVTable;
use vortex_arrow::ArrowImport;
Expand All @@ -38,10 +46,12 @@ use vortex_arrow::ArrowSession;
use vortex_arrow::ArrowSessionExt;
use vortex_arrow::FromArrowArray;
use vortex_arrow::FromArrowType;
use vortex_buffer::Buffer;
use vortex_error::VortexError;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_error::vortex_ensure;
use vortex_error::vortex_ensure_eq;
use vortex_error::vortex_err;
use vortex_session::registry::CachedId;
use vortex_session::registry::Id;
Expand Down Expand Up @@ -103,6 +113,76 @@ pub(crate) fn linestring_dimension(dtype: &DType) -> VortexResult<Dimension> {
coordinate_dimension(coords)
}

/// Return one coordinate ordinate, filling an ordinate absent from the point dimension with zero.
fn point_ordinate(
points: &StructArray,
dimension: Dimension,
name: &str,
) -> VortexResult<ArrayRef> {
if dimension.field_names().contains(&name) {
points.unmasked_field_by_name(name).cloned()
} else {
Ok(ConstantArray::new(0.0f64, points.len()).into_array())
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in what cases do you ever run into an "absent ordinate", and is returning 0.0 really ok here? instead of null, for example?

its a bit hard for me to understand why we want this behavior in the create linestring functionality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“absent ordinate” only occurs with mixed-dimensional inputs. We promote the output dimension to the union of both inputs and fill missing Z/M values with zero, matching DuckDB’s ST_MakeLine behavior.

Null would represent a null coordinate inside an otherwise valid geometry and is not supported by the native LineString coordinate storage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Build one native [`LineString`] per corresponding pair of point coordinate rows.
pub(crate) fn linestring_array_from_point_pairs(
ext_dtype: &ExtDType<LineString>,
starts: &StructArray,
ends: &StructArray,
validity: Validity,
) -> VortexResult<ArrayRef> {
let len = starts.len();
vortex_ensure_eq!(
len,
ends.len(),
"spatial: line string point columns must have equal lengths"
);
let vertex_count = len
.checked_mul(2)
.ok_or_else(|| vortex_err!("spatial: two-vertex line string length overflow"))?;
let last_offset = i32::try_from(vertex_count)
.map_err(|_| vortex_err!("spatial: two-vertex line string offset overflow"))?;
let row_count = u32::try_from(len)
.map_err(|_| vortex_err!("spatial: two-vertex line string row count overflow"))?;
let dimension = linestring_dimension(ext_dtype.storage_dtype())?;
let start_dimension = coordinate_dimension(starts.dtype())?;
let end_dimension = coordinate_dimension(ends.dtype())?;

let array_indices = PrimitiveArray::from_iter((0..len).flat_map(|_| [0u8, 1])).into_array();
let row_indices = Buffer::from_iter((0..row_count).flat_map(|row| [row; 2])).into_array();

let ordinates = dimension
.field_names()
.iter()
.map(|name| {
Ok(InterleaveArray::try_new(
vec![
point_ordinate(starts, start_dimension, name)?,
point_ordinate(ends, end_dimension, name)?,
],
array_indices.clone(),
row_indices.clone(),
)?
.into_array())
})
.collect::<VortexResult<Vec<_>>>()?;

let vertices = StructArray::try_new(
FieldNames::from(dimension.field_names()),
ordinates,
vertex_count,
Validity::NonNullable,
)?
.into_array();

let offsets = Buffer::from_iter((0..=last_offset).step_by(2)).into_array();
let storage = ListArray::try_new(vertices, offsets, validity)?.into_array();

Ok(ExtensionArray::try_new(ext_dtype.clone().erased(), storage)?.into_array())
}

static ARROW_LINESTRING: CachedId = CachedId::new(LineStringType::NAME);

/// The `geoarrow.linestring` extension type for `dimension`, with separated (struct) coordinates
Expand Down
2 changes: 2 additions & 0 deletions vortex-spatial/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::scalar_fn::contains::SpatialContains;
use crate::scalar_fn::distance::SpatialDistance;
use crate::scalar_fn::envelope::SpatialEnvelope;
use crate::scalar_fn::intersects::SpatialIntersects;
use crate::scalar_fn::make_line::SpatialMakeLine;

pub mod aggregate_fn;
pub mod extension;
Expand Down Expand Up @@ -68,6 +69,7 @@ pub fn initialize(session: &VortexSession) {
session.scalar_fns().register(SpatialContains);
session.scalar_fns().register(SpatialDistance);
session.scalar_fns().register(SpatialIntersects);
session.scalar_fns().register(SpatialMakeLine);

// The axis-aligned bounding-box (AABB) aggregate; self-declares as a per-chunk zone stat for
// geometry columns.
Expand Down
11 changes: 6 additions & 5 deletions vortex-spatial/src/scalar_fn/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@

//! Shared execution for native geometry scalar functions.
//!
//! [`dispatch_unary`] and the binary dispatcher handle constant/column operands and strict null
//! [`dispatch_unary`] and [`dispatch_binary`] handle constant/column operands and strict null
//! propagation without prescribing how a kernel represents geometries or builds its output.
//! Native columnar kernels such as `ST_Envelope` use the unary dispatcher directly.
//! Native columnar kernels such as `ST_MakeLine` use these dispatchers directly.
//!
//! [`execute_binary_geo_types`] adapts row-oriented algorithms from the `geo` ecosystem. It decodes
//! valid inputs into `geo_types::Geometry`; the final output is still a Vortex [`ArrayRef`], such
//! as an `f64` or boolean array.
//! [`execute_binary_geo_types`] is a convenience adapter for row-oriented algorithms from the
//! `geo` ecosystem. It decodes valid inputs into `geo_types::Geometry`; the final output is still
//! a Vortex [`ArrayRef`], such as an `f64` or boolean array.

mod binary;
mod geo_types;
mod unary;

pub(crate) use binary::dispatch_binary;
pub(crate) use binary::execute_binary_geo_types;
pub(crate) use unary::dispatch_unary;
use vortex_array::ArrayRef;
Expand Down
Loading
Loading