Skip to content

Loading Vector Data into the Raster Substrate

DisSModel can load any vector file (Shapefile, GeoJSON, GeoPackage, or a .zip containing a shapefile) and convert it directly into a RasterBackend, so raster models can run on real geographic data without intermediate GIS steps.

How it works

vector file → GeoDataFrame → rasterize → NumPy arrays → RasterBackend → raster model

The rasterization step (powered by rasterio.features.rasterize) runs once; after that the model runs entirely on NumPy arrays.

!!! note "Grid regularity" This workflow is most accurate when the input already contains a regular grid of equal-area polygons (e.g. 100×100 m cells), the typical output of spatial homogenization tools. For irregular polygons (municipalities, watersheds), cell values are burned by centroid or by touch — inspect the result before running long simulations.


vector_to_raster_backend

from dissmodel.io.convert import vector_to_raster_backend

b = vector_to_raster_backend(
    source     = "data/mangue_grid.shp",   # path, .zip, or a GeoDataFrame
    resolution = 100,                      # 100 m cells
    attrs      = ["uso", "alt", "solo"],
    crs        = "EPSG:31984",             # reproject if needed
)

print(b.shape)             # (rows, cols) from bounding box + resolution
print(b.band_names())      # ['mask', 'uso', 'alt', 'solo']

Parameters

Parameter Type Description
source str, Path or GeoDataFrame Vector file or in-memory GeoDataFrame
resolution float Cell size in CRS units (metres for a metric CRS)
attrs list[str] or dict[str, default] Columns to rasterize
crs str, int, or None Target CRS — reprojects if needed
all_touched bool Burn all touched cells (default: centre only)
nodata int or float Fill value for uncovered cells (default: 0)
add_mask bool Add a "mask" band marking covered cells (default: True)

To set per-column defaults for uncovered cells, pass a dict:

b = vector_to_raster_backend(
    source     = "data/mangue_grid.shp",
    resolution = 100,
    attrs      = {"uso": 5, "alt": 0.0, "solo": 1},
    crs        = "EPSG:31984",
)

shapefile_to_raster_backend is a deprecated alias of this function and emits a FutureWarning.


Full example — raster model from a vector file

from dissmodel.core import Environment
from dissmodel.io.convert import vector_to_raster_backend
from dissmodel.visualization.raster_map import RasterMap

# your own RasterModel subclass, e.g. the BR-MANGUE flood model
from myproject.flood import FloodRasterModel

# 1. vector file → RasterBackend
b = vector_to_raster_backend(
    source     = "data/mangue_grid.shp",
    resolution = 100,
    attrs      = {"uso": 5, "alt": 0.0, "solo": 1},
    crs        = "EPSG:31984",
)
print(f"Grid: {b.shape[0]} rows × {b.shape[1]} cols = {b.shape[0] * b.shape[1]:,} cells")

# 2. run the raster model
env = Environment(start_time=2012, end_time=2100)
FloodRasterModel(backend=b, taxa=0.011)
RasterMap(backend=b, band="uso", title="Land Use")
env.run()

Saving results to GeoTIFF

vector_to_raster_backend stores the CRS and affine transform on the backend, so save_geotiff writes a georeferenced file without extra arguments:

from dissmodel.io.raster import save_geotiff

checksum = save_geotiff(b, "output/flood_result.tif")   # returns SHA-256

Same data, two substrates

# ── vector substrate ─────────────────────────────────────────────────────────
import geopandas as gpd

gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984")
# → GeoDataFrame with real geometries; use with SpatialModel / CellularAutomaton

# ── raster substrate ─────────────────────────────────────────────────────────
from dissmodel.io.convert import vector_to_raster_backend

b = vector_to_raster_backend(
    "data/mangue_grid.shp", resolution=100,
    attrs=["uso", "alt", "solo"], crs="EPSG:31984",
)
# → RasterBackend with the same attributes; use with RasterModel

The data source is the same file; only the substrate changes. For measured run times of the two substrates on the same model, see the benchmarks in benchmarks/ and the brmangue-dissmodel benchmark executor.


API Reference

dissmodel.io.convert.vector_to_raster_backend(source, resolution, attrs, crs=None, all_touched=False, nodata=0, nodata_value=None, add_mask=True)

Convert a vector source to a RasterBackend.

Accepts a file path (Shapefile, GeoJSON, GPKG, .zip) or an in-memory GeoDataFrame. Each requested attribute column is rasterized into a separate band.

Parameters:

Name Type Description Default
source str, Path, or GeoDataFrame

Vector source. File paths are read with GeoPandas; GeoDataFrames are used directly (a copy is made before any reprojection).

required
resolution float

Cell size in the units of the CRS.

required
attrs list[str] or dict[str, Any]

Columns to rasterize. A list uses nodata as fill for all columns; a dict maps column names to per-column fill defaults.

required
crs str, int, or None

Target CRS for reprojection. If None and source is a GeoDataFrame without a CRS, a ValueError is raised.

None
all_touched bool

If True, burn all cells touched by a geometry edge.

False
nodata int or float

Default fill for cells outside geometries. Default: 0.

0
nodata_value int or float or None

Sentinel for out-of-extent cells. Useful when 0 is a valid value (e.g. nodata_value=-1 for proportion arrays). Default: None.

None
add_mask bool

If True (default), adds a "mask" band — 1.0 where a cell is covered by at least one geometry, 0.0 elsewhere.

True

Returns:

Type Description
RasterBackend

Raises:

Type Description
ImportError

If geopandas or rasterio are not installed.

FileNotFoundError

If a file path does not exist.

ValueError

If attrs is empty, a requested column is missing, or the GeoDataFrame has no CRS and crs is also None.

Examples:

# From file path
b = vector_to_raster_backend(
    "data/mangue_grid.shp", resolution=100, attrs=["uso", "alt"]
)

# From in-memory GeoDataFrame
import geopandas as gpd
gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984")
b = vector_to_raster_backend(gdf, resolution=100, attrs={"uso": -1})
Source code in dissmodel/io/convert.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def vector_to_raster_backend(
    source: str | pathlib.Path | Any,   # Any = gpd.GeoDataFrame
    resolution: float,
    attrs: list[str] | dict[str, Any],
    crs: str | int | None = None,
    all_touched: bool = False,
    nodata: float = 0,
    nodata_value: float | None = None,
    add_mask: bool = True,
) -> RasterBackend:
    """
    Convert a vector source to a RasterBackend.

    Accepts a file path (Shapefile, GeoJSON, GPKG, .zip) or an in-memory
    GeoDataFrame. Each requested attribute column is rasterized into a
    separate band.

    Parameters
    ----------
    source : str, Path, or GeoDataFrame
        Vector source. File paths are read with GeoPandas; GeoDataFrames
        are used directly (a copy is made before any reprojection).
    resolution : float
        Cell size in the units of the CRS.
    attrs : list[str] or dict[str, Any]
        Columns to rasterize. A list uses ``nodata`` as fill for all columns;
        a dict maps column names to per-column fill defaults.
    crs : str, int, or None
        Target CRS for reprojection. If ``None`` and source is a GeoDataFrame
        without a CRS, a ValueError is raised.
    all_touched : bool
        If ``True``, burn all cells touched by a geometry edge.
    nodata : int or float
        Default fill for cells outside geometries. Default: ``0``.
    nodata_value : int or float or None
        Sentinel for out-of-extent cells. Useful when ``0`` is a valid value
        (e.g. ``nodata_value=-1`` for proportion arrays). Default: ``None``.
    add_mask : bool
        If ``True`` (default), adds a ``"mask"`` band — ``1.0`` where a cell
        is covered by at least one geometry, ``0.0`` elsewhere.

    Returns
    -------
    RasterBackend

    Raises
    ------
    ImportError
        If ``geopandas`` or ``rasterio`` are not installed.
    FileNotFoundError
        If a file path does not exist.
    ValueError
        If ``attrs`` is empty, a requested column is missing, or the
        GeoDataFrame has no CRS and ``crs`` is also ``None``.

    Examples
    --------
    ```python
    # From file path
    b = vector_to_raster_backend(
        "data/mangue_grid.shp", resolution=100, attrs=["uso", "alt"]
    )

    # From in-memory GeoDataFrame
    import geopandas as gpd
    gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984")
    b = vector_to_raster_backend(gdf, resolution=100, attrs={"uso": -1})
    ```
    """
    try:
        import rasterio
        import rasterio.features
        import rasterio.transform
    except ImportError:
        raise ImportError("rasterio is required — pip install rasterio")

    try:
        import geopandas as gpd
    except ImportError:
        raise ImportError("geopandas is required — pip install geopandas")

    # ── resolve source → GeoDataFrame ────────────────────────────────────────
    if isinstance(source, (str, pathlib.Path)):
        path = pathlib.Path(source)
        if not str(source).startswith("zip://") and not path.exists():
            raise FileNotFoundError(f"File not found: {path}")
        gdf = gpd.read_file(str(source))
    else:
        # Assume GeoDataFrame — make a copy to avoid mutating the caller's data
        gdf = source.copy()

    # ── CRS validation and reprojection ───────────────────────────────────────
    if gdf.crs is None and crs is None:
        raise ValueError(
            "Source GeoDataFrame has no CRS and no target CRS was provided. "
            "Pass crs= to specify the coordinate reference system."
        )

    if crs is not None:
        gdf = gdf.to_crs(crs)

    # ── resolve attrs → {column: fill_default} ────────────────────────────────
    if isinstance(attrs, list):
        attr_defaults: dict[str, Any] = {col: nodata for col in attrs}
    else:
        attr_defaults = dict(attrs)

    if not attr_defaults:
        raise ValueError("attrs must not be empty")

    missing = [col for col in attr_defaults if col not in gdf.columns]
    if missing:
        raise ValueError(f"Columns not found in source: {missing}")

    # ── compute grid from bounding box ────────────────────────────────────────
    xmin, ymin, xmax, ymax = gdf.total_bounds
    n_cols = int(np.ceil((xmax - xmin) / resolution))
    n_rows = int(np.ceil((ymax - ymin) / resolution))

    transform = rasterio.transform.from_bounds(
        xmin, ymin, xmax, ymax, n_cols, n_rows
    )

    backend = RasterBackend(
        shape        = (n_rows, n_cols),
        nodata_value = nodata_value,
        transform    = transform,  
        crs          = gdf.crs     
    )

    # ── rasterize geometry coverage → "mask" band ─────────────────────────────
    valid_geoms = [geom for geom in gdf.geometry if geom is not None]
    coverage = rasterio.features.rasterize(
        shapes    = ((geom, 1) for geom in valid_geoms),
        out_shape = (n_rows, n_cols),
        transform = transform,
        fill      = 0,
        all_touched = all_touched,
        dtype     = np.uint8,
    )
    mask = coverage.astype(bool)

    if add_mask:
        backend.set("mask", mask.astype(np.float32))

    # ── rasterize each attribute column ───────────────────────────────────────
    for col, default in attr_defaults.items():
        values = gdf[col]

        dtype = np.int32 if np.issubdtype(values.dtype, np.integer) else np.float32

        arr = rasterio.features.rasterize(
            shapes = (
                (geom, float(val))
                for geom, val in zip(gdf.geometry, values)
                if geom is not None
            ),
            out_shape = (n_rows, n_cols),
            transform = transform,
            fill      = float(default),
            all_touched = all_touched,
            dtype     = dtype,
        )

        sentinel = nodata_value if nodata_value is not None else default
        arr = np.where(mask, arr, sentinel).astype(dtype)
        backend.set(col, arr)

    n_valid = int(mask.sum())
    n_total = n_rows * n_cols
    print(
        f"  rasterized: {n_valid:,} valid cells"
        f" / {n_total:,} total"
        f" ({100 * n_valid / n_total:.1f}% coverage)"
    )

    return backend

dissmodel.io.raster.save_geotiff(data, uri, minio_client=None, band_spec=None, crs=None, transform=None, compress='deflate', **kwargs)

Save a RasterBackend as GeoTIFF to any URI.

Supports local path and s3://. Returns sha256 checksum of the saved file.

Parameters:

Name Type Description Default
data (RasterBackend, dict) or RasterBackend

Backend and metadata dict (as returned by load_geotiff), or the backend alone.

required
uri str

Destination URI. Local path or s3://bucket/key.

required
band_spec list of (name, dtype, nodata) or None

Bands to write in order. Missing bands are filled with nodata. If None, all arrays in the backend are written.

None
crs str or None

CRS string (e.g. "EPSG:31984"). Overrides meta["crs"], which overrides backend.crs.

None
transform Affine or None

Affine geotransform. Overrides meta["transform"], which overrides backend.transform.

None
compress str

Compression algorithm. Default: "deflate".

'deflate'
Notes

GeoTIFF stores a single dtype per file. When band dtypes differ (e.g. an int32 categorical band alongside a float32 continuous band), all bands are promoted to their common NumPy result type (np.result_type) before writing.

Source code in dissmodel/io/raster.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def save_geotiff(
    data,
    uri:          str,
    minio_client = None,
    band_spec:   list[tuple[str, str, float]] | None = None,
    crs          = None,
    transform    = None,
    compress:    str = "deflate",
    **kwargs,
) -> str:
    """
    Save a RasterBackend as GeoTIFF to any URI.

    Supports local path and s3://.
    Returns sha256 checksum of the saved file.

    Parameters
    ----------
    data : (RasterBackend, dict) or RasterBackend
        Backend and metadata dict (as returned by load_geotiff), or the
        backend alone.
    uri : str
        Destination URI. Local path or s3://bucket/key.
    band_spec : list of (name, dtype, nodata) or None
        Bands to write in order. Missing bands are filled with nodata.
        If None, all arrays in the backend are written.
    crs : str or None
        CRS string (e.g. "EPSG:31984"). Overrides meta["crs"], which
        overrides ``backend.crs``.
    transform : Affine or None
        Affine geotransform. Overrides meta["transform"], which overrides
        ``backend.transform``.
    compress : str
        Compression algorithm. Default: "deflate".

    Notes
    -----
    GeoTIFF stores a single dtype per file. When band dtypes differ
    (e.g. an int32 categorical band alongside a float32 continuous band),
    all bands are promoted to their common NumPy result type
    (``np.result_type``) before writing.
    """
    if not HAS_RASTERIO:
        raise ImportError("rasterio is required — pip install rasterio")

    backend, meta = data if isinstance(data, tuple) else (data, None)
    resolved_crs       = crs       or (meta.get("crs") if meta else None) or backend.crs
    resolved_transform = transform or (meta.get("transform") if meta else None) or backend.transform

    with tempfile.NamedTemporaryFile(suffix=".tif", delete=False) as f:
        tmp = f.name

    try:
        _write_geotiff(
            backend, tmp,
            band_spec = band_spec,
            crs       = resolved_crs,
            transform = resolved_transform,
            compress  = compress,
        )

        with open(tmp, "rb") as f:
            content = f.read()

        if uri.startswith("s3://"):
            if minio_client is None:
                from dissmodel.io._storage import get_default_client
                minio_client = get_default_client()
            bucket, key = uri[5:].split("/", 1)
            minio_client.put_object(
                bucket_name  = bucket,
                object_name  = key,
                data         = io.BytesIO(content),
                length       = len(content),
                content_type = "image/tiff",
            )
        else:
            pathlib.Path(uri).parent.mkdir(parents=True, exist_ok=True)
            shutil.copy(tmp, uri)

    finally:
        os.unlink(tmp)

    return sha256_bytes(content)