Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.3.0] - 2026-04

### Added

#### Sync models
- `SyncSpatialModel` — `SpatialModel` with automatic `_past` snapshot semantics,
equivalent to TerraME's `cs:synchronize()`. Declare `self.land_use_types` in
`setup()` and `<col>_past` columns are managed automatically.
- `SyncRasterModel` — raster analogue of `SyncSpatialModel`. Copies each array in
`land_use_types` to `<n>_past` in the `RasterBackend` before and after each step.
- Both expose a public `synchronize()` method that can also be called manually.

#### I/O — `dissmodel.geo.raster.io`
- `shapefile_to_raster_backend` — loads any GeoPandas-supported vector format
(Shapefile, GeoJSON, GeoPackage, ZIP) and rasterizes attribute columns into a
`RasterBackend`. Adds a `"mask"` band marking valid cells. Supports
`nodata_value` to distinguish out-of-extent cells from valid zero values.
- `save_raster_backend` — convenience wrapper that writes all (or selected) arrays
to a GeoTIFF without requiring a `band_spec`.
- `load_geotiff` and `save_geotiff` updated with full docstrings and support for
`.zip` archives containing a single GeoTIFF.

#### `RasterMap`
- `scheme` parameter: `"manual"` (default), `"equal_interval"`, `"quantiles"`.
- `k` — number of colour classes for `equal_interval`. Default: `5`.
- `legend` — show or hide the colorbar. Default: `True`.
- `save_frames` — force PNG output even in interactive mode.
- `auto_mask` — automatically applies the `"mask"` band from the backend so
out-of-extent cells are transparent. Default: `True`.

#### `Map`
- `figsize`, `interval`, `save_frames` parameters.
- Headless fallback: saves PNGs to `map_frames/` when no display is available.

### Changed
- `RasterMap` no longer calls `matplotlib.use()` at import time, preventing side
effects when imported alongside other visualization components.
- `make_raster_grid()` renamed to `raster_grid()`. Old name kept as a
`DeprecationWarning` alias and will be removed in v0.4.0.

### Fixed
- `Map`: fixed `AttributeError: 'Map' object has no attribute 'fig'` when used
with Streamlit (`plot_area=st.empty()`).
- `Map`: no longer raises `RuntimeError` when no display is available — falls back
to saving PNGs to `map_frames/`.
- `RasterMap`: fixed `plt.close("all")` closing all figures when multiple
`RasterMap` instances are active. Each instance now maintains its own persistent
figure and updates independently.

---

## [0.2.1] - 2026-03

### Changed
Expand Down
5 changes: 4 additions & 1 deletion dissmodel/geo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@
from .raster.band_spec import BandSpec # se tiver classe exportável

# raster io — opcional, não importa por padrão (requer rasterio)
# from .raster.io import load_geotiff, save_geotiff
# from .raster.io import load_geotiff, save_geotiff

from .vector.sync_model import SyncSpatialModel
from .raster.sync_model import SyncRasterModel
78 changes: 45 additions & 33 deletions dissmodel/geo/raster/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@


# Moore neighbourhood (8 directions) — framework constant, not domain-specific.
# Models import from here; projects do not need to redefine it.
DIRS_MOORE: list[tuple[int, int]] = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1), ( 0, 1),
Expand Down Expand Up @@ -64,18 +63,56 @@ class RasterBackend:
----------
shape : tuple[int, int]
Grid shape as ``(rows, cols)``.
nodata_value : float | int | None
Sentinel value used to mark cells outside the study extent.
When provided, ``nodata_mask`` derives the extent mask automatically,
so ``RasterMap`` renders those cells as transparent without any extra
configuration. Default: ``None``.

Examples
--------
>>> b = RasterBackend(shape=(10, 10))
>>> b.set("state", np.zeros((10, 10), dtype=np.int8))
>>> b.get("state").shape
(10, 10)

>>> b = RasterBackend(shape=(10, 10), nodata_value=-1)
>>> b.nodata_mask # True = valid cell, False = outside extent
"""

def __init__(self, shape: tuple[int, int]) -> None:
self.shape = shape # (rows, cols)
def __init__(
self,
shape: tuple[int, int],
nodata_value: float | int | None = None,
) -> None:
self.shape = shape
self.arrays: dict[str, np.ndarray] = {}
self.nodata_value = nodata_value # sentinel for out-of-extent cells

# ── extent mask ───────────────────────────────────────────────────────────

@property
def nodata_mask(self) -> np.ndarray | None:
"""
Boolean mask: ``True`` = valid cell, ``False`` = outside extent / nodata.

Derived in priority order:
1. ``arrays["mask"]`` — explicit mask band (dissluc / coastal convention:
non-zero = valid).
2. ``nodata_value`` — applied over the first available array.
3. ``None`` — no information; ``RasterMap`` skips auto-masking.

Used by ``RasterMap`` (``auto_mask=True``) to render out-of-extent pixels
as transparent without any per-project configuration.
"""
if "mask" in self.arrays:
return self.arrays["mask"] != 0

if self.nodata_value is not None and self.arrays:
first = next(iter(self.arrays.values()))
return first != self.nodata_value

return None

# ── read / write ──────────────────────────────────────────────────────────

Expand Down Expand Up @@ -120,14 +157,9 @@ def shift2d(arr: np.ndarray, dr: int, dc: int) -> np.ndarray:
Shift ``arr`` by ``(dr, dc)`` rows/columns without wrap-around.
Edges are filled with zero.

Equivalent to reading the neighbour in direction ``(dr, dc)`` for every
cell simultaneously — replaces ``forEachNeighbor`` with a vectorized
operation.

Parameters
----------
arr : np.ndarray
2D source array.
dr : int
Row offset (positive = down, negative = up).
dc : int
Expand All @@ -137,11 +169,6 @@ def shift2d(arr: np.ndarray, dr: int, dc: int) -> np.ndarray:
-------
np.ndarray
Shifted array of the same shape as ``arr``.

Examples
--------
>>> shift2d(alt, -1, 0) # altitude of the northern neighbour of each cell
>>> shift2d(alt, 1, 1) # altitude of the south-eastern neighbour
"""
rows, cols = arr.shape
out = np.zeros_like(arr)
Expand All @@ -164,26 +191,16 @@ def neighbor_contact(
Parameters
----------
condition : np.ndarray
Boolean array marking the source cells.
neighborhood : list[tuple[int, int]] | None
Directions to check. ``None`` uses Moore neighbourhood via
``binary_dilation`` with a 3×3 structuring element (includes the
cell itself). Pass ``DIRS_VON_NEUMANN`` or a custom list for other
neighbourhoods.
``None`` uses Moore neighbourhood via ``binary_dilation``.

Returns
-------
np.ndarray
Boolean array; ``True`` where a cell neighbours at least one
``True`` cell in ``condition``.

Notes
-----
Equivalent to ``forEachNeighbor`` checking membership in a set.
Boolean array.
"""
if neighborhood is None:
return binary_dilation(condition.astype(bool), structure=np.ones((3, 3)))
# custom neighbourhood via manual shifts
result = np.zeros_like(condition, dtype=bool)
for dr, dc in neighborhood:
result |= RasterBackend.shift2d(condition.astype(np.int8), dr, dc) > 0
Expand All @@ -198,19 +215,15 @@ def focal_sum(
Focal sum: for each cell, sum the values of ``name`` across its neighbours.
The cell itself is not included.

Useful for counting neighbours in a given state, computing gradients, etc.

Parameters
----------
name : str
Name of the array to aggregate.
neighborhood : list[tuple[int, int]]
Directions to include. Default: ``DIRS_MOORE``.
Default: ``DIRS_MOORE``.

Returns
-------
np.ndarray
Float array with per-cell neighbour sums.
"""
arr = self.arrays[name]
result = np.zeros_like(arr, dtype=float)
Expand All @@ -229,9 +242,8 @@ def focal_sum_mask(
Parameters
----------
mask : np.ndarray
Boolean array marking the cells to count.
neighborhood : list[tuple[int, int]]
Directions to include. Default: ``DIRS_MOORE``.
Default: ``DIRS_MOORE``.

Returns
-------
Expand All @@ -250,4 +262,4 @@ def __repr__(self) -> str:
bands = ", ".join(
f"{k}:{v.dtype}[{v.shape}]" for k, v in self.arrays.items()
)
return f"RasterBackend(shape={self.shape}, arrays=[{bands}])"
return f"RasterBackend(shape={self.shape}, arrays=[{bands}])"
Loading
Loading