|
2 | 2 | layout: post |
3 | 3 | title: "Two Ways to Simulate the Same World: Raster and Vector Backends in DisSModel" |
4 | 4 | date: 2026-03-10 |
5 | | -categories: [research, simulation, python] |
6 | | -tags: [dissmodel, geospatial, numpy, geopandas, salabim, mangrove, coastal-dynamics] |
7 | 5 | categories: news |
| 6 | +tags: [dissmodel, geospatial, numpy, geopandas, salabim, mangrove, coastal-dynamics] |
8 | 7 | permalink: /blog/raster-vs-vector-dissmodel/ |
9 | 8 | author: Sergio Souza Costa |
10 | 9 | --- |
@@ -70,7 +69,45 @@ Model (salabim Component) |
70 | 69 |
|
71 | 70 | The two branches are independent. A model that inherits `SpatialModel` never touches NumPy. A model that inherits `RasterModel` never touches GeoDataFrame. But both live in the same salabim environment, share the same clock, and are registered the same way. |
72 | 71 |
|
73 | | -### The Vector Side: legibility |
| 72 | +--- |
| 73 | + |
| 74 | +## A Simple Example First: Conway's Game of Life |
| 75 | + |
| 76 | +Before getting into the coastal models, it's worth showing what this symmetry looks like for a simple cellular automaton. |
| 77 | + |
| 78 | +Conway's Game of Life has three rules: a live cell with 2 or 3 live neighbors survives; a dead cell with exactly 3 live neighbors becomes alive; all others die or stay dead. |
| 79 | + |
| 80 | +Here it is in the vector version: |
| 81 | + |
| 82 | +```python |
| 83 | +class GameOfLife(CellularAutomaton): |
| 84 | + def rule(self, idx): |
| 85 | + state = self.gdf.loc[idx, self.state_attr] |
| 86 | + neighbors = (self.neighbor_values(idx, self.state_attr) == 1).sum() |
| 87 | + if state == 1: |
| 88 | + return 1 if neighbors in (2, 3) else 0 |
| 89 | + return 1 if neighbors == 3 else 0 |
| 90 | +``` |
| 91 | + |
| 92 | +And in the raster version: |
| 93 | + |
| 94 | +```python |
| 95 | +class GameOfLife(RasterCellularAutomaton): |
| 96 | + def rule(self, arrays): |
| 97 | + state = arrays[self.state_attr] |
| 98 | + neighbors = self.backend.focal_sum_mask(state == 1) |
| 99 | + survive = (state == 1) & np.isin(neighbors, [2, 3]) |
| 100 | + born = (state == 0) & (neighbors == 3) |
| 101 | + return {self.state_attr: np.where(survive | born, 1, 0).astype(np.int8)} |
| 102 | +``` |
| 103 | + |
| 104 | +The rules are identical. The only thing that changes is the contract of `rule()`: the vector version receives a cell index and returns a single value; the raster version receives a snapshot of all arrays and returns a dict of updated arrays. |
| 105 | + |
| 106 | +For a 20×20 grid this difference barely matters. Both versions run in milliseconds. But the pattern is the same one that scales to a million cells when the domain demands it. |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +## The Vector Side: legibility |
74 | 111 |
|
75 | 112 | The vector backend uses GeoDataFrame + libpysal. Each cell is a polygon row. Neighborhoods are computed once via Queen or Rook weights and cached. The transition rule is a Python function called once per cell per step. |
76 | 113 |
|
|
0 commit comments