From 422f3e0bb341d76509caf8268131a31565852c85 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 10:13:04 -0300 Subject: [PATCH 01/12] include raster_io --- raster_io.py | 199 +++++++++++++++++++++++++++++++++++++++++ shapefile_to_raster.md | 141 +++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 raster_io.py create mode 100644 shapefile_to_raster.md diff --git a/raster_io.py b/raster_io.py new file mode 100644 index 0000000..c547f6b --- /dev/null +++ b/raster_io.py @@ -0,0 +1,199 @@ +""" +dissmodel/geo/raster/io.py +=========================== +Utilities for loading external spatial data into RasterBackend. + +Functions +--------- +shapefile_to_raster_backend(path, resolution, attrs, crs, all_touched, nodata) + Load a shapefile (or any vector format) and rasterize one or more + attribute columns into a RasterBackend. +""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import numpy as np +import geopandas as gpd +import rasterio +import rasterio.features +import rasterio.transform + +from dissmodel.geo.raster.backend import RasterBackend + + +def shapefile_to_raster_backend( + path: str | Path, + resolution: float, + attrs: list[str] | dict[str, Any], + crs: str | int | None = None, + all_touched: bool = False, + nodata: int | float = 0, +) -> RasterBackend: + """ + Load a vector file and rasterize attribute columns into a RasterBackend. + + Reads any format supported by GeoPandas (Shapefile, GeoJSON, GPKG, …), + reprojects to ``crs`` if provided, computes the grid dimensions from the + bounding box and ``resolution``, and rasterizes each requested attribute + column using ``rasterio.features.rasterize``. + + Parameters + ---------- + path : str or Path + Path to the vector file (Shapefile, GeoJSON, GeoPackage, …). + resolution : float + Cell size in the units of the coordinate reference system. + For metric CRS (e.g. EPSG:31984) this is metres. + attrs : list[str] or dict[str, Any] + Column names to rasterize. + + - ``list[str]`` — rasterize each column using its own values. + - ``dict[str, Any]`` — keys are column names, values are fill defaults + used when a cell is not covered by any geometry. + crs : str, int, or None + Target CRS for reprojection before rasterization. + If ``None``, the file's native CRS is used. + all_touched : bool + If ``True``, all cells touched by a geometry are burned. + If ``False`` (default), only cells whose centre falls inside are burned. + nodata : int or float + Fill value for cells not covered by any geometry. Default: ``0``. + + Returns + ------- + RasterBackend + Backend with one array per requested attribute, shape ``(rows, cols)``. + + Raises + ------ + ValueError + If ``attrs`` is empty or a requested column is not in the GeoDataFrame. + FileNotFoundError + If ``path`` does not exist. + + Examples + -------- + >>> b = shapefile_to_raster_backend( + ... path = "data/mangue_grid.shp", + ... resolution = 100, + ... attrs = ["uso", "alt", "solo"], + ... crs = "EPSG:31984", + ... ) + >>> b.shape + (947, 1003) + >>> b.get("uso").dtype + dtype('int32') + """ + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"File not found: {path}") + + # ── load and reproject ──────────────────────────────────────────────────── + gdf = gpd.read_file(path) + if crs is not None: + gdf = gdf.to_crs(crs) + + # ── resolve attrs ───────────────────────────────────────────────────────── + if isinstance(attrs, list): + attr_defaults: dict[str, Any] = {col: nodata for col in attrs} + else: + attr_defaults = dict(attrs) + + missing = [col for col in attr_defaults if col not in gdf.columns] + if missing: + raise ValueError(f"Columns not found in file: {missing}") + + # ── compute grid dimensions ─────────────────────────────────────────────── + xmin, ymin, xmax, ymax = gdf.total_bounds + cols = int(np.ceil((xmax - xmin) / resolution)) + rows = int(np.ceil((ymax - ymin) / resolution)) + + transform = rasterio.transform.from_bounds( + xmin, ymin, xmax, ymax, cols, rows + ) + + backend = RasterBackend(shape=(rows, cols)) + + # ── rasterize each attribute ────────────────────────────────────────────── + for col, default in attr_defaults.items(): + values = gdf[col] + + # choose dtype from the column + if np.issubdtype(values.dtype, np.integer): + dtype = np.int32 + else: + dtype = 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=(rows, cols), + transform=transform, + fill=default, + all_touched=all_touched, + dtype=dtype, + ) + + backend.set(col, arr) + + return backend + + +def save_raster_backend( + backend: RasterBackend, + path: str | Path, + bands: list[str] | None = None, + crs: str | int | None = None, + transform: rasterio.transform.Affine | None = None, +) -> None: + """ + Save one or more RasterBackend arrays to a multi-band GeoTIFF. + + Parameters + ---------- + backend : RasterBackend + Source backend. + path : str or Path + Output file path (e.g. ``"output/result.tif"``). + bands : list[str] or None + Array names to write. If ``None``, all arrays are written. + crs : str, int, or None + CRS for the output file. If ``None``, the GeoTIFF has no CRS. + transform : Affine or None + Geotransform for the output file. If ``None``, an identity transform + is used (pixel coordinates only). + + Examples + -------- + >>> save_raster_backend(b, "result.tif", bands=["uso", "alt"], + ... crs="EPSG:31984", transform=t) + """ + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + + bands = bands or list(backend.arrays.keys()) + rows, cols = backend.shape + + arrays = [backend.get(b) for b in bands] + dtype = arrays[0].dtype + + if transform is None: + transform = rasterio.transform.from_bounds(0, 0, cols, rows, cols, rows) + + with rasterio.open( + path, "w", + driver="GTiff", + height=rows, width=cols, + count=len(bands), + dtype=dtype, + crs=crs, + transform=transform, + ) as dst: + for i, (name, arr) in enumerate(zip(bands, arrays), start=1): + dst.write(arr.astype(dtype), i) + dst.update_tags(i, name=name) diff --git a/shapefile_to_raster.md b/shapefile_to_raster.md new file mode 100644 index 0000000..6bdc781 --- /dev/null +++ b/shapefile_to_raster.md @@ -0,0 +1,141 @@ +# Loading Shapefiles into the Raster Substrate + +DisSModel can load any vector file (Shapefile, GeoJSON, GeoPackage) and +convert it directly into a `RasterBackend`, making it possible to run +high-performance raster models on real geographic data without any +intermediate GIS steps. + +## How it works + +``` +Shapefile → GeoDataFrame → rasterize → NumPy arrays → RasterBackend → raster model +``` + +The rasterization step (powered by `rasterio.features.rasterize`) is fast +and happens once — after that, the model runs entirely in NumPy at full +vectorized speed. + +!!! note "Grid regularity" + This workflow is most accurate when the input shapefile already contains + a **regular grid** of equal-area polygons (e.g. 100×100m cells), which is + 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. + +--- + +## `shapefile_to_raster_backend` + +```python +from dissmodel.geo.raster.io import shapefile_to_raster_backend + +b = shapefile_to_raster_backend( + path = "data/mangue_grid.shp", + resolution = 100, # 100m cells + attrs = ["uso", "alt", "solo"], + crs = "EPSG:31984", # reproject if needed +) + +print(b.shape) # (rows, cols) derived from bounding box + resolution +print(b.get("uso").dtype) # int32 +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `path` | `str` or `Path` | Path to the vector file | +| `resolution` | `float` | Cell size in CRS units (metres for 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`) | + +To set per-column defaults for uncovered cells, pass a dict: + +```python +b = shapefile_to_raster_backend( + path = "data/mangue_grid.shp", + resolution = 100, + attrs = {"uso": 5, "alt": 0.0, "solo": 1}, + crs = "EPSG:31984", +) +``` + +--- + +## Full example — flood model from shapefile + +```python +from dissmodel.core import Environment +from dissmodel.geo.raster.io import shapefile_to_raster_backend +from dissmodel.visualization.raster_map import RasterMap + +# from coastal_dynamics.raster import FloodRasterModel +# (or your own RasterModel subclass) +from myproject.flood import FloodRasterModel + +# 1. load shapefile → RasterBackend +b = shapefile_to_raster_backend( + path = "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 back to GeoTIFF + +```python +from dissmodel.geo.raster.io import save_raster_backend + +save_raster_backend( + backend = b, + path = "output/flood_result.tif", + bands = ["uso", "alt"], + crs = "EPSG:31984", + transform = transform, # rasterio Affine from original bounds +) +``` + +--- + +## Comparison: vector vs raster workflow from the same shapefile + +```python +# ── vector workflow ─────────────────────────────────────────────────────────── +import geopandas as gpd +from dissmodel.geo.vector.model import SpatialModel + +gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984") +# → GeoDataFrame with real geometries, Queen neighbourhood, ~2 min/step @ 94k cells + +# ── raster workflow ─────────────────────────────────────────────────────────── +from dissmodel.geo.raster.io import shapefile_to_raster_backend + +b = shapefile_to_raster_backend( + "data/mangue_grid.shp", resolution=100, + attrs=["uso", "alt", "solo"], crs="EPSG:31984" +) +# → RasterBackend, same data, ~8 ms/step @ 94k cells (≈ 4,500× faster) +``` + +The data source is the same shapefile. The only difference is the substrate. + +--- + +## API Reference + +::: dissmodel.geo.raster.io.shapefile_to_raster_backend + +::: dissmodel.geo.raster.io.save_raster_backend From 01af766695d85933f9c1caa1b07b55556fddcb80 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 10:34:10 -0300 Subject: [PATCH 02/12] update raster map --- dissmodel/visualization/raster_map.py | 257 ++++++++++++++------------ 1 file changed, 134 insertions(+), 123 deletions(-) diff --git a/dissmodel/visualization/raster_map.py b/dissmodel/visualization/raster_map.py index f065bab..968ef77 100644 --- a/dissmodel/visualization/raster_map.py +++ b/dissmodel/visualization/raster_map.py @@ -4,14 +4,6 @@ Visualization component for RasterBackend — the raster analogue of the vector ``Map`` component in DisSModel. -Responsibility --------------- -Render any named array from a ``RasterBackend`` without any domain -knowledge — no land-use constants, no CRS, no hard-coded colours. - -Visual definitions (colours, labels, colormaps) are injected by the -project at instantiation time. - Supported render targets ------------------------ 1. **Streamlit** — ``plot_area=st.empty()`` @@ -19,41 +11,19 @@ 3. **Interactive** — ``RASTER_MAP_INTERACTIVE=1`` (TkAgg / Qt) 4. **Headless** — saves PNGs to ``raster_map_frames/`` (default) -Minimal usage (headless / no colour map) ------------------------------------------ - from dissmodel.geo.raster.backend import RasterBackend - from dissmodel.visualization.raster_map import RasterMap - from dissmodel.core import Environment - - env = Environment(start_time=1, end_time=10) - YourModel(backend=b) - RasterMap(backend=b, band="state") - env.run() - # → raster_map_frames/state_step_001.png … state_step_010.png - -Categorical usage (domain colour map) ---------------------------------------- - from myproject.constants import LAND_USE_COLORS, LAND_USE_LABELS - - RasterMap( - backend = b, - band = "uso", - title = "Land Use", - color_map = LAND_USE_COLORS, # dict[int, str] value → hex colour - labels = LAND_USE_LABELS, # dict[int, str] value → legend label - ) - -Continuous usage (altimetry, temperature, …) ---------------------------------------------- - RasterMap( - backend = b, - band = "alt", - title = "Altimetry", - cmap = "terrain", - colorbar_label = "Altitude (m)", - mask_band = "uso", # optional: mask cells where uso == mask_value - mask_value = 3, # e.g. SEA = 3 - ) +Usage examples +-------------- + # categorical + RasterMap(backend=b, band="uso", color_map=COLORS, labels=LABELS) + + # continuous — paridade com Map vetorial + RasterMap(backend=b, band="f", cmap="Greens", + scheme="equal_interval", k=5, legend=True) + + # continuous — vmin/vmax explícitos + RasterMap(backend=b, band="alt", cmap="terrain", + vmin=0.0, vmax=100.0, colorbar_label="Altitude (m)", + mask_band="uso", mask_value=3) """ from __future__ import annotations @@ -63,9 +33,9 @@ import matplotlib if os.environ.get("RASTER_MAP_INTERACTIVE", "0") == "1": - pass # let matplotlib choose TkAgg / Qt + pass else: - matplotlib.use("Agg") # headless — no display window + matplotlib.use("Agg") import numpy as np import matplotlib.pyplot as plt @@ -87,86 +57,96 @@ class RasterMap(Model): Parameters ---------- backend : RasterBackend - Backend shared with the simulation models. band : str - Name of the array to visualize. + Array to visualize. title : str Figure title prefix. Default: ``"RasterMap"``. figsize : tuple[int, int] - Figure size in inches. Default: ``(7, 7)``. + Default: ``(7, 7)``. pause : bool Use ``plt.pause()`` in interactive mode. Default: ``True``. interval : float Seconds between steps in interactive mode. Default: ``0.5``. plot_area : st.empty() | None - Streamlit placeholder. Default: ``None``. - - Categorical mode (``color_map`` provided) - ------------------------------------------ - color_map : dict[int, str] | None - Value-to-hex-colour mapping. Example: ``{1: "#006400", 3: "#00008b"}``. - When provided, renders with a ``ListedColormap`` and a legend. - labels : dict[int, str] | None - Value-to-label mapping for the legend. - If ``None`` and ``color_map`` is provided, uses ``str(value)``. - - Continuous mode (``color_map`` absent) + Streamlit placeholder. + + Categorical mode (``color_map`` provided) + ------------------------------------------- + color_map : dict[int, str] + ``{value: "#rrggbb"}`` + labels : dict[int, str] + ``{value: "label"}`` — used in the legend. + + Continuous mode (``color_map`` absent) ---------------------------------------- cmap : str Matplotlib colormap name. Default: ``"viridis"``. - vmin : float | None - Minimum value for the colour scale. Default: array minimum. - vmax : float | None - Maximum value for the colour scale. Default: array maximum. + scheme : str + ``"manual"`` — use ``vmin`` / ``vmax`` (default). + ``"equal_interval"`` — divide [min, max] of valid data into ``k`` classes. + ``"quantiles"`` — p2–p98 of valid data, robust to outliers. + k : int + Number of colour classes for ``scheme="equal_interval"``. + Analogous to ``k`` in the vector Map. Default: ``5``. + vmin, vmax : float | None + Bounds for ``scheme="manual"``. + legend : bool + Show the colorbar. Default: ``True``. colorbar_label : str - Colorbar label. Default: value of ``band``. + Colorbar label. Default: ``band``. mask_band : str | None - Name of another array used to mask cells. + Array used to mask cells. mask_value : int | float | None - Value in ``mask_band`` to mask (e.g. ``SEA=3`` for altimetry). + Value in ``mask_band`` to mask. - Examples - -------- - >>> env = Environment(start_time=1, end_time=10) - >>> RasterMap(backend=b, band="state") - >>> env.run() + Notes + ----- + NaN / Inf cells — including pixels outside the study extent — are + rendered as fully transparent so they never inherit the colormap's + "under" colour. """ def setup( self, backend, - band: str = "state", - title: str = "RasterMap", - figsize: tuple[int, int] = (7, 7), - pause: bool = True, - interval: float = 0.5, - plot_area: Any = None, - # categorical mode + band: str = "state", + title: str = "RasterMap", + figsize: tuple[int, int] = (7, 7), + pause: bool = True, + interval: float = 0.5, + plot_area: Any = None, + # categorical color_map: dict[int, str] | None = None, labels: dict[int, str] | None = None, - # continuous mode - cmap: str = "viridis", - vmin: float | None = None, - vmax: float | None = None, - colorbar_label: str | None = None, - mask_band: str | None = None, + # continuous + cmap: str = "viridis", + scheme: str = "manual", + k: int = 5, + vmin: float | None = None, + vmax: float | None = None, + legend: bool = True, + colorbar_label: str | None = None, + mask_band: str | None = None, mask_value: int | float | None = None, ) -> None: - self.backend = backend - self.band = band - self.title = title - self.figsize = figsize - self.pause = pause - self.interval = interval - self.plot_area = plot_area - self.color_map = color_map - self.labels = labels or {} - self.cmap = cmap - self.vmin = vmin - self.vmax = vmax - self.colorbar_label = colorbar_label or band - self.mask_band = mask_band - self.mask_value = mask_value + self.backend = backend + self.band = band + self.title = title + self.figsize = figsize + self.pause = pause + self.interval = interval + self.plot_area = plot_area + self.color_map = color_map + self.labels = labels or {} + self.cmap = cmap + self.scheme = scheme + self.k = k + self.vmin = vmin + self.vmax = vmax + self.legend = legend + self.colorbar_label = colorbar_label or band + self.mask_band = mask_band + self.mask_value = mask_value # ── rendering ───────────────────────────────────────────────────────────── @@ -187,79 +167,110 @@ def _render(self, step: float) -> matplotlib.figure.Figure: else: self._render_continuous(ax, arr) - ax.set_xticks([]); ax.set_yticks([]) + ax.set_xticks([]) + ax.set_yticks([]) ax.set_title(f"{self.title} [{self.band}] — Step {int(step)}") plt.tight_layout() return fig def _render_categorical(self, ax, arr: np.ndarray) -> None: - """Render integer arrays using a ListedColormap with value-to-colour mapping.""" vals = sorted(self.color_map) - cmap = mcolors.ListedColormap([self.color_map[k] for k in vals]) + cmap = mcolors.ListedColormap([self.color_map[v] for v in vals]) norm = mcolors.BoundaryNorm( [v - 0.5 for v in vals] + [vals[-1] + 0.5], cmap.N ) - ax.imshow(arr, cmap=cmap, norm=norm, aspect="equal", interpolation="nearest") + # NaN → transparent + data = np.ma.masked_invalid(arr.astype(float)) + cmap.set_bad(color="white", alpha=0) + + ax.imshow(data, cmap=cmap, norm=norm, aspect="equal", interpolation="nearest") - # legend shows only values present in the current array - present = set(np.unique(arr)) + present = set(np.unique(arr[~np.isnan(arr.astype(float))])) patches = [ - matplotlib.patches.Patch( - color=self.color_map[k], - label=self.labels.get(k, str(k)), - ) - for k in vals if k in present + matplotlib.patches.Patch(color=self.color_map[v], + label=self.labels.get(v, str(v))) + for v in vals if v in present ] if patches: ax.legend(handles=patches, loc="lower right", fontsize=7, framealpha=0.7) def _render_continuous(self, ax, arr: np.ndarray) -> None: - """Render continuous arrays with a colorbar and optional mask.""" data = arr.astype(float) + # 1. máscara explícita de no-data (ex: células de mar) if self.mask_band is not None and self.mask_value is not None: mask_arr = self.backend.arrays.get(self.mask_band) if mask_arr is not None: - data = np.ma.masked_where(mask_arr == self.mask_value, data) + data = np.where(mask_arr == self.mask_value, np.nan, data) + + # 2. mascara NaN/Inf — cobre pixels fora do extent + data = np.ma.masked_invalid(data) + + # 3. colormap com células mascaradas transparentes + cmap = plt.get_cmap(self.cmap).copy() + cmap.set_bad(color="white", alpha=0) + + # 4. limites da escala de cor + valid = data.compressed() # apenas valores não mascarados + + if len(valid) == 0: + vmin, vmax = 0.0, 1.0 + + elif self.scheme == "equal_interval": + # divide [min, max] em k classes iguais — análogo ao Map vetorial + vmin = float(valid.min()) + vmax = float(valid.max()) + if vmin != vmax and self.k > 1: + bounds = np.linspace(vmin, vmax, self.k + 1) + norm = mcolors.BoundaryNorm(bounds, plt.get_cmap(self.cmap).N) + im = ax.imshow(data, cmap=cmap, norm=norm, aspect="equal", + interpolation="nearest") + if self.legend: + plt.colorbar(im, ax=ax, label=self.colorbar_label, + fraction=0.03, pad=0.02) + return # saída antecipada — norm já aplicado + + elif self.scheme == "quantiles": + vmin = float(np.percentile(valid, 2)) + vmax = float(np.percentile(valid, 98)) + + else: # "manual" + vmin = self.vmin if self.vmin is not None else float(valid.min()) + vmax = self.vmax if self.vmax is not None else float(valid.max()) - vmin = self.vmin if self.vmin is not None else float(np.nanmin(data)) - vmax = self.vmax if self.vmax is not None else float(np.nanmax(data)) if vmin == vmax: vmax = vmin + 1.0 - im = ax.imshow(data, cmap=self.cmap, aspect="equal", + im = ax.imshow(data, cmap=cmap, aspect="equal", interpolation="nearest", vmin=vmin, vmax=vmax) - plt.colorbar(im, ax=ax, label=self.colorbar_label, fraction=0.03, pad=0.02) + if self.legend: + plt.colorbar(im, ax=ax, label=self.colorbar_label, + fraction=0.03, pad=0.02) # ── execute ─────────────────────────────────────────────────────────────── def execute(self) -> None: - """Render the current array state and dispatch to the active output target.""" step = self.env.now() fig = self._render(step) plt.draw() if self.plot_area is not None: - # Streamlit self.plot_area.pyplot(fig) plt.close(fig) elif is_notebook(): - # Jupyter from IPython.display import clear_output, display clear_output(wait=True) display(fig) plt.close(fig) elif self.pause and _is_interactive(): - # interactive window (TkAgg / Qt) plt.pause(self.interval) if step == getattr(self.env, "end_time", step): input("Simulation complete — press Enter to close...") plt.close("all") else: - # headless — save PNG to raster_map_frames/ out_dir = pathlib.Path("raster_map_frames") out_dir.mkdir(exist_ok=True) fname = out_dir / f"{self.band}_step_{int(step):03d}.png" @@ -267,4 +278,4 @@ def execute(self) -> None: facecolor=fig.get_facecolor()) plt.close(fig) if int(step) % 10 == 0 or step == getattr(self.env, "end_time", step): - print(f" RasterMap [{self.band}] step {int(step):3d} → {fname}") \ No newline at end of file + print(f" RasterMap [{self.band}] step {int(step):3d} → {fname}") From 14a3da443df4edf642b4ae6a21081a833b1c4c27 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 11:07:13 -0300 Subject: [PATCH 03/12] update --- dissmodel/geo/raster/backend.py | 78 ++++--- dissmodel/geo/raster/io.py | 312 ++++++++++++++++++-------- dissmodel/visualization/raster_map.py | 114 +++++++--- raster_io.py | 199 ---------------- 4 files changed, 349 insertions(+), 354 deletions(-) delete mode 100644 raster_io.py diff --git a/dissmodel/geo/raster/backend.py b/dissmodel/geo/raster/backend.py index f9436b1..7d5c89d 100644 --- a/dissmodel/geo/raster/backend.py +++ b/dissmodel/geo/raster/backend.py @@ -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), @@ -64,6 +63,11 @@ 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 -------- @@ -71,11 +75,44 @@ class RasterBackend: >>> 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 ────────────────────────────────────────────────────────── @@ -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 @@ -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) @@ -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 @@ -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) @@ -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 ------- @@ -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}])" \ No newline at end of file + return f"RasterBackend(shape={self.shape}, arrays=[{bands}])" diff --git a/dissmodel/geo/raster/io.py b/dissmodel/geo/raster/io.py index 8bc6af0..3bca519 100644 --- a/dissmodel/geo/raster/io.py +++ b/dissmodel/geo/raster/io.py @@ -1,120 +1,246 @@ """ -dissmodel.geo.raster_io -======================= +dissmodel/geo/raster/io.py +=========================== +Utilities for loading external spatial data into RasterBackend. -Generic GeoTIFF read/write utilities for RasterBackend. - -No domain knowledge is included here. - -The meaning of bands is defined by band_spec. - -band_spec +Functions --------- -list of tuples: - - (name, dtype, nodata) - -example: - [ - ("landuse", "int8", -1), - ("elevation", "float32", -9999), - ] +shapefile_to_raster_backend(path, resolution, attrs, crs, all_touched, nodata) + Load a shapefile (or any vector format) and rasterize one or more + attribute columns into a RasterBackend. """ - from __future__ import annotations -import pathlib +from pathlib import Path +from typing import Any + import numpy as np +import geopandas as gpd +import rasterio +import rasterio.features +import rasterio.transform from dissmodel.geo.raster.backend import RasterBackend -try: - import rasterio - HAS_RASTERIO = True -except ImportError: - HAS_RASTERIO = False - - -def load_geotiff( - path: str | pathlib.Path, - band_spec: list[tuple[str, str, float]], -) -> tuple[RasterBackend, dict]: - - if not HAS_RASTERIO: - raise ImportError("rasterio is required") - - path = str(path) - - if path.endswith(".zip"): - import zipfile - with zipfile.ZipFile(path) as z: - tif = next(f for f in z.namelist() if f.endswith(".tif")) - path = f"zip://{path}!{tif}" - - with rasterio.open(path) as ds: - - rows, cols = ds.height, ds.width - backend = RasterBackend((rows, cols)) - - for i, (name, dtype, nodata) in enumerate(band_spec, start=1): - - if i > ds.count: - break - - arr = ds.read(i).astype(dtype) +def shapefile_to_raster_backend( + path: str | Path, + resolution: float, + attrs: list[str] | dict[str, Any], + crs: str | int | None = None, + all_touched: bool = False, + nodata: int | float = 0, + nodata_value: int | float | None = None, + add_mask: bool = True, +) -> RasterBackend: + """ + Load a vector file and rasterize attribute columns into a RasterBackend. + + Reads any format supported by GeoPandas (Shapefile, GeoJSON, GPKG, …), + reprojects to ``crs`` if provided, computes the grid dimensions from the + bounding box and ``resolution``, and rasterizes each requested attribute + column using ``rasterio.features.rasterize``. + + Cells not covered by any geometry receive the ``nodata`` fill value. + If ``add_mask=True`` (default), a boolean ``"mask"`` band is added to the + backend — ``True`` where a cell is covered by at least one geometry, + ``False`` elsewhere. Models can use this band to avoid operating on + cells outside the study area. + + Parameters + ---------- + path : str or Path + Path to the vector file (Shapefile, GeoJSON, GeoPackage, …). + resolution : float + Cell size in the units of the coordinate reference system. + For metric CRS (e.g. EPSG:31984) this is metres. + attrs : list[str] or dict[str, Any] + Column names to rasterize. + + - ``list[str]`` — rasterize each column using its own values. + - ``dict[str, Any]`` — keys are column names, values are fill defaults + used when a cell is not covered by any geometry. + crs : str, int, or None + Target CRS for reprojection before rasterization. + If ``None``, the file's native CRS is used. + all_touched : bool + If ``True``, all cells touched by a geometry are burned. + If ``False`` (default), only cells whose centre falls inside are burned. + nodata : int or float + Fill value for cells not covered by any geometry. Default: ``0``. + nodata_value : int or float or None + If provided, cells where ALL attribute bands equal this value are + treated as nodata in the mask. Useful when the shapefile itself uses + a sentinel (e.g. -9999) for missing data. + add_mask : bool + If ``True`` (default), adds a boolean ``"mask"`` band indicating + valid cells (covered by at least one geometry). + + Returns + ------- + RasterBackend + Backend with one array per requested attribute plus an optional + ``"mask"`` band, shape ``(rows, cols)``. + + Raises + ------ + ValueError + If ``attrs`` is empty or a requested column is not in the GeoDataFrame. + FileNotFoundError + If ``path`` does not exist. + + Examples + -------- + >>> b = shapefile_to_raster_backend( + ... path = "data/mangue_grid.shp", + ... resolution = 100, + ... attrs = ["uso", "alt", "solo"], + ... crs = "EPSG:31984", + ... ) + >>> b.shape + (947, 1003) + >>> b.get("mask").sum() # number of valid cells + 94704 + """ + path = Path(path) if not str(path).startswith("zip://") else path + if isinstance(path, Path) and not path.exists(): + raise FileNotFoundError(f"File not found: {path}") + + # ── load and reproject ──────────────────────────────────────────────────── + gdf = gpd.read_file(str(path)) + if crs is not None: + gdf = gdf.to_crs(crs) + + # ── resolve attrs ───────────────────────────────────────────────────────── + if isinstance(attrs, list): + attr_defaults: dict[str, Any] = {col: nodata for col in attrs} + else: + attr_defaults = dict(attrs) + + missing = [col for col in attr_defaults if col not in gdf.columns] + if missing: + raise ValueError(f"Columns not found in file: {missing}") + + # ── compute grid dimensions ─────────────────────────────────────────────── + xmin, ymin, xmax, ymax = gdf.total_bounds + cols = int(np.ceil((xmax - xmin) / resolution)) + rows = int(np.ceil((ymax - ymin) / resolution)) + + transform = rasterio.transform.from_bounds( + xmin, ymin, xmax, ymax, cols, rows + ) + + backend = RasterBackend(shape=(rows, cols)) + + # ── build geometry mask (valid cells) ───────────────────────────────────── + 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=(rows, cols), + transform=transform, + fill=0, + all_touched=all_touched, + dtype=np.uint8, + ) + mask = coverage.astype(bool) # True = covered by at least one polygon + + if add_mask: + backend.set("mask", mask) + + # ── rasterize each attribute ────────────────────────────────────────────── + for col, default in attr_defaults.items(): + values = gdf[col] + + # choose dtype from the column + if np.issubdtype(values.dtype, np.integer): + dtype = np.int32 + else: + dtype = 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=(rows, cols), + transform=transform, + fill=float(default), + all_touched=all_touched, + dtype=dtype, + ) - if np.all(arr == nodata): - continue + # apply nodata_value sentinel if requested + if nodata_value is not None: + arr = np.where(mask, arr, nodata_value).astype(dtype) + else: + # cells outside polygons keep the fill default — mask is the + # authoritative source for "is this cell valid?" + arr = np.where(mask, arr, default).astype(dtype) - backend.arrays[name] = arr + backend.set(col, arr) - meta = dict( - transform=ds.transform, - crs=ds.crs, - tags=ds.tags(), - ) + n_valid = int(mask.sum()) + n_total = rows * cols + print( + f" rasterized: {n_valid:,} valid cells " + f"/ {n_total:,} total " + f"({100 * n_valid / n_total:.1f}% coverage)" + ) - return backend, meta + return backend -def save_geotiff( +def save_raster_backend( backend: RasterBackend, - path: str | pathlib.Path, - band_spec: list[tuple[str, str, float]], - crs: str, - transform, - compress: str = "deflate", -): - - if not HAS_RASTERIO: - raise ImportError("rasterio is required") - + path: str | Path, + bands: list[str] | None = None, + crs: str | int | None = None, + transform: rasterio.transform.Affine | None = None, +) -> None: + """ + Save one or more RasterBackend arrays to a multi-band GeoTIFF. + + Parameters + ---------- + backend : RasterBackend + Source backend. + path : str or Path + Output file path (e.g. ``"output/result.tif"``). + bands : list[str] or None + Array names to write. If ``None``, all arrays are written. + crs : str, int, or None + CRS for the output file. If ``None``, the GeoTIFF has no CRS. + transform : Affine or None + Geotransform for the output file. If ``None``, an identity transform + is used (pixel coordinates only). + + Examples + -------- + >>> save_raster_backend(b, "result.tif", bands=["uso", "alt"], + ... crs="EPSG:31984", transform=t) + """ + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + + bands = bands or list(backend.arrays.keys()) rows, cols = backend.shape - arrays = [] - - for name, dtype, nodata in band_spec: + arrays = [backend.get(b) for b in bands] + dtype = arrays[0].dtype - arr = backend.arrays.get( - name, - np.full((rows, cols), nodata, dtype=dtype) - ) - - arrays.append(arr.astype(dtype)) + if transform is None: + transform = rasterio.transform.from_bounds(0, 0, cols, rows, cols, rows) with rasterio.open( - path, - "w", + path, "w", driver="GTiff", - height=rows, - width=cols, - count=len(arrays), - dtype=str(arrays[0].dtype), + height=rows, width=cols, + count=len(bands), + dtype=dtype, crs=crs, transform=transform, - compress=compress, ) as dst: - - for i, arr in enumerate(arrays, start=1): - dst.write(arr, i) \ No newline at end of file + for i, (name, arr) in enumerate(zip(bands, arrays), start=1): + dst.write(arr.astype(dtype), i) + dst.update_tags(i, name=name) \ No newline at end of file diff --git a/dissmodel/visualization/raster_map.py b/dissmodel/visualization/raster_map.py index 968ef77..e40aef3 100644 --- a/dissmodel/visualization/raster_map.py +++ b/dissmodel/visualization/raster_map.py @@ -17,13 +17,27 @@ RasterMap(backend=b, band="uso", color_map=COLORS, labels=LABELS) # continuous — paridade com Map vetorial + # auto_mask=True (default) aplica o extent mask do backend automaticamente RasterMap(backend=b, band="f", cmap="Greens", scheme="equal_interval", k=5, legend=True) - # continuous — vmin/vmax explícitos + # continuous — vmin/vmax explícitos + mask de domínio (ex: mar na altimetria) RasterMap(backend=b, band="alt", cmap="terrain", vmin=0.0, vmax=100.0, colorbar_label="Altitude (m)", mask_band="uso", mask_value=3) + + # desligar auto_mask se quiser o comportamento pré-v2 + RasterMap(backend=b, band="f", auto_mask=False) + +Extent mask automático +----------------------- +Quando ``auto_mask=True`` (default), o RasterMap consulta +``backend.nodata_mask`` antes de renderizar. Células fora do extent +ficam transparentes sem nenhuma configuração adicional. + +O ``backend.nodata_mask`` é derivado automaticamente pelo RasterBackend: + - se existe a banda ``"mask"``: True onde mask != 0 + - senão, usa ``nodata_value`` do backend sobre o primeiro array disponível """ from __future__ import annotations @@ -50,6 +64,37 @@ def _is_interactive() -> bool: return matplotlib.get_backend().lower() not in ("agg", "cairo", "svg", "pdf", "ps") +def _get_nodata_mask(backend) -> np.ndarray | None: + """ + Deriva a extent mask do backend sem exigir que o backend implemente + um atributo específico — funciona com qualquer RasterBackend existente. + + Prioridade: + 1. backend.nodata_mask — se o backend já expõe a propriedade + 2. arrays["mask"] — convenção dissluc (mask != 0 = válido) + 3. nodata_value — aplica sobre o primeiro array disponível + """ + # 1. propriedade nativa (futura) + if hasattr(backend, "nodata_mask"): + mask = backend.nodata_mask + if mask is not None: + return mask + + arrays = getattr(backend, "arrays", {}) + + # 2. banda "mask" — convenção dissluc + if "mask" in arrays: + return arrays["mask"] != 0 + + # 3. nodata_value sobre o primeiro array + nodata = getattr(backend, "nodata_value", None) + if nodata is not None and arrays: + first = next(iter(arrays.values())) + return first != nodata + + return None + + class RasterMap(Model): """ Visualization model for RasterBackend. @@ -69,6 +114,10 @@ class RasterMap(Model): Seconds between steps in interactive mode. Default: ``0.5``. plot_area : st.empty() | None Streamlit placeholder. + auto_mask : bool + Apply the backend's extent mask automatically so pixels outside + the study area are transparent. Default: ``True``. + Set to ``False`` to restore the pre-v2 behaviour. Categorical mode (``color_map`` provided) ------------------------------------------- @@ -86,8 +135,7 @@ class RasterMap(Model): ``"equal_interval"`` — divide [min, max] of valid data into ``k`` classes. ``"quantiles"`` — p2–p98 of valid data, robust to outliers. k : int - Number of colour classes for ``scheme="equal_interval"``. - Analogous to ``k`` in the vector Map. Default: ``5``. + Number of colour classes for ``scheme="equal_interval"``. Default: ``5``. vmin, vmax : float | None Bounds for ``scheme="manual"``. legend : bool @@ -95,15 +143,10 @@ class RasterMap(Model): colorbar_label : str Colorbar label. Default: ``band``. mask_band : str | None - Array used to mask cells. + Additional domain mask (e.g. mask sea cells for altimetry). + Applied on top of the automatic extent mask. mask_value : int | float | None Value in ``mask_band`` to mask. - - Notes - ----- - NaN / Inf cells — including pixels outside the study extent — are - rendered as fully transparent so they never inherit the colormap's - "under" colour. """ def setup( @@ -115,6 +158,7 @@ def setup( pause: bool = True, interval: float = 0.5, plot_area: Any = None, + auto_mask: bool = True, # categorical color_map: dict[int, str] | None = None, labels: dict[int, str] | None = None, @@ -136,6 +180,7 @@ def setup( self.pause = pause self.interval = interval self.plot_area = plot_area + self.auto_mask = auto_mask self.color_map = color_map self.labels = labels or {} self.cmap = cmap @@ -148,6 +193,11 @@ def setup( self.mask_band = mask_band self.mask_value = mask_value + # resolve extent mask once at setup — not on every frame + self._extent_mask: np.ndarray | None = ( + _get_nodata_mask(backend) if auto_mask else None + ) + # ── rendering ───────────────────────────────────────────────────────────── def _render(self, step: float) -> matplotlib.figure.Figure: @@ -173,15 +223,34 @@ def _render(self, step: float) -> matplotlib.figure.Figure: plt.tight_layout() return fig + def _apply_masks(self, data: np.ndarray) -> np.ma.MaskedArray: + """ + Aplica todas as máscaras em ordem e retorna um MaskedArray: + 1. extent mask automático (auto_mask=True) + 2. mask_band / mask_value (domínio, ex: mar para altimetria) + 3. NaN / Inf residuais + """ + # 1. extent mask — pixels fora do estudo + if self._extent_mask is not None: + data = np.where(self._extent_mask, data, np.nan) + + # 2. domain mask — ex: mascarar mar na visualização de altimetria + if self.mask_band is not None and self.mask_value is not None: + mask_arr = self.backend.arrays.get(self.mask_band) + if mask_arr is not None: + data = np.where(mask_arr == self.mask_value, np.nan, data) + + # 3. cobre NaN / Inf (inclui os inseridos pelos passos acima) + return np.ma.masked_invalid(data) + def _render_categorical(self, ax, arr: np.ndarray) -> None: vals = sorted(self.color_map) cmap = mcolors.ListedColormap([self.color_map[v] for v in vals]) norm = mcolors.BoundaryNorm( [v - 0.5 for v in vals] + [vals[-1] + 0.5], cmap.N ) - # NaN → transparent - data = np.ma.masked_invalid(arr.astype(float)) cmap.set_bad(color="white", alpha=0) + data = self._apply_masks(arr.astype(float)) ax.imshow(data, cmap=cmap, norm=norm, aspect="equal", interpolation="nearest") @@ -195,29 +264,16 @@ def _render_categorical(self, ax, arr: np.ndarray) -> None: ax.legend(handles=patches, loc="lower right", fontsize=7, framealpha=0.7) def _render_continuous(self, ax, arr: np.ndarray) -> None: - data = arr.astype(float) - - # 1. máscara explícita de no-data (ex: células de mar) - if self.mask_band is not None and self.mask_value is not None: - mask_arr = self.backend.arrays.get(self.mask_band) - if mask_arr is not None: - data = np.where(mask_arr == self.mask_value, np.nan, data) - - # 2. mascara NaN/Inf — cobre pixels fora do extent - data = np.ma.masked_invalid(data) - - # 3. colormap com células mascaradas transparentes - cmap = plt.get_cmap(self.cmap).copy() + data = self._apply_masks(arr.astype(float)) + cmap = plt.get_cmap(self.cmap).copy() cmap.set_bad(color="white", alpha=0) - # 4. limites da escala de cor - valid = data.compressed() # apenas valores não mascarados + valid = data.compressed() if len(valid) == 0: vmin, vmax = 0.0, 1.0 elif self.scheme == "equal_interval": - # divide [min, max] em k classes iguais — análogo ao Map vetorial vmin = float(valid.min()) vmax = float(valid.max()) if vmin != vmax and self.k > 1: @@ -228,7 +284,7 @@ def _render_continuous(self, ax, arr: np.ndarray) -> None: if self.legend: plt.colorbar(im, ax=ax, label=self.colorbar_label, fraction=0.03, pad=0.02) - return # saída antecipada — norm já aplicado + return elif self.scheme == "quantiles": vmin = float(np.percentile(valid, 2)) diff --git a/raster_io.py b/raster_io.py deleted file mode 100644 index c547f6b..0000000 --- a/raster_io.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -dissmodel/geo/raster/io.py -=========================== -Utilities for loading external spatial data into RasterBackend. - -Functions ---------- -shapefile_to_raster_backend(path, resolution, attrs, crs, all_touched, nodata) - Load a shapefile (or any vector format) and rasterize one or more - attribute columns into a RasterBackend. -""" -from __future__ import annotations - -from pathlib import Path -from typing import Any - -import numpy as np -import geopandas as gpd -import rasterio -import rasterio.features -import rasterio.transform - -from dissmodel.geo.raster.backend import RasterBackend - - -def shapefile_to_raster_backend( - path: str | Path, - resolution: float, - attrs: list[str] | dict[str, Any], - crs: str | int | None = None, - all_touched: bool = False, - nodata: int | float = 0, -) -> RasterBackend: - """ - Load a vector file and rasterize attribute columns into a RasterBackend. - - Reads any format supported by GeoPandas (Shapefile, GeoJSON, GPKG, …), - reprojects to ``crs`` if provided, computes the grid dimensions from the - bounding box and ``resolution``, and rasterizes each requested attribute - column using ``rasterio.features.rasterize``. - - Parameters - ---------- - path : str or Path - Path to the vector file (Shapefile, GeoJSON, GeoPackage, …). - resolution : float - Cell size in the units of the coordinate reference system. - For metric CRS (e.g. EPSG:31984) this is metres. - attrs : list[str] or dict[str, Any] - Column names to rasterize. - - - ``list[str]`` — rasterize each column using its own values. - - ``dict[str, Any]`` — keys are column names, values are fill defaults - used when a cell is not covered by any geometry. - crs : str, int, or None - Target CRS for reprojection before rasterization. - If ``None``, the file's native CRS is used. - all_touched : bool - If ``True``, all cells touched by a geometry are burned. - If ``False`` (default), only cells whose centre falls inside are burned. - nodata : int or float - Fill value for cells not covered by any geometry. Default: ``0``. - - Returns - ------- - RasterBackend - Backend with one array per requested attribute, shape ``(rows, cols)``. - - Raises - ------ - ValueError - If ``attrs`` is empty or a requested column is not in the GeoDataFrame. - FileNotFoundError - If ``path`` does not exist. - - Examples - -------- - >>> b = shapefile_to_raster_backend( - ... path = "data/mangue_grid.shp", - ... resolution = 100, - ... attrs = ["uso", "alt", "solo"], - ... crs = "EPSG:31984", - ... ) - >>> b.shape - (947, 1003) - >>> b.get("uso").dtype - dtype('int32') - """ - path = Path(path) - if not path.exists(): - raise FileNotFoundError(f"File not found: {path}") - - # ── load and reproject ──────────────────────────────────────────────────── - gdf = gpd.read_file(path) - if crs is not None: - gdf = gdf.to_crs(crs) - - # ── resolve attrs ───────────────────────────────────────────────────────── - if isinstance(attrs, list): - attr_defaults: dict[str, Any] = {col: nodata for col in attrs} - else: - attr_defaults = dict(attrs) - - missing = [col for col in attr_defaults if col not in gdf.columns] - if missing: - raise ValueError(f"Columns not found in file: {missing}") - - # ── compute grid dimensions ─────────────────────────────────────────────── - xmin, ymin, xmax, ymax = gdf.total_bounds - cols = int(np.ceil((xmax - xmin) / resolution)) - rows = int(np.ceil((ymax - ymin) / resolution)) - - transform = rasterio.transform.from_bounds( - xmin, ymin, xmax, ymax, cols, rows - ) - - backend = RasterBackend(shape=(rows, cols)) - - # ── rasterize each attribute ────────────────────────────────────────────── - for col, default in attr_defaults.items(): - values = gdf[col] - - # choose dtype from the column - if np.issubdtype(values.dtype, np.integer): - dtype = np.int32 - else: - dtype = 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=(rows, cols), - transform=transform, - fill=default, - all_touched=all_touched, - dtype=dtype, - ) - - backend.set(col, arr) - - return backend - - -def save_raster_backend( - backend: RasterBackend, - path: str | Path, - bands: list[str] | None = None, - crs: str | int | None = None, - transform: rasterio.transform.Affine | None = None, -) -> None: - """ - Save one or more RasterBackend arrays to a multi-band GeoTIFF. - - Parameters - ---------- - backend : RasterBackend - Source backend. - path : str or Path - Output file path (e.g. ``"output/result.tif"``). - bands : list[str] or None - Array names to write. If ``None``, all arrays are written. - crs : str, int, or None - CRS for the output file. If ``None``, the GeoTIFF has no CRS. - transform : Affine or None - Geotransform for the output file. If ``None``, an identity transform - is used (pixel coordinates only). - - Examples - -------- - >>> save_raster_backend(b, "result.tif", bands=["uso", "alt"], - ... crs="EPSG:31984", transform=t) - """ - path = Path(path) - path.parent.mkdir(parents=True, exist_ok=True) - - bands = bands or list(backend.arrays.keys()) - rows, cols = backend.shape - - arrays = [backend.get(b) for b in bands] - dtype = arrays[0].dtype - - if transform is None: - transform = rasterio.transform.from_bounds(0, 0, cols, rows, cols, rows) - - with rasterio.open( - path, "w", - driver="GTiff", - height=rows, width=cols, - count=len(bands), - dtype=dtype, - crs=crs, - transform=transform, - ) as dst: - for i, (name, arr) in enumerate(zip(bands, arrays), start=1): - dst.write(arr.astype(dtype), i) - dst.update_tags(i, name=name) From dc851c41c2dd904aa3f50aec55fa6d4e27e93a3f Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 15:00:30 -0300 Subject: [PATCH 04/12] update io --- dissmodel/geo/raster/io.py | 375 ++++++++++++++++++++++++++++--------- 1 file changed, 287 insertions(+), 88 deletions(-) diff --git a/dissmodel/geo/raster/io.py b/dissmodel/geo/raster/io.py index 3bca519..36dbdd9 100644 --- a/dissmodel/geo/raster/io.py +++ b/dissmodel/geo/raster/io.py @@ -1,30 +1,59 @@ """ dissmodel/geo/raster/io.py =========================== -Utilities for loading external spatial data into RasterBackend. +I/O utilities for RasterBackend — load from vector files and GeoTIFFs, +save back to GeoTIFF. -Functions ---------- -shapefile_to_raster_backend(path, resolution, attrs, crs, all_touched, nodata) - Load a shapefile (or any vector format) and rasterize one or more - attribute columns into a RasterBackend. +Public API +---------- +shapefile_to_raster_backend(path, resolution, attrs, ...) + Rasterize a vector file (Shapefile, GeoJSON, GPKG, …) into a + RasterBackend. Each attribute column becomes a named array. + +load_geotiff(path, band_spec) + Read a GeoTIFF (plain or zipped) into a RasterBackend. + +save_geotiff(backend, path, band_spec, crs, transform) + Write selected RasterBackend arrays to a multi-band GeoTIFF. + +save_raster_backend(backend, path, bands, crs, transform) + Convenience wrapper — writes all (or selected) arrays without + requiring a band_spec. + +Notes +----- +All functions are domain-agnostic: no land-use classes, no CRS +assumptions, no project-specific constants. """ from __future__ import annotations -from pathlib import Path +import pathlib +import zipfile from typing import Any import numpy as np -import geopandas as gpd -import rasterio -import rasterio.features -import rasterio.transform from dissmodel.geo.raster.backend import RasterBackend +try: + import rasterio + import rasterio.features + import rasterio.transform + HAS_RASTERIO = True +except ImportError: + HAS_RASTERIO = False + +try: + import geopandas as gpd + HAS_GEOPANDAS = True +except ImportError: + HAS_GEOPANDAS = False + + +# ── vector → RasterBackend ──────────────────────────────────────────────────── def shapefile_to_raster_backend( - path: str | Path, + path: str | pathlib.Path, resolution: float, attrs: list[str] | dict[str, Any], crs: str | int | None = None, @@ -37,57 +66,60 @@ def shapefile_to_raster_backend( Load a vector file and rasterize attribute columns into a RasterBackend. Reads any format supported by GeoPandas (Shapefile, GeoJSON, GPKG, …), - reprojects to ``crs`` if provided, computes the grid dimensions from the - bounding box and ``resolution``, and rasterizes each requested attribute - column using ``rasterio.features.rasterize``. + optionally reprojects to ``crs``, derives the grid shape from the bounding + box and ``resolution``, and rasterizes each requested attribute column + with ``rasterio.features.rasterize``. Cells not covered by any geometry receive the ``nodata`` fill value. - If ``add_mask=True`` (default), a boolean ``"mask"`` band is added to the - backend — ``True`` where a cell is covered by at least one geometry, - ``False`` elsewhere. Models can use this band to avoid operating on - cells outside the study area. + When ``add_mask=True`` (default), a ``"mask"`` band is added to the + backend — ``1`` where a cell is covered by at least one geometry, ``0`` + elsewhere. Models use this band to skip cells outside the study area. Parameters ---------- path : str or Path - Path to the vector file (Shapefile, GeoJSON, GeoPackage, …). + Vector file path. Accepts Shapefile, GeoJSON, GeoPackage, or a + ``.zip`` archive containing any of these formats. resolution : float - Cell size in the units of the coordinate reference system. - For metric CRS (e.g. EPSG:31984) this is metres. + Cell size in the units of the CRS (metres for metric CRS). attrs : list[str] or dict[str, Any] - Column names to rasterize. + Columns to rasterize. - - ``list[str]`` — rasterize each column using its own values. - - ``dict[str, Any]`` — keys are column names, values are fill defaults - used when a cell is not covered by any geometry. + - ``list[str]`` — rasterize each column with ``nodata`` as fill. + - ``dict[str, Any]`` — keys are column names, values are per-column + fill defaults for cells outside the geometries. crs : str, int, or None - Target CRS for reprojection before rasterization. + Target CRS for reprojection before rasterization (e.g. ``"EPSG:31984"``). If ``None``, the file's native CRS is used. all_touched : bool - If ``True``, all cells touched by a geometry are burned. - If ``False`` (default), only cells whose centre falls inside are burned. + If ``True``, burn all cells touched by a geometry. + If ``False`` (default), burn only cells whose centre falls inside. nodata : int or float - Fill value for cells not covered by any geometry. Default: ``0``. + Default fill value for cells outside geometries. Default: ``0``. nodata_value : int or float or None - If provided, cells where ALL attribute bands equal this value are - treated as nodata in the mask. Useful when the shapefile itself uses - a sentinel (e.g. -9999) for missing data. + When provided, cells outside geometries are set to this sentinel + value instead of ``nodata``. Useful to distinguish "outside extent" + from "valid zero" (e.g. ``nodata_value=-1`` for proportion arrays + where ``0.0`` is a legitimate value). Default: ``None``. add_mask : bool - If ``True`` (default), adds a boolean ``"mask"`` band indicating - valid cells (covered by at least one geometry). + If ``True`` (default), adds a ``"mask"`` band (``float32``, values + ``0.0`` / ``1.0``) marking valid cells. Returns ------- RasterBackend - Backend with one array per requested attribute plus an optional - ``"mask"`` band, shape ``(rows, cols)``. + Backend with one array per requested attribute, plus an optional + ``"mask"`` band. Shape is ``(rows, cols)`` derived from the bounding + box and ``resolution``. Raises ------ - ValueError - If ``attrs`` is empty or a requested column is not in the GeoDataFrame. + ImportError + If ``geopandas`` or ``rasterio`` are not installed. FileNotFoundError If ``path`` does not exist. + ValueError + If ``attrs`` is empty or a requested column is not in the file. Examples -------- @@ -99,59 +131,70 @@ def shapefile_to_raster_backend( ... ) >>> b.shape (947, 1003) - >>> b.get("mask").sum() # number of valid cells + >>> b.get("mask").sum() 94704 """ - path = Path(path) if not str(path).startswith("zip://") else path - if isinstance(path, Path) and not path.exists(): + if not HAS_RASTERIO: + raise ImportError("rasterio is required — pip install rasterio") + if not HAS_GEOPANDAS: + raise ImportError("geopandas is required — pip install geopandas") + + path = pathlib.Path(path) if not str(path).startswith("zip://") else path + if isinstance(path, pathlib.Path) and not path.exists(): raise FileNotFoundError(f"File not found: {path}") - # ── load and reproject ──────────────────────────────────────────────────── + # ── load and optionally reproject ───────────────────────────────────────── gdf = gpd.read_file(str(path)) if crs is not None: gdf = gdf.to_crs(crs) - # ── resolve attrs ───────────────────────────────────────────────────────── + # ── 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 file: {missing}") - # ── compute grid dimensions ─────────────────────────────────────────────── + # ── compute grid shape from bounding box ────────────────────────────────── xmin, ymin, xmax, ymax = gdf.total_bounds - cols = int(np.ceil((xmax - xmin) / resolution)) - rows = int(np.ceil((ymax - ymin) / resolution)) + 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, cols, rows + xmin, ymin, xmax, ymax, n_cols, n_rows ) - backend = RasterBackend(shape=(rows, cols)) + backend = RasterBackend( + shape=(n_rows, n_cols), + nodata_value=nodata_value, # stored for nodata_mask property + ) - # ── build geometry mask (valid cells) ───────────────────────────────────── + # ── 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=(rows, cols), + out_shape=(n_rows, n_cols), transform=transform, fill=0, all_touched=all_touched, dtype=np.uint8, ) - mask = coverage.astype(bool) # True = covered by at least one polygon + mask = coverage.astype(bool) # True = cell covered by at least one polygon if add_mask: - backend.set("mask", mask) + backend.set("mask", mask.astype(np.float32)) - # ── rasterize each attribute ────────────────────────────────────────────── + # ── rasterize each attribute column ─────────────────────────────────────── for col, default in attr_defaults.items(): values = gdf[col] - # choose dtype from the column + # preserve integer dtypes when possible if np.issubdtype(values.dtype, np.integer): dtype = np.int32 else: @@ -163,64 +206,219 @@ def shapefile_to_raster_backend( for geom, val in zip(gdf.geometry, values) if geom is not None ), - out_shape=(rows, cols), + out_shape=(n_rows, n_cols), transform=transform, fill=float(default), all_touched=all_touched, dtype=dtype, ) - # apply nodata_value sentinel if requested - if nodata_value is not None: - arr = np.where(mask, arr, nodata_value).astype(dtype) - else: - # cells outside polygons keep the fill default — mask is the - # authoritative source for "is this cell valid?" - arr = np.where(mask, arr, default).astype(dtype) + # apply sentinel for out-of-extent cells if requested + 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 = rows * cols + 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)" + f" rasterized: {n_valid:,} valid cells" + f" / {n_total:,} total" + f" ({100 * n_valid / n_total:.1f}% coverage)" ) return backend +# ── GeoTIFF → RasterBackend ─────────────────────────────────────────────────── + +def load_geotiff( + path: str | pathlib.Path, + band_spec: list[tuple[str, str, float]], +) -> tuple[RasterBackend, dict]: + """ + Read a GeoTIFF into a RasterBackend. + + Supports plain ``.tif`` files and ``.zip`` archives containing a single + GeoTIFF. Bands are mapped to named arrays in the backend using + ``band_spec``. + + Parameters + ---------- + path : str or Path + Path to a ``.tif`` or ``.zip`` file. + band_spec : list of (name, dtype, nodata) + Mapping from band index (1-based) to array name and dtype. + Bands whose data equals ``nodata`` everywhere are skipped. + + Example:: + + [ + ("uso", "int8", -1), + ("alt", "float32", -9999.0), + ("solo", "int8", -1), + ] + + Returns + ------- + (RasterBackend, dict) + The backend with one array per non-empty band, and a metadata dict + with keys ``"transform"``, ``"crs"``, and ``"tags"``. + + Raises + ------ + ImportError + If ``rasterio`` is not installed. + """ + if not HAS_RASTERIO: + raise ImportError("rasterio is required — pip install rasterio") + + path_str = str(path) + + # unwrap zip archives + if path_str.endswith(".zip"): + with zipfile.ZipFile(path_str) as z: + tif_name = next(f for f in z.namelist() if f.endswith(".tif")) + path_str = f"zip://{path_str}!{tif_name}" + + with rasterio.open(path_str) as ds: + rows, cols = ds.height, ds.width + backend = RasterBackend(shape=(rows, cols)) + + for i, (name, dtype, nodata) in enumerate(band_spec, start=1): + if i > ds.count: + break + + arr = ds.read(i).astype(dtype) + + # skip bands that are entirely nodata (e.g. uninitialised saves) + if np.all(arr == nodata): + continue + + backend.arrays[name] = arr + + meta = { + "transform": ds.transform, + "crs": ds.crs, + "tags": ds.tags(), + } + + return backend, meta + + +# ── RasterBackend → GeoTIFF ─────────────────────────────────────────────────── + +def save_geotiff( + backend: RasterBackend, + path: str | pathlib.Path, + band_spec: list[tuple[str, str, float]], + crs: str | None = None, + transform=None, + compress: str = "deflate", +) -> None: + """ + Write selected RasterBackend arrays to a multi-band GeoTIFF. + + Parameters + ---------- + backend : RasterBackend + Source backend. + path : str or Path + Output file path. Parent directories are created if needed. + band_spec : list of (name, dtype, nodata) + Bands to write, in order. Arrays missing from the backend are + written as constant ``nodata`` fills. + + Example:: + + [ + ("uso", "int8", -1), + ("alt", "float32", -9999.0), + ] + + crs : str or None + CRS string for the output file (e.g. ``"EPSG:31984"``). + If ``None``, the GeoTIFF is written without a CRS. + transform : Affine or None + Affine geotransform. If ``None``, a pixel-coordinate identity + transform is used. + compress : str + Compression algorithm. Default: ``"deflate"``. + + Raises + ------ + ImportError + If ``rasterio`` is not installed. + """ + if not HAS_RASTERIO: + raise ImportError("rasterio is required — pip install rasterio") + + path = pathlib.Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + + rows, cols = backend.shape + + # build arrays — fill missing bands with nodata + arrays = [] + for name, dtype, nodata in band_spec: + arr = backend.arrays.get( + name, + np.full((rows, cols), nodata, dtype=dtype), + ) + arrays.append(arr.astype(dtype)) + + if transform is None: + transform = rasterio.transform.from_bounds(0, 0, cols, rows, cols, rows) + + with rasterio.open( + path, "w", + driver = "GTiff", + height = rows, + width = cols, + count = len(arrays), + dtype = str(arrays[0].dtype), + crs = crs, + transform= transform, + compress = compress, + ) as dst: + for i, (arr, (name, _, _)) in enumerate(zip(arrays, band_spec), start=1): + dst.write(arr, i) + dst.update_tags(i, name=name) + + def save_raster_backend( backend: RasterBackend, - path: str | Path, + path: str | pathlib.Path, bands: list[str] | None = None, crs: str | int | None = None, - transform: rasterio.transform.Affine | None = None, + transform=None, ) -> None: """ - Save one or more RasterBackend arrays to a multi-band GeoTIFF. + Convenience wrapper: write all (or selected) arrays to a GeoTIFF + without requiring a band_spec. + + dtype and nodata are inferred from each array. Use ``save_geotiff`` + when you need explicit dtype control or nodata sentinels. Parameters ---------- backend : RasterBackend - Source backend. path : str or Path - Output file path (e.g. ``"output/result.tif"``). bands : list[str] or None - Array names to write. If ``None``, all arrays are written. + Arrays to write. If ``None``, all arrays in the backend are written + in insertion order. crs : str, int, or None - CRS for the output file. If ``None``, the GeoTIFF has no CRS. transform : Affine or None - Geotransform for the output file. If ``None``, an identity transform - is used (pixel coordinates only). - Examples - -------- - >>> save_raster_backend(b, "result.tif", bands=["uso", "alt"], - ... crs="EPSG:31984", transform=t) + Raises + ------ + ImportError + If ``rasterio`` is not installed. """ - path = Path(path) + if not HAS_RASTERIO: + raise ImportError("rasterio is required — pip install rasterio") + + path = pathlib.Path(path) path.parent.mkdir(parents=True, exist_ok=True) bands = bands or list(backend.arrays.keys()) @@ -234,13 +432,14 @@ def save_raster_backend( with rasterio.open( path, "w", - driver="GTiff", - height=rows, width=cols, - count=len(bands), - dtype=dtype, - crs=crs, - transform=transform, + driver = "GTiff", + height = rows, + width = cols, + count = len(arrays), + dtype = dtype, + crs = crs, + transform= transform, ) as dst: for i, (name, arr) in enumerate(zip(bands, arrays), start=1): dst.write(arr.astype(dtype), i) - dst.update_tags(i, name=name) \ No newline at end of file + dst.update_tags(i, name=name) From 02c1767098ad85ed2230b19228d0f850eb4cdd71 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 15:18:39 -0300 Subject: [PATCH 05/12] map headless --- dissmodel/visualization/map.py | 217 ++++++++++++++++++++------------- 1 file changed, 133 insertions(+), 84 deletions(-) diff --git a/dissmodel/visualization/map.py b/dissmodel/visualization/map.py index 3901913..e8f6f7e 100644 --- a/dissmodel/visualization/map.py +++ b/dissmodel/visualization/map.py @@ -1,7 +1,51 @@ +""" +dissmodel/visualization/map.py +================================ +Visualization component for GeoDataFrame — choropleth map rendered at +every simulation step. + +Supported render targets +------------------------ +1. **Streamlit** — ``plot_area=st.empty()`` +2. **Jupyter** — detected automatically +3. **Interactive** — matplotlib window (TkAgg / Qt) +4. **Headless** — saves PNGs to ``map_frames/`` (default fallback) + +The headless fallback means the component never raises ``RuntimeError`` +in CI or server environments — it simply writes one PNG per step. + +Usage examples +-------------- + # basic + Map(gdf=grid, plot_params={"column": "state", "cmap": "viridis"}) + + # with legend and classification scheme + Map(gdf=grid, plot_params={ + "column": "f", + "cmap": "Greens", + "scheme": "equal_interval", + "k": 5, + "legend": True, + }) + + # Streamlit + plot_area = st.empty() + Map(gdf=grid, plot_params={"column": "uso"}, plot_area=plot_area) + + # force frame saving even in interactive mode + Map(gdf=grid, plot_params={"column": "uso"}, save_frames=True) + +Notes +----- +Analogous to ``RasterMap`` for vector data. Both components share the +same rendering targets and the same ``save_frames`` / headless behaviour. +""" from __future__ import annotations +import pathlib from typing import Any +import matplotlib import matplotlib.pyplot as plt import matplotlib.figure import matplotlib.axes @@ -13,27 +57,39 @@ class Map(Model): """ - Simulation model that renders a live choropleth map. + Simulation model that renders a live choropleth map of a GeoDataFrame. Extends :class:`~dissmodel.core.Model` and redraws the map at every - time step. Supports three rendering targets: - - - **Streamlit** — pass a ``plot_area`` (``st.empty()``). - - **Jupyter** — detected automatically via :func:`is_notebook`. - - **Matplotlib window** — fallback for plain Python scripts. + simulation step. Parameters ---------- gdf : geopandas.GeoDataFrame - GeoDataFrame to render. + GeoDataFrame to render. Updated in-place by the simulation models + sharing the same reference. plot_params : dict Keyword arguments forwarded to :meth:`GeoDataFrame.plot` - (e.g. ``column``, ``cmap``, ``legend``). - pause : bool, optional - If ``True``, call ``plt.pause()`` after each update, by default - ``True``. Required for live updates outside notebooks. - plot_area : any, optional - Streamlit ``st.empty()`` placeholder, by default ``None``. + (e.g. ``column``, ``cmap``, ``scheme``, ``legend``). + figsize : tuple[int, int] + Figure size in inches. Default: ``(10, 6)``. + pause : bool + Call ``plt.pause()`` after each update in interactive mode. + Default: ``True``. + interval : float + Seconds passed to ``plt.pause()``. Default: ``0.01``. + plot_area : st.empty() | None + Streamlit placeholder. Default: ``None``. + save_frames : bool + If ``True``, save one PNG per step to ``map_frames/`` regardless + of the rendering environment. Default: ``False``. + + Notes + ----- + **Headless / CI behaviour** — when no interactive backend is available + and ``plot_area`` is ``None`` and the code is not running in a notebook, + the component automatically saves PNGs to ``map_frames/`` instead of + raising an error. This makes it safe to use in CI pipelines and remote + servers without a display. Examples -------- @@ -43,94 +99,87 @@ class Map(Model): """ fig: matplotlib.figure.Figure - ax: matplotlib.axes.Axes - gdf: gpd.GeoDataFrame - plot_params: dict[str, Any] - pause: bool - plot_area: Any + ax: matplotlib.axes.Axes def setup( self, - gdf: gpd.GeoDataFrame, + gdf: gpd.GeoDataFrame, plot_params: dict[str, Any], - pause: bool = True, - plot_area: Any = None, + figsize: tuple[int, int] = (10, 6), + pause: bool = True, + interval: float = 0.01, + plot_area: Any = None, + save_frames: bool = False, ) -> None: - """ - Configure the map. - - Called automatically by salabim during component initialisation. - - Parameters - ---------- - gdf : geopandas.GeoDataFrame - GeoDataFrame to render. - plot_params : dict - Keyword arguments forwarded to :meth:`GeoDataFrame.plot` - (e.g. ``column``, ``cmap``, ``legend``). - pause : bool, optional - If ``True``, call ``plt.pause()`` after each update, - by default ``True``. - plot_area : any, optional - Streamlit ``st.empty()`` placeholder, by default ``None``. - """ - self.gdf = gdf + self.gdf = gdf self.plot_params = plot_params - self.pause = pause - self.plot_area = plot_area - - if not is_notebook(): - self.fig, self.ax = plt.subplots(1, 1, figsize=(10, 6)) - - def update(self, year: float, gdf: gpd.GeoDataFrame) -> None: - """ - Redraw the map for a given simulation time. - - Parameters - ---------- - year : float - Current simulation time, displayed in the map title. - gdf : geopandas.GeoDataFrame - GeoDataFrame snapshot to render. - - Raises - ------ - RuntimeError - If no interactive matplotlib backend is detected and the code is - not running in a notebook or Streamlit context. - """ + self.figsize = figsize + self.pause = pause + self.interval = interval + self.plot_area = plot_area + self.save_frames = save_frames + + # pre-create figure for interactive mode to avoid flicker + if not is_notebook() and plot_area is None: + self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) + + # ── rendering ───────────────────────────────────────────────────────────── + + def _render(self, step: float) -> matplotlib.figure.Figure: + """Build and return the figure for the current step.""" if is_notebook(): - from IPython.display import clear_output, display + from IPython.display import clear_output clear_output(wait=True) - self.fig, self.ax = plt.subplots(1, 1, figsize=(10, 6)) + self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) else: self.fig.clf() self.ax = self.fig.add_subplot(1, 1, 1) - gdf.plot(ax=self.ax, **self.plot_params) - self.ax.set_title(f"Map — Step {year}") + self.gdf.plot(ax=self.ax, **self.plot_params) + self.ax.set_title(f"Map — Step {int(step)}") plt.tight_layout() plt.draw() + return self.fig + + def _save_frame(self, fig: matplotlib.figure.Figure, step: float) -> None: + """Save the current figure to map_frames/{column}_step_NNN.png.""" + col = self.plot_params.get("column", "map") + out_dir = pathlib.Path("map_frames") + out_dir.mkdir(exist_ok=True) + fname = out_dir / f"{col}_step_{int(step):03d}.png" + fig.savefig(fname, dpi=100, bbox_inches="tight", + facecolor=fig.get_facecolor()) + plt.close(fig) + end_time = getattr(self.env, "end_time", step) + if int(step) % 10 == 0 or step == end_time: + print(f" Map [{col}] step {int(step):3d} → {fname}") + + # ── execute ─────────────────────────────────────────────────────────────── + def execute(self) -> None: + """Redraw the map for the current simulation step.""" + step = self.env.now() + fig = self._render(step) + + # ── Streamlit ───────────────────────────────────────────────────────── if self.plot_area is not None: - self.plot_area.pyplot(self.fig) + self.plot_area.pyplot(fig) + plt.close(fig) + + # ── Jupyter ─────────────────────────────────────────────────────────── elif is_notebook(): from IPython.display import display - display(self.fig) - plt.close(self.fig) - elif self.pause: - if is_interactive_backend(): - plt.pause(0.01) - if self.env.now() == self.env.end_time: - plt.show() - else: - raise RuntimeError( - "No interactive matplotlib backend detected. " - "On Linux, install tkinter:\n\n" - " sudo apt install python3-tk\n" - ) + display(fig) + plt.close(fig) + # ── save frames (explicit or headless fallback) ─────────────────────── + elif self.save_frames or not is_interactive_backend(): + self._save_frame(fig, step) - def execute(self) -> None: - """Redraw the map for the current simulation time step.""" - self.update(year=self.env.now(), gdf=self.gdf) + # ── interactive window ──────────────────────────────────────────────── + else: + if self.pause: + plt.pause(self.interval) + end_time = getattr(self.env, "end_time", step) + if step == end_time: + plt.show() From 4bf53dd5f2fd6344c05fad4821030ebfa298d672 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 15:21:53 -0300 Subject: [PATCH 06/12] organized visualization module --- dissmodel/visualization/__init__.py | 4 ++-- dissmodel/visualization/{ => raster}/raster_map.py | 0 dissmodel/visualization/{ => vector}/map.py | 0 examples/cli/ca/ca_game_of_life.py | 2 +- examples/cli/ca/ca_game_of_life_raster.py | 2 +- examples/cli/demos/geo_load_shapefile.py | 2 +- examples/streamlit/ca_all.py | 2 +- examples/streamlit/ca_fire_model.py | 2 +- examples/streamlit/ca_fire_model_prob.py | 2 +- examples/streamlit/ca_game_of_life.py | 2 +- examples/streamlit/ca_snow.py | 2 +- tests/raster/test_raster_map.py | 2 +- 12 files changed, 11 insertions(+), 11 deletions(-) rename dissmodel/visualization/{ => raster}/raster_map.py (100%) rename dissmodel/visualization/{ => vector}/map.py (100%) diff --git a/dissmodel/visualization/__init__.py b/dissmodel/visualization/__init__.py index 2aaab5b..875f5d7 100644 --- a/dissmodel/visualization/__init__.py +++ b/dissmodel/visualization/__init__.py @@ -1,4 +1,4 @@ -from dissmodel.visualization.map import Map -from dissmodel.visualization.raster_map import RasterMap +from dissmodel.visualization.vector.map import Map +from dissmodel.visualization.raster.raster_map import RasterMap from dissmodel.visualization.chart import Chart, track_plot from dissmodel.visualization.widgets import display_inputs \ No newline at end of file diff --git a/dissmodel/visualization/raster_map.py b/dissmodel/visualization/raster/raster_map.py similarity index 100% rename from dissmodel/visualization/raster_map.py rename to dissmodel/visualization/raster/raster_map.py diff --git a/dissmodel/visualization/map.py b/dissmodel/visualization/vector/map.py similarity index 100% rename from dissmodel/visualization/map.py rename to dissmodel/visualization/vector/map.py diff --git a/examples/cli/ca/ca_game_of_life.py b/examples/cli/ca/ca_game_of_life.py index 4aaa2a1..5719612 100644 --- a/examples/cli/ca/ca_game_of_life.py +++ b/examples/cli/ca/ca_game_of_life.py @@ -21,7 +21,7 @@ from dissmodel.core import Environment from dissmodel.geo import vector_grid from dissmodel.models.ca import GameOfLife -from dissmodel.visualization.map import Map +from dissmodel.visualization import Map # --------------------------------------------------------------------------- # Setup diff --git a/examples/cli/ca/ca_game_of_life_raster.py b/examples/cli/ca/ca_game_of_life_raster.py index 146026c..b9ec978 100644 --- a/examples/cli/ca/ca_game_of_life_raster.py +++ b/examples/cli/ca/ca_game_of_life_raster.py @@ -15,7 +15,7 @@ from dissmodel.core import Environment from dissmodel.geo import raster_grid from dissmodel.models.ca.game_of_life_raster import GameOfLife -from dissmodel.visualization.raster_map import RasterMap +from dissmodel.visualization.raster.raster_map import RasterMap # --------------------------------------------------------------------------- # Setup diff --git a/examples/cli/demos/geo_load_shapefile.py b/examples/cli/demos/geo_load_shapefile.py index 714b3c4..ff4111e 100644 --- a/examples/cli/demos/geo_load_shapefile.py +++ b/examples/cli/demos/geo_load_shapefile.py @@ -13,7 +13,7 @@ import geopandas as gpd from dissmodel.core import Environment -from dissmodel.visualization.map import Map +from dissmodel.visualization.vector.map import Map # --------------------------------------------------------------------------- # Setup diff --git a/examples/streamlit/ca_all.py b/examples/streamlit/ca_all.py index b2e994b..29486f4 100644 --- a/examples/streamlit/ca_all.py +++ b/examples/streamlit/ca_all.py @@ -25,7 +25,7 @@ import dissmodel.models.ca as ca_models from dissmodel.core import Environment from dissmodel.geo import CellularAutomaton, vector_grid -from dissmodel.visualization.map import Map +from dissmodel.visualization import Map from dissmodel.visualization.widgets import display_inputs # --------------------------------------------------------------------------- diff --git a/examples/streamlit/ca_fire_model.py b/examples/streamlit/ca_fire_model.py index a038e2e..ad12f57 100644 --- a/examples/streamlit/ca_fire_model.py +++ b/examples/streamlit/ca_fire_model.py @@ -23,7 +23,7 @@ from dissmodel.models.ca import FireModel from dissmodel.models.ca.fire_model import FireState from dissmodel.visualization import display_inputs -from dissmodel.visualization.map import Map +from dissmodel.visualization import Map # --------------------------------------------------------------------------- # Page config diff --git a/examples/streamlit/ca_fire_model_prob.py b/examples/streamlit/ca_fire_model_prob.py index 2ab97f0..d9fc93b 100644 --- a/examples/streamlit/ca_fire_model_prob.py +++ b/examples/streamlit/ca_fire_model_prob.py @@ -26,7 +26,7 @@ from dissmodel.models.ca import FireModelProb from dissmodel.models.ca.fire_model import FireState from dissmodel.visualization import display_inputs -from dissmodel.visualization.map import Map +from dissmodel.visualization import Map # --------------------------------------------------------------------------- # Page config diff --git a/examples/streamlit/ca_game_of_life.py b/examples/streamlit/ca_game_of_life.py index 92c7453..f152771 100644 --- a/examples/streamlit/ca_game_of_life.py +++ b/examples/streamlit/ca_game_of_life.py @@ -22,7 +22,7 @@ from dissmodel.geo import vector_grid from dissmodel.models.ca import GameOfLife from dissmodel.models.ca.game_of_life import PATTERNS -from dissmodel.visualization.map import Map +from dissmodel.visualization import Map # --------------------------------------------------------------------------- # Page config diff --git a/examples/streamlit/ca_snow.py b/examples/streamlit/ca_snow.py index d5d5306..3b9d367 100644 --- a/examples/streamlit/ca_snow.py +++ b/examples/streamlit/ca_snow.py @@ -25,7 +25,7 @@ from dissmodel.core import Environment from dissmodel.geo import vector_grid from dissmodel.models.ca.snow import Snow, SnowState -from dissmodel.visualization.map import Map +from dissmodel.visualization import Map from dissmodel.visualization.widgets import display_inputs # --------------------------------------------------------------------------- diff --git a/tests/raster/test_raster_map.py b/tests/raster/test_raster_map.py index febcfc9..da2f1d3 100644 --- a/tests/raster/test_raster_map.py +++ b/tests/raster/test_raster_map.py @@ -16,7 +16,7 @@ from dissmodel.core import Environment from dissmodel.geo.raster.backend import RasterBackend from dissmodel.geo import raster_grid -from dissmodel.visualization.raster_map import RasterMap +from dissmodel.visualization.raster.raster_map import RasterMap # ── force headless for all tests ────────────────────────────────────────────── From af1853477650a103f1d55bc857e348b843bae17b Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 15:55:09 -0300 Subject: [PATCH 07/12] rastermap update --- dissmodel/visualization/raster/raster_map.py | 43 ++++++++++---------- docs/api/visualization.md | 4 +- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/dissmodel/visualization/raster/raster_map.py b/dissmodel/visualization/raster/raster_map.py index e40aef3..54399c9 100644 --- a/dissmodel/visualization/raster/raster_map.py +++ b/dissmodel/visualization/raster/raster_map.py @@ -8,8 +8,15 @@ ------------------------ 1. **Streamlit** — ``plot_area=st.empty()`` 2. **Jupyter** — detected automatically -3. **Interactive** — ``RASTER_MAP_INTERACTIVE=1`` (TkAgg / Qt) -4. **Headless** — saves PNGs to ``raster_map_frames/`` (default) +3. **Interactive** — matplotlib window (TkAgg / Qt), used when a display + is available +4. **Headless** — saves PNGs to ``raster_map_frames/`` when no display + is detected or ``save_frames=True`` + +The component does NOT call ``matplotlib.use()`` at import time — the +backend is left to matplotlib's own detection. This prevents side effects +when ``RasterMap`` is imported alongside other visualization components +(e.g. via ``dissmodel.visualization.__init__``). Usage examples -------------- @@ -41,27 +48,17 @@ """ from __future__ import annotations -import os import pathlib from typing import Any import matplotlib -if os.environ.get("RASTER_MAP_INTERACTIVE", "0") == "1": - pass -else: - matplotlib.use("Agg") - import numpy as np import matplotlib.pyplot as plt import matplotlib.colors as mcolors import matplotlib.patches from dissmodel.core import Model -from dissmodel.visualization._utils import is_notebook - - -def _is_interactive() -> bool: - return matplotlib.get_backend().lower() not in ("agg", "cairo", "svg", "pdf", "ps") +from dissmodel.visualization._utils import is_notebook, is_interactive_backend def _get_nodata_mask(backend) -> np.ndarray | None: @@ -159,6 +156,7 @@ def setup( interval: float = 0.5, plot_area: Any = None, auto_mask: bool = True, + save_frames: bool = False, # categorical color_map: dict[int, str] | None = None, labels: dict[int, str] | None = None, @@ -181,6 +179,7 @@ def setup( self.interval = interval self.plot_area = plot_area self.auto_mask = auto_mask + self.save_frames = save_frames self.color_map = color_map self.labels = labels or {} self.cmap = cmap @@ -320,18 +319,20 @@ def execute(self) -> None: display(fig) plt.close(fig) - elif self.pause and _is_interactive(): - plt.pause(self.interval) - if step == getattr(self.env, "end_time", step): - input("Simulation complete — press Enter to close...") - plt.close("all") - - else: + elif self.save_frames or not is_interactive_backend(): out_dir = pathlib.Path("raster_map_frames") out_dir.mkdir(exist_ok=True) fname = out_dir / f"{self.band}_step_{int(step):03d}.png" fig.savefig(fname, dpi=100, bbox_inches="tight", facecolor=fig.get_facecolor()) plt.close(fig) - if int(step) % 10 == 0 or step == getattr(self.env, "end_time", step): + end_time = getattr(self.env, "end_time", step) + if int(step) % 10 == 0 or step == end_time: print(f" RasterMap [{self.band}] step {int(step):3d} → {fname}") + + else: + if self.pause: + plt.pause(self.interval) + end_time = getattr(self.env, "end_time", step) + if step == end_time: + plt.show() diff --git a/docs/api/visualization.md b/docs/api/visualization.md index fe1a7da..dbad34d 100644 --- a/docs/api/visualization.md +++ b/docs/api/visualization.md @@ -177,6 +177,6 @@ if run_btn: ::: dissmodel.visualization.Chart -::: dissmodel.visualization.map.Map +::: dissmodel.visualization.Map -::: dissmodel.visualization.raster_map.RasterMap \ No newline at end of file +::: dissmodel.visualization.RasterMap \ No newline at end of file From fdebc282f1afa91ba2854275175758781e5f4676 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 17:29:47 -0300 Subject: [PATCH 08/12] update --- dissmodel/visualization/__init__.py | 8 +- dissmodel/visualization/{vector => }/map.py | 0 .../visualization/{raster => }/raster_map.py | 0 docs/api/'' | 0 docs/api/geo/index.md | 2 +- docs/api/visualization.md | 8 +- docs/examples/notebooks/ca_game_of_life.ipynb | 2 +- docs/getting_started.md | 2 +- docs/models/ca/fire_model.md | 2 +- docs/models/ca/game_of_life.md | 2 +- docs/models/ca/index.md | 4 +- docs/why_dissmodel.md | 2 +- examples/cli/ca/ca_game_of_life_raster.py | 2 +- examples/cli/demos/geo_load_shapefile.py | 2 +- examples/notebooks/ca_game_of_life.ipynb | 95 +++++++++++++------ mkdocs.yml | 1 + tests/raster/test_raster_map.py | 2 +- 17 files changed, 88 insertions(+), 46 deletions(-) rename dissmodel/visualization/{vector => }/map.py (100%) rename dissmodel/visualization/{raster => }/raster_map.py (100%) create mode 100644 docs/api/'' diff --git a/dissmodel/visualization/__init__.py b/dissmodel/visualization/__init__.py index 875f5d7..645ca53 100644 --- a/dissmodel/visualization/__init__.py +++ b/dissmodel/visualization/__init__.py @@ -1,4 +1,6 @@ -from dissmodel.visualization.vector.map import Map -from dissmodel.visualization.raster.raster_map import RasterMap from dissmodel.visualization.chart import Chart, track_plot -from dissmodel.visualization.widgets import display_inputs \ No newline at end of file +from dissmodel.visualization.widgets import display_inputs +from dissmodel.visualization.map import Map +from dissmodel.visualization.raster_map import RasterMap + +__all__ = ["Chart", "track_plot", "display_inputs", "Map", "RasterMap"] \ No newline at end of file diff --git a/dissmodel/visualization/vector/map.py b/dissmodel/visualization/map.py similarity index 100% rename from dissmodel/visualization/vector/map.py rename to dissmodel/visualization/map.py diff --git a/dissmodel/visualization/raster/raster_map.py b/dissmodel/visualization/raster_map.py similarity index 100% rename from dissmodel/visualization/raster/raster_map.py rename to dissmodel/visualization/raster_map.py diff --git a/docs/api/'' b/docs/api/'' new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/geo/index.md b/docs/api/geo/index.md index f27a0dd..c1d701b 100644 --- a/docs/api/geo/index.md +++ b/docs/api/geo/index.md @@ -40,7 +40,7 @@ no conversion step. ```python import geopandas as gpd from dissmodel.core import Model, Environment -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map gdf = gpd.read_file("area.shp") gdf.set_index("object_id", inplace=True) diff --git a/docs/api/visualization.md b/docs/api/visualization.md index dbad34d..90dcb7e 100644 --- a/docs/api/visualization.md +++ b/docs/api/visualization.md @@ -80,7 +80,7 @@ Chart(plot_area=st.empty()) Renders spatial data from a GeoDataFrame, updated at every simulation step. ```python -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map from matplotlib.colors import ListedColormap Map( @@ -175,8 +175,8 @@ if run_btn: ## API Reference -::: dissmodel.visualization.Chart +::: dissmodel.visualization.chart.Chart -::: dissmodel.visualization.Map +::: dissmodel.visualization.map.Map -::: dissmodel.visualization.RasterMap \ No newline at end of file +::: dissmodel.visualization.raster_map.RasterMap \ No newline at end of file diff --git a/docs/examples/notebooks/ca_game_of_life.ipynb b/docs/examples/notebooks/ca_game_of_life.ipynb index 25de857..3c96951 100644 --- a/docs/examples/notebooks/ca_game_of_life.ipynb +++ b/docs/examples/notebooks/ca_game_of_life.ipynb @@ -47,7 +47,7 @@ "from dissmodel.geo import vector_grid\n", "from dissmodel.models.ca import GameOfLife\n", "from dissmodel.models.ca.game_of_life import PATTERNS\n", - "from dissmodel.visualization.map import Map" + "from dissmodel.visualization.map import Map" ] }, { diff --git a/docs/getting_started.md b/docs/getting_started.md index 6bc173d..b7f0ff3 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -90,7 +90,7 @@ detects the Jupyter environment automatically and renders visualizations inline. from dissmodel.core import Environment from dissmodel.geo import regular_grid from dissmodel.models.ca import GameOfLife -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map from matplotlib.colors import ListedColormap gdf = regular_grid(dimension=(30, 30), resolution=1) diff --git a/docs/models/ca/fire_model.md b/docs/models/ca/fire_model.md index 1ef8076..54ec84f 100644 --- a/docs/models/ca/fire_model.md +++ b/docs/models/ca/fire_model.md @@ -15,7 +15,7 @@ from dissmodel.core import Environment from dissmodel.geo import vector_grid from dissmodel.models.ca import FireModel from dissmodel.models.ca.fire_model import FireState -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map gdf = vector_grid(dimension=(30, 30), resolution=1, attrs={"state": FireState.FOREST}) diff --git a/docs/models/ca/game_of_life.md b/docs/models/ca/game_of_life.md index 6e21264..7c22b7c 100644 --- a/docs/models/ca/game_of_life.md +++ b/docs/models/ca/game_of_life.md @@ -13,7 +13,7 @@ from dissmodel.geo import vector_grid from dissmodel.models.ca import GameOfLife from matplotlib.colors import ListedColormap -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map gdf = vector_grid(dimension=(30, 30), resolution=1) env = Environment(end_time=20) diff --git a/docs/models/ca/index.md b/docs/models/ca/index.md index 0af396c..153449c 100644 --- a/docs/models/ca/index.md +++ b/docs/models/ca/index.md @@ -25,7 +25,7 @@ before running the simulation. from dissmodel.core import Environment from dissmodel.geo import vector_grid from dissmodel.models.ca import GameOfLife -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map gdf = vector_grid(dimension=(30, 30), resolution=1) @@ -43,7 +43,7 @@ Subclass `CellularAutomaton` and implement the `rule(idx)` method: ```python from dissmodel.core import Environment from dissmodel.geo import vector_grid, fill, FillStrategy -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map from libpysal.weights import Queen from dissmodel.geo.vector.cellular_automaton import CellularAutomaton diff --git a/docs/why_dissmodel.md b/docs/why_dissmodel.md index 005cdf0..4ad86ed 100644 --- a/docs/why_dissmodel.md +++ b/docs/why_dissmodel.md @@ -78,7 +78,7 @@ This works. But the researcher is responsible for: from dissmodel.core import Environment from dissmodel.geo import vector_grid from dissmodel.geo.vector.model import SpatialModel -from dissmodel.visualization.map import Map +from dissmodel.visualization.map import Map from libpysal.weights import Queen gdf = vector_grid(dimension=(100, 100), resolution=100, diff --git a/examples/cli/ca/ca_game_of_life_raster.py b/examples/cli/ca/ca_game_of_life_raster.py index b9ec978..146026c 100644 --- a/examples/cli/ca/ca_game_of_life_raster.py +++ b/examples/cli/ca/ca_game_of_life_raster.py @@ -15,7 +15,7 @@ from dissmodel.core import Environment from dissmodel.geo import raster_grid from dissmodel.models.ca.game_of_life_raster import GameOfLife -from dissmodel.visualization.raster.raster_map import RasterMap +from dissmodel.visualization.raster_map import RasterMap # --------------------------------------------------------------------------- # Setup diff --git a/examples/cli/demos/geo_load_shapefile.py b/examples/cli/demos/geo_load_shapefile.py index ff4111e..714b3c4 100644 --- a/examples/cli/demos/geo_load_shapefile.py +++ b/examples/cli/demos/geo_load_shapefile.py @@ -13,7 +13,7 @@ import geopandas as gpd from dissmodel.core import Environment -from dissmodel.visualization.vector.map import Map +from dissmodel.visualization.map import Map # --------------------------------------------------------------------------- # Setup diff --git a/examples/notebooks/ca_game_of_life.ipynb b/examples/notebooks/ca_game_of_life.ipynb index 25de857..1500f54 100644 --- a/examples/notebooks/ca_game_of_life.ipynb +++ b/examples/notebooks/ca_game_of_life.ipynb @@ -37,9 +37,21 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'dissmodel.visualization.vector.map'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 7\u001b[39m\n\u001b[32m 5\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdissmodel\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mmodels\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mca\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m GameOfLife\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdissmodel\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mmodels\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mca\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mgame_of_life\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m PATTERNS\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdissmodel\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mvisualization\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mvector\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mmap\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Map\n", + "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'dissmodel.visualization.vector.map'" + ] + } + ], "source": [ "from matplotlib.colors import ListedColormap\n", "\n", @@ -69,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -88,7 +100,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -161,7 +173,7 @@ "4-0 POLYGON ((1 4, 1 5, 0 5, 0 4, 1 4)) 0" ] }, - "execution_count": 34, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -186,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -224,9 +236,26 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "ValueError", + "evalue": "no default environment. Did yout forget to call sim.Environment()?", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[32m/tmp/ipykernel_660454/2465766586.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# 2. Model connects to the active environment automatically\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m gol = GameOfLife(gdf=gdf)\n\u001b[32m 3\u001b[39m \n\u001b[32m 4\u001b[39m \u001b[38;5;66;03m# Choose one:\u001b[39;00m\n\u001b[32m 5\u001b[39m gol.initialize_patterns([\u001b[33m\"glider\"\u001b[39m, \u001b[33m\"blinker\"\u001b[39m, \u001b[33m\"toad\"\u001b[39m, \u001b[33m\"beacon\"\u001b[39m])\n", + "\u001b[32m~/dev/github/lambdageo/dissmodel/dissmodel/geo/vector/cellular_automaton.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, gdf, state_attr, step, start_time, end_time, name, dim, **kwargs)\u001b[39m\n\u001b[32m 67\u001b[39m **kwargs: Any,\n\u001b[32m 68\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 69\u001b[39m self.state_attr = state_attr\n\u001b[32m 70\u001b[39m self.dim = dim\n\u001b[32m---> \u001b[39m\u001b[32m71\u001b[39m super().__init__(\n\u001b[32m 72\u001b[39m gdf=gdf,\n\u001b[32m 73\u001b[39m step=step,\n\u001b[32m 74\u001b[39m start_time=start_time,\n", + "\u001b[32m~/dev/github/lambdageo/dissmodel/dissmodel/geo/vector/spatial_model.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, gdf, step, start_time, end_time, name, **kwargs)\u001b[39m\n\u001b[32m 92\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 93\u001b[39m self.gdf: gpd.GeoDataFrame = gdf\n\u001b[32m 94\u001b[39m self._neighborhood_created: bool = \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[32m 95\u001b[39m self._neighs_cache: dict = {}\n\u001b[32m---> \u001b[39m\u001b[32m96\u001b[39m super().__init__(\n\u001b[32m 97\u001b[39m step=step,\n\u001b[32m 98\u001b[39m start_time=start_time,\n\u001b[32m 99\u001b[39m end_time=end_time,\n", + "\u001b[32m~/dev/github/lambdageo/dissmodel/dissmodel/core/model.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, step, start_time, end_time, name, *args, **kwargs)\u001b[39m\n\u001b[32m 53\u001b[39m name: str = \u001b[33m\"\"\u001b[39m,\n\u001b[32m 54\u001b[39m *args: Any,\n\u001b[32m 55\u001b[39m **kwargs: Any,\n\u001b[32m 56\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m57\u001b[39m super().__init__(*args, **kwargs)\n\u001b[32m 58\u001b[39m self._step = step\n\u001b[32m 59\u001b[39m self.start_time = start_time\n\u001b[32m 60\u001b[39m self.end_time = end_time\n", + "\u001b[32m~/dev/github/lambdageo/dissmodel/.venv/lib/python3.12/site-packages/salabim/salabim.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, name, at, delay, priority, urgent, process, suppress_trace, suppress_pause_at_step, skip_standby, mode, cap_now, env, **kwargs)\u001b[39m\n\u001b[32m 7078\u001b[39m cap_now: bool = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 7079\u001b[39m env: \u001b[33m\"Environment\"\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 7080\u001b[39m **kwargs,\n\u001b[32m 7081\u001b[39m ):\n\u001b[32m-> \u001b[39m\u001b[32m7082\u001b[39m self.env = _set_env(env)\n\u001b[32m 7083\u001b[39m \n\u001b[32m 7084\u001b[39m _check_overlapping_parameters(self, \u001b[33m\"__init__\"\u001b[39m, \u001b[33m\"setup\"\u001b[39m)\n\u001b[32m 7085\u001b[39m \n", + "\u001b[32m~/dev/github/lambdageo/dissmodel/.venv/lib/python3.12/site-packages/salabim/salabim.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(env)\u001b[39m\n\u001b[32m 27746\u001b[39m \"\"\"\n\u001b[32m 27747\u001b[39m \n\u001b[32m 27748\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m env \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 27749\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m g.default_env \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m> \u001b[39m\u001b[32m27750\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m ValueError(\u001b[33m\"no default environment. Did yout forget to call sim.Environment()?\"\u001b[39m)\n\u001b[32m 27751\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m g.default_env\n\u001b[32m 27752\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m env\n", + "\u001b[31mValueError\u001b[39m: no default environment. Did yout forget to call sim.Environment()?" + ] + } + ], "source": [ "# 2. Model connects to the active environment automatically\n", "gol = GameOfLife(gdf=gdf)\n", @@ -248,18 +277,19 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 3, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "Map (map.0)" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" + "ename": "NameError", + "evalue": "name 'Map' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# 3. Map also connects to the active environment automatically\u001b[39;00m\n\u001b[32m 2\u001b[39m cmap = ListedColormap([\u001b[33m\"\u001b[39m\u001b[33mwhite\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mblack\u001b[39m\u001b[33m\"\u001b[39m])\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[43mMap\u001b[49m(\n\u001b[32m 4\u001b[39m gdf=gdf,\n\u001b[32m 5\u001b[39m plot_params={\u001b[33m\"\u001b[39m\u001b[33mcolumn\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mstate\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mcmap\u001b[39m\u001b[33m\"\u001b[39m: cmap, \u001b[33m\"\u001b[39m\u001b[33mec\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mgray\u001b[39m\u001b[33m\"\u001b[39m},\n\u001b[32m 6\u001b[39m )\n", + "\u001b[31mNameError\u001b[39m: name 'Map' is not defined" + ] } ], "source": [ @@ -280,18 +310,19 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 2, "metadata": {}, "outputs": [ { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkgAAAJOCAYAAABMR/iyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARdBJREFUeJzt3Xt8VPWd//H3ACEhmgwpAZJoAqgRVBCUNjR4wQgVqFXAO2qBeOu6YStNrTauykV3ab1rQXT7ELCmVMVVbLXVYhyilosOyCq2pSQLBJZJDGwyQ0ACP3J+f2wzNd9cyIFzMpzh9Xw85vFwzpzzzuecM1PePTOT+CzLsgQAAICobrEeAAAA4HhDQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJCAOLB06VL5fD75fD599NFHrR63LEvZ2dny+Xz63ve+F4MJ3bNt2zYVFhbq9NNPV1JSkjIyMnTxxRdr9uzZLdZ79tlntXTp0tgM+Xf/9m//piuvvFL9+/eXz+fTnDlz2lxvzpw50fP59VtSUlKnf9bq1at14YUXKjk5WRkZGfrhD3+ohoYGh/YEiH89Yj0AAOckJSVp2bJluvDCC1ssLy8v186dO5WYmBijydxRUVGhb33rW+rVq5duueUWDRw4UKFQSBs2bNDPf/5zzZ07N7rus88+q/T0dM2YMSNm895///3KyMjQeeedp3ffffeI6y9atEgnn3xy9H737t079XM2btyosWPH6qyzztITTzyhnTt36rHHHtOWLVv0hz/84ajnB04kFCQgjnz3u9/V8uXL9cwzz6hHj3+8vJctW6aRI0dq9+7dMZzOeU8++aQaGhq0ceNGDRgwoMVjX375ZYymat/WrVs1cOBA7d69W3379j3i+tdcc43S09Nt/5z77rtPaWlpWrVqlVJTUyVJAwcO1O23364//vGPuuyyy2xnAica3mID4sjUqVO1Z88erVy5Mrrs4MGDeu2113TjjTe2uc1jjz2m0aNHq0+fPurVq5dGjhyp1157rdV6Pp9PM2fO1K9//WsNHjxYSUlJGjlypD744APX9udIKisrdeqpp7YqR5LUr1+/6H8PHDhQX3zxhcrLy6NvV11yySXRx+vr6zVr1ixlZ2crMTFRZ5xxhn7+85+rqakpus62bdvk8/n02GOP6cknn9SAAQPUq1cvjRkzRps2berUvAMHDrS1f5ZlKRKJyLKsTm8TiUS0cuVK3XzzzdFyJEnTpk3TySefrFdffdXWDMCJioIExJGBAwcqPz9fv/nNb6LL/vCHPygcDuuGG25oc5unn35a5513nubNm6d///d/V48ePXTttdfq7bffbrVueXm5Zs2apZtvvlnz5s3Tnj17NGHChE4XBKcNGDBAO3bs0Pvvv9/hek899ZROPfVUDRkyRC+99JJeeukl/eu//qskaf/+/RozZoxKS0s1bdo0PfPMM7rgggtUUlKi4uLiVlm/+tWv9Mwzz6ioqEglJSXatGmTLr30UtXU1Di+f6eddpr8fr9SUlJ08803d+pnfP755/p//+//6Zvf/GaL5T179tSIESP06aefOj4nEJcsAJ63ZMkSS5L1ySefWAsWLLBSUlKs/fv3W5ZlWddee61VUFBgWZZlDRgwwLr88stbbNu8XrODBw9aQ4cOtS699NIWyyVZkqxgMBhdtn37dispKcmaMmWKG7t1RJs2bbJ69eplSbJGjBhh3XXXXdaKFSusffv2tVr3nHPOscaMGdNq+UMPPWSddNJJ1t/+9rcWy3/6059a3bt3t6qqqizLsqytW7dakqxevXpZO3fujK63bt06S5L1ox/9qNNz19bWWpKs2bNnt/n4U089Zc2cOdP69a9/bb322mvWXXfdZfXo0cPKzc21wuFwh9nLly+3JFkffPBBq8euvfZaKyMjo9NzAicyriABcea6667TV199pbfeekt79+7VW2+91e7ba5LUq1ev6H/X1dUpHA7roosu0oYNG1qtm5+fr5EjR0bv5+TkaNKkSXr33Xd1+PBhZ3ekE8455xxt3LhRN998s7Zt26ann35akydPVv/+/fXLX/6yUxnLly/XRRddpLS0NO3evTt6GzdunA4fPtzqLcTJkyfrlFNOid7Py8vTqFGj9Pvf/96x/brrrrv0i1/8QjfeeKOuvvpqPfXUU3rxxRe1ZcsWPfvssx1u+9VXX0lSmx/IT0pKij4OoGN8SBuIM3379tW4ceO0bNky7d+/X4cPH9Y111zT7vpvvfWWHn74YW3cuFGNjY3R5T6fr9W6ubm5rZadeeaZ2r9/v2pra5WRkdHmz6iurj6KPfk/7WV+/ee/9NJLOnz4sP785z/rrbfe0iOPPKI77rhDgwYN0rhx4zrcfsuWLfrss8/a/dC0+WHv9o6B25/tufHGG/XjH/9Y7733nn7605+2u15z4f36uWx24MCBFoUYQPsoSEAcuvHGG3X77berurpaEydOVO/evdtc78MPP9SVV16piy++WM8++6wyMzOVkJCgJUuWaNmyZY7Nk5mZedTbWp38gHL37t01bNgwDRs2TPn5+SooKNCvf/3rIxakpqYmfec739E999zT5uNnnnmm7Zndkp2drf/93//tcJ3mYx0KhVo9FgqFlJWV5cpsQLyhIAFxaMqUKfrBD36gtWvX6pVXXml3vf/8z/9UUlKS3n333RZvySxZsqTN9bds2dJq2d/+9jclJyd3+LX1r3+rris0f0D56yWhrStiknT66aeroaHhiEWqWXvHwO431OyyLEvbtm3Teeed1+F6Q4cOVY8ePRQMBnXddddFlx88eFAbN25ssQxA+yhIQBw6+eSTtWjRIm3btk1XXHFFu+t1795dPp+vxeeHtm3bphUrVrS5/po1a7Rhwwadf/75kqQdO3bozTff1IQJEzr8JYadLR92ffjhh/r2t7+thISEFsubPw80ePDg6LKTTjpJ9fX1rTKuu+46zZkzR++++67Gjx/f4rH6+nqdfPLJLX6n1IoVK/Q///M/0c8hffzxx1q3bp1mzZrl0F5JtbW1rQrnokWLVFtbqwkTJrRY/te//lXJycnKycmRJPn9fo0bN06lpaV64IEHlJKSIkl66aWX1NDQoGuvvdaxOYF45rM6e/0awHFr6dKlKiws1CeffNLq691fN3DgQA0dOlRvvfWWJOn999/X2LFjddFFF+nGG2/Ul19+qYULFyojI0OfffZZi7e3fD6fhg4dqurqav3whz9UYmKinn32WdXU1GjdunU699xzXd9P0/e+9z2tX79eV111VfTnb9iwQb/61a+UnJysYDCoQYMGSZKKioq0aNEizZs3T2eccYb69eunSy+9VPv379dFF12kzz77TDNmzNDIkSO1b98+ff7553rttde0bds2paena9u2bRo0aJCGDRumvXv36s4771RjY6Oeeuop+Xw+ff7550d8K/Gll17S9u3btX//fs2fP18FBQW69NJLJUnf//73o7/PKTk5Wddff72GDRumpKQkffTRR3r55Zc1fPhw/elPf1JycnI00+fzacyYMVq1alV02YYNGzR6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74m0rxL7G89OpH2V3NvfI105asaHtAEAAAwUJAAAAAMFqRMSExM1e/ZsJSYmxnqULnEi7e+JtK/SibW/J9K+SuxvPDuR9lU6fvY3Lj6kDQAA4CSuIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAg/d3ChQs1cOBAJSUladSoUfr44487XH/58uUaMmSIkpKSNGzYMP3+97/vokmPzfz58/Wtb31LKSkp6tevnyZPnqzNmzd3uM3SpUvl8/la3JKSkrpo4qM3Z86cVnMPGTKkw228el4laeDAga321+fzqaioqM31vXZeP/jgA11xxRXKysqSz+fTihUrWjxuWZYefPBBZWZmqlevXho3bpy2bNlyxFy7r/2u0NG+Hjp0SPfee6+GDRumk046SVlZWZo2bZp27drVYebRvB66ypHO7YwZM1rNPmHChCPmeu3cSmrzNezz+fToo4+2m3m8ntvO/Htz4MABFRUVqU+fPjr55JN19dVXq6ampsPco32t20VBkvTKK6+ouLhYs2fP1oYNGzR8+HCNHz9eX375ZZvrr169WlOnTtWtt96qTz/9VJMnT9bkyZO1adOmLp7cvvLychUVFWnt2rVauXKlDh06pMsuu0z79u3rcLvU1FSFQqHobfv27V008bE555xzWsz90Ucftbuul8+rJH3yySct9nXlypWSpGuvvbbdbbx0Xvft26fhw4dr4cKFbT7+yCOP6JlnntFzzz2ndevW6aSTTtL48eN14MCBdjPtvva7Skf7un//fm3YsEEPPPCANmzYoNdff12bN2/WlVdeecRcO6+HrnSkcytJEyZMaDH7b37zmw4zvXhuJbXYx1AopMWLF8vn8+nqq6/uMPd4PLed+ffmRz/6kX73u99p+fLlKi8v165du3TVVVd1mHs0r/WjYsHKy8uzioqKovcPHz5sZWVlWfPnz29z/euuu866/PLLWywbNWqU9YMf/MDVOd3w5ZdfWpKs8vLydtdZsmSJ5ff7u24oh8yePdsaPnx4p9ePp/NqWZZ11113WaeffrrV1NTU5uNePa+WZVmSrDfeeCN6v6mpycrIyLAeffTR6LL6+norMTHR+s1vftNujt3XfiyY+9qWjz/+2JJkbd++vd117L4eYqWt/Z0+fbo1adIkWznxcm4nTZpkXXrppR2u45Vza/57U19fbyUkJFjLly+PrvOXv/zF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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" + "ename": "NameError", + "evalue": "name 'env' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43menv\u001b[49m.run()\n", + "\u001b[31mNameError\u001b[39m: name 'env' is not defined" + ] } ], "source": [ @@ -315,13 +346,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": ".venv (3.12.3)", "language": "python", "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.10.0" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" } }, "nbformat": 4, diff --git a/mkdocs.yml b/mkdocs.yml index c65c9da..5d0a115 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ plugins: - mkdocstrings: handlers: python: + paths: ["."] options: docstring_style: numpy show_source: true diff --git a/tests/raster/test_raster_map.py b/tests/raster/test_raster_map.py index da2f1d3..febcfc9 100644 --- a/tests/raster/test_raster_map.py +++ b/tests/raster/test_raster_map.py @@ -16,7 +16,7 @@ from dissmodel.core import Environment from dissmodel.geo.raster.backend import RasterBackend from dissmodel.geo import raster_grid -from dissmodel.visualization.raster.raster_map import RasterMap +from dissmodel.visualization.raster_map import RasterMap # ── force headless for all tests ────────────────────────────────────────────── From 8647dd52ea244e0d7c3387721fa07d8ed0d821b5 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 17:37:43 -0300 Subject: [PATCH 09/12] update --- dissmodel/visualization/map.py | 35 ++++++---- examples/notebooks/ca_game_of_life.ipynb | 83 ++++++++---------------- 2 files changed, 47 insertions(+), 71 deletions(-) diff --git a/dissmodel/visualization/map.py b/dissmodel/visualization/map.py index e8f6f7e..da3c0aa 100644 --- a/dissmodel/visualization/map.py +++ b/dissmodel/visualization/map.py @@ -126,20 +126,27 @@ def setup( # ── rendering ───────────────────────────────────────────────────────────── def _render(self, step: float) -> matplotlib.figure.Figure: - """Build and return the figure for the current step.""" - if is_notebook(): - from IPython.display import clear_output - clear_output(wait=True) - self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) - else: - self.fig.clf() - self.ax = self.fig.add_subplot(1, 1, 1) - - self.gdf.plot(ax=self.ax, **self.plot_params) - self.ax.set_title(f"Map — Step {int(step)}") - plt.tight_layout() - plt.draw() - return self.fig + """Build and return the figure for the current step.""" + # Verifica se é notebook OU se a figura ainda não existe (Streamlit) + if is_notebook() or not hasattr(self, 'fig'): + if is_notebook(): + from IPython.display import clear_output + clear_output(wait=True) + # Cria a figura para Streamlit ou recria para Jupyter + self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) + else: + # Reutiliza a janela em modo interativo (TkAgg/Qt) + self.fig.clf() + self.ax = self.fig.add_subplot(1, 1, 1) + + # Renderiza o mapa e ajusta os títulos + self.gdf.plot(ax=self.ax, **self.plot_params) + self.ax.set_title(f"Map — Step {int(step)}") + + plt.tight_layout() + plt.draw() + + return self.fig def _save_frame(self, fig: matplotlib.figure.Figure, step: float) -> None: """Save the current figure to map_frames/{column}_step_NNN.png.""" diff --git a/examples/notebooks/ca_game_of_life.ipynb b/examples/notebooks/ca_game_of_life.ipynb index 1500f54..1cddeaa 100644 --- a/examples/notebooks/ca_game_of_life.ipynb +++ b/examples/notebooks/ca_game_of_life.ipynb @@ -37,21 +37,9 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'dissmodel.visualization.vector.map'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 7\u001b[39m\n\u001b[32m 5\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdissmodel\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mmodels\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mca\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m GameOfLife\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdissmodel\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mmodels\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mca\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mgame_of_life\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m PATTERNS\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdissmodel\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mvisualization\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mvector\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mmap\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Map\n", - "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'dissmodel.visualization.vector.map'" - ] - } - ], + "outputs": [], "source": [ "from matplotlib.colors import ListedColormap\n", "\n", @@ -81,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -100,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -173,7 +161,7 @@ "4-0 POLYGON ((1 4, 1 5, 0 5, 0 4, 1 4)) 0" ] }, - "execution_count": 4, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -198,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -236,26 +224,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 13, "metadata": {}, - "outputs": [ - { - "ename": "ValueError", - "evalue": "no default environment. Did yout forget to call sim.Environment()?", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[32m/tmp/ipykernel_660454/2465766586.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# 2. Model connects to the active environment automatically\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m gol = GameOfLife(gdf=gdf)\n\u001b[32m 3\u001b[39m \n\u001b[32m 4\u001b[39m \u001b[38;5;66;03m# Choose one:\u001b[39;00m\n\u001b[32m 5\u001b[39m gol.initialize_patterns([\u001b[33m\"glider\"\u001b[39m, \u001b[33m\"blinker\"\u001b[39m, \u001b[33m\"toad\"\u001b[39m, \u001b[33m\"beacon\"\u001b[39m])\n", - "\u001b[32m~/dev/github/lambdageo/dissmodel/dissmodel/geo/vector/cellular_automaton.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, gdf, state_attr, step, start_time, end_time, name, dim, **kwargs)\u001b[39m\n\u001b[32m 67\u001b[39m **kwargs: Any,\n\u001b[32m 68\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 69\u001b[39m self.state_attr = state_attr\n\u001b[32m 70\u001b[39m self.dim = dim\n\u001b[32m---> \u001b[39m\u001b[32m71\u001b[39m super().__init__(\n\u001b[32m 72\u001b[39m gdf=gdf,\n\u001b[32m 73\u001b[39m step=step,\n\u001b[32m 74\u001b[39m start_time=start_time,\n", - "\u001b[32m~/dev/github/lambdageo/dissmodel/dissmodel/geo/vector/spatial_model.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, gdf, step, start_time, end_time, name, **kwargs)\u001b[39m\n\u001b[32m 92\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 93\u001b[39m self.gdf: gpd.GeoDataFrame = gdf\n\u001b[32m 94\u001b[39m self._neighborhood_created: bool = \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[32m 95\u001b[39m self._neighs_cache: dict = {}\n\u001b[32m---> \u001b[39m\u001b[32m96\u001b[39m super().__init__(\n\u001b[32m 97\u001b[39m step=step,\n\u001b[32m 98\u001b[39m start_time=start_time,\n\u001b[32m 99\u001b[39m end_time=end_time,\n", - "\u001b[32m~/dev/github/lambdageo/dissmodel/dissmodel/core/model.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, step, start_time, end_time, name, *args, **kwargs)\u001b[39m\n\u001b[32m 53\u001b[39m name: str = \u001b[33m\"\"\u001b[39m,\n\u001b[32m 54\u001b[39m *args: Any,\n\u001b[32m 55\u001b[39m **kwargs: Any,\n\u001b[32m 56\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m57\u001b[39m super().__init__(*args, **kwargs)\n\u001b[32m 58\u001b[39m self._step = step\n\u001b[32m 59\u001b[39m self.start_time = start_time\n\u001b[32m 60\u001b[39m self.end_time = end_time\n", - "\u001b[32m~/dev/github/lambdageo/dissmodel/.venv/lib/python3.12/site-packages/salabim/salabim.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(self, name, at, delay, priority, urgent, process, suppress_trace, suppress_pause_at_step, skip_standby, mode, cap_now, env, **kwargs)\u001b[39m\n\u001b[32m 7078\u001b[39m cap_now: bool = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 7079\u001b[39m env: \u001b[33m\"Environment\"\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 7080\u001b[39m **kwargs,\n\u001b[32m 7081\u001b[39m ):\n\u001b[32m-> \u001b[39m\u001b[32m7082\u001b[39m self.env = _set_env(env)\n\u001b[32m 7083\u001b[39m \n\u001b[32m 7084\u001b[39m _check_overlapping_parameters(self, \u001b[33m\"__init__\"\u001b[39m, \u001b[33m\"setup\"\u001b[39m)\n\u001b[32m 7085\u001b[39m \n", - "\u001b[32m~/dev/github/lambdageo/dissmodel/.venv/lib/python3.12/site-packages/salabim/salabim.py\u001b[39m in \u001b[36m?\u001b[39m\u001b[34m(env)\u001b[39m\n\u001b[32m 27746\u001b[39m \"\"\"\n\u001b[32m 27747\u001b[39m \n\u001b[32m 27748\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m env \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 27749\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m g.default_env \u001b[38;5;28;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m> \u001b[39m\u001b[32m27750\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m ValueError(\u001b[33m\"no default environment. Did yout forget to call sim.Environment()?\"\u001b[39m)\n\u001b[32m 27751\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m g.default_env\n\u001b[32m 27752\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m env\n", - "\u001b[31mValueError\u001b[39m: no default environment. Did yout forget to call sim.Environment()?" - ] - } - ], + "outputs": [], "source": [ "# 2. Model connects to the active environment automatically\n", "gol = GameOfLife(gdf=gdf)\n", @@ -277,19 +248,18 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 14, "metadata": {}, "outputs": [ { - "ename": "NameError", - "evalue": "name 'Map' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# 3. Map also connects to the active environment automatically\u001b[39;00m\n\u001b[32m 2\u001b[39m cmap = ListedColormap([\u001b[33m\"\u001b[39m\u001b[33mwhite\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mblack\u001b[39m\u001b[33m\"\u001b[39m])\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[43mMap\u001b[49m(\n\u001b[32m 4\u001b[39m gdf=gdf,\n\u001b[32m 5\u001b[39m plot_params={\u001b[33m\"\u001b[39m\u001b[33mcolumn\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mstate\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mcmap\u001b[39m\u001b[33m\"\u001b[39m: cmap, \u001b[33m\"\u001b[39m\u001b[33mec\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mgray\u001b[39m\u001b[33m\"\u001b[39m},\n\u001b[32m 6\u001b[39m )\n", - "\u001b[31mNameError\u001b[39m: name 'Map' is not defined" - ] + "data": { + "text/plain": [ + "Map (map.0)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -310,19 +280,18 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 15, "metadata": {}, "outputs": [ { - "ename": "NameError", - "evalue": "name 'env' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43menv\u001b[49m.run()\n", - "\u001b[31mNameError\u001b[39m: name 'env' is not defined" - ] + "data": { + "image/png": "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JkzQT3/6U61fv17vvvtus+v+8z//U3Fxcfr0008bfEfSwoULm1y/bdu2Rrf99a9/VXx8fIsfWz/xU3Vt4Xvf+54kye/3h25r6oyYJF144YU6ePDgSYtUveYeg3A/oRYuy7K0Y8cOXXbZZY7+HOBsQUECzkLnnnuu5s+frx07duj6669vdl379u3l8XgavH9ox44dWr58eZPr161bp02bNunyyy+XJO3atUsffvihxowZ0+KXGLa2fITriy++0Pe//33FxMQ0uL3+/UB9+/YN3XbOOeeopqamUcatt96qmTNn6tNPP9Xo0aMb3FdTU6Nzzz23wXdKLV++XH/7299C70P68ssvtWHDBk2fPt2mvZKqqqoaFc758+erqqpKY8aMse3nAGczChJwlpo8efJJ11x33XV6/vnnNWbMGN1xxx3au3ev5s2bpz59+ujrr79utL5///4aPXq0/vVf/1WxsbGhNwyf+I3Vbenpp5/Wxo0bdeONN2rgwIGSpE2bNunNN9/Ueeed16C0DBkyRPPnz9dTTz2lPn36qFu3brrmmmv00EMP6be//a1+/OMfa8qUKRoyZIgOHTqkb775Ru+//7527NjR4Msa+/Tpo6uuukr333+/amtr9eKLL6pLly7NvkR3orfeeks7d+7U4cOHJUmrV6/WU089JUn6yU9+Evo+p549e+q2227TgAEDFBcXpzVr1mjJkiUaPHiwfvrTn9r18AFnt0h/jA6A8078mH9LmvqY/+uvv25lZWVZsbGxVr9+/ayFCxdaM2bMsMz/+ZBk5efnW0VFRaH1l112meX1eu3enVb7wx/+YOXn51v9+/e3kpKSrJiYGCszM9OaMmWKVVZW1mBtRUWFdd1111kJCQmWpAYf+T9w4IBVWFho9enTx+rYsaOVkpJiDRs2zHruueeso0ePWpb1j4/5P/vss9avfvUrKyMjw4qNjbWuvvpq67//+79bNe/w4cNDX5dgXk58HO+55x7rkksusRISEqyYmBirT58+1iOPPGIFg8HTfswA/B+PZfEVrABOn8fjUX5+vubOnRvpUSJix44d6t27t5599lk9+OCDkR4HwGnie5AAAAAMFCQAAAADBQkAAMDAe5AAAAAMnEECAAAwUJAAAAAMUfFFkXV1ddqzZ48SEhKa/XMBAADg7GZZlg4cOKD09HS1a9fyOaKoKEh79uxR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XL927VpNnDhRU6dO1VdffaXx48dr/Pjx2rJlSxtPHr6SkhLl5+dr/fr1WrlypY4dO6Zrr71Whw4danG7xMRE+f3+0GXnzp1tNPHpufTSSxvMvWbNmmbXuvm4StIf//jHBvu6cuVKSdItt9zS7DZuOq6HDh3SoEGDNG/evCbvf+aZZ/Tyyy/r1Vdf1YYNG3TOOedo9OjROnLkSLOZ4T7320pL+3r48GFt2rRJjz/+uDZt2qQPPvhAW7du1Q033HDS3HCeD23pZMdWksaMGdNg9nfeeafFTDceW0kN9tHv92vBggXyeDy66aabWsw9E49ta37f/OxnP9Pvfvc7LV26VCUlJdqzZ49uvPHGFnNP5bl+SixY2dnZVn5+fuj68ePHrfT0dGvOnDlNrr/11lut6667rsFtQ4cOtX760586OqcT9u7da0mySkpKml2zcOFCKykpqe2GssmMGTOsQYMGtXp9NB1Xy7KsBx54wLrwwguturq6Ju9363G1LMuSZC1btix0va6uzkpNTbWeffbZ0G01NTVWbGys9c477zSbE+5zPxLMfW3Kl19+aUmydu7c2eyacJ8PkdLU/k6ePNkaN25cWDnRcmzHjRtnXXPNNS2uccuxNX/f1NTUWDExMdbSpUtDa/785z9bkqx169Y1mXGqz/VTcdafQTp69Kg2btyoUaNGhW5r166dRo0apXXr1jW5zbp16xqsl6TRo0c3u/5MFggEJEnnnXdei+sOHjyonj17KiMjQ+PGjdO3337bFuOdtm3btik9PV0XXHCB7rzzTpWXlze7NpqO69GjR1VUVKS77767xT/g7Nbjatq+fbsqKioaHL+kpCQNHTq02eN3Ks/9M1UgEJDH41Hnzp1bXBfO8+FMs2rVKnXr1k19+/bV/fffr/379ze7NlqObWVlpT7++GNNnTr1pGvdcGzN3zcbN27UsWPHGhynfv36KTMzs9njdCrP9VN11hekffv26fjx4+revXuD27t3766Kioomt6moqAhr/Zmqrq5O06dP15VXXqn+/fs3u65v375asGCBPvzwQxUVFamurk7Dhg3T7t2723Da8A0dOlSLFi3SihUrNH/+fG3fvl1XX321Dhw40OT6aDmukrR8+XLV1NRoypQpza5x63FtSv0xCuf4ncpz/0x05MgRPfLII5o4cWKLf9gz3OfDmWTMmDF68803VVxcrKefflolJSUaO3asjh8/3uT6aDm2b7zxhhISEk76kpMbjm1Tv28qKirUsWPHRsX+ZL9/69e0dptT1cHWNLhKfn6+tmzZctLXqnNycpSTkxO6PmzYMF188cV67bXX9OSTTzo95ikbO3Zs6L8HDhyooUOHqmfPnnrvvfda9f/I3Oz111/X2LFjlZ6e3uwatx5X/MOxY8d06623yrIszZ8/v8W1bn4+3H777aH/HjBggAYOHKgLL7xQq1at0siRIyM4mbMWLFigO++886QfnnDDsW3t75szyVl/BiklJUXt27dv9K75yspKpaamNrlNampqWOvPRNOmTdNHH30kr9erHj16hLVtTEyMLrvsMpWWljo0nTM6d+6siy66qNm5o+G4StLOnTv12Wef6Z577glrO7ceV0mhYxTO8TuV5/6ZpL4c7dy5UytXrmzx7FFTTvZ8OJNdcMEFSklJaXZ2tx9bSfriiy+0devWsJ/H0pl3bJv7fZOamqqjR4+qpqamwfqT/f6tX9PabU7VWV+QOnbsqCFDhqi4uDh0W11dnYqLixv8v+sT5eTkNFgvSStXrmx2/ZnEsixNmzZNy5Yt0+eff67evXuHnXH8+HF98803SktLc2BC5xw8eFBlZWXNzu3m43qihQsXqlu3brruuuvC2s6tx1WSevfurdTU1AbHLxgMasOGDc0ev1N57p8p6svRtm3b9Nlnn6lLly5hZ5zs+XAm2717t/bv39/s7G4+tvVef/11DRkyRIMGDQp72zPl2J7s982QIUMUExPT4Dht3bpV5eXlzR6nU3mun84OnPWWLFlixcbGWosWLbL+9Kc/Wffdd5/VuXNnq6KiwrIsy/rJT35iPfroo6H1f/jDH6wOHTpYzz33nPXnP//ZmjFjhhUTE2N98803kdqFVrv//vutpKQka9WqVZbf7w9dDh8+HFpj7u+sWbOsTz/91CorK7M2btxo3X777VZcXJz17bffRmIXWu3nP/+5tWrVKmv79u3WH/7wB2vUqFFWSkqKtXfvXsuyouu41jt+/LiVmZlpPfLII43uc/txPXDggPXVV19ZX331lSXJev75562vvvoq9MmtX/7yl1bnzp2tDz/80Pr666+tcePGWb1797a+++67UMY111xj/frXvw5dP9lzP1Ja2tejR49aN9xwg9WjRw9r8+bNDZ7HtbW1oQxzX0/2fIiklvb3wIED1oMPPmitW7fO2r59u/XZZ59Zl19+uZWVlWUdOXIklBENx7ZeIBCw4uPjrfnz5zeZ4ZZj25rfN//0T/9kZWZmWp9//rnl8/msnJwcKycnp0FO3759rQ8++CB0vTXPdTtQkP7u17/+tZWZmWl17NjRys7OttavXx+6b/jw4dbkyZMbrH/vvfesiy66yOrYsaN16aWXWh9//HEbT3xqJDV5WbhwYWiNub/Tp08PPTbdu3e3fvSjH1mbNm1q++HDdNttt1lpaWlWx44drfPPP9+67bbbrNLS0tD90XRc63366aeWJGvr1q2N7nP7cfV6vU3+263fp7q6Ouvxxx+3unfvbsXGxlojR45s9Dj07NnTmjFjRoPbWnruR0pL+7p9+/Zmn8derzeUYe7ryZ4PkdTS/h4+fNi69tprra5du1oxMTFWz549rXvvvbdR0YmGY1vvtddeszp16mTV1NQ0meGWY9ua3zffffed9c///M9WcnKyFR8fb02YMMHy+/2Nck7cpjXPdTt4/v7DAQAA8Hdn/XuQAAAATBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMPx/Af75AVCE8EkAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ From 066ef2c5018a66a85332e54433e204f41e2f79fb Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 17:40:13 -0300 Subject: [PATCH 10/12] fix map --- dissmodel/visualization/map.py | 119 +++++++---------------- examples/notebooks/ca_game_of_life.ipynb | 20 ++-- 2 files changed, 43 insertions(+), 96 deletions(-) diff --git a/dissmodel/visualization/map.py b/dissmodel/visualization/map.py index da3c0aa..babca7e 100644 --- a/dissmodel/visualization/map.py +++ b/dissmodel/visualization/map.py @@ -10,35 +10,6 @@ 2. **Jupyter** — detected automatically 3. **Interactive** — matplotlib window (TkAgg / Qt) 4. **Headless** — saves PNGs to ``map_frames/`` (default fallback) - -The headless fallback means the component never raises ``RuntimeError`` -in CI or server environments — it simply writes one PNG per step. - -Usage examples --------------- - # basic - Map(gdf=grid, plot_params={"column": "state", "cmap": "viridis"}) - - # with legend and classification scheme - Map(gdf=grid, plot_params={ - "column": "f", - "cmap": "Greens", - "scheme": "equal_interval", - "k": 5, - "legend": True, - }) - - # Streamlit - plot_area = st.empty() - Map(gdf=grid, plot_params={"column": "uso"}, plot_area=plot_area) - - # force frame saving even in interactive mode - Map(gdf=grid, plot_params={"column": "uso"}, save_frames=True) - -Notes ------ -Analogous to ``RasterMap`` for vector data. Both components share the -same rendering targets and the same ``save_frames`` / headless behaviour. """ from __future__ import annotations @@ -59,37 +30,22 @@ class Map(Model): """ Simulation model that renders a live choropleth map of a GeoDataFrame. - Extends :class:`~dissmodel.core.Model` and redraws the map at every - simulation step. - Parameters ---------- gdf : geopandas.GeoDataFrame - GeoDataFrame to render. Updated in-place by the simulation models - sharing the same reference. + GeoDataFrame to render. plot_params : dict - Keyword arguments forwarded to :meth:`GeoDataFrame.plot` - (e.g. ``column``, ``cmap``, ``scheme``, ``legend``). + Keyword arguments forwarded to :meth:`GeoDataFrame.plot`. figsize : tuple[int, int] Figure size in inches. Default: ``(10, 6)``. pause : bool Call ``plt.pause()`` after each update in interactive mode. - Default: ``True``. interval : float Seconds passed to ``plt.pause()``. Default: ``0.01``. plot_area : st.empty() | None Streamlit placeholder. Default: ``None``. save_frames : bool - If ``True``, save one PNG per step to ``map_frames/`` regardless - of the rendering environment. Default: ``False``. - - Notes - ----- - **Headless / CI behaviour** — when no interactive backend is available - and ``plot_area`` is ``None`` and the code is not running in a notebook, - the component automatically saves PNGs to ``map_frames/`` instead of - raising an error. This makes it safe to use in CI pipelines and remote - servers without a display. + Save one PNG per step to ``map_frames/``. Default: ``False``. Examples -------- @@ -98,18 +54,15 @@ class Map(Model): >>> env.run() """ - fig: matplotlib.figure.Figure - ax: matplotlib.axes.Axes - def setup( self, gdf: gpd.GeoDataFrame, plot_params: dict[str, Any], - figsize: tuple[int, int] = (10, 6), - pause: bool = True, - interval: float = 0.01, - plot_area: Any = None, - save_frames: bool = False, + figsize: tuple[int, int] = (10, 6), + pause: bool = True, + interval: float = 0.01, + plot_area: Any = None, + save_frames: bool = False, ) -> None: self.gdf = gdf self.plot_params = plot_params @@ -119,37 +72,31 @@ def setup( self.plot_area = plot_area self.save_frames = save_frames - # pre-create figure for interactive mode to avoid flicker - if not is_notebook() and plot_area is None: - self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) + # always create fig so _render() can always call self.fig.clf() + self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) + + # close immediately if not needed for interactive mode + if is_notebook() or plot_area is not None: + plt.close(self.fig) # ── rendering ───────────────────────────────────────────────────────────── def _render(self, step: float) -> matplotlib.figure.Figure: - """Build and return the figure for the current step.""" - # Verifica se é notebook OU se a figura ainda não existe (Streamlit) - if is_notebook() or not hasattr(self, 'fig'): - if is_notebook(): - from IPython.display import clear_output - clear_output(wait=True) - # Cria a figura para Streamlit ou recria para Jupyter - self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) - else: - # Reutiliza a janela em modo interativo (TkAgg/Qt) - self.fig.clf() - self.ax = self.fig.add_subplot(1, 1, 1) - - # Renderiza o mapa e ajusta os títulos - self.gdf.plot(ax=self.ax, **self.plot_params) - self.ax.set_title(f"Map — Step {int(step)}") - - plt.tight_layout() - plt.draw() - - return self.fig + if is_notebook() or self.plot_area is not None: + # create a fresh figure every step + self.fig, self.ax = plt.subplots(1, 1, figsize=self.figsize) + else: + # reuse existing figure — clear and redraw + self.fig.clf() + self.ax = self.fig.add_subplot(1, 1, 1) + + self.gdf.plot(ax=self.ax, **self.plot_params) + self.ax.set_title(f"Map — Step {int(step)}") + plt.tight_layout() + plt.draw() + return self.fig def _save_frame(self, fig: matplotlib.figure.Figure, step: float) -> None: - """Save the current figure to map_frames/{column}_step_NNN.png.""" col = self.plot_params.get("column", "map") out_dir = pathlib.Path("map_frames") out_dir.mkdir(exist_ok=True) @@ -164,27 +111,27 @@ def _save_frame(self, fig: matplotlib.figure.Figure, step: float) -> None: # ── execute ─────────────────────────────────────────────────────────────── def execute(self) -> None: - """Redraw the map for the current simulation step.""" step = self.env.now() fig = self._render(step) - # ── Streamlit ───────────────────────────────────────────────────────── if self.plot_area is not None: + # Streamlit self.plot_area.pyplot(fig) plt.close(fig) - # ── Jupyter ─────────────────────────────────────────────────────────── elif is_notebook(): - from IPython.display import display + # Jupyter + from IPython.display import clear_output, display + clear_output(wait=True) display(fig) plt.close(fig) - # ── save frames (explicit or headless fallback) ─────────────────────── elif self.save_frames or not is_interactive_backend(): + # headless / CI self._save_frame(fig, step) - # ── interactive window ──────────────────────────────────────────────── else: + # interactive window if self.pause: plt.pause(self.interval) end_time = getattr(self.env, "end_time", step) diff --git a/examples/notebooks/ca_game_of_life.ipynb b/examples/notebooks/ca_game_of_life.ipynb index 1cddeaa..6e0ed3a 100644 --- a/examples/notebooks/ca_game_of_life.ipynb +++ b/examples/notebooks/ca_game_of_life.ipynb @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -69,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -88,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -161,7 +161,7 @@ "4-0 POLYGON ((1 4, 1 5, 0 5, 0 4, 1 4)) 0" ] }, - "execution_count": 11, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -186,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -224,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -248,7 +248,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -257,7 +257,7 @@ "Map (map.0)" ] }, - "execution_count": 14, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -280,12 +280,12 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkgAAAJOCAYAAABMR/iyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARC9JREFUeJzt3Xt8VPWd//H3ACEhmoRIgCSSAEoElZtSQ4NaiFCBWgW8oxaIqF037EpTb+lP5aK71Eu9FUS3DwE1Ioor2KrFxTgEKRc7IKvYlpIsECiTENhkhosEHuT8/thmSr65kIFzMpzh9Xw85vFwZr7nnc+ZwzTvnrnEY1mWJQAAAIS0i/QAAAAAZxoKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggScBRYtWiSPxyOPx6M1a9Y0ut+yLGVkZMjj8ejHP/5xBCZ0zo4dO5SXl6cLL7xQcXFxSk1N1Q9+8APNmDGjwbpXXnlFixYtisyQf/dv//ZvuuGGG9S9e3d5PB7NnDmzyXUzZ84MHc8TL3FxcW07MBDFOkR6AABtJy4uTosXL9ZVV13V4PaSkhLt3r1bsbGxEZrMGaWlpbriiivUqVMn3X333erVq5f8fr82bdqkp59+WrNmzQqtfeWVV5SSkqIpU6ZEbN7HHntMqampuuyyy/Tpp5+edP38+fN17rnnhq63b9/eyfGAswoFCTiL/OhHP9LSpUv18ssvq0OHfzz9Fy9erCFDhmjfvn0RnM5+L7zwgg4ePKjNmzerZ8+eDe7bu3dvhKZq3vbt29WrVy/t27dPXbt2Pen6m2++WSkpKW0wGXD24SU24CwyceJE7d+/XytXrgzddvToUb3//vu64447mtzmueee07Bhw9SlSxd16tRJQ4YM0fvvv99oncfj0bRp0/T222+rb9++iouL05AhQ7R69WrH9udkysrK1KNHj0blSJK6desW+u9evXrp22+/VUlJSejlqhEjRoTur6mp0fTp05WRkaHY2Fj16dNHTz/9tOrq6kJrduzYIY/Ho+eee04vvPCCevbsqU6dOmn48OHasmVLq+bt1atXWPtnWZaCwaAsywprOwAnR0ECziK9evVSTk6O3nnnndBtv//97xUIBHT77bc3uc1LL72kyy67TLNnz9a///u/q0OHDrrlllv08ccfN1pbUlKi6dOn66677tLs2bO1f/9+jRkzptUFwW49e/bUrl279Pnnn7e47sUXX1SPHj3Ur18/vfXWW3rrrbf0//7f/5MkHT58WMOHD1dRUZEmTZqkl19+WVdeeaUKCwtVUFDQKOvNN9/Uyy+/rPz8fBUWFmrLli265pprVFlZafv+XXDBBUpKSlJCQoLuuusuR34GcNayAES9hQsXWpKsP/7xj9bcuXOthIQE6/Dhw5ZlWdYtt9xi5ebmWpZlWT179rSuu+66BtvWr6t39OhRq3///tY111zT4HZJliTL5/OFbtu5c6cVFxdnTZgwwYndOqktW7ZYnTp1siRZgwcPth544AFr+fLl1qFDhxqtvfTSS63hw4c3uv3JJ5+0zjnnHOuvf/1rg9sfffRRq3379lZ5ebllWZa1fft2S5LVqVMna/fu3aF1GzZssCRZP/vZz1o9d1VVlSXJmjFjRpP3v/jii9a0adOst99+23r//fetBx54wOrQoYOVlZVlBQKBVv8cAM3jDBJwlrn11lv13Xff6aOPPtKBAwf00UcfNfvymiR16tQp9N/V1dUKBAK6+uqrtWnTpkZrc3JyNGTIkND1zMxMjRs3Tp9++qmOHz9u7460wqWXXqrNmzfrrrvu0o4dO/TSSy9p/Pjx6t69u37zm9+0KmPp0qW6+uqrlZycrH379oUuo0aN0vHjxxu9hDh+/Hidf/75oevZ2dkaOnSoPvnkE9v264EHHtCvf/1r3XHHHbrpppv04osv6o033tC2bdv0yiuv2PZzgLMZb9IGzjJdu3bVqFGjtHjxYh0+fFjHjx/XzTff3Oz6jz76SE899ZQ2b96s2tra0O0ej6fR2qysrEa3XXTRRTp8+LCqqqqUmpra5M+oqKg4hT35P81lnvjz33rrLR0/flx/+tOf9NFHH+mZZ57Rfffdp969e2vUqFEtbr9t2zZ9/fXXzb5p2nyzd3OPwXvvvXeSPTk9d9xxh37+85/rs88+06OPPurozwLOBhQk4Cx0xx136N5771VFRYXGjh2rzp07N7nuiy++0A033KAf/OAHeuWVV5SWlqaYmBgtXLhQixcvtm2etLS0U97WauUblNu3b68BAwZowIABysnJUW5urt5+++2TFqS6ujr98Ic/1MMPP9zk/RdddFHYMzslIyND//u//xvpMYCoQEECzkITJkzQT3/6U61fv17vvvtus+v+8z//U3Fxcfr0008bfEfSwoULm1y/bdu2Rrf99a9/VXx8fIsfWz/xU3Vt4Xvf+54kye/3h25r6oyYJF144YU6ePDgSYtUveYeg3A/oRYuy7K0Y8cOXXbZZY7+HOBsQUECzkLnnnuu5s+frx07duj6669vdl379u3l8XgavH9ox44dWr58eZPr161bp02bNunyyy+XJO3atUsffvihxowZ0+KXGLa2fITriy++0Pe//33FxMQ0uL3+/UB9+/YN3XbOOeeopqamUcatt96qmTNn6tNPP9Xo0aMb3FdTU6Nzzz23wXdKLV++XH/7299C70P68ssvtWHDBk2fPt2mvZKqqqoaFc758+erqqpKY8aMse3nAGczChJwlpo8efJJ11x33XV6/vnnNWbMGN1xxx3au3ev5s2bpz59+ujrr79utL5///4aPXq0/vVf/1WxsbGhNwyf+I3Vbenpp5/Wxo0bdeONN2rgwIGSpE2bNunNN9/Ueeed16C0DBkyRPPnz9dTTz2lPn36qFu3brrmmmv00EMP6be//a1+/OMfa8qUKRoyZIgOHTqkb775Ru+//7527NjR4Msa+/Tpo6uuukr333+/amtr9eKLL6pLly7NvkR3orfeeks7d+7U4cOHJUmrV6/WU089JUn6yU9+Evo+p549e+q2227TgAEDFBcXpzVr1mjJkiUaPHiwfvrTn9r18AFnt0h/jA6A8078mH9LmvqY/+uvv25lZWVZsbGxVr9+/ayFCxdaM2bMsMz/+ZBk5efnW0VFRaH1l112meX1eu3enVb7wx/+YOXn51v9+/e3kpKSrJiYGCszM9OaMmWKVVZW1mBtRUWFdd1111kJCQmWpAYf+T9w4IBVWFho9enTx+rYsaOVkpJiDRs2zHruueeso0ePWpb1j4/5P/vss9avfvUrKyMjw4qNjbWuvvpq67//+79bNe/w4cNDX5dgXk58HO+55x7rkksusRISEqyYmBirT58+1iOPPGIFg8HTfswA/B+PZfEVrABOn8fjUX5+vubOnRvpUSJix44d6t27t5599lk9+OCDkR4HwGnie5AAAAAMFCQAAAADBQkAAMDAe5AAAAAMnEECAAAwUJAAAAAMUfFFkXV1ddqzZ48SEhKa/XMBAADg7GZZlg4cOKD09HS1a9fyOaKoKEh79uxR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XL927VpNnDhRU6dO1VdffaXx48dr/Pjx2rJlSxtPHr6SkhLl5+dr/fr1WrlypY4dO6Zrr71Whw4danG7xMRE+f3+0GXnzp1tNPHpufTSSxvMvWbNmmbXuvm4StIf//jHBvu6cuVKSdItt9zS7DZuOq6HDh3SoEGDNG/evCbvf+aZZ/Tyyy/r1Vdf1YYNG3TOOedo9OjROnLkSLOZ4T7320pL+3r48GFt2rRJjz/+uDZt2qQPPvhAW7du1Q033HDS3HCeD23pZMdWksaMGdNg9nfeeafFTDceW0kN9tHv92vBggXyeDy66aabWsw9E49ta37f/OxnP9Pvfvc7LV26VCUlJdqzZ49uvPHGFnNP5bl+SixY2dnZVn5+fuj68ePHrfT0dGvOnDlNrr/11lut6667rsFtQ4cOtX760586OqcT9u7da0mySkpKml2zcOFCKykpqe2GssmMGTOsQYMGtXp9NB1Xy7KsBx54wLrwwguturq6Ju9363G1LMuSZC1btix0va6uzkpNTbWeffbZ0G01NTVWbGys9c477zSbE+5zPxLMfW3Kl19+aUmydu7c2eyacJ8PkdLU/k6ePNkaN25cWDnRcmzHjRtnXXPNNS2uccuxNX/f1NTUWDExMdbSpUtDa/785z9bkqx169Y1mXGqz/VTcdafQTp69Kg2btyoUaNGhW5r166dRo0apXXr1jW5zbp16xqsl6TRo0c3u/5MFggEJEnnnXdei+sOHjyonj17KiMjQ+PGjdO3337bFuOdtm3btik9PV0XXHCB7rzzTpWXlze7NpqO69GjR1VUVKS77767xT/g7Nbjatq+fbsqKioaHL+kpCQNHTq02eN3Ks/9M1UgEJDH41Hnzp1bXBfO8+FMs2rVKnXr1k19+/bV/fffr/379ze7NlqObWVlpT7++GNNnTr1pGvdcGzN3zcbN27UsWPHGhynfv36KTMzs9njdCrP9VN11hekffv26fjx4+revXuD27t3766Kioomt6moqAhr/Zmqrq5O06dP15VXXqn+/fs3u65v375asGCBPvzwQxUVFamurk7Dhg3T7t2723Da8A0dOlSLFi3SihUrNH/+fG3fvl1XX321Dhw40OT6aDmukrR8+XLV1NRoypQpza5x63FtSv0xCuf4ncpz/0x05MgRPfLII5o4cWKLf9gz3OfDmWTMmDF68803VVxcrKefflolJSUaO3asjh8/3uT6aDm2b7zxhhISEk76kpMbjm1Tv28qKirUsWPHRsX+ZL9/69e0dptT1cHWNLhKfn6+tmzZctLXqnNycpSTkxO6PmzYMF188cV67bXX9OSTTzo95ikbO3Zs6L8HDhyooUOHqmfPnnrvvfda9f/I3Oz111/X2LFjlZ6e3uwatx5X/MOxY8d06623yrIszZ8/v8W1bn4+3H777aH/HjBggAYOHKgLL7xQq1at0siRIyM4mbMWLFigO++886QfnnDDsW3t75szyVl/BiklJUXt27dv9K75yspKpaamNrlNampqWOvPRNOmTdNHH30kr9erHj16hLVtTEyMLrvsMpWWljo0nTM6d+6siy66qNm5o+G4StLOnTv12Wef6Z577glrO7ceV0mhYxTO8TuV5/6ZpL4c7dy5UytXrmzx7FFTTvZ8OJNdcMEFSklJaXZ2tx9bSfriiy+0devWsJ/H0pl3bJv7fZOamqqjR4+qpqamwfqT/f6tX9PabU7VWV+QOnbsqCFDhqi4uDh0W11dnYqLixv8v+sT5eTkNFgvSStXrmx2/ZnEsixNmzZNy5Yt0+eff67evXuHnXH8+HF98803SktLc2BC5xw8eFBlZWXNzu3m43qihQsXqlu3brruuuvC2s6tx1WSevfurdTU1AbHLxgMasOGDc0ev1N57p8p6svRtm3b9Nlnn6lLly5hZ5zs+XAm2717t/bv39/s7G4+tvVef/11DRkyRIMGDQp72zPl2J7s982QIUMUExPT4Dht3bpV5eXlzR6nU3mun84OnPWWLFlixcbGWosWLbL+9Kc/Wffdd5/VuXNnq6KiwrIsy/rJT35iPfroo6H1f/jDH6wOHTpYzz33nPXnP//ZmjFjhhUTE2N98803kdqFVrv//vutpKQka9WqVZbf7w9dDh8+HFpj7u+sWbOsTz/91CorK7M2btxo3X777VZcXJz17bffRmIXWu3nP/+5tWrVKmv79u3WH/7wB2vUqFFWSkqKtXfvXsuyouu41jt+/LiVmZlpPfLII43uc/txPXDggPXVV19ZX331lSXJev75562vvvoq9MmtX/7yl1bnzp2tDz/80Pr666+tcePGWb1797a+++67UMY111xj/frXvw5dP9lzP1Ja2tejR49aN9xwg9WjRw9r8+bNDZ7HtbW1oQxzX0/2fIiklvb3wIED1oMPPmitW7fO2r59u/XZZ59Zl19+uZWVlWUdOXIklBENx7ZeIBCw4uPjrfnz5zeZ4ZZj25rfN//0T/9kZWZmWp9//rnl8/msnJwcKycnp0FO3759rQ8++CB0vTXPdTtQkP7u17/+tZWZmWl17NjRys7OttavXx+6b/jw4dbkyZMbrH/vvfesiy66yOrYsaN16aWXWh9//HEbT3xqJDV5WbhwYWiNub/Tp08PPTbdu3e3fvSjH1mbNm1q++HDdNttt1lpaWlWx44drfPPP9+67bbbrNLS0tD90XRc63366aeWJGvr1q2N7nP7cfV6vU3+263fp7q6Ouvxxx+3unfvbsXGxlojR45s9Dj07NnTmjFjRoPbWnruR0pL+7p9+/Zmn8derzeUYe7ryZ4PkdTS/h4+fNi69tprra5du1oxMTFWz549rXvvvbdR0YmGY1vvtddeszp16mTV1NQ0meGWY9ua3zffffed9c///M9WcnKyFR8fb02YMMHy+/2Nck7cpjXPdTt4/v7DAQAA8Hdn/XuQAAAATBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMPx/Af75AVCE8EkAAAAASUVORK5CYII=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkgAAAJOCAYAAABMR/iyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARHZJREFUeJzt3Xt8VPWd//H3ACEhmoRIgCSQAGoAlZumhgZvRKhArQLeqV0gonZt6EqzVk23CkF3adV6K4huV0Ab8UIr2KLFYhzihYsOSBXbUpIFAmUSAo9khosEfuT8/thmar65kIFzMpzh9Xw85vFgzpzzzueckzFvz8wkHsuyLAEAACCkU6QHAAAAON1QkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCTgDLZkyRJ5PB55PB599NFHzR63LEsZGRnyeDz6zne+E4EJnbNjxw7l5+frvPPOU1xcnFJTU3XllVdq9uzZTdZ77rnntGTJksgM+Q9+v1933323BgwYoG7duum8885TYWGh9u/fH9G5gGjWJdIDAIi8uLg4LV26VJdffnmT5WVlZdq9e7diY2MjNJkzysvLdemll6pbt26644471L9/f/n9fm3atEk///nPVVxcHFr3ueeeU0pKiqZPnx6RWQ8ePKjc3FwdOnRIP/jBD5SRkaE//elPmj9/vrxerzZu3KhOnfh/XcBuFCQA+va3v61ly5bp2WefVZcu//zPwtKlS5Wdna19+/ZFcDr7PfXUUzp48KA2b96sfv36NXls7969EZqqZb/73e+0c+dOrVy5Utdee21o+TnnnKO5c+fqT3/6ky6++OIITghEJ/63A4CmTJmi/fv3a/Xq1aFlR48e1W9+8xt997vfbXGbJ554QqNGjVKPHj3UrVs3ZWdn6ze/+U2z9Twej2bOnKlXXnlFgwYNUlxcnLKzs/XBBx84tj8nUlFRob59+zYrR5LUq1ev0L/79++vL7/8UmVlZaGXIkePHh16vK6uTrNmzVJGRoZiY2N1/vnn6+c//7kaGhpC6+zYsUMej0dPPPGEnnrqKfXr10/dunXTVVddpS1btpxw1mAwKEnq3bt3k+VpaWmSpG7duoW17wDah4IEQP3791dubq5effXV0LI//OEPCgQCuu2221rc5plnntHFF1+suXPn6r/+67/UpUsX3XzzzXr77bebrVtWVqZZs2bpe9/7nubOnav9+/dr/Pjx7SoITujXr5927dql999/v831nn76afXt21eDBw/Wr3/9a/3617/Wf/zHf0iSDh8+rKuuukolJSWaOnWqnn32WV122WUqKipSYWFhs6yXX35Zzz77rAoKClRUVKQtW7bo6quvVnV1dZszXHnllerUqZPuvfderV+/Xrt379Y777yj//zP/9SkSZM0ePDgkz8QAFpnAThjLV682JJkffrpp9b8+fOthIQE6/Dhw5ZlWdbNN99s5eXlWZZlWf369bOuvfbaJts2rtfo6NGj1pAhQ6yrr766yXJJliTL5/OFlu3cudOKi4uzJk+e7MRundCWLVusbt26WZKsESNGWPfee6+1YsUK69ChQ83Wveiii6yrrrqq2fJHHnnEOuuss6y//e1vTZY/+OCDVufOna3KykrLsixr+/btliSrW7du1u7du0PrbdiwwZJk/ehHPzrhvP/zP/9jde/ePXQsJVnTpk2zjh07FuaeA2gvriABkCTdcsst+uqrr7Ry5UodOHBAK1eubPXlNanpSzu1tbUKBAK64oortGnTpmbr5ubmKjs7O3Q/MzNTEydO1Lvvvqvjx4/buyPtcNFFF2nz5s363ve+px07duiZZ57RpEmT1Lt3b/3qV79qV8ayZct0xRVXKDk5Wfv27Qvdxo4dq+PHjzd7CXHSpEnq06dP6H5OTo5Gjhypd95554Rfq0+fPsrJydHTTz+t5cuXq7CwUK+88ooefPDB8HYcQLvxJm0AkqSePXtq7NixWrp0qQ4fPqzjx4/rpptuanX9lStX6tFHH9XmzZtVX18fWu7xeJqtm5WV1WzZwIEDdfjwYdXU1Cg1NbXFr1FVVXUSe/J/Wsv8+tf/9a9/rePHj+vPf/6zVq5cqcceeyz0cfqxY8e2uf22bdv0+eefq2fPni0+br7Zu7Vj8MYbb7T5dT7++GN95zvf0fr16/WNb3xD0v+VrcTERBUXF+uOO+7QhRde2GYGgPBRkACEfPe739Vdd92lqqoqTZgwQd27d29xvQ8//FDXX3+9rrzySj333HNKS0tTTEyMFi9erKVLl9o2T+MbkU+GZVntWq9z584aOnSohg4dqtzcXOXl5emVV145YUFqaGjQt771Ld1///0tPj5w4MCwZ27JCy+8oN69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H3+81czT9dy25+fNkSNHVFBQoB49eujss8/WjTfeqOrq6jZzT/a5Hi4KkqTXX39dhYWFmj17tjZt2qThw4dr3Lhx2rt3b4vrr127VlOmTNGMGTP02WefadKkSZo0aZK2bNnSwZOHr6ysTAUFBVq/fr1Wr16tY8eO6ZprrtGhQ4fa3C4xMVF+vz9027lzZwdNfGouuuiiJnN/9NFHra7r5vMqSZ9++mmTfV29erUk6eabb251Gzed10OHDmn48OFasGBBi48/9thjevbZZ/X8889rw4YNOuusszRu3DgdOXKk1cxwn/sdpa19PXz4sDZt2qSHHnpImzZt0ptvvqmtW7fq+uuvP2FuOM+HjnSicytJ48ePbzL7q6++2mamG8+tpCb76Pf7tWjRInk8Ht14441t5p6O57Y9P29+9KMf6fe//72WLVumsrIy7dmzRzfccEObuSfzXD8pFqycnByroKAgdP/48eNWenq6NW/evBbXv+WWW6xrr722ybKRI0da3//+9x2d0wl79+61JFllZWWtrrN48WIrKSmp44ayyezZs63hw4e3e/1oOq+WZVn33nuvdd5551kNDQ0tPu7W82pZliXJWr58eeh+Q0ODlZqaaj3++OOhZXV1dVZsbKz16quvtpoT7nM/Esx9bcknn3xiSbJ27tzZ6jrhPh8ipaX9nTZtmjVx4sSwcqLl3E6cONG6+uqr21zHLefW/HlTV1dnxcTEWMuWLQut85e//MWSZK1bt67FjJN9rp+MM/4K0tGjR7Vx40aNHTs2tKxTp04aO3as1q1b1+I269ata7K+JI0bN67V9U9ngUBAknTOOee0ud7BgwfVr18/ZWRkaOLEifryyy87YrxTtm3bNqWnp+vcc8/V7bffrsrKylbXjabzevToUZWUlOiOO+5o8w84u/W8mrZv366qqqom5y8pKUkjR45s9fydzHP/dBUIBOTxeNS9e/c21wvn+XC6WbNmjXr16qVBgwbpnnvu0f79+1tdN1rObXV1td5++23NmDHjhOu64dyaP282btyoY8eONTlPgwcPVmZmZqvn6WSe6yfrjC9I+/bt0/Hjx9W7d+8my3v37q2qqqoWt6mqqgpr/dNVQ0ODZs2apcsuu0xDhgxpdb1BgwZp0aJFeuutt1RSUqKGhgaNGjVKu3fv7sBpwzdy5EgtWbJEq1at0sKFC7V9+3ZdccUVOnDgQIvrR8t5laQVK1aorq5O06dPb3Udt57XljSeo3DO38k8909HR44c0QMPPKApU6a0+Yc9w30+nE7Gjx+vl19+WaWlpfr5z3+usrIyTZgwQcePH29x/Wg5ty+99JISEhJO+JKTG85tSz9vqqqq1LVr12bF/kQ/fxvXae82J6uLrWlwlYKCAm3ZsuWEr1Xn5uYqNzc3dH/UqFG64IIL9MILL+iRRx5xesyTNmHChNC/hw0bppEjR6pfv35644032vV/ZG724osvasKECUpPT291HbeeV/zTsWPHdMstt8iyLC1cuLDNdd38fLjttttC/x46dKiGDRum8847T2vWrNGYMWMiOJmzFi1apNtvv/2EH55ww7lt78+b08kZfwUpJSVFnTt3bvau+erqaqWmpra4TWpqaljrn45mzpyplStXyuv1qm/fvmFtGxMTo4svvljl5eUOTeeM7t27a+DAga3OHQ3nVZJ27typ9957T3feeWdY27n1vEoKnaNwzt/JPPdPJ43laOfOnVq9enWbV49acqLnw+ns3HPPVUpKSquzu/3cStKHH36orVu3hv08lk6/c9vaz5vU1FQdPXpUdXV1TdY/0c/fxnXau83JOuMLUteuXZWdna3S0tLQsoaGBpWWljb5v+uvy83NbbK+JK1evbrV9U8nlmVp5syZWr58ud5//30NGDAg7Izjx4/riy++UFpamgMTOufgwYOqqKhodW43n9evW7x4sXr16qVrr702rO3cel4lacCAAUpNTW1y/oLBoDZs2NDq+TuZ5/7porEcbdu2Te+995569OgRdsaJng+ns927d2v//v2tzu7mc9voxRdfVHZ2toYPHx72tqfLuT3Rz5vs7GzFxMQ0OU9bt25VZWVlq+fpZJ7rp7IDZ7zXXnvNio2NtZYsWWL9+c9/tu6++26re/fuVlVVlWVZlvUv//Iv1oMPPhha/+OPP7a6dOliPfHEE9Zf/vIXa/bs2VZMTIz1xRdfRGoX2u2ee+6xkpKSrDVr1lh+vz90O3z4cGgdc3+Li4utd99916qoqLA2btxo3XbbbVZcXJz15ZdfRmIX2u3f//3frTVr1ljbt2+3Pv74Y2vs2LFWSkqKtXfvXsuyouu8Njp+/LiVmZlpPfDAA80ec/t5PXDggPXZZ59Zn332mSXJevLJJ63PPvss9Mmtn/3sZ1b37t2tt956y/r888+tiRMnWgMGDLC++uqrUMbVV19t/fKXvwzdP9FzP1La2tejR49a119/vdW3b19r8+bNTZ7H9fX1oQxzX0/0fIiktvb3wIED1n333WetW7fO2r59u/Xee+9Zl1xyiZWVlWUdOXIklBEN57ZRIBCw4uPjrYULF7aY4ZZz256fN//6r/9qZWZmWu+//77l8/ms3NxcKzc3t0nOoEGDrDfffDN0vz3PdTtQkP7hl7/8pZWZmWl17drVysnJsdavXx967KqrrrKmTZvWZP033njDGjhwoNW1a1froosust5+++0OnvjkSGrxtnjx4tA65v7OmjUrdGx69+5tffvb37Y2bdrU8cOH6dZbb7XS0tKsrl27Wn369LFuvfVWq7y8PPR4NJ3XRu+++64lydq6dWuzx9x+Xr1eb4vfu4371NDQYD300ENW7969rdjYWGvMmDHNjkO/fv2s2bNnN1nW1nM/Utra1+3bt7f6PPZ6vaEMc19P9HyIpLb29/Dhw9Y111xj9ezZ04qJibH69etn3XXXXc2KTjSc20YvvPCC1a1bN6uurq7FDLec2/b8vPnqq6+sH/zgB1ZycrIVHx9vTZ482fL7/c1yvr5Ne57rdvD844sDAADgH8749yABAACYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABg+P9KUFqoXhSHrgAAAABJRU5ErkJggg==", "text/plain": [ "
" ] From 786a3e81928169ad9945bd53ed6011d277486bd5 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 19 Mar 2026 22:13:42 -0300 Subject: [PATCH 11/12] feat(geo): add SyncSpatialModel and SyncRasterModel --- dissmodel/geo/__init__.py | 5 +- dissmodel/geo/raster/sync_model.py | 126 +++++++++++++++++++++++ dissmodel/geo/vector/sync_model.py | 123 ++++++++++++++++++++++ examples/notebooks/ca_game_of_life.ipynb | 4 +- 4 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 dissmodel/geo/raster/sync_model.py create mode 100644 dissmodel/geo/vector/sync_model.py diff --git a/dissmodel/geo/__init__.py b/dissmodel/geo/__init__.py index 3d01942..16d4d24 100644 --- a/dissmodel/geo/__init__.py +++ b/dissmodel/geo/__init__.py @@ -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 \ No newline at end of file +# from .raster.io import load_geotiff, save_geotiff + +from .vector.sync_model import SyncSpatialModel +from .raster.sync_model import SyncRasterModel \ No newline at end of file diff --git a/dissmodel/geo/raster/sync_model.py b/dissmodel/geo/raster/sync_model.py new file mode 100644 index 0000000..865c116 --- /dev/null +++ b/dissmodel/geo/raster/sync_model.py @@ -0,0 +1,126 @@ +""" +dissmodel/geo/raster/sync_model.py +===================================== +SyncRasterModel — RasterModel with automatic _past snapshot semantics. + +This module provides the snapshot mechanism equivalent to TerraME's +``cs:synchronize()``, as a reusable base class for any raster model that +needs per-step state history (LUCC, fire spread, epidemic models, etc.). + +How it works +------------ +At the start of each step, ``synchronize()`` copies the current NumPy array +for every name in ``land_use_types`` to a ``_past`` array in the +RasterBackend. Models can call ``self.backend.get("f_past")`` to access the +state at the beginning of the current step, regardless of changes made +during execution. + +Usage +----- +Subclass ``SyncRasterModel`` instead of ``RasterModel`` and declare +``self.land_use_types`` in ``setup()``: + + class MyRasterModel(SyncRasterModel): + def setup(self, backend, ...): + super().setup(backend) # RasterModel setup + self.land_use_types = ["f", "d"] # arrays to snapshot + + def execute(self): + f_past = self.backend.get("f_past") # state at step start + ... + +The ``synchronize()`` method is called automatically: + - once before the first ``execute()`` → snapshot of the initial state + - once after each ``execute()`` → snapshot for the next step + +It can also be called manually when needed. + +Relationship to domain libraries +---------------------------------- +``dissluc`` uses this class as the base for its raster LUCC components, +exposing it under the domain-specific alias ``LUCRasterModel``: + + # dissluc/raster/core.py + from dissmodel.geo.raster.sync_model import SyncRasterModel as LUCRasterModel +""" +from __future__ import annotations + +import numpy as np + +from dissmodel.geo import RasterModel + + +class SyncRasterModel(RasterModel): + """ + ``RasterModel`` with automatic ``_past`` snapshot semantics. + + Extends :class:`~dissmodel.geo.raster.model.RasterModel` with a + ``synchronize()`` method that copies each array listed in + ``self.land_use_types`` to a ``_past`` array in the + :class:`~dissmodel.geo.raster.backend.RasterBackend` before and after + every simulation step. + + This is the raster analogue of + :class:`~dissmodel.geo.vector.sync_model.SyncSpatialModel` and the + Python equivalent of TerraME's ``cs:synchronize()``. + + Subclass contract + ----------------- + Declare ``self.land_use_types`` (list of array names) in ``setup()``. + ``SyncRasterModel`` will manage all ``_past`` arrays automatically. + Subclasses must **not** create or update ``_past`` arrays manually. + + Parameters + ---------- + backend : RasterBackend + Passed through to :class:`~dissmodel.geo.raster.model.RasterModel`. + **kwargs + Any additional keyword arguments accepted by the parent class. + + Examples + -------- + >>> class ForestRaster(SyncRasterModel): + ... def setup(self, backend, rate=0.01): + ... super().setup(backend) + ... self.land_use_types = ["forest", "defor"] + ... self.rate = rate + ... + ... def execute(self): + ... forest_past = self.backend.get("forest_past") + ... gain = forest_past * self.rate + ... self.backend.arrays["forest"] = forest_past + gain + """ + + def process(self) -> None: + """ + Simulation loop with automatic snapshot management. + + Overrides :meth:`~dissmodel.core.Model.process` to insert + :meth:`synchronize` calls before the first step and after each step. + """ + if self.env.now() < self.start_time: + self.hold(self.start_time - self.env.now()) + + # initial snapshot — captures state at t=0 before any execution + self.synchronize() + + while self.env.now() < self.end_time: + self.execute() + self.synchronize() # update snapshot for the next step + self.hold(self._step) + + def synchronize(self) -> None: + """ + Copy each array in ``land_use_types`` to ``_past`` in the backend. + + Equivalent to ``cs:synchronize()`` in TerraME. Called automatically + before the first step and after each ``execute()``. Can also be + called manually when an explicit mid-step snapshot is needed. + + Does nothing if ``land_use_types`` has not been set yet (safe to + call before ``setup()`` completes). + """ + if not hasattr(self, "land_use_types"): + return + for name in self.land_use_types: + self.backend.set(name + "_past", self.backend.get(name).copy()) diff --git a/dissmodel/geo/vector/sync_model.py b/dissmodel/geo/vector/sync_model.py new file mode 100644 index 0000000..9539271 --- /dev/null +++ b/dissmodel/geo/vector/sync_model.py @@ -0,0 +1,123 @@ +""" +dissmodel/geo/vector/sync_model.py +===================================== +SyncSpatialModel — SpatialModel with automatic _past snapshot semantics. + +This module provides the snapshot mechanism equivalent to TerraME's +``cs:synchronize()``, as a reusable base class for any vector model that +needs per-step state history (LUCC, fire spread, epidemic models, etc.). + +How it works +------------ +At the start of each step, ``synchronize()`` copies the current value of +every column in ``land_use_types`` to a ``_past`` column in the +GeoDataFrame. Models can read ``gdf["f_past"]`` to access the state at the +beginning of the current step, regardless of changes made during execution. + +Usage +----- +Subclass ``SyncSpatialModel`` instead of ``SpatialModel`` and declare +``self.land_use_types`` in ``setup()``: + + class MyModel(SyncSpatialModel): + def setup(self, gdf, ...): + super().setup(gdf) # SpatialModel setup + self.land_use_types = ["f", "d"] # columns to snapshot + + def execute(self): + past_f = self.gdf["f_past"] # state at step start + ... + +The ``synchronize()`` method is called automatically: + - once before the first ``execute()`` → snapshot of the initial state + - once after each ``execute()`` → snapshot for the next step + +It can also be called manually when needed (e.g. mid-step resets). + +Relationship to domain libraries +---------------------------------- +``dissluc`` uses this class as the base for its LUCC components, +exposing it under the domain-specific alias ``LUCSpatialModel``: + + # dissluc/core.py + from dissmodel.geo.vector.sync_model import SyncSpatialModel as LUCSpatialModel +""" +from __future__ import annotations + +from dissmodel.geo.vector.spatial_model import SpatialModel + + +class SyncSpatialModel(SpatialModel): + """ + ``SpatialModel`` with automatic ``_past`` snapshot semantics. + + Extends :class:`~dissmodel.geo.vector.spatial_model.SpatialModel` with + a ``synchronize()`` method that copies each column listed in + ``self.land_use_types`` to a ``_past`` column before and after + every simulation step. + + This is the Python equivalent of TerraME's ``cs:synchronize()`` — it + ensures that every model reads a consistent snapshot of the state at + the beginning of the current step, even when multiple models share the + same GeoDataFrame. + + Subclass contract + ----------------- + Declare ``self.land_use_types`` (list of column names) in ``setup()``. + ``SyncSpatialModel`` will manage all ``_past`` columns automatically. + Subclasses must **not** create or update ``_past`` columns manually. + + Parameters + ---------- + gdf : geopandas.GeoDataFrame + Passed through to :class:`~dissmodel.geo.vector.spatial_model.SpatialModel`. + **kwargs + Any additional keyword arguments accepted by the parent class. + + Examples + -------- + >>> class ForestCA(SyncSpatialModel): + ... def setup(self, gdf, rate=0.01): + ... super().setup(gdf) + ... self.land_use_types = ["forest", "defor"] + ... self.rate = rate + ... + ... def execute(self): + ... # forest_past holds the state at the start of this step + ... gain = self.gdf["forest_past"] * self.rate + ... self.gdf["forest"] = self.gdf["forest_past"] + gain + """ + + def process(self) -> None: + """ + Simulation loop with automatic snapshot management. + + Overrides :meth:`~dissmodel.core.Model.process` to insert + :meth:`synchronize` calls before the first step and after each step. + """ + if self.env.now() < self.start_time: + self.hold(self.start_time - self.env.now()) + + # initial snapshot — captures state at t=0 before any execution + self.synchronize() + + while self.env.now() < self.end_time: + self.execute() + self.synchronize() # update snapshot for the next step + self.hold(self._step) + + def synchronize(self) -> None: + """ + Copy each column in ``land_use_types`` to ``_past``. + + Equivalent to ``cs:synchronize()`` in TerraME. Called automatically + before the first step and after each ``execute()``. Can also be + called manually when an explicit mid-step snapshot is needed. + + Does nothing if ``land_use_types`` has not been set yet (safe to + call before ``setup()`` completes). + """ + if not hasattr(self, "land_use_types"): + return + for col in self.land_use_types: + self.gdf[col + "_past"] = self.gdf[col].copy() diff --git a/examples/notebooks/ca_game_of_life.ipynb b/examples/notebooks/ca_game_of_life.ipynb index 6e0ed3a..f30d25e 100644 --- a/examples/notebooks/ca_game_of_life.ipynb +++ b/examples/notebooks/ca_game_of_life.ipynb @@ -280,12 +280,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkgAAAJOCAYAAABMR/iyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARHZJREFUeJzt3Xt8VPWd//H3ACEhmoRIgCSQAGoAlZumhgZvRKhArQLeqV0gonZt6EqzVk23CkF3adV6K4huV0Ab8UIr2KLFYhzihYsOSBXbUpIFAmUSAo9khosEfuT8/thmar65kIFzMpzh9Xw85vFgzpzzzueckzFvz8wkHsuyLAEAACCkU6QHAAAAON1QkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCTgDLZkyRJ5PB55PB599NFHzR63LEsZGRnyeDz6zne+E4EJnbNjxw7l5+frvPPOU1xcnFJTU3XllVdq9uzZTdZ77rnntGTJksgM+Q9+v1933323BgwYoG7duum8885TYWGh9u/fH9G5gGjWJdIDAIi8uLg4LV26VJdffnmT5WVlZdq9e7diY2MjNJkzysvLdemll6pbt26644471L9/f/n9fm3atEk///nPVVxcHFr3ueeeU0pKiqZPnx6RWQ8ePKjc3FwdOnRIP/jBD5SRkaE//elPmj9/vrxerzZu3KhOnfh/XcBuFCQA+va3v61ly5bp2WefVZcu//zPwtKlS5Wdna19+/ZFcDr7PfXUUzp48KA2b96sfv36NXls7969EZqqZb/73e+0c+dOrVy5Utdee21o+TnnnKO5c+fqT3/6ky6++OIITghEJ/63A4CmTJmi/fv3a/Xq1aFlR48e1W9+8xt997vfbXGbJ554QqNGjVKPHj3UrVs3ZWdn6ze/+U2z9Twej2bOnKlXXnlFgwYNUlxcnLKzs/XBBx84tj8nUlFRob59+zYrR5LUq1ev0L/79++vL7/8UmVlZaGXIkePHh16vK6uTrNmzVJGRoZiY2N1/vnn6+c//7kaGhpC6+zYsUMej0dPPPGEnnrqKfXr10/dunXTVVddpS1btpxw1mAwKEnq3bt3k+VpaWmSpG7duoW17wDah4IEQP3791dubq5effXV0LI//OEPCgQCuu2221rc5plnntHFF1+suXPn6r/+67/UpUsX3XzzzXr77bebrVtWVqZZs2bpe9/7nubOnav9+/dr/Pjx7SoITujXr5927dql999/v831nn76afXt21eDBw/Wr3/9a/3617/Wf/zHf0iSDh8+rKuuukolJSWaOnWqnn32WV122WUqKipSYWFhs6yXX35Zzz77rAoKClRUVKQtW7bo6quvVnV1dZszXHnllerUqZPuvfderV+/Xrt379Y777yj//zP/9SkSZM0ePDgkz8QAFpnAThjLV682JJkffrpp9b8+fOthIQE6/Dhw5ZlWdbNN99s5eXlWZZlWf369bOuvfbaJts2rtfo6NGj1pAhQ6yrr766yXJJliTL5/OFlu3cudOKi4uzJk+e7MRundCWLVusbt26WZKsESNGWPfee6+1YsUK69ChQ83Wveiii6yrrrqq2fJHHnnEOuuss6y//e1vTZY/+OCDVufOna3KykrLsixr+/btliSrW7du1u7du0PrbdiwwZJk/ehHPzrhvP/zP/9jde/ePXQsJVnTpk2zjh07FuaeA2gvriABkCTdcsst+uqrr7Ry5UodOHBAK1eubPXlNanpSzu1tbUKBAK64oortGnTpmbr5ubmKjs7O3Q/MzNTEydO1Lvvvqvjx4/buyPtcNFFF2nz5s363ve+px07duiZZ57RpEmT1Lt3b/3qV79qV8ayZct0xRVXKDk5Wfv27Qvdxo4dq+PHjzd7CXHSpEnq06dP6H5OTo5Gjhypd95554Rfq0+fPsrJydHTTz+t5cuXq7CwUK+88ooefPDB8HYcQLvxJm0AkqSePXtq7NixWrp0qQ4fPqzjx4/rpptuanX9lStX6tFHH9XmzZtVX18fWu7xeJqtm5WV1WzZwIEDdfjwYdXU1Cg1NbXFr1FVVXUSe/J/Wsv8+tf/9a9/rePHj+vPf/6zVq5cqcceeyz0cfqxY8e2uf22bdv0+eefq2fPni0+br7Zu7Vj8MYbb7T5dT7++GN95zvf0fr16/WNb3xD0v+VrcTERBUXF+uOO+7QhRde2GYGgPBRkACEfPe739Vdd92lqqoqTZgwQd27d29xvQ8//FDXX3+9rrzySj333HNKS0tTTEyMFi9erKVLl9o2T+MbkU+GZVntWq9z584aOnSohg4dqtzcXOXl5emVV145YUFqaGjQt771Ld1///0tPj5w4MCwZ27JCy+8oN69e4fKUaPrr79ec+bM0dq1aylIgAMoSABCJk+erO9///tav369Xn/99VbX++1vf6u4uDi9++67TX5H0uLFi1tcf9u2bc2W/e1vf1N8fHyrV2AkNflUXUdoLCF+vz+0rKUrYpJ03nnn6eDBgycsUo1aOwb9+/dvc7vq6uoWX4Y8duyYJOn//b//166vDyA8FCQAIWeffbYWLlyoHTt26Lrrrmt1vc6dO8vj8TT5wb1jxw6tWLGixfXXrVunTZs26ZJLLpEk7dq1S2+99ZbGjx+vzp07t/p12ls+wvXhhx/qm9/8pmJiYposb3w/0KBBg0LLzjrrLNXV1TXLuOWWWzRnzhy9++67GjduXJPH6urqdPbZZzf5nVIrVqzQ3//+99D7kD755BNt2LBBs2bNanPWgQMH6o9//KPWrFnT5FcMNH7ikN+BBDjDY7X3OjSAqLNkyRLl5+fr008/bfYSztf1799fQ4YM0cqVKyVJ77//vsaMGaMrrrhC3/3ud7V3714tWLBAqamp+vzzz5u8vOXxeDRkyBBVVVXp3/7t3xQbG6vnnntO1dXV2rBhg4YNG+b4fpq+853vaOPGjbrhhhtCX3/Tpk16+eWXFR8fL5/PpwEDBkiSCgoKtHDhQs2dO1fnn3++evXqpauvvlqHDx/WFVdcoc8//1zTp09Xdna2Dh06pC+++EK/+c1vtGPHDqWkpGjHjh0aMGCAhg4dqgMHDuiee+5RfX29nn76aXk8Hn3xxRdtvpS4detWZWdny+Px6Ic//KH69eunsrIyvfrqq/rWt76lP/7xjx1yzIAzTmQ/RAcgkr7+Mf+2tPQx/xdffNHKysqyYmNjrcGDB1uLFy+2Zs+ebZn/WZFkFRQUWCUlJaH1L774Ysvr9dq9O+328ccfWwUFBdaQIUOspKQkKyYmxsrMzLSmT59uVVRUNFm3qqrKuvbaa62EhARLUpOP/B84cMAqKiqyzj//fKtr165WSkqKNWrUKOuJJ56wjh49alnWPz/m//jjj1u/+MUvrIyMDCs2Nta64oorrD/96U/tmvevf/2rddNNN1kZGRlWTEyM1a9fP+u+++5r8dcSALAHV5AAOMrj8aigoEDz58+P9CgR0XgF6fHHH9d9990X6XEAtBO/BwkAAMBAQQIAADBQkAAAAAy8BwkAAMDAFSQAAAADBQkAAMAQFb9Ju6GhQXv27FFCQkKrfxYAAACc2SzL0oEDB5Senq5Ondq+RhQVBWnPnj3KyMiI9BgAAMAFdu3apb59+7a5TlQUpISEBEn/t8OJiYkRngYAAJyOgsGgMjIyQr2hLVFRkBpfVktMTKQgAQCANrXn7Ti8SRsAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwBBWQZo3b54uvfRSJSQkqFevXpo0aZK2bt3aZJ0jR46ooKBAPXr00Nlnn60bb7xR1dXVbeZalqWHH35YaWlp6tatm8aOHatt27aFvzcAAAA2CKsglZWVqaCgQOvXr9fq1at17NgxXXPNNTp06FBonR/96Ef6/e9/r2XLlqmsrEx79uzRDTfc0GbuY489pmeffVbPP/+8NmzYoLPOOkvjxo3TkSNHTm6vAAAAToHHsizrZDeuqalRr169VFZWpiuvvFKBQEA9e/bU0qVLddNNN0mS/vrXv+qCCy7QunXr9M1vfrNZhmVZSk9P17//+7/rvvvukyQFAgH17t1bS5Ys0W233XbCOYLBoJKSkhQIBJSYmHiyuwMAAKJYOH2hy6l8oUAgIEk655xzJEkbN27UsWPHNHbs2NA6gwcPVmZmZqsFafv27aqqqmqyTVJSkkaOHKl169a1WJDq6+tVX18fuh8MBk9lN9pl//79Onr0qGP5hw8fVnx8PPkRyHfz7ORHLpv86M538+zRkN+1a1f16NHDsfz2OOmC1NDQoFmzZumyyy7TkCFDJElVVVXq2rWrunfv3mTd3r17q6qqqsWcxuW9e/du9zbz5s1TcXHxyY4etv3792v+/Pkd9vUAADjTzZw5M6Il6aQLUkFBgbZs2aKPPvrIznnapaioSIWFhaH7wWBQGRkZjn29xitHkydPVs+ePW3P37Ztm7xer+vzf/vb32rfvn22559//vkaM2aMI/NHy7Env2OzyY/ufDfPHg35NTU1Wr58uaOv2rTHSRWkmTNnauXKlfrggw/Ut2/f0PLU1FQdPXpUdXV1Ta4iVVdXKzU1tcWsxuXV1dVKS0trss2IESNa3CY2NlaxsbEnM/op6dmzZ5MZ7dJYKtyev2/fPvn9ftvzU1JSJDkzf7Qce/I7Npv86M538+zRkH+6COtTbJZlaebMmVq+fLnef/99DRgwoMnj2dnZiomJUWlpaWjZ1q1bVVlZqdzc3BYzBwwYoNTU1CbbBINBbdiwodVtAAAAnBRWQSooKFBJSYmWLl2qhIQEVVVVqaqqSl999ZWk/3tz9YwZM1RYWCiv16uNGzcqPz9fubm5Td6gPXjwYC1fvlyS5PF4NGvWLD366KP63e9+py+++EJTp05Venq6Jk2aZN+eAgAAtFNYL7EtXLhQkjR69OgmyxcvXqzp06dLkp566il16tRJN954o+rr6zVu3Dg999xzTdbfunVr6BNwknT//ffr0KFDuvvuu1VXV6fLL79cq1atUlxc3EnsEgAAwKkJqyC151cmxcXFacGCBVqwYEG7czwej+bOnau5c+eGMw4AAIAj+FtsAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYukR6ADepqalxNNft+SkpKY7kN+Y6MX+0HHvyOzab/OjOd/Ps0ZDv9/sdyQ2Xx7IsK9JDnKpgMKikpCQFAgElJibanl9RUaGSkhLbcwEAQMvy8/OVmZlpa2Y4fYErSO0QHx8vScrLy1NycrLt+ZWVlfL5fK7PLy0tVV1dne35GRkZysnJcWT+aDn25HdsNvnRne/m2aMhv7a2Vl6vVzExMbZnh4OCFIasrCylpaU5ku3z+VyfX15e7til0ZycHMfmj4ZjT37HZ5Mf3flunt3t+X6/X16v1/bccPEmbQAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMXSI9gJvU1NQ4muv2/JSUFEfyG3OdmD9ajj35HZtNfnTnu3n2aMj3+/2O5IbLY1mWFekhTlUwGFRSUpICgYASExNtz6+oqFBJSYntuQAAoGX5+fnKzMy0NTOcvsAVpHaIj4+XJOXl5Sk5Odn2/MrKSvl8Ptfnl5aWqq6uzvb8jIwM5eTkODJ/tBx78js2m/zoznfz7NGQX1tbK6/Xq5iYGNuzw0FBCkNWVpbS0tIcyfb5fK7PLy8vd+zSaE5OjmPzR8OxJ7/js8mP7nw3z+72fL/fL6/Xa3tuuHiTNgAAgIGCBAAAYKAgAQAAGChIAAAAhrAL0gcffKDrrrtO6enp8ng8WrFiRZPHPR5Pi7fHH3+81cw5c+Y0W3/w4MFh7wwAAIAdwi5Ihw4d0vDhw7VgwYIWH/f7/U1uixYtksfj0Y033thm7kUXXdRku48++ijc0QAAAGwR9sf8J0yYoAkTJrT6eGpqapP7b731lvLy8nTuuee2PUiXLs22BQAAiARH34NUXV2tt99+WzNmzDjhutu2bVN6errOPfdc3X777aqsrHRyNAAAgFY5+osiX3rpJSUkJOiGG25oc72RI0dqyZIlGjRokPx+v4qLi3XFFVdoy5YtSkhIaLZ+fX296uvrQ/eDwaDtswMAgDOXowVp0aJFuv322xUXF9fmel9/yW7YsGEaOXKk+vXrpzfeeKPFq0/z5s1TcXGx7fMCAABIDr7E9uGHH2rr1q268847w962e/fuGjhwoMrLy1t8vKioSIFAIHTbtWvXqY4LAAAQ4lhBevHFF5Wdna3hw4eHve3BgwdVUVHR6t94iY2NVWJiYpMbAACAXcIuSAcPHtTmzZu1efNmSdL27du1efPmJm+qDgaDWrZsWatXj8aMGaP58+eH7t93330qKyvTjh07tHbtWk2ePFmdO3fWlClTwh0PAADglIX9HiSfz6e8vLzQ/cLCQknStGnTtGTJEknSa6+9JsuyWi04FRUV2rdvX+j+7t27NWXKFO3fv189e/bU5ZdfrvXr16tnz57hjgcAAHDKwi5Io0ePlmVZba5z99136+6772718R07djS5/9prr4U7BgAAgGP4W2wAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAZH/9RItKmpqXE01+35KSkpjuQ35joxf7Qce/I7Npv86M538+zRkO/3+x3JDZfHOtFn9l0gGAwqKSlJgUDAkd+qXVFRoZKSEttzAQBAy/Lz85WZmWlrZjh9gStI7RAfHy9JysvLU3Jysu35lZWVoV/ASX7H5rt5dvIjl01+dOe7efZoyK+trZXX61VMTIzt2eGgIIUhKyur1b8Pd6p8Ph/5Ecp38+zkRy6b/OjOd/Psbs/3+/3yer2254aLN2kDAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgKFLpAdwk5qaGkdzye/4fDfPTn7kssmP7nw3zx4N+X6/35HccHksy7IiPcSpCgaDSkpKUiAQUGJiou35FRUVKikpsT0XAAC0LD8/X5mZmbZmhtMXuILUDvHx8ZKkvLw8JScn255fWVkpn89HfgTy3Tw7+ZHLJj+68908ezTk19bWyuv1KiYmxvbscFCQwpCVlaW0tDRHsn0+H/kRynfz7ORHLpv86M538+xuz/f7/fJ6vbbnhos3aQMAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGMIuSB988IGuu+46paeny+PxaMWKFU0enz59ujweT5Pb+PHjT5i7YMEC9e/fX3FxcRo5cqQ++eSTcEcDAACwRdgF6dChQxo+fLgWLFjQ6jrjx4+X3+8P3V599dU2M19//XUVFhZq9uzZ2rRpk4YPH65x48Zp79694Y4HAABwyrqEu8GECRM0YcKENteJjY1VampquzOffPJJ3XXXXcrPz5ckPf/883r77be1aNEiPfjgg+GOCAAAcErCLkjtsWbNGvXq1UvJycm6+uqr9eijj6pHjx4trnv06FFt3LhRRUVFoWWdOnXS2LFjtW7duha3qa+vV319feh+MBi0dwdaUVNT42gu+R2f7+bZyY9cNvnRne/m2aMh3+/3O5IbLo9lWdZJb+zxaPny5Zo0aVJo2Wuvvab4+HgNGDBAFRUV+slPfqKzzz5b69atU+fOnZtl7NmzR3369NHatWuVm5sbWn7//ferrKxMGzZsaLbNnDlzVFxc3Gx5IBBQYmLiye5OqyoqKlRSUmJ7LgAAaFl+fr4yMzNtzQwGg0pKSmpXX7D9CtJtt90W+vfQoUM1bNgwnXfeeVqzZo3GjBljy9coKipSYWFh6H4wGFRGRoYt2S2Jj4+XJOXl5Sk5Odn2/MrKSvl8PvIjkO/m2cmPXDb50Z3v5tmjIb+2tlZer1cxMTG2Z4fDkZfYvu7cc89VSkqKysvLWyxIKSkp6ty5s6qrq5ssr66ubvV9TLGxsYqNjXVk3rZkZWUpLS3NkWyfz0d+hPLdPDv5kcsmP7rz3Ty72/P9fr+8Xq/tueFy/Pcg7d69W/v372/1IHbt2lXZ2dkqLS0NLWtoaFBpaWmTl9wAAAA6StgF6eDBg9q8ebM2b94sSdq+fbs2b96syspKHTx4UD/+8Y+1fv167dixQ6WlpZo4caLOP/98jRs3LpQxZswYzZ8/P3S/sLBQv/rVr/TSSy/pL3/5i+655x4dOnQo9Kk2AACAjhT2S2yNrzs2anwv0LRp07Rw4UJ9/vnneumll1RXV6f09HRdc801euSRR5q8JFZRUaF9+/aF7t96662qqanRww8/rKqqKo0YMUKrVq1S7969T2XfAAAATkrYBWn06NFq64Nv77777gkzduzY0WzZzJkzNXPmzHDHAQAAsB1/iw0AAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAEOXSA/gJjU1NY7mkt/x+W6enfzIZZMf3flunj0a8v1+vyO54fJYlmVFeohTFQwGlZSUpEAgoMTERNvzKyoqVFJSYnsuAABoWX5+vjIzM23NDKcvcAWpHeLj4yVJeXl5Sk5Otj2/srJSPp+P/Ajku3l28iOXTX5057t59mjIr62tldfrVUxMjO3Z4aAghSErK0tpaWmOZPt8PvIjlO/m2cmPXDb50Z3v5tndnu/3++X1em3PDRdv0gYAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADA0CXSA7hJTU2No7nkd3y+m2cnP3LZ5Ed3vptnj4Z8v9/vSG64PJZlWZEe4lQFg0ElJSUpEAgoMTHR9vyKigqVlJTYngsAAFqWn5+vzMxMWzPD6QtcQWqH+Ph4SVJeXp6Sk5Ntz6+srJTP5yM/Avlunp38yGWTH935bp49GvJra2vl9XoVExNje3Y4KEhhyMrKUlpamiPZPp+P/Ajlu3l28iOXTX5057t5drfn+/1+eb1e23PDxZu0AQAADBQkAAAAAwUJAADAQEECAAAwhF2QPvjgA1133XVKT0+Xx+PRihUrQo8dO3ZMDzzwgIYOHaqzzjpL6enpmjp1qvbs2dNm5pw5c+TxeJrcBg8eHPbOAAAA2CHsgnTo0CENHz5cCxYsaPbY4cOHtWnTJj300EPatGmT3nzzTW3dulXXX3/9CXMvuugi+f3+0O2jjz4KdzQAAABbhP0x/wkTJmjChAktPpaUlKTVq1c3WTZ//nzl5OSosrKyzV/41KVLF6WmpoY7DgAAgO0cfw9SIBCQx+NR9+7d21xv27ZtSk9P17nnnqvbb79dlZWVTo8GAADQIkd/UeSRI0f0wAMPaMqUKW3+Su+RI0dqyZIlGjRokPx+v4qLi3XFFVdoy5YtSkhIaLZ+fX296uvrQ/eDwaAj8wMAgDOTYwXp2LFjuuWWW2RZlhYuXNjmul9/yW7YsGEaOXKk+vXrpzfeeEMzZsxotv68efNUXFxs+8wAAACSQy+xNZajnTt3avXq1WH/Adnu3btr4MCBKi8vb/HxoqIiBQKB0G3Xrl12jA0AACDJgYLUWI62bdum9957Tz169Ag74+DBg6qoqGj1b7zExsYqMTGxyQ0AAMAuYRekgwcPavPmzdq8ebMkafv27dq8ebMqKyt17Ngx3XTTTfL5fHrllVd0/PhxVVVVqaqqSkePHg1ljBkzRvPnzw/dv++++1RWVqYdO3Zo7dq1mjx5sjp37qwpU6ac+h4CAACEKez3IPl8PuXl5YXuFxYWSpKmTZumOXPm6He/+50kacSIEU2283q9Gj16tCSpoqJC+/btCz22e/duTZkyRfv371fPnj11+eWXa/369erZs2e44wEAAJyysAvS6NGjZVlWq4+39VijHTt2NLn/2muvhTsGAACAY/hbbAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABkf/Flu0qampcTSX/I7Pd/Ps5Ecum/zoznfz7NGQ7/f7HckNl8dqz+fyT3PBYFBJSUkKBAKO/FbtiooKlZSU2J4LAABalp+fr8zMTFszw+kLXEFqh/j4eElSXl6ekpOTbc+vrKwM/QJO8js2382zkx+5bPKjO9/Ns0dDfm1trbxer2JiYmzPDgcFKQxZWVmt/n24U+Xz+ciPUL6bZyc/ctnkR3e+m2d3e77f75fX67U9N1y8SRsAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADF0iPYCb1NTUOJpLfsfnu3l28iOXTX5057t59mjI9/v9juSGy2NZlhXpIU5VMBhUUlKSAoGAEhMTbc+vqKhQSUmJ7bkAAKBl+fn5yszMtDUznL7AFaR2iI+PlyTl5eUpOTnZ9vzKykr5fD7yI5Dv5tnJj1w2+adPfmlpqerq6mzNzsjIUE5OjiPZX893+7F3Kr+2tlZer1cxMTG2Z4eDghSGrKwspaWlOZLt8/nIj1C+m2cnP3LZ5J8e+eXl5Y68JJOTk+NYdmO+24+9U/l+v19er9f23HDxJm0AAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAEPYBemDDz7Qddddp/T0dHk8Hq1YsaLJ45Zl6eGHH1ZaWpq6deumsWPHatu2bSfMXbBggfr376+4uDiNHDlSn3zySbijAQAA2CLsgnTo0CENHz5cCxYsaPHxxx57TM8++6yef/55bdiwQWeddZbGjRunI0eOtJr5+uuvq7CwULNnz9amTZs0fPhwjRs3Tnv37g13PAAAgFMWdkGaMGGCHn30UU2ePLnZY5Zl6emnn9ZPf/pTTZw4UcOGDdPLL7+sPXv2NLvS9HVPPvmk7rrrLuXn5+vCCy/U888/r/j4eC1atCjc8QAAAE5ZFzvDtm/frqqqKo0dOza0LCkpSSNHjtS6det02223Ndvm6NGj2rhxo4qKikLLOnXqpLFjx2rdunUtfp36+nrV19eH7geDQRv3onU1NTWO5pLf8flunp38yGWTf/rkp6Sk2J7dmOlE9tdz3X7sncr3+/2O5IbLY1mWddIbezxavny5Jk2aJElau3atLrvsMu3Zs0dpaWmh9W655RZ5PB69/vrrzTL27NmjPn36aO3atcrNzQ0tv//++1VWVqYNGzY022bOnDkqLi5utjwQCCgxMfFkd6dVFRUVKikpsT0XAAC0LD8/X5mZmbZmBoNBJSUltasv2HoFqaMUFRWpsLAwdD8YDCojI8OxrxcfHy9JysvLU3Jysu35lZWV8vl85Ecg382zkx+5bPLbn19aWqq6ujrb8zMyMpSTk+Pq7x03HhvJ+eNTW1srr9ermJgY27PDYWtBSk1NlSRVV1c3uYJUXV2tESNGtLhNSkqKOnfurOrq6ibLq6urQ3mm2NhYxcbG2jN0GLKysprsl518Ph/5Ecp38+zkRy6b/Pbll5eXO/aSSU5Ojqu/d9x6bCRnj4/f75fX67U9N1y2/h6kAQMGKDU1VaWlpaFlwWBQGzZsaPLy2dd17dpV2dnZTbZpaGhQaWlpq9sAAAA4KewrSAcPHlR5eXno/vbt27V582adc845yszM1KxZs/Too48qKytLAwYM0EMPPaT09PTQ+5QkacyYMZo8ebJmzpwpSSosLNS0adP0jW98Qzk5OXr66ad16NAh5efnn/oeAgAAhCnsgtT4umOjxvcCTZs2TUuWLNH999+vQ4cO6e6771ZdXZ0uv/xyrVq1SnFxcaFtKioqtG/fvtD9W2+9VTU1NXr44YdVVVWlESNGaNWqVerdu/ep7BsAAMBJCbsgjR49Wm198M3j8Wju3LmaO3duq+vs2LGj2bKZM2eGrigBAABEEn+LDQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAQ5dID+AmNTU1juaS3/H5bp6d/Mhlk9/+/JSUFEfyG3Pd/L3jxmPz9Vyn8v1+vyO54fJYlmVFeohTFQwGlZSUpEAgoMTERNvzKyoqVFJSYnsuAABoWX5+vjIzM23NDKcvcAWpHeLj4yVJeXl5Sk5Otj2/srJSPp+P/Ajku3l28iOXTX7780tLS1VXV2d7fkZGhnJyclz9veP0sXHjsZek2tpaeb1excTE2J4dDgpSGLKyspSWluZIts/nIz9C+W6enfzIZZPfvvzy8nLHXjLJyclx9feO08fGrcfe7/fL6/Xanhsu3qQNAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgKFLpAdwk5qaGkdzye/4fDfPTn7ksslvf35KSooj+Y25bv7ecfrYuPHYS5Lf73ckN1wey7KsSA9xqoLBoJKSkhQIBJSYmGh7fkVFhUpKSmzPBQAALcvPz1dmZqatmeH0Ba4gtUN8fLwkKS8vT8nJybbnV1ZWyufzkR+BfDfPTn7ksqMpv7S0VHV1dbbnZ2RkKCcnx5XHp6OODce+ZbW1tfJ6vYqJibE9OxwUpDBkZWUpLS3NkWyfz0d+hPLdPDv5kcuOlvzy8nLHXtLIyclx7fHpiGPDsW+Z3++X1+u1PTdcvEkbAADAQEECAAAwUJAAAAAMFCQAAACD7QWpf//+8ng8zW4FBQUtrr9kyZJm68bFxdk9FgAAQLvZ/im2Tz/9VMePHw/d37Jli771rW/p5ptvbnWbxMREbd26NXTf4/HYPRYAAEC72V6Qevbs2eT+z372M5133nm66qqrWt3G4/EoNTXV7lEAAABOiqPvQTp69KhKSkp0xx13tHlV6ODBg+rXr58yMjI0ceJEffnll06OBQAA0CZHC9KKFStUV1en6dOnt7rOoEGDtGjRIr311lsqKSlRQ0ODRo0apd27d7e6TX19vYLBYJMbAACAXRwtSC+++KImTJig9PT0VtfJzc3V1KlTNWLECF111VV688031bNnT73wwgutbjNv3jwlJSWFbhkZGU6MDwAAzlCOFaSdO3fqvffe05133hnWdjExMbr44otVXl7e6jpFRUUKBAKh265du051XAAAgBDHCtLixYvVq1cvXXvttWFtd/z4cX3xxRdt/n2X2NhYJSYmNrkBAADYxZGC1NDQoMWLF2vatGnq0qXpB+WmTp2qoqKi0P25c+fqj3/8o/73f/9XmzZt0ve+9z3t3Lkz7CtPAAAAdrH9Y/6S9N5776myslJ33HFHs8cqKyvVqdM/e1ltba3uuusuVVVVKTk5WdnZ2Vq7dq0uvPBCJ0YDAAA4IUcK0jXXXCPLslp8bM2aNU3uP/XUU3rqqaecGAMAAOCk8LfYAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMjnyKLVrV1NQ4mkt+x+e7eXbyI5cdTfkpKSmO5DfmuvH4dNSx4di3zO/3O5IbLo/V2ufxXSQYDCopKUmBQMCR36pdUVGhkpIS23MBAEDL8vPzlZmZaWtmOH2BK0jtEB8fL0nKy8tTcnKy7fmVlZXy+XzkRyDfzbOTH7nsaMovLS1VXV2d7fkZGRnKyclxZX5Hze727x2n8mtra+X1ehUTE2N7djgoSGHIyspq82/EnQqfz0d+hPLdPDv5kcuOlvzy8nLHXtLIyclxbX5HzO727x2n8v1+v7xer+254eJN2gAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYukR6ADepqalxNJf8js938+zkRy47mvJTUlIcyW/MdWN+R83u9u8dp/L9fr8jueHyWJZlRXqIUxUMBpWUlKRAIKDExETb8ysqKlRSUmJ7LgAAaFl+fr4yMzNtzQynL3AFqR3i4+MlSXl5eUpOTrY9v7KyUj6fj/wI5Lt5dvIjl01+dOe7efZoyK+trZXX61VMTIzt2eGgIIUhKytLaWlpjmT7fD7yI5Tv5tnJj1w2+dGd7+bZ3Z7v9/vl9Xptzw0Xb9IGAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBge0GaM2eOPB5Pk9vgwYPb3GbZsmUaPHiw4uLiNHToUL3zzjt2jwUAANBujlxBuuiii+T3+0O3jz76qNV1165dqylTpmjGjBn67LPPNGnSJE2aNElbtmxxYjQAAIATcqQgdenSRampqaFbSkpKq+s+88wzGj9+vH784x/rggsu0COPPKJLLrlE8+fPd2I0AACAE+riROi2bduUnp6uuLg45ebmat68ecrMzGxx3XXr1qmwsLDJsnHjxmnFihWt5tfX16u+vj50PxgM2jL3idTU1DiaS37H57t5dvIjl01+dOe7efZoyPf7/Y7khstjWZZlZ+Af/vAHHTx4UIMGDZLf71dxcbH+/ve/a8uWLUpISGi2fteuXfXSSy9pypQpoWXPPfeciouLVV1d3eLXmDNnjoqLi5stDwQCSkxMtG9n/qGiokIlJSW25wIAgJbl5+e3enHlZAWDQSUlJbWrL9h+BWnChAmhfw8bNkwjR45Uv3799MYbb2jGjBm2fI2ioqImV52CwaAyMjJsyW5JfHy8JCkvL0/Jycm251dWVsrn8zmeX1paqrq6OtvzMzIylJOT43i+E8eno449+R2f7+bZyY9svptnj4b82tpaeb1excTE2J4dDkdeYvu67t27a+DAgSovL2/x8dTU1GZXiqqrq5WamtpqZmxsrGJjY22dsz2ysrKUlpbmSLbP53M8v7y83LFLlzk5OY7nO3V8OuLYkx+ZfDfPTn5k8908u9vz/X6/vF6v7bnhcvz3IB08eFAVFRWtHsTc3FyVlpY2WbZ69Wrl5uY6PRoAAECLbC9I9913n8rKyrRjxw6tXbtWkydPVufOnUPvMZo6daqKiopC6997771atWqVfvGLX+ivf/2r5syZI5/Pp5kzZ9o9GgAAQLvY/hLb7t27NWXKFO3fv189e/bU5ZdfrvXr16tnz56S/u+1y06d/tnLRo0apaVLl+qnP/2pfvKTnygrK0srVqzQkCFD7B4NAACgXWwvSK+99lqbj69Zs6bZsptvvlk333yz3aMAAACcFP4WGwAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAAhi6RHsBNampqHM11Oj8lJcWR/MZcp/OdOD4ddezJ7/h8N89OfmTz3Tx7NOT7/X5HcsPlsSzLivQQpyoYDCopKUmBQECJiYm251dUVKikpMT2XAAA0LL8/HxlZmbamhlOX+AKUjvEx8dLkvLy8pScnGx7fmVlpXw+n+P5paWlqqursz0/IyNDOTk5rjw+HXXsye/4fDfPTn5k8908ezTk19bWyuv1KiYmxvbscFCQwpCVlaW0tDRHsn0+n+P55eXljl26zMnJce3x6YhjT35k8t08O/mRzXfz7G7P9/v98nq9tueGizdpAwAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggQAAGDoEukB3KSmpsbRXKfzU1JSHMlvzHXj8emoY09+x+e7eXbyI5vv5tmjId/v9zuSGy6PZVlWpIc4VcFgUElJSQoEAkpMTLQ9v6KiQiUlJbbnAgCAluXn5yszM9PWzHD6AleQ2iE+Pl6SlJeXp+TkZNvzKysr5fP5yI9AfmN2aWmp6urqbM2WpIyMDOXk5Ljy2Lg9382zkx/ZfDfPHg35tbW18nq9iomJsT07HBSkMGRlZSktLc2RbJ/PR36E8n0+n8rLyx27rJuTk+PaY+P2fDfPTn5k8908u9vz/X6/vF6v7bnh4k3aAAAABgoSAACAgYIEAABgoCABAAAYbC9I8+bN06WXXqqEhAT16tVLkyZN0tatW9vcZsmSJfJ4PE1ucXFxdo8GAADQLrYXpLKyMhUUFGj9+vVavXq1jh07pmuuuUaHDh1qc7vExET5/f7QbefOnXaPBgAA0C62f8x/1apVTe4vWbJEvXr10saNG3XllVe2up3H41Fqaqrd4wAAAITN8fcgBQIBSdI555zT5noHDx5Uv379lJGRoYkTJ+rLL790ejQAAIAWOVqQGhoaNGvWLF122WUaMmRIq+sNGjRIixYt0ltvvaWSkhI1NDRo1KhR2r17d4vr19fXKxgMNrkBAADYxdHfpF1QUKAtW7boo48+anO93Nxc5ebmhu6PGjVKF1xwgV544QU98sgjzdafN2+eiouLbZ8XAABAcvAK0syZM7Vy5Up5vV717ds3rG1jYmJ08cUXq7y8vMXHi4qKFAgEQrddu3bZMTIAAIAkB64gWZalH/7wh1q+fLnWrFmjAQMGhJ1x/PhxffHFF/r2t7/d4uOxsbGKjY091VEBAABaZHtBKigo0NKlS/XWW28pISFBVVVVkqSkpCR169ZNkjR16lT16dNH8+bNkyTNnTtX3/zmN3X++eerrq5Ojz/+uHbu3Kk777zT7vEAAABOyPaCtHDhQknS6NGjmyxfvHixpk+fLkmqrKxUp07/fHWvtrZWd911l6qqqpScnKzs7GytXbtWF154od3jAQAAnJAjL7GdyJo1a5rcf+qpp/TUU0/ZPQoAAMBJ4W+xAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYHP1TI9GmpqbG0VzyOz6/MTMlJcX27K/nuvHYuD3fzbOTH9l8N88eDfl+v9+R3HB5rPZ8Lv80FwwGlZSUpEAgoMTERNvzKyoqVFJSYnsuAABoWX5+vjIzM23NDKcvcAWpHeLj4yVJeXl5Sk5Otj2/srJSPp+P/BPkl5aWqq6uztbsjIwM5eTkOJL99Xy3H3s35rt5dvIjm+/m2aMhv7a2Vl6vVzExMbZnh4OCFIasrCylpaU5ku3z+cg/QX55ebkjl15zcnIcy27Md/uxd2u+m2cnP7L5bp7d7fl+v19er9f23HDxJm0AAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADF0iPYCb1NTUOJpLftv5KSkptmc3ZjqR/fVctx97N+a7eXbyI5vv5tmjId/v9zuSGy6PZVlWpIc4VcFgUElJSQoEAkpMTLQ9v6KiQiUlJbbnAgCAluXn5yszM9PWzHD6AleQ2iE+Pl6SlJeXp+TkZNvzKysr5fP5yI9AvptnJz9y2eRHd76bZ4+G/NraWnm9XsXExNieHQ4KUhiysrKUlpbmSLbP5yM/Qvlunp38yGWTH935bp7d7fl+v19er9f23HDxJm0AAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAyOFaQFCxaof//+iouL08iRI/XJJ5+0uf6yZcs0ePBgxcXFaejQoXrnnXecGg0AAKBNjhSk119/XYWFhZo9e7Y2bdqk4cOHa9y4cdq7d2+L669du1ZTpkzRjBkz9Nlnn2nSpEmaNGmStmzZ4sR4AAAAbXKkID355JO66667lJ+frwsvvFDPP/+84uPjtWjRohbXf+aZZzR+/Hj9+Mc/1gUXXKBHHnlEl1xyiebPn+/EeAAAAG3qYnfg0aNHtXHjRhUVFYWWderUSWPHjtW6deta3GbdunUqLCxssmzcuHFasWJFi+vX19ervr4+dD8YDJ764O1QU1PjaC75HZ/v5tnJj1w2+dGd7+bZoyHf7/c7khsuj2VZlp2Be/bsUZ8+fbR27Vrl5uaGlt9///0qKyvThg0bmm3TtWtXvfTSS5oyZUpo2XPPPafi4mJVV1c3W3/OnDkqLi5utjwQCCgxMdGmPfmn/fv3czULAIAOVFhYqISEBFszg8GgkpKS2tUXbL+C1BGKioqaXHEKBoPKyMhw7Ov16NFDM2fO1NGjRx37GseOHVNMTAz5Ech38+zkRy6b/OjOd/Ps0ZB/9tln216OwmV7QUpJSVHnzp2bXfmprq5Wampqi9ukpqaGtX5sbKxiY2PtGbidevTo0aFfDwAARI7tb9Lu2rWrsrOzVVpaGlrW0NCg0tLSJi+5fV1ubm6T9SVp9erVra4PAADgJEdeYissLNS0adP0jW98Qzk5OXr66ad16NAh5efnS5KmTp2qPn36aN68eZKke++9V1dddZV+8Ytf6Nprr9Vrr70mn8+n//7v/3ZiPAAAgDY5UpBuvfVW1dTU6OGHH1ZVVZVGjBihVatWqXfv3pKkyspKder0z4tXo0aN0tKlS/XTn/5UP/nJT5SVlaUVK1ZoyJAhTowHAADQJts/xRYJ4bwrHQAAnJnC6Qv8LTYAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMFCQAAAADBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAxdIj2AHSzLkiQFg8EITwIAAE5XjT2hsTe0JSoK0oEDByRJGRkZEZ4EAACc7g4cOKCkpKQ21/FY7alRp7mGhgbt2bNHCQkJ8ng8tucHg0FlZGRo165dSkxMtD3/dHMm7e+ZtK/SmbW/Z9K+SuxvNDuT9lVydn8ty9KBAweUnp6uTp3afpdRVFxB6tSpk/r27ev410lMTDwjvjkbnUn7eybtq3Rm7e+ZtK8S+xvNzqR9lZzb3xNdOWrEm7QBAAAMFCQAAAADBakdYmNjNXv2bMXGxkZ6lA5xJu3vmbSv0pm1v2fSvkrsbzQ7k/ZVOn32NyrepA0AAGAnriABAAAYKEgAAAAGChIAAICBggQAAGCgIP3DggUL1L9/f8XFxWnkyJH65JNP2lx/2bJlGjx4sOLi4jR06FC98847HTTpqZk3b54uvfRSJSQkqFevXpo0aZK2bt3a5jZLliyRx+NpcouLi+ugiU/enDlzms09ePDgNrdx63mVpP79+zfbX4/Ho4KCghbXd9t5/eCDD3TdddcpPT1dHo9HK1asaPK4ZVl6+OGHlZaWpm7dumns2LHatm3bCXPDfe53hLb29dixY3rggQc0dOhQnXXWWUpPT9fUqVO1Z8+eNjNP5vnQUU50bqdPn95s9vHjx58w123nVlKLz2GPx6PHH3+81czT9dy25+fNkSNHVFBQoB49eujss8/WjTfeqOrq6jZzT/a5Hi4KkqTXX39dhYWFmj17tjZt2qThw4dr3Lhx2rt3b4vrr127VlOmTNGMGTP02WefadKkSZo0aZK2bNnSwZOHr6ysTAUFBVq/fr1Wr16tY8eO6ZprrtGhQ4fa3C4xMVF+vz9027lzZwdNfGouuuiiJnN/9NFHra7r5vMqSZ9++mmTfV29erUk6eabb251Gzed10OHDmn48OFasGBBi48/9thjevbZZ/X8889rw4YNOuusszRu3DgdOXKk1cxwn/sdpa19PXz4sDZt2qSHHnpImzZt0ptvvqmtW7fq+uuvP2FuOM+HjnSicytJ48ePbzL7q6++2mamG8+tpCb76Pf7tWjRInk8Ht14441t5p6O57Y9P29+9KMf6fe//72WLVumsrIy7dmzRzfccEObuSfzXD8pFqycnByroKAgdP/48eNWenq6NW/evBbXv+WWW6xrr722ybKRI0da3//+9x2d0wl79+61JFllZWWtrrN48WIrKSmp44ayyezZs63hw4e3e/1oOq+WZVn33nuvdd5551kNDQ0tPu7W82pZliXJWr58eeh+Q0ODlZqaaj3++OOhZXV1dVZsbKz16quvtpoT7nM/Esx9bcknn3xiSbJ27tzZ6jrhPh8ipaX9nTZtmjVx4sSwcqLl3E6cONG6+uqr21zHLefW/HlTV1dnxcTEWMuWLQut85e//MWSZK1bt67FjJN9rp+MM/4K0tGjR7Vx40aNHTs2tKxTp04aO3as1q1b1+I269ata7K+JI0bN67V9U9ngUBAknTOOee0ud7BgwfVr18/ZWRkaOLEifryyy87YrxTtm3bNqWnp+vcc8/V7bffrsrKylbXjabzevToUZWUlOiOO+5o8w84u/W8mrZv366qqqom5y8pKUkjR45s9fydzHP/dBUIBOTxeNS9e/c21wvn+XC6WbNmjXr16qVBgwbpnnvu0f79+1tdN1rObXV1td5++23NmDHjhOu64dyaP282btyoY8eONTlPgwcPVmZmZqvn6WSe6yfrjC9I+/bt0/Hjx9W7d+8my3v37q2qqqoWt6mqqgpr/dNVQ0ODZs2apcsuu0xDhgxpdb1BgwZp0aJFeuutt1RSUqKGhgaNGjVKu3fv7sBpwzdy5EgtWbJEq1at0sKFC7V9+3ZdccUVOnDgQIvrR8t5laQVK1aorq5O06dPb3Udt57XljSeo3DO38k8909HR44c0QMPPKApU6a0+Yc9w30+nE7Gjx+vl19+WaWlpfr5z3+usrIyTZgwQcePH29x/Wg5ty+99JISEhJO+JKTG85tSz9vqqqq1LVr12bF/kQ/fxvXae82J6uLrWlwlYKCAm3ZsuWEr1Xn5uYqNzc3dH/UqFG64IIL9MILL+iRRx5xesyTNmHChNC/hw0bppEjR6pfv35644032vV/ZG724osvasKECUpPT291HbeeV/zTsWPHdMstt8iyLC1cuLDNdd38fLjttttC/x46dKiGDRum8847T2vWrNGYMWMiOJmzFi1apNtvv/2EH55ww7lt78+b08kZfwUpJSVFnTt3bvau+erqaqWmpra4TWpqaljrn45mzpyplStXyuv1qm/fvmFtGxMTo4svvljl5eUOTeeM7t27a+DAga3OHQ3nVZJ27typ9957T3feeWdY27n1vEoKnaNwzt/JPPdPJ43laOfOnVq9enWbV49acqLnw+ns3HPPVUpKSquzu/3cStKHH36orVu3hv08lk6/c9vaz5vU1FQdPXpUdXV1TdY/0c/fxnXau83JOuMLUteuXZWdna3S0tLQsoaGBpWWljb5v+uvy83NbbK+JK1evbrV9U8nlmVp5syZWr58ud5//30NGDAg7Izjx4/riy++UFpamgMTOufgwYOqqKhodW43n9evW7x4sXr16qVrr702rO3cel4lacCAAUpNTW1y/oLBoDZs2NDq+TuZ5/7porEcbdu2Te+995569OgRdsaJng+ns927d2v//v2tzu7mc9voxRdfVHZ2toYPHx72tqfLuT3Rz5vs7GzFxMQ0OU9bt25VZWVlq+fpZJ7rp7IDZ7zXXnvNio2NtZYsWWL9+c9/tu6++26re/fuVlVVlWVZlvUv//Iv1oMPPhha/+OPP7a6dOliPfHEE9Zf/vIXa/bs2VZMTIz1xRdfRGoX2u2ee+6xkpKSrDVr1lh+vz90O3z4cGgdc3+Li4utd99916qoqLA2btxo3XbbbVZcXJz15ZdfRmIX2u3f//3frTVr1ljbt2+3Pv74Y2vs2LFWSkqKtXfvXsuyouu8Njp+/LiVmZlpPfDAA80ec/t5PXDggPXZZ59Zn332mSXJevLJJ63PPvss9Mmtn/3sZ1b37t2tt956y/r888+tiRMnWgMGDLC++uqrUMbVV19t/fKXvwzdP9FzP1La2tejR49a119/vdW3b19r8+bNTZ7H9fX1oQxzX0/0fIiktvb3wIED1n333WetW7fO2r59u/Xee+9Zl1xyiZWVlWUdOXIklBEN57ZRIBCw4uPjrYULF7aY4ZZz256fN//6r/9qZWZmWu+//77l8/ms3NxcKzc3t0nOoEGDrDfffDN0vz3PdTtQkP7hl7/8pZWZmWl17drVysnJsdavXx967KqrrrKmTZvWZP033njDGjhwoNW1a1froosust5+++0OnvjkSGrxtnjx4tA65v7OmjUrdGx69+5tffvb37Y2bdrU8cOH6dZbb7XS0tKsrl27Wn369LFuvfVWq7y8PPR4NJ3XRu+++64lydq6dWuzx9x+Xr1eb4vfu4371NDQYD300ENW7969rdjYWGvMmDHNjkO/fv2s2bNnN1nW1nM/Utra1+3bt7f6PPZ6vaEMc19P9HyIpLb29/Dhw9Y111xj9ezZ04qJibH69etn3XXXXc2KTjSc20YvvPCC1a1bN6uurq7FDLec2/b8vPnqq6+sH/zgB1ZycrIVHx9vTZ482fL7/c1yvr5Ne57rdvD844sDAADgH8749yABAACYKEgAAAAGChIAAICBggQAAGCgIAEAABgoSAAAAAYKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABg+P9KUFqoXhSHrgAAAABJRU5ErkJggg==", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkgAAAJOCAYAAABMR/iyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARC9JREFUeJzt3Xt8VPWd//H3ACEhmoRIgCSSAEoElZtSQ4NaiFCBWgW8oxaIqF037EpTb+lP5aK71Eu9FUS3DwE1Ioor2KrFxTgEKRc7IKvYlpIsECiTENhkhosEHuT8/thmSr65kIFzMpzh9Xw85vFwZr7nnc+ZwzTvnrnEY1mWJQAAAIS0i/QAAAAAZxoKEgAAgIGCBAAAYKAgAQAAGChIAAAABgoSAACAgYIEAABgoCABAAAYKEgAAAAGChIAAICBggScBRYtWiSPxyOPx6M1a9Y0ut+yLGVkZMjj8ejHP/5xBCZ0zo4dO5SXl6cLL7xQcXFxSk1N1Q9+8APNmDGjwbpXXnlFixYtisyQf/dv//ZvuuGGG9S9e3d5PB7NnDmzyXUzZ84MHc8TL3FxcW07MBDFOkR6AABtJy4uTosXL9ZVV13V4PaSkhLt3r1bsbGxEZrMGaWlpbriiivUqVMn3X333erVq5f8fr82bdqkp59+WrNmzQqtfeWVV5SSkqIpU6ZEbN7HHntMqampuuyyy/Tpp5+edP38+fN17rnnhq63b9/eyfGAswoFCTiL/OhHP9LSpUv18ssvq0OHfzz9Fy9erCFDhmjfvn0RnM5+L7zwgg4ePKjNmzerZ8+eDe7bu3dvhKZq3vbt29WrVy/t27dPXbt2Pen6m2++WSkpKW0wGXD24SU24CwyceJE7d+/XytXrgzddvToUb3//vu64447mtzmueee07Bhw9SlSxd16tRJQ4YM0fvvv99oncfj0bRp0/T222+rb9++iouL05AhQ7R69WrH9udkysrK1KNHj0blSJK6desW+u9evXrp22+/VUlJSejlqhEjRoTur6mp0fTp05WRkaHY2Fj16dNHTz/9tOrq6kJrduzYIY/Ho+eee04vvPCCevbsqU6dOmn48OHasmVLq+bt1atXWPtnWZaCwaAsywprOwAnR0ECziK9evVSTk6O3nnnndBtv//97xUIBHT77bc3uc1LL72kyy67TLNnz9a///u/q0OHDrrlllv08ccfN1pbUlKi6dOn66677tLs2bO1f/9+jRkzptUFwW49e/bUrl279Pnnn7e47sUXX1SPHj3Ur18/vfXWW3rrrbf0//7f/5MkHT58WMOHD1dRUZEmTZqkl19+WVdeeaUKCwtVUFDQKOvNN9/Uyy+/rPz8fBUWFmrLli265pprVFlZafv+XXDBBUpKSlJCQoLuuusuR34GcNayAES9hQsXWpKsP/7xj9bcuXOthIQE6/Dhw5ZlWdYtt9xi5ebmWpZlWT179rSuu+66BtvWr6t39OhRq3///tY111zT4HZJliTL5/OFbtu5c6cVFxdnTZgwwYndOqktW7ZYnTp1siRZgwcPth544AFr+fLl1qFDhxqtvfTSS63hw4c3uv3JJ5+0zjnnHOuvf/1rg9sfffRRq3379lZ5ebllWZa1fft2S5LVqVMna/fu3aF1GzZssCRZP/vZz1o9d1VVlSXJmjFjRpP3v/jii9a0adOst99+23r//fetBx54wOrQoYOVlZVlBQKBVv8cAM3jDBJwlrn11lv13Xff6aOPPtKBAwf00UcfNfvymiR16tQp9N/V1dUKBAK6+uqrtWnTpkZrc3JyNGTIkND1zMxMjRs3Tp9++qmOHz9u7460wqWXXqrNmzfrrrvu0o4dO/TSSy9p/Pjx6t69u37zm9+0KmPp0qW6+uqrlZycrH379oUuo0aN0vHjxxu9hDh+/Hidf/75oevZ2dkaOnSoPvnkE9v264EHHtCvf/1r3XHHHbrpppv04osv6o033tC2bdv0yiuv2PZzgLMZb9IGzjJdu3bVqFGjtHjxYh0+fFjHjx/XzTff3Oz6jz76SE899ZQ2b96s2tra0O0ej6fR2qysrEa3XXTRRTp8+LCqqqqUmpra5M+oqKg4hT35P81lnvjz33rrLR0/flx/+tOf9NFHH+mZZ57Rfffdp969e2vUqFEtbr9t2zZ9/fXXzb5p2nyzd3OPwXvvvXeSPTk9d9xxh37+85/rs88+06OPPurozwLOBhQk4Cx0xx136N5771VFRYXGjh2rzp07N7nuiy++0A033KAf/OAHeuWVV5SWlqaYmBgtXLhQixcvtm2etLS0U97WauUblNu3b68BAwZowIABysnJUW5urt5+++2TFqS6ujr98Ic/1MMPP9zk/RdddFHYMzslIyND//u//xvpMYCoQEECzkITJkzQT3/6U61fv17vvvtus+v+8z//U3Fxcfr0008bfEfSwoULm1y/bdu2Rrf99a9/VXx8fIsfWz/xU3Vt4Xvf+54kye/3h25r6oyYJF144YU6ePDgSYtUveYeg3A/oRYuy7K0Y8cOXXbZZY7+HOBsQUECzkLnnnuu5s+frx07duj6669vdl379u3l8XgavH9ox44dWr58eZPr161bp02bNunyyy+XJO3atUsffvihxowZ0+KXGLa2fITriy++0Pe//33FxMQ0uL3+/UB9+/YN3XbOOeeopqamUcatt96qmTNn6tNPP9Xo0aMb3FdTU6Nzzz23wXdKLV++XH/7299C70P68ssvtWHDBk2fPt2mvZKqqqoaFc758+erqqpKY8aMse3nAGczChJwlpo8efJJ11x33XV6/vnnNWbMGN1xxx3au3ev5s2bpz59+ujrr79utL5///4aPXq0/vVf/1WxsbGhNwyf+I3Vbenpp5/Wxo0bdeONN2rgwIGSpE2bNunNN9/Ueeed16C0DBkyRPPnz9dTTz2lPn36qFu3brrmmmv00EMP6be//a1+/OMfa8qUKRoyZIgOHTqkb775Ru+//7527NjR4Msa+/Tpo6uuukr333+/amtr9eKLL6pLly7NvkR3orfeeks7d+7U4cOHJUmrV6/WU089JUn6yU9+Evo+p549e+q2227TgAEDFBcXpzVr1mjJkiUaPHiwfvrTn9r18AFnt0h/jA6A8078mH9LmvqY/+uvv25lZWVZsbGxVr9+/ayFCxdaM2bMsMz/+ZBk5efnW0VFRaH1l112meX1eu3enVb7wx/+YOXn51v9+/e3kpKSrJiYGCszM9OaMmWKVVZW1mBtRUWFdd1111kJCQmWpAYf+T9w4IBVWFho9enTx+rYsaOVkpJiDRs2zHruueeso0ePWpb1j4/5P/vss9avfvUrKyMjw4qNjbWuvvpq67//+79bNe/w4cNDX5dgXk58HO+55x7rkksusRISEqyYmBirT58+1iOPPGIFg8HTfswA/B+PZfEVrABOn8fjUX5+vubOnRvpUSJix44d6t27t5599lk9+OCDkR4HwGnie5AAAAAMFCQAAAADBQkAAMDAe5AAAAAMnEECAAAwUJAAAAAMUfFFkXV1ddqzZ48SEhKa/XMBAADg7GZZlg4cOKD09HS1a9fyOaKoKEh79uxR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XL927VpNnDhRU6dO1VdffaXx48dr/Pjx2rJlSxtPHr6SkhLl5+dr/fr1WrlypY4dO6Zrr71Whw4danG7xMRE+f3+0GXnzp1tNPHpufTSSxvMvWbNmmbXuvm4StIf//jHBvu6cuVKSdItt9zS7DZuOq6HDh3SoEGDNG/evCbvf+aZZ/Tyyy/r1Vdf1YYNG3TOOedo9OjROnLkSLOZ4T7320pL+3r48GFt2rRJjz/+uDZt2qQPPvhAW7du1Q033HDS3HCeD23pZMdWksaMGdNg9nfeeafFTDceW0kN9tHv92vBggXyeDy66aabWsw9E49ta37f/OxnP9Pvfvc7LV26VCUlJdqzZ49uvPHGFnNP5bl+SixY2dnZVn5+fuj68ePHrfT0dGvOnDlNrr/11lut6667rsFtQ4cOtX760586OqcT9u7da0mySkpKml2zcOFCKykpqe2GssmMGTOsQYMGtXp9NB1Xy7KsBx54wLrwwguturq6Ju9363G1LMuSZC1btix0va6uzkpNTbWeffbZ0G01NTVWbGys9c477zSbE+5zPxLMfW3Kl19+aUmydu7c2eyacJ8PkdLU/k6ePNkaN25cWDnRcmzHjRtnXXPNNS2uccuxNX/f1NTUWDExMdbSpUtDa/785z9bkqx169Y1mXGqz/VTcdafQTp69Kg2btyoUaNGhW5r166dRo0apXXr1jW5zbp16xqsl6TRo0c3u/5MFggEJEnnnXdei+sOHjyonj17KiMjQ+PGjdO3337bFuOdtm3btik9PV0XXHCB7rzzTpWXlze7NpqO69GjR1VUVKS77767xT/g7Nbjatq+fbsqKioaHL+kpCQNHTq02eN3Ks/9M1UgEJDH41Hnzp1bXBfO8+FMs2rVKnXr1k19+/bV/fffr/379ze7NlqObWVlpT7++GNNnTr1pGvdcGzN3zcbN27UsWPHGhynfv36KTMzs9njdCrP9VN11hekffv26fjx4+revXuD27t3766Kioomt6moqAhr/Zmqrq5O06dP15VXXqn+/fs3u65v375asGCBPvzwQxUVFamurk7Dhg3T7t2723Da8A0dOlSLFi3SihUrNH/+fG3fvl1XX321Dhw40OT6aDmukrR8+XLV1NRoypQpza5x63FtSv0xCuf4ncpz/0x05MgRPfLII5o4cWKLf9gz3OfDmWTMmDF68803VVxcrKefflolJSUaO3asjh8/3uT6aDm2b7zxhhISEk76kpMbjm1Tv28qKirUsWPHRsX+ZL9/69e0dptT1cHWNLhKfn6+tmzZctLXqnNycpSTkxO6PmzYMF188cV67bXX9OSTTzo95ikbO3Zs6L8HDhyooUOHqmfPnnrvvfda9f/I3Oz111/X2LFjlZ6e3uwatx5X/MOxY8d06623yrIszZ8/v8W1bn4+3H777aH/HjBggAYOHKgLL7xQq1at0siRIyM4mbMWLFigO++886QfnnDDsW3t75szyVl/BiklJUXt27dv9K75yspKpaamNrlNampqWOvPRNOmTdNHH30kr9erHj16hLVtTEyMLrvsMpWWljo0nTM6d+6siy66qNm5o+G4StLOnTv12Wef6Z577glrO7ceV0mhYxTO8TuV5/6ZpL4c7dy5UytXrmzx7FFTTvZ8OJNdcMEFSklJaXZ2tx9bSfriiy+0devWsJ/H0pl3bJv7fZOamqqjR4+qpqamwfqT/f6tX9PabU7VWV+QOnbsqCFDhqi4uDh0W11dnYqLixv8v+sT5eTkNFgvSStXrmx2/ZnEsixNmzZNy5Yt0+eff67evXuHnXH8+HF98803SktLc2BC5xw8eFBlZWXNzu3m43qihQsXqlu3brruuuvC2s6tx1WSevfurdTU1AbHLxgMasOGDc0ev1N57p8p6svRtm3b9Nlnn6lLly5hZ5zs+XAm2717t/bv39/s7G4+tvVef/11DRkyRIMGDQp72zPl2J7s982QIUMUExPT4Dht3bpV5eXlzR6nU3mun84OnPWWLFlixcbGWosWLbL+9Kc/Wffdd5/VuXNnq6KiwrIsy/rJT35iPfroo6H1f/jDH6wOHTpYzz33nPXnP//ZmjFjhhUTE2N98803kdqFVrv//vutpKQka9WqVZbf7w9dDh8+HFpj7u+sWbOsTz/91CorK7M2btxo3X777VZcXJz17bffRmIXWu3nP/+5tWrVKmv79u3WH/7wB2vUqFFWSkqKtXfvXsuyouu41jt+/LiVmZlpPfLII43uc/txPXDggPXVV19ZX331lSXJev75562vvvoq9MmtX/7yl1bnzp2tDz/80Pr666+tcePGWb1797a+++67UMY111xj/frXvw5dP9lzP1Ja2tejR49aN9xwg9WjRw9r8+bNDZ7HtbW1oQxzX0/2fIiklvb3wIED1oMPPmitW7fO2r59u/XZZ59Zl19+uZWVlWUdOXIklBENx7ZeIBCw4uPjrfnz5zeZ4ZZj25rfN//0T/9kZWZmWp9//rnl8/msnJwcKycnp0FO3759rQ8++CB0vTXPdTtQkP7u17/+tZWZmWl17NjRys7OttavXx+6b/jw4dbkyZMbrH/vvfesiy66yOrYsaN16aWXWh9//HEbT3xqJDV5WbhwYWiNub/Tp08PPTbdu3e3fvSjH1mbNm1q++HDdNttt1lpaWlWx44drfPPP9+67bbbrNLS0tD90XRc63366aeWJGvr1q2N7nP7cfV6vU3+263fp7q6Ouvxxx+3unfvbsXGxlojR45s9Dj07NnTmjFjRoPbWnruR0pL+7p9+/Zmn8derzeUYe7ryZ4PkdTS/h4+fNi69tprra5du1oxMTFWz549rXvvvbdR0YmGY1vvtddeszp16mTV1NQ0meGWY9ua3zffffed9c///M9WcnKyFR8fb02YMMHy+/2Nck7cpjXPdTt4/v7DAQAA8Hdn/XuQAAAATBQkAAAAAwUJAADAQEECAAAwUJAAAAAMFCQAAAADBQkAAMBAQQIAADBQkAAAAAwUJAAAAAMFCQAAwEBBAgAAMPx/Af75AVCE8EkAAAAASUVORK5CYII=", "text/plain": [ "
" ] From d738930b3264f83c5c53e55fef82513687586cff Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Fri, 20 Mar 2026 07:48:22 -0300 Subject: [PATCH 12/12] git commit -m "chore: release v0.3.0" --- CHANGELOG.md | 51 +++++++++++++++++++++++++++ dissmodel/visualization/raster_map.py | 15 +++++--- pyproject.toml | 2 +- tests/raster/test_raster_map.py | 14 ++++---- 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aadc511..91ecc6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `_past` columns are managed automatically. +- `SyncRasterModel` — raster analogue of `SyncSpatialModel`. Copies each array in + `land_use_types` to `_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 diff --git a/dissmodel/visualization/raster_map.py b/dissmodel/visualization/raster_map.py index 54399c9..8e6207c 100644 --- a/dissmodel/visualization/raster_map.py +++ b/dissmodel/visualization/raster_map.py @@ -200,9 +200,16 @@ def setup( # ── rendering ───────────────────────────────────────────────────────────── def _render(self, step: float) -> matplotlib.figure.Figure: - plt.close("all") - fig, ax = plt.subplots(figsize=self.figsize) - self.fig, self.ax = fig, ax + # each RasterMap owns its own figure — reuse across steps + # so multiple instances show as separate windows simultaneously + if not hasattr(self, "_fig") or self._fig is None \ + or not plt.fignum_exists(self._fig.number): + self._fig, self._ax = plt.subplots(figsize=self.figsize) + else: + self._fig.clf() + self._ax = self._fig.add_subplot(1, 1, 1) + + fig, ax = self._fig, self._ax arr = self.backend.arrays.get(self.band) if arr is None: @@ -335,4 +342,4 @@ def execute(self) -> None: plt.pause(self.interval) end_time = getattr(self.env, "end_time", step) if step == end_time: - plt.show() + plt.show() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 90ece03..35a1d2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dissmodel" -version = "0.2.1" +version = "0.3.0" description = "Discrete Spatial Modeling framework for raster and vector simulations" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" diff --git a/tests/raster/test_raster_map.py b/tests/raster/test_raster_map.py index febcfc9..e8120ef 100644 --- a/tests/raster/test_raster_map.py +++ b/tests/raster/test_raster_map.py @@ -43,18 +43,18 @@ class TestInstantiation: def test_categorical_mode(self, backend): env = Environment(start_time=1, end_time=1) color_map = {i: f"#{i:02x}{i:02x}{i:02x}" for i in range(1, 10)} - m = RasterMap(backend=backend, band="uso", color_map=color_map) + m = RasterMap(backend=backend, band="uso", save_frames=True, color_map=color_map) assert m is not None def test_continuous_mode(self, backend): env = Environment(start_time=1, end_time=1) - m = RasterMap(backend=backend, band="alt", cmap="terrain", + m = RasterMap(backend=backend, band="alt", save_frames=True, cmap="terrain", vmin=0.0, vmax=1.0) assert m is not None def test_default_band(self, backend): env = Environment(start_time=1, end_time=1) - m = RasterMap(backend=backend, band="uso") + m = RasterMap(backend=backend, save_frames=True, band="uso") assert m.band == "uso" @@ -68,12 +68,12 @@ def test_runs_without_display(self, backend): """RasterMap must not raise when no display is available.""" env = Environment(start_time=1, end_time=3) color_map = {i: "blue" for i in range(1, 10)} - RasterMap(backend=backend, band="uso", color_map=color_map) + RasterMap(backend=backend, band="uso", save_frames=True,color_map=color_map) env.run() # must complete without error def test_continuous_runs_without_display(self, backend): env = Environment(start_time=1, end_time=3) - RasterMap(backend=backend, band="alt", cmap="viridis", + RasterMap(backend=backend, band="alt",save_frames=True, cmap="viridis", vmin=0.0, vmax=1.0) env.run() @@ -94,7 +94,7 @@ def execute(self): self.backend.arrays["uso"] += 1 env = Environment(start_time=1, end_time=3) IncModel(backend=backend) - RasterMap(backend=backend, band="uso") + RasterMap(backend=backend,save_frames=True, band="uso") env.run() # model ran 3 steps — original values 1..9, now 4..12 @@ -104,6 +104,6 @@ def test_map_does_not_mutate_backend(self, backend): """RasterMap must only read arrays, never write to them.""" original = backend.get("uso").copy() env = Environment(start_time=1, end_time=3) - RasterMap(backend=backend, band="uso") + RasterMap(backend=backend,save_frames=True, band="uso") env.run() np.testing.assert_array_equal(backend.get("uso"), original)