Usage#
Point assignment#
import cuspa as cs
polygons = cs.io.from_geoarrow(geoarrow_table)
cell_ids = cs.tl.assign_points(points_xy, polygons)
cell_ids is an int32 device array. A value of -1 means the point was not
assigned to a polygon.
Transcript aggregation#
aggregate_to_cells consumes an iterable of (points_xy, gene_ids) CuPy
batches. It does not read files or choose a batch size. Pass a one-item
iterable for in-memory data:
csr = cs.tl.aggregate_to_cells(
[(points_xy, gene_ids)],
polygons,
num_genes=num_genes,
)
Yield one CUDA batch at a time for larger inputs:
import cupy as cp
def gpu_batches(points_host, gene_ids_host, batch_size):
for start in range(0, len(gene_ids_host), batch_size):
stop = start + batch_size
yield (
cp.asarray(points_host[start:stop], dtype=cp.float32),
cp.asarray(gene_ids_host[start:stop], dtype=cp.int32),
)
csr = cs.tl.aggregate_to_cells(
gpu_batches(points_host, gene_ids_host, batch_size=1_000_000),
polygons,
num_genes,
)
The returned CSRMatrix contains three device arrays:
indptrindicesdata
Its shape field is a Python tuple.
Use csr.to_cupy_csr() or csr.to_torch_sparse() to create a framework-native
sparse matrix without copying through host memory.
Batches are merged with exact integer arithmetic. Counts are int32, so each
(cell, gene) count must be at most 2**31 - 1; aggregate_to_cells raises
OverflowError otherwise. indptr and indices are int32 until the result
has more than 2**31 - 1 non-zeros, and int64 after that. cupyx.scipy.sparse
only supports int32 indices, so to_cupy_csr() raises OverflowError for
such a matrix; to_torch_sparse() and the raw arrays still work.
Choosing a batch size#
GPU memory must hold the largest input batch, its aggregation workspace, the polygon index, the accumulated CSR, and the merged CSR while a batch is added. Batching limits input memory; the final CSR must still fit on the GPU.
SpatialData adapter#
import cuspa as cs
polygons = cs.io.from_spatialdata(sdata, "cell_boundaries")
transcripts = cs.io.transcripts_from_spatialdata(sdata, "transcripts")
csr = cs.tl.aggregate_to_cells(
[(transcripts.xy, transcripts.gene_ids)],
polygons,
num_genes=len(transcripts.gene_names),
)
For large transcript tables, read x, y, and gene columns in record batches
instead of materializing a point GeoDataFrame.
Buffer lifetime and streams#
Polygons validates its buffers at construction and caches derived AABBs and a
uniform-grid index. Treat the buffers as immutable. After changing them in
place, call polygons.clear_cache() before the next query.
The optional stream= argument is a raw CUDA stream handle used by the native
kernels. Operations may synchronize it while sizing indexes or results. For
aggregate_to_cells, also make the corresponding CuPy stream current because
buffer copies and merge buffers use CuPy’s current stream. Keep inputs alive
and synchronize or use the same stream before consuming results elsewhere.
Limits#
Polygon, ring, and spatial-index offsets use signed 32-bit indices and are
checked against the 2^31-1 limit. Point and polygon indices in
overlap_pairs results are int32, but the number of pairs uses 64-bit
offsets and is limited only by device memory (8 bytes per pair). Each
aggregation batch is limited to 2^31-1 transcripts. The aggregated CSR
switches to int64 indptr and indices past 2^31-1 non-zero
(cell, gene) pairs, so its size is limited only by device memory. Each count
is int32 and limited to 2^31-1; larger counts raise OverflowError.