Skip to main content

mlx-geospatial

Fast point-in-polygon queries, spatial joins, and great-circle distances on Apple silicon, powered by MLX.

Use it to assign GPS points to zones, find nearby places, or build spatial features for an MLX model. It accepts NumPy and Shapely data and can keep query results as MLX arrays for further GPU processing.

Install

pip install mlx-geospatial

Requires an Apple silicon Mac, Python 3.10+, and MLX 0.32.0+. NumPy and Shapely are installed automatically. For GeoPandas support:

pip install "mlx-geospatial[geo]"

Quickstart

Find which polygon contains each point:

import numpy as np
from shapely.geometry import box
import mlx_geospatial as ms

# Coordinates are (longitude, latitude). Keep host coordinates in float64.
points = np.array([
    [-74.00, 40.72],
    [-73.98, 40.75],
    [-73.90, 40.80],
])
zones = [
    box(-74.02, 40.70, -73.99, 40.74),
    box(-74.00, 40.73, -73.96, 40.78),
]

labels = ms.point_in_polygon(points, zones)
print(labels)  # [ 0  1 -1]

Each label is a polygon's position in zones; -1 means no match. If polygons overlap, the lowest polygon index wins. To get every match:

point_ids, polygon_ids = ms.point_in_polygon(points, zones, all_matches=True)

Containment returns NumPy arrays for host inputs and MLX arrays for MLX inputs. Points very close to polygon edges need extra care; see accuracy.

Reuse polygons and points

Build the polygon set and index once when querying the same zones repeatedly:

polygons = ms.PolygonSet(zones)
index = ms.GridIndex(polygons)
labels = index.query(points)  # NumPy result

For an MLX pipeline, prepare the points once too. to_local shifts them to the polygons' local coordinate system before converting to float32, preserving more precision than casting raw longitude and latitude directly.

import mlx.core as mx

local = polygons.to_local(points)
labels_mx = index.query(local)  # MLX result; local[:batch_size] works too
mx.eval(labels_mx)

These results can feed MLX operations directly. The feature pipeline example combines zone embeddings and distance features in a small neural network.

Distances and nearest neighbors

Haversine functions take (longitude, latitude) in degrees and return meters as MLX arrays. Use degrees=False for radians.

landmarks = np.array([
    [-73.9857, 40.7484],
    [-74.0445, 40.6892],
])

distance = ms.haversine(points[0], points[1])            # one distance
matrix = ms.haversine_cross(points, landmarks)           # shape (3, 2)
distances, indices = ms.haversine_knn(points, landmarks, k=1)  # shape (3, 1) each

Keep the original coordinates for these functions: LocalPoints are shifted coordinates and cannot be used for distances.

End-to-end spatial join performance

Commit 8805228 was measured on M1 Ultra and M4 Max on 2026-10-19. It was faster than native GeoPandas in all six tested workloads on each machine: 3.78–18.66× on M1 Ultra and 2.55–16.51× on M4 Max. The median of the six per-workload speedup ratios was 8.00× and 6.35×, respectively. These are synthetic point-in-polygon results, not a guarantee for every dataset or spatial operation.

Each time below is the median of three trials after one full warmup. Both methods start with ready GeoDataFrames and build fresh indexes on every call. Timers include exact boundary handling, every matching pair, complete joined GeoDataFrames with attributes, and the same final sorting.

Points × polygons Vertices M1 Ultra MLX M1 Ultra GeoPandas Speedup M4 Max MLX M4 Max GeoPandas Speedup
100k × 260 48 22.9 ms 86.6 ms 3.78× 18.6 ms 47.5 ms 2.55×
1M × 260 48 157 ms 1.67 s 10.66× 118 ms 805 ms 6.81×
10M × 260 48 2.34 s 22.23 s 9.52× 1.38 s 11.02 s 7.96×
1M × 10k 48 263 ms 1.60 s 6.08× 191 ms 923 ms 4.84×
1M × 260 2,048 246 ms 1.60 s 6.49× 172 ms 1.02 s 5.90×
1M × 1 150,000 146 ms 2.73 s 18.66× 98.1 ms 1.62 s 16.51×

End-to-end spatial join speedups on M1 Ultra and M4 Max

The latest changes add threaded Point coordinate extraction, a float64 host edge grid for exact-boundary checks, and polygon-major GEOS refinement for detailed single-ring polygons. MLX uses up to eight host extraction workers in addition to the GPU; GeoPandas uses its native join without parallel partitioning. Observed MLX join times improved 1.16–1.95× on M1 Ultra and 1.20–1.97× on M4 Max versus the preceding measured build. These comparisons use the same inputs and runtime in separate sessions; background activity can also affect the differences.

All 48 paired full-output checks passed, including row order, attributes, indexes, dtypes, geometry and CRS. A fresh full suite passed 721 tests on each machine with Metal validation enabled. Both hosts used Python 3.14.2, MLX 0.32.4 and GeoPandas 1.2.0; the benchmark guide records the remaining dependencies, hardware and differing macOS versions.

The comparison uses native geopandas.sjoin(..., how="inner", predicate="within") against the unchanged MLX all-pairs query and GeoDataFrame assembly adapter. It does not time the public ms.sjoin wrapper. Input frame construction/copies, process startup, cold kernel compilation, I/O and validation are excluded. The 2,048-vertex zones resample 48-vertex boundaries; extra vertices do not imply equivalent new shape complexity. The 150k-vertex case uses an undulating polygon.

These results describe the source snapshot in the build manifest. Version 0.1.1 includes the measured optimizations. See the benchmark guide for full methodology and reproduction, raw measurements for individual trial samples, and the fresh component tables for warm-array, distance, kNN and crossover measurements, including the new chord-distance kNN filter. These use different timing and output contracts. Historical measurements remain archived separately.

Warm-index point-in-polygon benchmarks

These component benchmarks reuse the MLX polygon index and report minimum wall times with exact_boundary=False. Each ratio compares a CPU baseline with MLX on the same machine; both panels use a 0–2,500× linear scale.

Warm-index point-in-polygon component comparisons on M1 Ultra and M4 Max, using the same ratio scale

MLX returns pair arrays, GeoPandas returns a joined frame, and DuckDB returns a count. Timed setup differs across methods, so these ratios are not equivalent end-to-end join speedups. See the component tables and timing contracts for details, or download the editable SVG.

API at a glance

API Result
point_in_polygon(points, polygons) One polygon index per point; all_matches=True returns every pair
contains_matrix(points, polygons) Dense boolean matrix of shape (points, polygons)
sjoin(points_df, polygons_df) Joined DataFrame; all_matches=True returns every matching polygon
haversine(a, b) Elementwise or broadcast great-circle distances
haversine_cross(a, b) All-pairs distance matrix
haversine_knn(queries, references, k) Nearest distances and reference indices
PolygonSet(geometries) Polygons prepared for reuse; to_local(points) prepares query points
GridIndex(polygons) Reusable spatial index; call query(points)

sjoin supports how="inner" and how="left" and preserves a GeoDataFrame input's geometry and CRS. By default, each point gets at most one match (the first polygon in right-row order). For overlapping polygons, request every match directly, with the right table's attributes and their original dtypes:

joined = ms.sjoin(
    points_df, polygons_df,
    how="left", all_matches=True, predicate="within", exact_boundary=True,
)

An inner join returns matching pairs; a left join also keeps each unmatched point once. The left index and right index labels are retained, including duplicate labels, and right attributes come from each matched polygon row.

Accuracy and limits

  • Polygon edges: containment uses float32 in local coordinates. Points inside a narrow tolerance band around an edge may fall on either side. Use exact_boundary=True with host inputs in point_in_polygon, GridIndex.query, or sjoin when exact Shapely boundary behavior matters. A conservative GPU filter narrows the CPU boundary checks where supported; detailed rings use cached edge bands for GPU work, while a float64 host edge grid narrows the remaining boundary checks. Suitable detailed single-ring polygons use prepared GEOS refinement. Exact handling still adds host work and synchronization.
  • Input precision: prefer float64 NumPy coordinates or prepare them with PolygonSet.to_local. Raw float32 MLX coordinates may already have lost precision; queries emit PrecisionWarning.
  • Coordinates: containment is planar and accepts coordinates in a shared coordinate system. Haversine uses a spherical Earth model. The library does not reproject data; it checks for a CRS mismatch when both inputs carry CRS metadata.
  • Scope: supports points against polygons, including holes and multipolygons. It does not provide polygon overlays or polygon-to-polygon predicates. Use PolygonSet(zones, validate=True) to reject invalid geometry.
  • Workload matters: small host queries can use the CPU automatically. Join performance depends on candidate counts, edge-band overlap and output size, as well as vertex count. A prebuilt BallTree can be faster for nearest-neighbor queries against large reference sets. Dense distance matrices and all-match outputs still need memory for their results.

The usage guide covers boundary predicates, precision, output types, backend selection, and the remaining API helpers. Design notes describe the implementation.

License

MIT

Metadata

Release files for mlx-geospatial 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mlx-geospatial 0.1.1
File Size Uploaded Provenance
mlx_geospatial-0.1.1.tar.gz 194.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mlx-geospatial 0.1.1
File Interpreter ABI Platform
mlx_geospatial-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 278.6 kB

Release files / mlx_geospatial-0.1.1.tar.gz

Download URL mlx_geospatial-0.1.1.tar.gz
Size 194.1 kB
Tags Source
SHA-256 checksum
How to use checksums
0c77fe1420093bd5d7c8f9a30858ad9f63270bf6598d0d94dadfcb5ee3855ef8
BLAKE2b-256 checksum
How to use checksums
c4f746c13690246968647140a70a99dd85ea2ea600e963184850057269766f9c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / mlx_geospatial-0.1.1-py3-none-any.whl

Download URL mlx_geospatial-0.1.1-py3-none-any.whl
Size 84.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bb3f78acfe2ab7144061249ea2a0fed5c01af589500207fdea2d402796cf49df
BLAKE2b-256 checksum
How to use checksums
63d543d9344758882f25c62c61b281ac71aa4043c8644770be09d8b974a12f1f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for mlx-geospatial 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for mlx_geospatial-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page