Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
A
Axiom
/Marketplace/christiangeorgelucas/spatial-tools
Sign in

christiangeorgelucas/spatial-tools

v0.1.1Python

The spatial-tools package provides deterministic computational-spatial algorithms for N-dimensional point sets, designed for AI agents. It wraps SciPy's spatial and distance functionalities to enable efficient operations such as nearest-neighbor searches, Delaunay triangulation, Voronoi diagrams, and convex hull computations, ensuring stateless and deterministic transformations.

Published by Christian George Lucas· MIT

spatialalgorithmsgeometrydelaunayvoronoiconvex-hullnearest-neighborsdistance-metricsn-dimensional

Use cases

  • Compute Delaunay triangulation for mesh generation
  • Analyze spatial coverage using Voronoi diagrams
  • Perform nearest-neighbor searches for clustering
  • Calculate convex hulls for shape analysis
  • Evaluate distances between high-dimensional point sets

Nodes (16)

BoundingBoxunary
BoundingBoxInput·BoundingBoxResult

The BoundingBox node computes the axis-aligned bounding box for a given set of points, determining the minimum and maximum values for each dimension using efficient numpy operations. It supports up to 50 dimensions and 100,000 points in a single linear pass.

View source
from gen.axiom_context import AxiomContext
from gen.messages_pb2 import BoundingBoxInput, BoundingBoxResult
from nodes._common import NodeInputError, array_to_vector, check_matrix_shape, err, matrix_to_array

def bounding_box(ax: AxiomContext, input: BoundingBoxInput) -> BoundingBoxResult:
    """Compute the axis-aligned bounding box of a point set: the
    per-dimension minimum and maximum across all rows of `points`, via
    numpy.min/numpy.max.
    """
    try:
        check_matrix_shape(input.points, name="points")
        points = matrix_to_array(input.points)

        return BoundingBoxResult(
            min=array_to_vector(points.min(axis=0)), max=array_to_vector(points.max(axis=0))
        )
    except NodeInputError as e:
        return BoundingBoxResult(error=e.error)
    except Exception as e:
        return BoundingBoxResult(error=err("BOUNDING_BOX_FAILED", str(e)))
ConvexHullunary
ConvexHullInput·ConvexHullResult

The ConvexHull node computes the convex hull of a general N-dimensional point set using the scipy.spatial.ConvexHull method. It returns the indices of the extreme points, the facet simplices, the D-dimensional volume, and the (D-1)-dimensional surface measure, while handling various input conditions and limitations.

View source
from scipy.spatial import ConvexHull as ScipyConvexHull
from scipy.spatial import QhullError

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import ConvexHullInput, ConvexHullResult
from nodes._common import (
    NodeInputError,
    array_to_int_matrix,
    array_to_int_vector,
    check_matrix_shape,
    err,
    matrix_to_array,
)

MIN_DIM = 2


def convex_hull(ax: AxiomContext, input: ConvexHullInput) -> ConvexHullResult:
    """Compute the convex hull of a general N-dimensional point set (D >= 2)
    via scipy.spatial.ConvexHull (Qhull). Returns the hull's
    extreme-point (vertex) indices, its facet simplices, its D-dimensional
    volume (area, for D=2), and its (D-1)-dimensional surface measure
    (perimeter, for D=2). Fewer than D+1 points returns
    INSUFFICIENT_POINTS; coplanar/collinear (rank-deficient) points return
    DEGENERATE_INPUT.
    """
    try:
        check_matrix_shape(input.points)
        points = matrix_to_array(input.points)
        n, d = points.shape
        if d < MIN_DIM:
            raise NodeInputError("INVALID_ARGUMENT", f"points must have D >= {MIN_DIM}, got D={d}")
        if n < d + 1:
            raise NodeInputError(
                "INSUFFICIENT_POINTS", f"need at least {d + 1} points for a {d}D hull, got {n}"
            )

        try:
            hull = ScipyConvexHull(points)
        except QhullError as e:
            raise NodeInputError(
                "DEGENERATE_INPUT", f"points are degenerate (coplanar/collinear, rank < {d}): {e}"
            )

        return ConvexHullResult(
            vertices=array_to_int_vector(hull.vertices),
            simplices=array_to_int_matrix(hull.simplices),
            volume=float(hull.volume),
            area=float(hull.area),
        )
    except NodeInputError as e:
        return ConvexHullResult(error=e.error)
    except Exception as e:
        return ConvexHullResult(error=err("HULL_FAILED", str(e)))
CrossDistanceMatrixunary
CdistInput·CdistResult

The CrossDistanceMatrix node computes the full cross distance matrix between two sets of points using the scipy.spatial.distance.cdist function. It returns an N x M matrix where each element represents the distance between corresponding points from the two sets, with a maximum output size constraint to ensure efficient transport.

View source
from scipy.spatial.distance import cdist

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import CdistInput, CdistResult
from nodes._common import (
    NodeInputError,
    array_to_matrix,
    check_matrix_shape,
    err,
    matrix_to_array,
    resolve_full_metric_kwargs,
)


def cross_distance_matrix(ax: AxiomContext, input: CdistInput) -> CdistResult:
    """Compute the full cross distance matrix between two point sets
    `points_a` (N x D) and `points_b` (M x D), via
    scipy.spatial.distance.cdist. Same metric/p/inv_cov options as
    PairwiseDistanceMatrix. Returns an N x M matrix,
    distances[i][j] = distance(points_a[i], points_b[j]).
    """
    try:
        check_matrix_shape(input.points_a, name="points_a")
        check_matrix_shape(input.points_b, name="points_b")
        a = matrix_to_array(input.points_a, name="points_a")
        b = matrix_to_array(input.points_b, name="points_b")
        if a.shape[1] != b.shape[1]:
            raise NodeInputError(
                "DIMENSION_MISMATCH", f"points_a has {a.shape[1]} columns, points_b has {b.shape[1]}"
            )

        metric, kwargs = resolve_full_metric_kwargs(
            input.metric, input.p, input.inv_cov if input.HasField("inv_cov") else None,
            require_inv_cov_for_mahalanobis=False,
        )

        distances = cdist(a, b, metric=metric, **kwargs)

        return CdistResult(distances=array_to_matrix(distances))
    except NodeInputError as e:
        return CdistResult(error=e.error)
    except Exception as e:
        return CdistResult(error=err("DISTANCE_FAILED", str(e)))
DelaunayTriangulateunary
DelaunayInput·DelaunayResult

The DelaunayTriangulate node computes the Delaunay triangulation of a set of 2D or 3D points using the scipy.spatial.Delaunay method, returning the simplices and their neighbor structure. It handles edge cases such as insufficient points and degenerate inputs gracefully without crashing.

View source
from scipy.spatial import Delaunay
from scipy.spatial import QhullError

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import DelaunayInput, DelaunayResult
from nodes._common import (
    NodeInputError,
    array_to_int_matrix,
    check_matrix_shape,
    err,
    matrix_to_array,
)

ALLOWED_DIMS = (2, 3)


def delaunay_triangulate(ax: AxiomContext, input: DelaunayInput) -> DelaunayResult:
    """Compute the Delaunay triangulation of a 2D or 3D point set via
    scipy.spatial.Delaunay (Qhull). Returns each simplex as D+1 point
    indices (a triangle for 2D, a tetrahedron for 3D) plus the per-simplex
    neighbor structure (-1 where a facet has no neighbor, i.e. lies on the
    convex hull boundary). Points must be 2D or 3D; fewer than D+1 points
    returns INSUFFICIENT_POINTS; collinear/coplanar points that admit no
    triangulation return DEGENERATE_INPUT rather than crashing.
    """
    try:
        check_matrix_shape(input.points)
        points = matrix_to_array(input.points)
        n, d = points.shape
        if d not in ALLOWED_DIMS:
            raise NodeInputError("INVALID_ARGUMENT", f"points must be 2D or 3D, got D={d}")
        if n < d + 1:
            raise NodeInputError(
                "INSUFFICIENT_POINTS", f"need at least {d + 1} points for a {d}D triangulation, got {n}"
            )

        try:
            tri = Delaunay(points)
        except QhullError as e:
            raise NodeInputError(
                "DEGENERATE_INPUT",
                f"points are degenerate (collinear/coplanar) for a {d}D triangulation: {e}",
            )

        return DelaunayResult(
            simplices=array_to_int_matrix(tri.simplices),
            neighbors=array_to_int_matrix(tri.neighbors),
        )
    except NodeInputError as e:
        return DelaunayResult(error=e.error)
    except Exception as e:
        return DelaunayResult(error=err("TRIANGULATION_FAILED", str(e)))
FindSimplexunary
FindSimplexInput·FindSimplexResult

The FindSimplex node identifies which Delaunay simplex each query point falls into within a given set of points, and calculates the barycentric weights for interpolation. It returns -1 for points outside the convex hull of the triangulation, along with a zero barycentric row.

View source
import numpy as np
from scipy.spatial import Delaunay, QhullError

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import FindSimplexInput, FindSimplexResult
from nodes._common import (
    NodeInputError,
    array_to_int_vector,
    array_to_matrix,
    check_matrix_shape,
    err,
    matrix_to_array,
)

ALLOWED_DIMS = (2, 3)


def find_simplex(ax: AxiomContext, input: FindSimplexInput) -> FindSimplexResult:
    """Locate which Delaunay simplex (of `points`' 2D/3D triangulation, via
    scipy.spatial.Delaunay.find_simplex) each row of `queries` falls into,
    and its barycentric weights within that simplex — set-up for
    barycentric interpolation. Returns -1 for a query point outside the
    triangulation's convex hull (with an all-zero barycentric row).
    """
    try:
        check_matrix_shape(input.points, name="points")
        check_matrix_shape(input.queries, name="queries")
        points = matrix_to_array(input.points)
        queries = matrix_to_array(input.queries)
        n, d = points.shape
        if d not in ALLOWED_DIMS:
            raise NodeInputError("INVALID_ARGUMENT", f"points must be 2D or 3D, got D={d}")
        if points.shape[1] != queries.shape[1]:
            raise NodeInputError(
                "DIMENSION_MISMATCH",
                f"points has {points.shape[1]} columns, queries has {queries.shape[1]}",
            )
        if n < d + 1:
            raise NodeInputError(
                "INSUFFICIENT_POINTS", f"need at least {d + 1} points for a {d}D triangulation, got {n}"
            )

        try:
            tri = Delaunay(points)
        except QhullError as e:
            raise NodeInputError(
                "DEGENERATE_INPUT", f"points are degenerate (collinear/coplanar) for a {d}D triangulation: {e}"
            )

        simplex_idx = tri.find_simplex(queries)
        q = queries.shape[0]
        bary = np.zeros((q, d + 1), dtype=np.float64)

        valid = simplex_idx >= 0
        if np.any(valid):
            valid_simplex = simplex_idx[valid]
            X = tri.transform[valid_simplex, :d, :d]
            Y = queries[valid] - tri.transform[valid_simplex, d, :]
            b = np.einsum("ijk,ik->ij", X, Y)
            last = 1.0 - b.sum(axis=1, keepdims=True)
            bary[valid] = np.hstack([b, last])

        return FindSimplexResult(
            simplex_indices=array_to_int_vector(simplex_idx),
            barycentric_coords=array_to_matrix(bary),
        )
    except NodeInputError as e:
        return FindSimplexResult(error=e.error)
    except Exception as e:
        return FindSimplexResult(error=err("FIND_SIMPLEX_FAILED", str(e)))
HausdorffDistanceunary
HausdorffInput·HausdorffResult

The HausdorffDistance node computes the symmetric Hausdorff distance between two point sets, determining how far the farthest point in one set is from its nearest neighbor in the other set. It returns the distance, the direction from which it was achieved, and the indices of the points that achieved this distance.

View source
from scipy.spatial.distance import directed_hausdorff

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import HausdorffInput, HausdorffResult
from nodes._common import (
    NodeInputError,
    array_to_int_vector,
    check_matrix_shape,
    err,
    matrix_to_array,
)

def hausdorff_distance(ax: AxiomContext, input: HausdorffInput) -> HausdorffResult:
    """Compute the symmetric Hausdorff distance between two point sets
    `points_a` and `points_b`, via
    max(scipy.spatial.distance.directed_hausdorff(A,B), directed_hausdorff(B,A)).
    Returns the distance, which direction achieved it (from_a), and the
    achieving index pair.
    """
    try:
        check_matrix_shape(input.points_a, name="points_a")
        check_matrix_shape(input.points_b, name="points_b")
        a = matrix_to_array(input.points_a, name="points_a")
        b = matrix_to_array(input.points_b, name="points_b")
        if a.shape[1] != b.shape[1]:
            raise NodeInputError(
                "DIMENSION_MISMATCH", f"points_a has {a.shape[1]} columns, points_b has {b.shape[1]}"
            )

        d_ab, i_a, j_b = directed_hausdorff(a, b)
        d_ba, i_b, j_a = directed_hausdorff(b, a)

        if d_ab >= d_ba:
            return HausdorffResult(
                distance=float(d_ab), from_a=True, index_pair=array_to_int_vector([i_a, j_b])
            )
        return HausdorffResult(
            distance=float(d_ba), from_a=False, index_pair=array_to_int_vector([i_b, j_a])
        )
    except NodeInputError as e:
        return HausdorffResult(error=e.error)
    except Exception as e:
        return HausdorffResult(error=err("DISTANCE_FAILED", str(e)))
KNearestNeighborsunary
KNNInput·KNNResult

The KNearestNeighbors node builds a KD-tree from a set of points and efficiently queries the k nearest neighbors for each row in a set of queries using specified distance metrics. It returns the indices and distances of the nearest neighbors in a structured format.

View source
import numpy as np
from scipy.spatial import cKDTree

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import KNNInput, KNNResult
from nodes._common import (
    NodeInputError,
    array_to_int_matrix,
    array_to_matrix,
    check_matrix_shape,
    err,
    matrix_to_array,
    resolve_kdtree_metric,
)


def k_nearest_neighbors(ax: AxiomContext, input: KNNInput) -> KNNResult:
    """Build a KD-tree over `points` (scipy.spatial.cKDTree) and query the
    k nearest neighbors of each row in `queries`. metric is "euclidean"
    (default), "cityblock", or "chebyshev". Returns Q x k index and
    distance matrices in ascending-distance order per row. k is rejected
    with INVALID_ARGUMENT if it exceeds len(points) — every returned row is
    always fully populated with k real neighbors, never padded.
    """
    try:
        check_matrix_shape(input.points, name="points")
        check_matrix_shape(input.queries, name="queries")
        points = matrix_to_array(input.points)
        queries = matrix_to_array(input.queries)
        if points.shape[1] != queries.shape[1]:
            raise NodeInputError(
                "DIMENSION_MISMATCH",
                f"points has {points.shape[1]} columns, queries has {queries.shape[1]}",
            )

        k = input.k
        if k < 1 or k > points.shape[0]:
            raise NodeInputError(
                "INVALID_ARGUMENT",
                f"k must satisfy 1 <= k <= len(points) = {points.shape[0]} (got {k})",
            )

        metric, p = resolve_kdtree_metric(input.metric)

        tree = cKDTree(points)
        dist, idx = tree.query(queries, k=k, p=p)
        dist = np.atleast_2d(dist.reshape(queries.shape[0], k))
        idx = np.atleast_2d(idx.reshape(queries.shape[0], k)).astype(np.int64)
        idx[np.isinf(dist)] = -1

        return KNNResult(indices=array_to_int_matrix(idx), distances=array_to_matrix(dist))
    except NodeInputError as e:
        return KNNResult(error=e.error)
    except Exception as e:
        return KNNResult(error=err("QUERY_FAILED", str(e)))
KNNGraphunary
KNNGraphInput·KNNGraphResult

The KNNGraph node constructs a spatial k-nearest-neighbor graph from a set of points, identifying the k nearest neighbors for each point using a cKDTree for efficient querying. It returns a directed edge list representing these relationships along with the distances to each neighbor.

View source
import numpy as np
from scipy.spatial import cKDTree

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import KNNGraphInput, KNNGraphResult
from nodes._common import (
    NodeInputError,
    array_to_int_matrix,
    array_to_vector,
    check_matrix_shape,
    err,
    matrix_to_array,
)


def knn_graph(ax: AxiomContext, input: KNNGraphInput) -> KNNGraphResult:
    """Build a spatial k-nearest-neighbor GRAPH over `points` (euclidean):
    for every point, find its k nearest OTHER points (self excluded) via
    scipy.spatial.cKDTree, and return the resulting directed edge list
    (i -> j meaning "j is one of i's k nearest neighbors") with per-edge
    distances. Not symmetrized — union/intersect the edges yourself for a
    mutual/undirected graph.
    """
    try:
        check_matrix_shape(input.points, name="points")
        points = matrix_to_array(input.points)
        n = points.shape[0]

        k = input.k
        max_k = n - 1
        if k < 1 or k > max_k:
            raise NodeInputError(
                "INVALID_ARGUMENT", f"k must satisfy 1 <= k <= len(points)-1 = {max_k} (got {k})"
            )

        tree = cKDTree(points)
        # Query k+1 to include self, then drop the self-match per row.
        dist, idx = tree.query(points, k=k + 1, p=2)
        dist = np.atleast_2d(dist)
        idx = np.atleast_2d(idx)

        edges = []
        weights = []
        for i in range(n):
            count = 0
            for j, dj in zip(idx[i], dist[i]):
                if int(j) == i:
                    continue
                edges.append((i, int(j)))
                weights.append(float(dj))
                count += 1
                if count == k:
                    break

        return KNNGraphResult(
            edges=array_to_int_matrix(np.array(edges, dtype=np.int64) if edges else np.empty((0, 2))),
            weights=array_to_vector(weights),
        )
    except NodeInputError as e:
        return KNNGraphResult(error=e.error)
    except Exception as e:
        return KNNGraphResult(error=err("GRAPH_FAILED", str(e)))
NearestSinglePointunary
NearestSinglePointInput·NearestSinglePointResult

The NearestSinglePoint node efficiently finds the closest point from a set of points to a specified query point using the cKDTree algorithm from scipy.spatial. It returns the index of the nearest point along with the distance to that point, supporting various distance metrics.

View source
from scipy.spatial import cKDTree

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import NearestSinglePointInput, NearestSinglePointResult
from nodes._common import (
    NodeInputError,
    check_matrix_shape,
    err,
    matrix_to_array,
    resolve_kdtree_metric,
    vector_to_array,
)

def nearest_single_point(ax: AxiomContext, input: NearestSinglePointInput) -> NearestSinglePointResult:
    """Find the single nearest point in `points` to one `query` point, via
    scipy.spatial.cKDTree — a convenience node for the common single-query
    case (see KNearestNeighbors for the batched-queries, k>=1 general case).
    metric is "euclidean" (default), "cityblock", or "chebyshev".
    """
    try:
        check_matrix_shape(input.points, name="points")
        points = matrix_to_array(input.points)
        query = vector_to_array(input.query, name="query")
        if len(query) == 0:
            raise NodeInputError("EMPTY_INPUT", "query must have at least one dimension")
        if points.shape[1] != len(query):
            raise NodeInputError(
                "DIMENSION_MISMATCH",
                f"points has {points.shape[1]} columns, query has {len(query)} dimensions",
            )

        metric, p = resolve_kdtree_metric(input.metric)

        tree = cKDTree(points)
        dist, idx = tree.query(query, k=1, p=p)

        return NearestSinglePointResult(index=int(idx), distance=float(dist))
    except NodeInputError as e:
        return NearestSinglePointResult(error=e.error)
    except Exception as e:
        return NearestSinglePointResult(error=err("QUERY_FAILED", str(e)))
PairwiseDistanceMatrixunary
PdistInput·PdistResult

The PairwiseDistanceMatrix node computes the condensed pairwise distance vector among all rows of input points using various distance metrics from scipy.spatial.distance.pdist. It returns the condensed vector along with the count of points for indexing or expansion purposes.

View source
from scipy.spatial.distance import pdist

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import PdistInput, PdistResult
from nodes._common import (
    NodeInputError,
    array_to_vector,
    check_matrix_shape,
    err,
    matrix_to_array,
    resolve_full_metric_kwargs,
)

def pairwise_distance_matrix(ax: AxiomContext, input: PdistInput) -> PdistResult:
    """Compute the condensed (upper-triangular) pairwise distance vector
    among all rows of `points`, via scipy.spatial.distance.pdist. metric is
    "euclidean" (default), "cityblock", "cosine", "chebyshev", "minkowski"
    (uses `p`), "hamming", "jaccard", or "mahalanobis" (uses `inv_cov` if
    supplied, else scipy's own internally-derived sample
    inverse-covariance). Returns the condensed vector (length n*(n-1)/2)
    plus `n` so the caller can index or squareform-expand it.
    """
    try:
        check_matrix_shape(input.points, min_rows=2, name="points")
        points = matrix_to_array(input.points)
        n = points.shape[0]

        metric, kwargs = resolve_full_metric_kwargs(
            input.metric, input.p, input.inv_cov if input.HasField("inv_cov") else None,
            require_inv_cov_for_mahalanobis=False,
        )

        condensed = pdist(points, metric=metric, **kwargs)

        return PdistResult(condensed=array_to_vector(condensed), n=n)
    except NodeInputError as e:
        return PdistResult(error=e.error)
    except Exception as e:
        return PdistResult(error=err("DISTANCE_FAILED", str(e)))
PointDistanceunary
PointDistanceInput·PointDistanceResult

The PointDistance node computes the distance between two D-dimensional points using a specified metric from the scipy.spatial.distance library. It supports various distance metrics including Euclidean, Cityblock, and Mahalanobis, among others.

View source
import numpy as np
from scipy.spatial.distance import pdist

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import PointDistanceInput, PointDistanceResult
from nodes._common import (
    NodeInputError,
    err,
    resolve_full_metric_kwargs,
    vector_to_array,
)

def point_distance(ax: AxiomContext, input: PointDistanceInput) -> PointDistanceResult:
    """Compute the distance between exactly two D-dimensional points under
    a chosen metric, via scipy.spatial.distance. metric is "euclidean"
    (default), "cityblock", "cosine", "chebyshev", "minkowski" (uses `p`),
    "hamming", "jaccard", or "mahalanobis" (requires `inv_cov` to be
    supplied explicitly — with only two points there is no sample to
    derive a covariance from). `a` and `b` must have equal length.
    """
    try:
        a = vector_to_array(input.a, name="a")
        b = vector_to_array(input.b, name="b")
        if len(a) == 0 or len(b) == 0:
            raise NodeInputError("EMPTY_INPUT", "a and b must each have at least one dimension")
        if len(a) != len(b):
            raise NodeInputError("DIMENSION_MISMATCH", f"a has {len(a)} dims, b has {len(b)} dims")

        metric, kwargs = resolve_full_metric_kwargs(
            input.metric, input.p, input.inv_cov if input.HasField("inv_cov") else None,
            require_inv_cov_for_mahalanobis=True,
        )

        # Route through pdist on the 2-row stack so this node's numeric
        # behavior is exactly consistent with Pdist/Cdist for the same
        # metric (rather than a separately hand-rolled formula).
        distance = float(pdist(np.vstack([a, b]), metric=metric, **kwargs)[0])

        return PointDistanceResult(distance=distance)
    except NodeInputError as e:
        return PointDistanceResult(error=e.error)
    except Exception as e:
        return PointDistanceResult(error=err("DISTANCE_FAILED", str(e)))
PointInConvexHullunary
PointInHullInput·PointInHullResult

The PointInConvexHull node determines whether each point in the 'queries' input lies within or on the boundary of the convex hull defined by the 'points' input, using the facet equations from scipy's ConvexHull. It handles edge cases such as insufficient points and degenerate inputs while allowing for a customizable tolerance level.

View source
import numpy as np
from scipy.spatial import ConvexHull, QhullError

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import PointInHullInput, PointInHullResult
from nodes._common import (
    NodeInputError,
    array_to_bool_vector,
    check_matrix_shape,
    err,
    matrix_to_array,
)

MIN_DIM = 2
DEFAULT_TOL = 1e-9


def point_in_convex_hull(ax: AxiomContext, input: PointInHullInput) -> PointInHullResult:
    """Test whether each point in `queries` lies inside (or on the boundary
    of, within `tol`) the convex hull of `points`, via
    scipy.spatial.ConvexHull's facet equations (hull.equations) — the
    standard scipy-documented point-in-hull technique. `tol` defaults to
    1e-9 when <= 0.
    """
    try:
        check_matrix_shape(input.points, name="points")
        check_matrix_shape(input.queries, name="queries")
        points = matrix_to_array(input.points)
        queries = matrix_to_array(input.queries)
        n, d = points.shape
        if d < MIN_DIM:
            raise NodeInputError("INVALID_ARGUMENT", f"points must have D >= {MIN_DIM}, got D={d}")
        if points.shape[1] != queries.shape[1]:
            raise NodeInputError(
                "DIMENSION_MISMATCH",
                f"points has {points.shape[1]} columns, queries has {queries.shape[1]}",
            )
        if n < d + 1:
            raise NodeInputError(
                "INSUFFICIENT_POINTS", f"need at least {d + 1} points for a {d}D hull, got {n}"
            )

        try:
            hull = ConvexHull(points)
        except QhullError as e:
            raise NodeInputError(
                "DEGENERATE_INPUT", f"points are degenerate (coplanar/collinear, rank < {d}): {e}"
            )

        tol = input.tol if input.tol > 0 else DEFAULT_TOL
        # hull.equations: (n_facets, D+1) rows [normal..., offset]; a point
        # is inside/on-boundary iff normal . p + offset <= tol for every facet.
        eq = hull.equations
        inside = np.all(eq[:, :-1] @ queries.T + eq[:, -1:] <= tol, axis=0)

        return PointInHullResult(inside=array_to_bool_vector(inside))
    except NodeInputError as e:
        return PointInHullResult(error=e.error)
    except Exception as e:
        return PointInHullResult(error=err("HULL_TEST_FAILED", str(e)))
ProcrustesDisparityunary
ProcrustesInput·ProcrustesResult

The ProcrustesDisparity node computes the Procrustes disparity between two equally-shaped point sets, standardizing and aligning them to determine the optimal rotation and the sum-of-squared-errors disparity. It ensures that both input point sets have the same shape and returns the aligned point sets along with the computed disparity.

View source
from scipy.spatial import procrustes

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import ProcrustesInput, ProcrustesResult
from nodes._common import (
    NodeInputError,
    array_to_matrix,
    check_matrix_shape,
    err,
    matrix_to_array,
)

def procrustes_disparity(ax: AxiomContext, input: ProcrustesInput) -> ProcrustesResult:
    """Compute the Procrustes disparity between two equally-shaped point
    sets (shape alignment), via scipy.spatial.procrustes — standardizes
    both point sets and finds the optimal rotation of `points_b` onto
    `points_a`, returning the sum-of-squared-errors disparity (0 =
    identical shape) plus both standardized/aligned point sets.
    """
    try:
        check_matrix_shape(input.points_a, min_rows=2, name="points_a")
        check_matrix_shape(input.points_b, min_rows=2, name="points_b")
        a = matrix_to_array(input.points_a, name="points_a")
        b = matrix_to_array(input.points_b, name="points_b")

        if a.shape != b.shape:
            raise NodeInputError(
                "DIMENSION_MISMATCH", f"points_a has shape {a.shape}, points_b has shape {b.shape}"
            )

        try:
            aligned_a, aligned_b, disparity = procrustes(a, b)
        except ValueError as e:
            raise NodeInputError("DEGENERATE_INPUT", str(e))

        return ProcrustesResult(
            disparity=float(disparity),
            aligned_a=array_to_matrix(aligned_a),
            aligned_b=array_to_matrix(aligned_b),
        )
    except NodeInputError as e:
        return ProcrustesResult(error=e.error)
    except Exception as e:
        return ProcrustesResult(error=err("PROCRUSTES_FAILED", str(e)))
QueryPairsunary
QueryPairsInput·QueryPairsResult

The QueryPairs node identifies all pairs of points within a specified distance from each other in a given point set using the cKDTree algorithm from the scipy.spatial library. It returns the indices of qualifying pairs in a structured format, while also managing large output sizes by limiting the number of pairs returned.

View source
from scipy.spatial import cKDTree

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import QueryPairsInput, QueryPairsResult
from nodes._common import (
    NodeInputError,
    array_to_int_matrix,
    check_matrix_shape,
    err,
    matrix_to_array,
    resolve_kdtree_metric,
)


def query_pairs(ax: AxiomContext, input: QueryPairsInput) -> QueryPairsResult:
    """Find every pair of points within `distance` of each other, inside a
    single point set, via scipy.spatial.cKDTree.query_pairs. metric is
    "euclidean" (default), "cityblock", or "chebyshev". Returns each
    qualifying pair (i, j) with i < j as row indices into `points`.
    """
    try:
        check_matrix_shape(input.points, name="points")
        points = matrix_to_array(input.points)
        if input.distance <= 0:
            raise NodeInputError("INVALID_ARGUMENT", f"distance must be > 0 (got {input.distance})")

        metric, p = resolve_kdtree_metric(input.metric)

        tree = cKDTree(points)
        pairs = tree.query_pairs(r=input.distance, p=p, output_type="ndarray")

        return QueryPairsResult(pairs=array_to_int_matrix(pairs))
    except NodeInputError as e:
        return QueryPairsResult(error=e.error)
    except Exception as e:
        return QueryPairsResult(error=err("QUERY_FAILED", str(e)))
RadiusNeighborsunary
RadiusNeighborsInput·RadiusNeighborsResult

The RadiusNeighbors node constructs a KD-tree from a set of points and performs a radius-based query for each query point, returning all points within the specified radius. It utilizes the scipy.spatial.cKDTree.query_ball_point method to efficiently find neighbors, with results that may vary in count per query and are not sorted by distance.

View source
from scipy.spatial import cKDTree

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import RadiusNeighborsInput, RadiusNeighborsResult
from nodes._common import (
    NodeInputError,
    check_matrix_shape,
    err,
    matrix_to_array,
    ragged_to_int_matrix,
    resolve_kdtree_metric,
)


def radius_neighbors(ax: AxiomContext, input: RadiusNeighborsInput) -> RadiusNeighborsResult:
    """Build a KD-tree over `points` and, for each row in `queries`, return
    every point within `radius` (a "ball query"), via
    scipy.spatial.cKDTree.query_ball_point. metric is "euclidean" (default),
    "cityblock", or "chebyshev". Result rows are ragged (variable neighbor
    count per query) and not guaranteed sorted by distance.
    """
    try:
        check_matrix_shape(input.points, name="points")
        check_matrix_shape(input.queries, name="queries")
        points = matrix_to_array(input.points)
        queries = matrix_to_array(input.queries)
        if points.shape[1] != queries.shape[1]:
            raise NodeInputError(
                "DIMENSION_MISMATCH",
                f"points has {points.shape[1]} columns, queries has {queries.shape[1]}",
            )
        if input.radius <= 0:
            raise NodeInputError("INVALID_ARGUMENT", f"radius must be > 0 (got {input.radius})")

        metric, p = resolve_kdtree_metric(input.metric)

        tree = cKDTree(points)
        neighbor_lists = tree.query_ball_point(queries, r=input.radius, p=p)

        return RadiusNeighborsResult(indices=ragged_to_int_matrix(neighbor_lists))
    except NodeInputError as e:
        return RadiusNeighborsResult(error=e.error)
    except Exception as e:
        return RadiusNeighborsResult(error=err("QUERY_FAILED", str(e)))
VoronoiDiagramunary
VoronoiInput·VoronoiResult

The VoronoiDiagram node computes the Voronoi diagram for a given set of 2D or 3D points using the scipy.spatial.Voronoi function, which serves as the geometric dual of Delaunay triangulation. It outputs the finite Voronoi vertices, the regions associated with each input point, and the ridge structure that defines the relationships between points.

View source
from scipy.spatial import QhullError, Voronoi

from gen.axiom_context import AxiomContext
from gen.messages_pb2 import VoronoiInput, VoronoiResult
from nodes._common import (
    NodeInputError,
    array_to_int_vector,
    array_to_matrix,
    check_matrix_shape,
    err,
    matrix_to_array,
    ragged_to_int_matrix,
)

ALLOWED_DIMS = (2, 3)


def voronoi_diagram(ax: AxiomContext, input: VoronoiInput) -> VoronoiResult:
    """Compute the Voronoi diagram of a 2D or 3D point set via
    scipy.spatial.Voronoi (Qhull) — the geometric dual of Delaunay
    triangulation. Returns the finite Voronoi vertices, each input point's
    (possibly unbounded, ragged) region as a list of vertex indices with -1
    marking an unbounded direction, the point_region mapping from input
    point to its region, and the ridge structure.
    """
    try:
        check_matrix_shape(input.points)
        points = matrix_to_array(input.points)
        n, d = points.shape
        if d not in ALLOWED_DIMS:
            raise NodeInputError("INVALID_ARGUMENT", f"points must be 2D or 3D, got D={d}")
        if n < d + 1:
            raise NodeInputError(
                "INSUFFICIENT_POINTS", f"need at least {d + 1} points for a {d}D Voronoi diagram, got {n}"
            )

        try:
            vor = Voronoi(points)
        except QhullError as e:
            raise NodeInputError(
                "DEGENERATE_INPUT",
                f"points are degenerate (collinear/coplanar) for a {d}D Voronoi diagram: {e}",
            )

        return VoronoiResult(
            vertices=array_to_matrix(vor.vertices),
            regions=ragged_to_int_matrix(vor.regions),
            point_region=array_to_int_vector(vor.point_region),
            ridge_points=ragged_to_int_matrix(vor.ridge_points),
            ridge_vertices=ragged_to_int_matrix(vor.ridge_vertices),
        )
    except NodeInputError as e:
        return VoronoiResult(error=e.error)
    except Exception as e:
        return VoronoiResult(error=err("VORONOI_FAILED", str(e)))

Messages (40)

Download .proto
BoolVector
values:[]bool
BoundingBoxInput
points:Matrix

A matrix representing the set of points for which the bounding box is to be computed, with each row corresponding to a point and each column representing a dimension.

BoundingBoxResult
min:Vector
max:Vector
error:Error
CdistInput
points_a:Matrix

The first set of points represented as an N x D matrix.

points_b:Matrix

The second set of points represented as an M x D matrix.

metric:string

The distance metric to be used for computation, such as 'euclidean' or 'cityblock'.

p:double

The power parameter for the Minkowski distance metric.

inv_cov:Matrix

The inverse covariance matrix used for the Mahalanobis distance, if applicable.

CdistResult
distances:Matrix

The resulting N x M matrix of distances between the points in points_a and points_b.

error:Error

An error message if the distance computation fails.

ConvexHullInput
points:Matrix

A matrix representing the N-dimensional points for which the convex hull is to be computed.

ConvexHullResult
vertices:IntVector

Indices of the extreme points (vertices) of the convex hull.

simplices:IntMatrix

The facet simplices of the convex hull.

volume:double

The D-dimensional volume of the convex hull, or area if D=2.

area:double

The (D-1)-dimensional surface measure of the convex hull, or perimeter if D=2.

error:Error

An error message indicating issues with input or computation, if applicable.

DelaunayInput
points:Matrix

A matrix representing the coordinates of the input points in 2D or 3D space.

DelaunayResult
simplices:IntMatrix

An array of indices representing the simplices formed by the triangulation, where each simplex is defined by D+1 points.

neighbors:IntMatrix

An array indicating the neighboring simplices for each simplex, with -1 indicating no neighbor.

error:Error
Error
code:string
message:string
FindSimplexInput
points:Matrix

A matrix representing the coordinates of the points used for Delaunay triangulation.

queries:Matrix

A matrix of query points for which the corresponding Delaunay simplex and barycentric coordinates are to be found.

FindSimplexResult
simplex_indices:IntVector

An array of indices indicating the Delaunay simplex each query point belongs to, with -1 for points outside the triangulation.

barycentric_coords:Matrix

A matrix containing the barycentric coordinates of the query points within their respective simplices.

error:Error
HausdorffInput
points_a:Matrix

The first set of points used for the Hausdorff distance calculation.

points_b:Matrix

The second set of points used for the Hausdorff distance calculation.

HausdorffResult
distance:double

The computed symmetric Hausdorff distance between the two point sets.

from_a:bool

A boolean indicating whether the distance was achieved from points_a.

index_pair:IntVector

The indices of the points in the two sets that achieved the Hausdorff distance.

error:Error
IntMatrix
rows:[]IntMatrixRow
IntMatrixRow
values:[]int32
IntVector
values:[]int32
KNNGraphInput
points:Matrix

A matrix of input points in Euclidean space for which the k-nearest neighbors will be determined.

k:int32

An integer specifying the number of nearest neighbors to find for each point, constrained between 1 and the lesser of N-1 or 256.

KNNGraphResult
edges:IntMatrix

A directed edge list where each entry (i, j) indicates that point j is one of the k nearest neighbors of point i.

weights:Vector

A list of distances corresponding to each edge in the edge list, representing the distance from point i to its neighbor j.

error:Error
KNNInput
points:Matrix

A matrix of points used to build the KD-tree.

queries:Matrix

A matrix of query points for which the nearest neighbors are to be found.

k:int32

The number of nearest neighbors to retrieve for each query, constrained between 1 and the number of points.

metric:string

The distance metric used for querying, which can be 'euclidean', 'cityblock', or 'chebyshev'.

KNNResult
indices:IntMatrix
distances:Matrix
error:Error
Matrix
rows:[]MatrixRow
MatrixRow
values:[]double
NearestSinglePointInput
points:Matrix

A matrix of points from which the nearest point will be identified.

query:Vector

A vector representing the single query point for which the nearest point is sought.

metric:string

The distance metric to use for the nearest neighbor search, which can be 'euclidean', 'cityblock', or 'chebyshev'.

NearestSinglePointResult
index:int32

The index of the nearest point found in the input points.

distance:double

The distance from the query point to the nearest point.

error:Error
PdistInput
points:Matrix

A matrix of input points where each row represents a point in the space.

metric:string

The distance metric to use for computation, with options including 'euclidean', 'cityblock', 'cosine', etc.

p:double

The parameter for the Minkowski distance metric, defaulting to 2.

inv_cov:Matrix

The inverse covariance matrix used for the Mahalanobis distance, if applicable.

PdistResult
condensed:Vector
n:int32
error:Error
PointDistanceInput
a:Vector

The first D-dimensional point as an array.

b:Vector

The second D-dimensional point as an array.

metric:string

The distance metric to use for the calculation, with 'euclidean' as the default.

p:double

The parameter for the Minkowski distance metric, defaulting to 2.

inv_cov:Matrix

The inverse covariance matrix required for the Mahalanobis distance metric, if applicable.

PointDistanceResult
distance:double
error:Error
PointInHullInput
points:Matrix

An array of points that define the convex hull.

queries:Matrix

An array of points to test against the convex hull.

tol:double

A tolerance value for determining if a point is on the boundary of the convex hull; defaults to 1e-9 if less than or equal to 0.

PointInHullResult
inside:BoolVector
error:Error
ProcrustesInput
points_a:Matrix

The first set of points to be aligned, which must have the same shape as points_b.

points_b:Matrix

The second set of points to be aligned with points_a, which must have the same shape.

ProcrustesResult
disparity:double

The sum-of-squared-errors disparity indicating the difference between the aligned shapes, where 0 indicates identical shapes.

aligned_a:Matrix

The standardized and aligned version of points_a after the Procrustes transformation.

aligned_b:Matrix

The standardized and aligned version of points_b after the Procrustes transformation.

error:Error
QueryPairsInput
points:Matrix

A matrix representing the set of points to be analyzed for pairwise proximity.

distance:double

A positive float indicating the maximum distance within which pairs of points are considered.

metric:string

A string specifying the distance metric to use, which can be 'euclidean', 'cityblock', or 'chebyshev'.

QueryPairsResult
pairs:IntMatrix

An array of pairs (i, j) where i < j, representing the indices of points that are within the specified distance of each other.

error:Error

An error message indicating any issues encountered during the query process.

RadiusNeighborsInput
points:Matrix

A matrix of points used to build the KD-tree, where each row represents a point in multi-dimensional space.

queries:Matrix

A matrix of query points for which neighbors are sought, with each row representing a query point in the same dimensional space as the points.

radius:double

A positive floating-point value that defines the radius within which to search for neighboring points.

metric:string

A string that specifies the distance metric to use for the KD-tree, which can be 'euclidean', 'cityblock', or 'chebyshev'.

RadiusNeighborsResult
indices:IntMatrix
error:Error
Vector
values:[]double
VoronoiInput
points:Matrix

A matrix representing the coordinates of the input points in 2D or 3D space.

VoronoiResult
vertices:Matrix

The finite vertices of the Voronoi diagram.

regions:IntMatrix

A list of regions corresponding to each input point, where each region is represented by a list of vertex indices.

point_region:IntVector

A mapping of each input point to its corresponding Voronoi region.

ridge_points:IntMatrix

A list of pairs of points that share a ridge in the Voronoi diagram.

ridge_vertices:IntMatrix

A list of vertices that define the ridges between points.

error:Error

Use as a tool in any AI agent

christiangeorgelucas/spatial-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.

claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"

Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.

MCP server setup & other clients →

Use this package

Sign in to import christiangeorgelucas/spatial-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.

Sign in to useOpen editor

Stats

Error rate (1h)0.0%
Back to marketplace