diff --git a/vortex-geo/src/extension/coordinate.rs b/vortex-geo/src/extension/coordinate.rs new file mode 100644 index 00000000000..45f9aefc45d --- /dev/null +++ b/vortex-geo/src/extension/coordinate.rs @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Coordinate building blocks for geometry extension types: the `Struct` storage, +//! its [`Dimension`], and the decoded [`Coordinate`] value. +//! +//! The coordinate fields, where `?` marks an optional field, are: +//! - `x` — longitude or easting +//! - `y` — latitude or northing +//! - `z?` — elevation +//! - `m?` — measure: an arbitrary per-point value such as distance along a route or a timestamp + +use std::fmt::Display; +use std::fmt::Formatter; + +use vortex_array::ArrayRef; +use vortex_array::ExecutionCtx; +use vortex_array::arrays::ExtensionArray; +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::Nullability; +use vortex_array::dtype::PType; +use vortex_array::scalar::Scalar; +use vortex_error::VortexResult; +use vortex_error::vortex_bail; +use vortex_error::vortex_ensure; +use vortex_error::vortex_err; + +/// Coordinate dimensions, matching GeoArrow. Field order is fixed: `x`, `y`, then `z` before `m`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Dimension { + /// 2D: `x`, `y`. + Xy, + /// 3D with elevation: `x`, `y`, `z`. + Xyz, + /// 3D with a measure: `x`, `y`, `m`. + Xym, + /// 4D: `x`, `y`, `z`, `m`. + Xyzm, +} + +impl Dimension { + /// Recover the dimension from a coordinate's field names, in GeoArrow order. + pub(crate) fn from_field_names(names: &[&str]) -> VortexResult { + Ok(match names { + ["x", "y"] => Dimension::Xy, + ["x", "y", "z"] => Dimension::Xyz, + ["x", "y", "m"] => Dimension::Xym, + ["x", "y", "z", "m"] => Dimension::Xyzm, + _ => vortex_bail!("not a valid GeoArrow coordinate dimension: {names:?}"), + }) + } +} + +/// A decoded coordinate. `z?`/`m?` are `Some` iff the storage dimension includes them. +/// +/// This is the native value produced when unpacking a [`Point`](crate::extension::Point) scalar; +/// the rest of the coordinate machinery is crate-internal. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Coordinate { + /// The x (longitude/easting) ordinate. + pub x: f64, + /// The y (latitude/northing) ordinate. + pub y: f64, + /// The optional `z?` (elevation) ordinate. + pub z: Option, + /// The optional `m?` (measure) ordinate. + pub m: Option, +} + +impl Coordinate { + /// A 2D coordinate (`z?`/`m?` unset). + pub fn xy(x: f64, y: f64) -> Self { + Coordinate { + x, + y, + z: None, + m: None, + } + } +} + +impl Display for Coordinate { + fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { + match (self.z, self.m) { + (None, None) => write!(fmt, "POINT({} {})", self.x, self.y), + (Some(z), None) => write!(fmt, "POINT Z ({} {} {})", self.x, self.y, z), + (None, Some(m)) => write!(fmt, "POINT M ({} {} {})", self.x, self.y, m), + (Some(z), Some(m)) => write!(fmt, "POINT ZM ({} {} {} {})", self.x, self.y, z, m), + } + } +} + +/// Validate that `dtype` is a coordinate struct of non-nullable `f64` fields, returning its +/// [`Dimension`]. Any of the four GeoArrow dimensions validates. +pub(crate) fn coordinate_dimension(dtype: &DType) -> VortexResult { + let DType::Struct(fields, _) = dtype else { + vortex_bail!("coordinate storage must be a Struct, was {dtype}"); + }; + let names: Vec<&str> = fields.names().iter().map(|n| n.as_ref()).collect(); + for (i, field) in fields.fields().enumerate() { + vortex_ensure!( + matches!( + field, + DType::Primitive(PType::F64, Nullability::NonNullable) + ), + "coordinate field {} must be non-nullable f64, was {field}", + names[i] + ); + } + Dimension::from_field_names(&names) +} + +/// Decode a [`Coordinate`] from a coordinate `Struct` scalar (`z?`/`m?` read iff +/// present, so the same decoder serves every dimension). +pub(crate) fn coordinate_from_struct(scalar: &Scalar) -> VortexResult { + let fields = scalar.as_struct(); + let required = |name: &str| -> VortexResult { + f64::try_from( + &fields + .field(name) + .ok_or_else(|| vortex_err!("coordinate missing {name}"))?, + ) + }; + let optional = |name: &str| -> VortexResult> { + fields + .field(name) + .map(|value| f64::try_from(&value)) + .transpose() + }; + Ok(Coordinate { + x: required("x")?, + y: required("y")?, + z: optional("z")?, + m: optional("m")?, + }) +} + +/// Decode a [`Coordinate`] from an extension-typed point scalar (unwrapped to its coordinate +/// storage) or a bare coordinate `Struct` scalar. The per-row decode used by the distance fns. +pub(crate) fn coordinate_from_scalar(scalar: &Scalar) -> VortexResult { + match scalar.as_extension_opt() { + Some(ext_scalar) => coordinate_from_struct(&ext_scalar.to_storage_scalar()), + None => coordinate_from_struct(scalar), + } +} + +/// Canonicalize a point column once and return its flat `x`/`y` `f64` columns. The bulk counterpart +/// to [`coordinate_from_scalar`]; distances use only `x`/`y`, so `z?`/`m?` are ignored. +pub(crate) fn xy_columns( + points: &ArrayRef, + ctx: &mut ExecutionCtx, +) -> VortexResult<(PrimitiveArray, PrimitiveArray)> { + let storage = points + .clone() + .execute::(ctx)? + .storage_array() + .clone() + .execute::(ctx)?; + let xs = storage + .unmasked_field_by_name("x")? + .clone() + .execute::(ctx)?; + let ys = storage + .unmasked_field_by_name("y")? + .clone() + .execute::(ctx)?; + Ok((xs, ys)) +} + +#[cfg(test)] +mod tests { + use super::Coordinate; + + /// Display emits WKT, including `z?`/`m?` when present. + #[test] + fn display_is_wkt() { + let coordinate = |z, m| Coordinate { + x: 1.0, + y: 2.0, + z, + m, + }; + assert_eq!(coordinate(None, None).to_string(), "POINT(1 2)"); + assert_eq!(coordinate(Some(3.0), None).to_string(), "POINT Z (1 2 3)"); + assert_eq!(coordinate(None, Some(4.0)).to_string(), "POINT M (1 2 4)"); + assert_eq!( + coordinate(Some(3.0), Some(4.0)).to_string(), + "POINT ZM (1 2 3 4)" + ); + } +} diff --git a/vortex-geo/src/extension/mod.rs b/vortex-geo/src/extension/mod.rs index f08b76ae83d..d69dd239d14 100644 --- a/vortex-geo/src/extension/mod.rs +++ b/vortex-geo/src/extension/mod.rs @@ -1,10 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +pub(crate) mod coordinate; +mod point; mod wkb; use std::fmt::Display; +pub use point::*; pub use wkb::*; /// Extension metadata that is common to all the geospatial extension types. diff --git a/vortex-geo/src/extension/point.rs b/vortex-geo/src/extension/point.rs new file mode 100644 index 00000000000..2b9ee2c5bc1 --- /dev/null +++ b/vortex-geo/src/extension/point.rs @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! The [`Point`] geometry extension type (`vortex.geo.point`): a location stored columnarly as +//! `Struct` of `f64`, tagged with [`GeoMetadata`] (CRS). `z?` is an optional +//! elevation and `m?` an optional measure — an arbitrary per-point value such as distance along a +//! route or a timestamp. + +use prost::Message; +use vortex_array::dtype::extension::ExtDType; +use vortex_array::dtype::extension::ExtId; +use vortex_array::dtype::extension::ExtVTable; +use vortex_array::scalar::Scalar; +use vortex_array::scalar::ScalarValue; +use vortex_error::VortexResult; + +use super::GeoMetadata; +use super::coordinate::Coordinate; +use super::coordinate::coordinate_dimension; +use super::coordinate::coordinate_from_struct; + +/// A single location: `geoarrow.point`, stored as `Struct` of `f64`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct Point; + +impl ExtVTable for Point { + type Metadata = GeoMetadata; + type NativeValue<'a> = Coordinate; + + fn id(&self) -> ExtId { + ExtId::new_static("vortex.geo.point") + } + + fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult> { + Ok(metadata.encode_to_vec()) + } + + fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult { + Ok(GeoMetadata::decode(metadata)?) + } + + fn validate_dtype(ext_dtype: &ExtDType) -> VortexResult<()> { + coordinate_dimension(ext_dtype.storage_dtype()).map(|_| ()) + } + + fn unpack_native<'a>( + ext_dtype: &'a ExtDType, + storage_value: &'a ScalarValue, + ) -> VortexResult { + let storage = Scalar::try_new( + ext_dtype.storage_dtype().clone(), + Some(storage_value.clone()), + )?; + coordinate_from_struct(&storage) + } +} + +#[cfg(test)] +mod tests { + use vortex_array::IntoArray; + use vortex_array::VortexSessionExecute; + use vortex_array::arrays::ExtensionArray; + use vortex_array::arrays::PrimitiveArray; + use vortex_array::arrays::StructArray; + use vortex_array::dtype::DType; + use vortex_array::dtype::FieldNames; + use vortex_array::dtype::Nullability; + use vortex_array::dtype::PType; + use vortex_array::dtype::StructFields; + use vortex_array::dtype::extension::ExtDType; + use vortex_array::session::ArraySession; + use vortex_error::VortexResult; + use vortex_session::VortexSession; + + use super::Point; + use crate::extension::GeoMetadata; + use crate::extension::coordinate::Coordinate; + use crate::extension::coordinate::Dimension; + use crate::extension::coordinate::coordinate_dimension; + use crate::extension::coordinate::coordinate_from_scalar; + + fn geo_meta() -> GeoMetadata { + GeoMetadata { + crs: Some("EPSG:4326".to_string()), + } + } + + /// A coordinate storage dtype with the given field names, non-nullable `f64` per field. + fn coordinate_dtype(names: &[&'static str]) -> DType { + let fields = std::iter::repeat_n( + DType::Primitive(PType::F64, Nullability::NonNullable), + names.len(), + ) + .collect::>(); + DType::Struct( + StructFields::new(FieldNames::from(names), fields), + Nullability::NonNullable, + ) + } + + /// `Point` accepts every GeoArrow dimension; the canonical field names round-trip to their + /// dimension, so a `z?`/`m?` swap or a mislabel would be caught. + #[test] + fn point_validates_every_dimension() -> VortexResult<()> { + let cases = [ + (Dimension::Xy, ["x", "y"].as_slice()), + (Dimension::Xyz, ["x", "y", "z"].as_slice()), + (Dimension::Xym, ["x", "y", "m"].as_slice()), + (Dimension::Xyzm, ["x", "y", "z", "m"].as_slice()), + ]; + for (dim, names) in cases { + let storage = coordinate_dtype(names); + assert_eq!(coordinate_dimension(&storage)?, dim); + ExtDType::::try_new(geo_meta(), storage)?; + } + Ok(()) + } + + /// Invalid storage is rejected at dtype construction: both non-struct storage and a struct whose + /// fields are not GeoArrow coordinates. + #[test] + fn point_rejects_invalid_storage() -> VortexResult<()> { + let primitive = DType::Primitive(PType::F64, Nullability::NonNullable); + assert!(ExtDType::::try_new(geo_meta(), primitive).is_err()); + + let wrong_fields = StructArray::from_fields(&[ + ("a", PrimitiveArray::from_iter(vec![0.0f64]).into_array()), + ("b", PrimitiveArray::from_iter(vec![0.0f64]).into_array()), + ])? + .into_array(); + assert!(ExtDType::::try_new(geo_meta(), wrong_fields.dtype().clone()).is_err()); + Ok(()) + } + + /// A `Point` column round-trips through scalar execution back to the original coordinates. + #[test] + fn point_unpacks_coordinates() -> VortexResult<()> { + let session = VortexSession::empty().with::(); + let mut ctx = session.create_execution_ctx(); + + let storage = StructArray::from_fields(&[ + ( + "x", + PrimitiveArray::from_iter(vec![1.0f64, -111.7610]).into_array(), + ), + ( + "y", + PrimitiveArray::from_iter(vec![2.0f64, 34.8697]).into_array(), + ), + ])? + .into_array(); + let dtype = ExtDType::::try_new(geo_meta(), storage.dtype().clone())?; + let points = ExtensionArray::new(dtype.erased(), storage).into_array(); + + assert_eq!( + coordinate_from_scalar(&points.execute_scalar(0, &mut ctx)?)?, + Coordinate::xy(1.0, 2.0) + ); + assert_eq!( + coordinate_from_scalar(&points.execute_scalar(1, &mut ctx)?)?, + Coordinate::xy(-111.7610, 34.8697) + ); + Ok(()) + } +} diff --git a/vortex-geo/src/lib.rs b/vortex-geo/src/lib.rs index 513caf85d92..9d0cde26f9e 100644 --- a/vortex-geo/src/lib.rs +++ b/vortex-geo/src/lib.rs @@ -5,17 +5,25 @@ use std::sync::Arc; use vortex_array::arrow::ArrowSessionExt; use vortex_array::dtype::session::DTypeSessionExt; +use vortex_array::scalar_fn::session::ScalarFnSessionExt; use vortex_session::VortexSession; +use crate::extension::Point; use crate::extension::WellKnownBinary; +use crate::scalar_fn::distance::GeoDistance; pub mod extension; +pub mod scalar_fn; /// Set up a session with support for geospatial extension types, encodings and layouts. pub fn initialize(session: &VortexSession) { - // register geospatial extension types + // Register the geospatial extension types. session.dtypes().register(WellKnownBinary); session.arrow().register_exporter(Arc::new(WellKnownBinary)); session.arrow().register_importer(Arc::new(WellKnownBinary)); + session.dtypes().register(Point); + + // Register the geometry scalar functions. + session.scalar_fns().register(GeoDistance); } #[cfg(test)] diff --git a/vortex-geo/src/scalar_fn/distance.rs b/vortex-geo/src/scalar_fn/distance.rs new file mode 100644 index 00000000000..0fda4fea6b2 --- /dev/null +++ b/vortex-geo/src/scalar_fn/distance.rs @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Straight-line (Euclidean) distance between points; "planar" distance in GIS terms. + +use vortex_array::ArrayRef; +use vortex_array::ExecutionCtx; +use vortex_array::IntoArray; +use vortex_array::arrays::Constant; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::arrays::ScalarFnArray; +use vortex_array::dtype::DType; +use vortex_array::dtype::Nullability; +use vortex_array::dtype::PType; +use vortex_array::scalar::Scalar; +use vortex_array::scalar_fn::Arity; +use vortex_array::scalar_fn::ChildName; +use vortex_array::scalar_fn::EmptyOptions; +use vortex_array::scalar_fn::ExecutionArgs; +use vortex_array::scalar_fn::ScalarFnId; +use vortex_array::scalar_fn::ScalarFnVTable; +use vortex_array::scalar_fn::TypedScalarFnInstance; +use vortex_error::VortexResult; +use vortex_session::VortexSession; + +use crate::extension::coordinate::coordinate_from_scalar; +use crate::extension::coordinate::xy_columns; + +/// Straight-line (L2) distance between `(ax, ay)` and `(bx, by)`. +fn euclidean_distance(ax: f64, ay: f64, bx: f64, by: f64) -> f64 { + let dx = ax - bx; + let dy = ay - by; + (dx * dx + dy * dy).sqrt() +} + +/// Straight-line (Euclidean) distance between two point operands — "planar" distance in GIS terms +/// (e.g. PostGIS `ST_Distance`). No geodesic correction, and `z?`/`m?` are ignored. +/// +/// The operands are two point columns of equal length; either (or both) may be constant, in which +/// case the constant query point is decoded once and broadcast. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct GeoDistance; + +impl GeoDistance { + /// A lazy `ScalarFnArray` computing the per-row distance between the point columns `a` and + /// `b`; either may be constant. The output length is taken from `a`. + pub fn try_new_array(a: ArrayRef, b: ArrayRef) -> VortexResult { + let len = a.len(); + ScalarFnArray::try_new( + TypedScalarFnInstance::new(GeoDistance, EmptyOptions).erased(), + vec![a, b], + len, + ) + } +} + +impl ScalarFnVTable for GeoDistance { + type Options = EmptyOptions; + + fn id(&self) -> ScalarFnId { + ScalarFnId::new("vortex.geo.distance") + } + + fn serialize(&self, _: &Self::Options) -> VortexResult>> { + Ok(Some(vec![])) + } + + fn deserialize(&self, _: &[u8], _: &VortexSession) -> VortexResult { + Ok(EmptyOptions) + } + + fn arity(&self, _: &Self::Options) -> Arity { + Arity::Exact(2) + } + + fn child_name(&self, _: &Self::Options, child_idx: usize) -> ChildName { + match child_idx { + 0 => ChildName::from("a"), + 1 => ChildName::from("b"), + _ => unreachable!("distance has exactly two children"), + } + } + + fn return_dtype(&self, _: &Self::Options, _: &[DType]) -> VortexResult { + Ok(DType::Primitive(PType::F64, Nullability::NonNullable)) + } + + fn execute( + &self, + _: &Self::Options, + args: &dyn ExecutionArgs, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + let a = args.get(0)?; + let b = args.get(1)?; + match (a.as_opt::(), b.as_opt::()) { + (Some(qa), Some(qb)) => { + let qa = coordinate_from_scalar(qa.scalar())?; + let qb = coordinate_from_scalar(qb.scalar())?; + let distance = euclidean_distance(qa.x, qa.y, qb.x, qb.y); + Ok(ConstantArray::new( + Scalar::primitive(distance, Nullability::NonNullable), + a.len(), + ) + .into_array()) + } + (Some(query), None) => distances_to_constant(&b, query.scalar(), ctx), + (None, Some(query)) => distances_to_constant(&a, query.scalar(), ctx), + (None, None) => { + let (axs, ays) = xy_columns(&a, ctx)?; + let (bxs, bys) = xy_columns(&b, ctx)?; + let distances = axs + .as_slice::() + .iter() + .zip(ays.as_slice::()) + .zip(bxs.as_slice::().iter().zip(bys.as_slice::())) + .map(|((&ax, &ay), (&bx, &by))| euclidean_distance(ax, ay, bx, by)); + Ok(PrimitiveArray::from_iter(distances).into_array()) + } + } + } +} + +/// Distance from each row of `points` to a constant `query` point, decoded once and broadcast. +/// Distance is symmetric, so this serves a constant on either side. +fn distances_to_constant( + points: &ArrayRef, + query: &Scalar, + ctx: &mut ExecutionCtx, +) -> VortexResult { + let query = coordinate_from_scalar(query)?; + let (xs, ys) = xy_columns(points, ctx)?; + let distances = xs + .as_slice::() + .iter() + .zip(ys.as_slice::()) + .map(|(&x, &y)| euclidean_distance(x, y, query.x, query.y)); + Ok(PrimitiveArray::from_iter(distances).into_array()) +} + +#[cfg(test)] +mod tests { + 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_array::arrays::ExtensionArray; + use vortex_array::arrays::PrimitiveArray; + use vortex_array::arrays::StructArray; + use vortex_array::dtype::extension::ExtDType; + use vortex_array::session::ArraySession; + use vortex_error::VortexResult; + use vortex_session::VortexSession; + + use super::GeoDistance; + use super::euclidean_distance; + use crate::extension::GeoMetadata; + use crate::extension::Point; + + /// A `Point` column (CRS `EPSG:4326`) over the given x/y coordinates. + fn point_column(xs: Vec, ys: Vec) -> VortexResult { + let storage = StructArray::from_fields(&[ + ("x", PrimitiveArray::from_iter(xs).into_array()), + ("y", PrimitiveArray::from_iter(ys).into_array()), + ])? + .into_array(); + let metadata = GeoMetadata { + crs: Some("EPSG:4326".to_string()), + }; + let dtype = ExtDType::::try_new(metadata, storage.dtype().clone())?; + Ok(ExtensionArray::new(dtype.erased(), storage).into_array()) + } + + /// A constant `Point` column of length `len`, every row at `(x, y)`. + fn point_constant( + x: f64, + y: f64, + len: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + let single = point_column(vec![x], vec![y])?.execute_scalar(0, ctx)?; + Ok(ConstantArray::new(single, len).into_array()) + } + + /// Execute a `GeoDistance` array and read back its per-row `f64` distances. + fn distances(distance: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult> { + Ok(distance + .execute::(ctx)? + .into_primitive() + .as_slice::() + .to_vec()) + } + + /// The kernel computes straight-line distance (the 3–4–5 triangle). + #[test] + fn euclidean_distance_is_straight_line() { + assert_eq!(euclidean_distance(0.0, 0.0, 3.0, 4.0), 5.0); + assert_eq!(euclidean_distance(1.5, -1.5, 1.5, -1.5), 0.0); + } + + /// `GeoDistance` returns the per-row distance between two point columns (here the second is a + /// constant query point). + #[test] + fn distance_over_points() -> VortexResult<()> { + let session = VortexSession::empty().with::(); + let mut ctx = session.create_execution_ctx(); + + let a = point_column(vec![0.0, 3.0, 0.0, 3.0], vec![0.0, 0.0, 4.0, 4.0])?; + let b = point_constant(0.0, 0.0, 4, &mut ctx)?; + let distance = GeoDistance::try_new_array(a, b)?.into_array(); + + assert_eq!(distances(distance, &mut ctx)?, vec![0.0, 3.0, 4.0, 5.0]); + Ok(()) + } + + /// Column-to-column distance pairs corresponding rows of the two columns. + #[test] + fn distance_between_columns() -> VortexResult<()> { + let session = VortexSession::empty().with::(); + let mut ctx = session.create_execution_ctx(); + + let a = point_column(vec![0.0, 1.0], vec![0.0, 1.0])?; + let b = point_column(vec![3.0, 1.0], vec![4.0, 1.0])?; + let distance = GeoDistance::try_new_array(a, b)?.into_array(); + + assert_eq!(distances(distance, &mut ctx)?, vec![5.0, 0.0]); + Ok(()) + } + + /// The constant query point may be either operand; distance is symmetric. + #[test] + fn distance_with_constant_first_operand() -> VortexResult<()> { + let session = VortexSession::empty().with::(); + let mut ctx = session.create_execution_ctx(); + + let a = point_constant(0.0, 0.0, 4, &mut ctx)?; + let b = point_column(vec![0.0, 3.0, 0.0, 3.0], vec![0.0, 0.0, 4.0, 4.0])?; + let distance = GeoDistance::try_new_array(a, b)?.into_array(); + + assert_eq!(distances(distance, &mut ctx)?, vec![0.0, 3.0, 4.0, 5.0]); + Ok(()) + } + + /// Two constant operands: every row has the same distance. + #[test] + fn distance_between_two_constants() -> VortexResult<()> { + let session = VortexSession::empty().with::(); + let mut ctx = session.create_execution_ctx(); + + let a = point_constant(0.0, 0.0, 3, &mut ctx)?; + let b = point_constant(3.0, 4.0, 3, &mut ctx)?; + let distance = GeoDistance::try_new_array(a, b)?.into_array(); + + assert_eq!(distances(distance, &mut ctx)?, vec![5.0, 5.0, 5.0]); + Ok(()) + } +} diff --git a/vortex-geo/src/scalar_fn/mod.rs b/vortex-geo/src/scalar_fn/mod.rs new file mode 100644 index 00000000000..385208f1991 --- /dev/null +++ b/vortex-geo/src/scalar_fn/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Geometry scalar functions over the [`Point`](crate::extension::Point) type. + +pub mod distance;