Splitting large datasets
=========================
For most tasks, ``snail split`` and ``snail process`` commands should just work.
Or, using the Python functions :func:`snail.intersection.split_linestrings`,
:func:`~snail.intersection.split_polygons` and
:func:`~snail.intersection.split_geometries` you can pass in a
:class:`~geopandas.GeoDataFrame` of geometries and get a split one back.
This page goes into more of the implementation details, which might help if you
want to split a source that is not already loaded as a GeoDataFrame - for
example, a GeoParquet file bigger than available memory - or if you are working
directly against the compiled extension.
How a split runs
-----------------
The splitting logic is all implemented in a C++ extension, which is compiled and
made available to Python as :module:`snail.core.intersections`.
Geometries are passed into the C++ extension as `GeoArrow
`_, using the `Arrow C stream interface
`_: the extension
pulls one batch of geometries at a time, splits it, and hands the pieces back
the same way, as a stream of record batches. Nothing is split until that stream
is read, and only one batch of the source is ever held at once - so it should be
possible for a file larger than memory to be split by a consumer that takes
the results as they come, without loading the whole thing first.
:func:`snail.core.intersections.split_linestrings`,
:func:`~snail.core.intersections.split_polygons` and
:func:`~snail.core.intersections.split_geometries` accept the geometries from
*any* object implementing the Arrow PyCapsule stream interface
(``__arrow_c_stream__``) or the single-array interface (``__arrow_c_array__``,
read as a stream of one batch) - a :class:`pyarrow.ChunkedArray`,
:class:`~pyarrow.Table` or :class:`~pyarrow.RecordBatchReader`, a GeoParquet or
:class:`pyarrow.dataset.Dataset` reader, or :meth:`GeoSeries.to_arrow()
`.
Geometry encoding
-----------------
GeoArrow specifies two possible encodings, native and well-known binary. Either
can be read:
**Native** - ``geoarrow.linestring``, ``geoarrow.polygon`` - keeps the
coordinates in Arrow buffers. They may be interleaved (as geopandas exports
them) or separated into x and y arrays (as GeoParquet stores them), in two or
more dimensions; splitting works on x and y dimensions, so a z or m co-ordinates
are stepped over.
**Well-known binary** - ``geoarrow.wkb`` - holds each geometry as a
serialised blob, which has to be decoded on the way in. This is what
:meth:`GeoDataFrame.to_parquet() `
writes unless told otherwise, so it is what a GeoParquet file usually
contains.
Geometries of more than one type
---------------------------------
:func:`snail.core.intersections.split_geometries` can handle a stream of mixed
geometry types:
- LineStrings and Polygons are split
- Points pass through
- multi-part geometries are split part by part
- GeometryCollections are split member by member
This returns ``geoarrow.wkb``, because an Arrow stream has one schema for all of
its batches and WKB is the only encoding that can carry every type in one. That
is also why a mixed layer cannot be written as native GeoArrow - geopandas
raises ``ValueError: Geometry type combination is not supported``.
There is some overhead to encoding and decoding `wkb`. Use the typed functions
where a column really does hold one type, and ``split_geometries`` where it does
not.
Splitting a file directly
--------------------------
This reads a GeoParquet file of linestrings in batches and splits each
batch as it arrives, never holding more than one batch of geometries and
one batch of pieces at a time - whatever the file's size::
import pyarrow
import pyarrow.parquet
from snail.core.intersections import split_linestrings
from snail.intersection import GridDefinition
grid = GridDefinition.from_raster("hazard.tif")
parquet_file = pyarrow.parquet.ParquetFile("edges.geoparquet")
geometry_batches = pyarrow.RecordBatchReader.from_batches(
parquet_file.schema_arrow, parquet_file.iter_batches(batch_size=10_000)
)
stream = split_linestrings(
geometry_batches, nrows=grid.height, ncols=grid.width, transform=grid.transform
)
reader = pyarrow.RecordBatchReader._import_from_c_capsule(stream.__arrow_c_stream__())
for batch in reader:
# batch has a "geometry" column of the pieces (GeoArrow-encoded)
# and a "parent" column: the index, in the source, of the
# geometry each piece was split from
...
Swap ``split_linestrings`` for
:func:`~snail.core.intersections.split_geometries` to split a file whose
geometries are not all one type; the ``"geometry"`` column of each batch is then
``geoarrow.wkb`` rather than native GeoArrow.
To get the pieces as shapely geometries instead, convert a batch with
:meth:`geopandas.GeoDataFrame.from_arrow`.
Or, to read the whole stream into memory, use
:func:`snail.intersection.read_split_stream`.