Skip to content

NumPy arrays & coordinates

gometry returns numeric bulk results as typed NumPy arrays — float64 for metrics and coordinates, bool_ for predicate masks, int64 for ids and labels, uint64 for grid-cell ids and space-filling-curve keys. NumPy is installed with the core package.

Numeric results

import numpy as np
import gometry as gm

zones = gm.GeometryArray([gm.box(i, 0, i + 2, 2) for i in range(4)])

areas = zones.area
mask = gm.intersects(zones, gm.box(0.5, 1, 1.5, 3))
left, right = gm.SpatialIndex(zones).self_join()

print("areas:", areas.dtype, repr(areas))
print("mask:", mask.dtype, repr(mask))
print("pairs:", left.dtype, list(zip(left.tolist(), right.tolist())))
areas: float64 array([4., 4., 4., 4.])
mask: bool array([ True,  True, False, False])
pairs: int64 [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]

The arrays come back read-only and already in the right dtype, so numpy.asarray(result) is a zero-copy view when the dtype matches and .tolist() returns plain Python lists. Per-row bounds are (rows, 4) float64 matrices (nan rows for empty geometries); array index queries return Groups, a CSR wrapper whose .values/.offsets and per-row views are int64 ndarrays. Text outputs (to_wkt, geohash tokens), byte outputs (to_wkb), and ragged diagnostics stay Python lists because they have no single dense ndarray shape.

Numeric inputs accept PEP-3118 buffers where an operation takes a FloatColumn or integer column. Geometry containers do not export this numeric buffer protocol. Input coordinates are copied into native float64 storage, converting float32 values when needed. Do not modify an input buffer until the call returns; later changes cannot affect the constructed geometry.

Coordinate views and matrix exports

geom.coords exposes per-axis columns directly. .x/.y are always read-only float64 ndarrays. .z/.m are also always read-only float64 ndarrays of the view length; they contain NaN when no coordinate carries that ordinate, and mixed-dimension rows use NaN for the rows that lack it.

import numpy as np
import gometry as gm

line = gm.LineString([(0.0, 0.0), (3.0, 4.0), (6.0, 8.0)])
xs = line.coords.x                 # read-only float64 column
print("x column:", xs.tolist(), "| read-only:", not xs.flags.writeable)

arr = gm.GeometryArray([gm.Point(1, 2), gm.LineString([(0, 0), (1, 1)])])
coords = arr.coords
matrix = np.asarray(coords, dtype=np.float64)
row_index = coords.row_index
print("coords:\n", matrix.tolist())
print("source-row index:", row_index.tolist())
x column: [0.0, 3.0, 6.0] | read-only: True
coords:
 [[1.0, 2.0], [0.0, 0.0], [1.0, 1.0]]
source-row index: [0, 1, 1]

Per-axis columns can borrow storage when the carried layout permits it; np.asarray(coords, dtype=np.float64) materializes a dense (N, dims) matrix and always copies, with NaN for absent Z/M.

coords.to_dict() returns read-only ndarray columns such as {'x': ndarray, 'y': ndarray} for direct Pandas/Polars construction. It omits Z/M when the view does not carry those axes; mixed views include their union and use NaN where a row lacks an axis.

Use coords.select('XY'), coords.select('XYZ'), coords.select('XYM'), or coords.select('XYZM') to choose a fixed coordinate-view layout. The selected layout governs iteration, nested output, column access, ndarray/dict exports, and equality without changing the geometry. Excluded Z/M columns contain NaN. Nested output preserves missing geometry rows as None.

For a flattened GeometryArray.coords view, use .row_index to map each coordinate back to its source geometry row. The matrix includes every carried axis; use coords.select('XY') before conversion when a fixed layout is needed.

For a foreign iterable, first use gm.GeometryArray(values).coords; this admits the same row formats as the array constructor.

Coordinate matrices

Geometry.with_coordinates and GeometryArray.with_coordinates accept one rank-2 coordinate matrix. Its shape is (N, 2) for XY, (N, 3) for XYZ or XYM, or (N, 4) for XYZM, and N must match the receiver's coordinate count (or the flattened present-row count for an array). The matrix width must match the receiver's axes. A rank other than 2, an unsupported width, or a row-count mismatch raises an error.

The matrix is converted to native float64 values, so this is a validated replacement input rather than a zero-copy view.

Keyword columns can instead replace selected ordinates; their scalar-broadcast, flattened ordering, omission, and dimension-removal rules are described in Z and M under operations.

The geometry ↔ object-array bridge

GeometryArray.to_numpy explicitly exports an object ndarray of geometry handles. NumPy's array protocol also supports np.asarray(arr) and np.asarray(arr, dtype=object), which materialize an object array when requested. The inverse is the GeometryArray constructor, which accepts an object ndarray (or any iterable) of gometry geometries or __geo_interface__ objects, including Shapely geometries.

Geometry and GeometryArray set __array_ufunc__ = None, so NumPy ufuncs defer with a clear TypeError instead of assigning numeric meaning to spatial values. GeometryArray and Coordinates implement __array__, so np.asarray(arr) gives an object array and np.asarray(coords) gives an (N, dims) float64 matrix. Coordinates therefore work directly with numeric ufuncs. Exported coordinate arrays are read-only inputs; a ufunc that allocates its result returns a normal writable NumPy array. A read-only export cannot be used as the ufunc's out= target.

NumPy ufunc results and out=

This example shows the distinction between a read-only coordinate export, a newly allocated ufunc result, and a rejected write into the export:

import numpy as np
import gometry as gm

coords = gm.LineString([(0.0, 1.0), (2.0, 3.0)]).coords
matrix = np.asarray(coords)
result = np.add(matrix, 1.0)

print("export read-only:", not matrix.flags.writeable)
print("new result writable:", result.flags.writeable)
assert not matrix.flags.writeable
assert result.flags.writeable
try:
    np.add(matrix, 1.0, out=matrix)
except ValueError:
    print("read-only out: rejected")
else:
    raise AssertionError("NumPy accepted a read-only array as its output")
export read-only: True
new result writable: True
read-only out: rejected

Integer-array selection preserves known NumPy rank in its result type: a zero-dimensional integer array selects one row, while a one-dimensional integer array selects a container; selectors with rank greater than one raise TypeError. An ndarray annotation with a variable-length tuple of integer dimensions gives a scalar-or-container result; when NumPy erases the shape to Any, annotate the rank for a narrower result. See array indexing.

Cell arrays

CellArray exposes H3, S2, and tile ids as uint64 through .to_numpy() (zero-copy when contiguous) and NumPy's array protocol. For those numeric grids, np.asarray(cells) gives raw ids, while np.asarray(cells, dtype=object) gives typed cell objects. Geohash's public identity is its base-32 string token, not an implementation integer, so geohash_cells.to_numpy() and np.asarray(geohash_cells) return a read-only object array of GeohashCell values; use .token for strings.

Cell arrays with missing rows are logical sequences of Cell | None. Their default NumPy export is a read-only object array containing the typed cells and None; dtype=object always has this behavior. Explicit dtype=uint64 is rejected for a masked array, and copy=False is rejected whenever an object, masked, or gathered export would need materialization. A selection containing only present rows can use the ordinary zero-copy numeric export.

import numpy as np
import gometry as gm

cells = gm.h3_cells([21.0, 22.0], [52.0, 52.5], resolution=7)
ids = cells.to_numpy()             # zero-copy ndarray
print("dtype:", ids.dtype, "| n:", ids.size)
print("object:", type(np.asarray(cells, dtype=object)[0]).__name__)
dtype: uint64 | n: 2
object: H3Cell

Handing off to SciPy and scikit-learn

Scattered-data interpolation (IDW, kriging, natural-neighbor) and coordinate clustering (k-means, DBSCAN, hierarchical) live in SciPy, scikit-gstat, and scikit-learn; gometry supplies their coordinate input. Pull a float64 matrix with np.asarray(geom.coords, dtype=np.float64) (or use per-axis .coords.x/.y) and pass it straight in. Project to a metric CRS first when planar XY is wrong for geographic data:

Install scikit-learn for the clustering example:

uv add scikit-learn
python -m pip install scikit-learn
import numpy as np
import gometry as gm
from sklearn.cluster import KMeans          # not a gometry dependency

lons = [13.35, 13.36, 13.37, 13.50, 13.51, 13.52]
lats = [52.48, 52.49, 52.50, 52.51, 52.52, 52.53]
pts = gm.points(lons, lats, crs=4326)
xy = np.asarray(pts.to_crs(pts.estimate_local_crs()).coords, dtype=np.float64)  # planar meters
labels = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(xy)
print("labels:", labels.tolist())
labels: [1, 1, 1, 0, 0, 0]

The same pattern feeds SciPy's cKDTree, cdist, and interpolators — the ndarray is the shared input format.

Python Array API compatibility

gometry implements the NumPy interchange protocols (__array__ and __array_ufunc__ = None) and returns typed ndarrays. Numeric input columns can consume PEP-3118 buffers, but geometry and cell containers do not export numeric PEP-3118 buffers. gometry is not a Python Array API provider and does not expose __array_namespace__. Use gometry to produce coordinate arrays, then pass them to NumPy, SciPy, or scikit-learn.

See also