Geo
The dissmodel.geo module provides the spatial infrastructure for building
simulation models. It handles grid generation, neighbourhood computation, and
attribute initialization — without imposing any domain logic.
from dissmodel.geo import vector_grid, fill, FillStrategy
from dissmodel.geo.vector.neighborhood import attach_neighbors
from dissmodel.geo.raster.backend import RasterBackend
from dissmodel.geo.raster.regular_grid import raster_grid
Dual-substrate design
The module provides two independent spatial substrates. Both share the same
salabim Environment and clock — a vector model and a raster model can run
side by side in the same env.run().
| Vector | Raster | |
|---|---|---|
| Module | dissmodel.geo.vector |
dissmodel.geo.raster |
| Data structure | GeoDataFrame (GeoPandas) |
RasterBackend (NumPy 2D arrays) |
| Grid factory | vector_grid() |
raster_grid() |
| Neighbourhood | Queen / Rook (libpysal) | Moore / Von Neumann (shift2d) |
| Rule pattern | rule(idx) per cell |
rule(arrays) → dict vectorized |
| GIS integration | CRS, projections, spatial joins | rasterio I/O via load_geotiff |
| Best for | Irregular grids, real-world data | Large grids, performance-critical models |
Vector substrate
The vector substrate uses a GeoDataFrame as the spatial grid. Any model can
operate directly on real geographic data — shapefiles, GeoJSON, real CRS — with
no conversion step.
import geopandas as gpd
from dissmodel.core import Model, Environment
from dissmodel.visualization.map import Map
gdf = gpd.read_file("area.shp")
gdf.set_index("object_id", inplace=True)
env = Environment(start_time=1, end_time=20)
class ElevationModel(Model):
def setup(self, gdf, rate=0.01):
self.gdf = gdf
self.rate = rate
def execute(self):
self.gdf["alt"] += self.rate
ElevationModel(gdf=gdf, rate=0.01)
Map(gdf=gdf, plot_params={"column": "alt", "cmap": "Blues", "legend": True})
env.run()
For abstract (non-georeferenced) grids, use vector_grid():
from dissmodel.geo import vector_grid
# from dimension + resolution
gdf = vector_grid(dimension=(10, 10), resolution=1)
# from bounding box + resolution
gdf = vector_grid(bounds=(0, 0, 1000, 1000), resolution=100)
# from an existing GeoDataFrame
gdf = vector_grid(gdf=base_gdf, resolution=50)
Raster substrate
The raster substrate stores named NumPy arrays in a RasterBackend. All
operations (shift2d, focal_sum, neighbor_contact) are fully vectorized —
no Python loops over cells.
from dissmodel.geo.raster.regular_grid import raster_grid
from dissmodel.geo.raster.backend import RasterBackend
import numpy as np
backend = raster_grid(rows=100, cols=100, attrs={"state": 0, "alt": 0.0})
# read / write arrays
state = backend.get("state").copy() # snapshot — equivalent to .past in TerraME
backend.arrays["state"] = new_state
# vectorized neighbourhood operations
shifted = RasterBackend.shift2d(state, -1, 0) # northern neighbour of each cell
n_active = backend.focal_sum_mask(state == 1) # count active Moore neighbours
has_active = backend.neighbor_contact(state == 1) # bool mask: any active neighbour?
Filling grid attributes
The fill() function initialises GeoDataFrame columns from spatial data sources,
avoiding manual cell-by-cell loops.
from dissmodel.geo import fill, FillStrategy
Zonal statistics from a raster
import rasterio
with rasterio.open("altitude.tif") as src:
raster = src.read(1)
affine = src.transform
fill(
FillStrategy.ZONAL_STATS,
vectors=gdf, raster_data=raster, affine=affine,
stats=["mean", "min", "max"], prefix="alt_",
)
# → adds columns alt_mean, alt_min, alt_max to gdf
Minimum distance to features
rivers = gpd.read_file("rivers.shp")
fill(FillStrategy.MIN_DISTANCE, from_gdf=gdf, to_gdf=rivers, attr_name="dist_river")
Random sampling
fill(
FillStrategy.RANDOM_SAMPLE,
gdf=gdf, attr="land_use",
data={0: 0.7, 1: 0.3}, # 70% class 0, 30% class 1
seed=42,
)
Fixed pattern (useful for tests)
pattern = [[1, 0, 0],
[0, 1, 0],
[0, 0, 1]]
fill(FillStrategy.PATTERN, gdf=gdf, attr="zone", pattern=pattern)
Custom strategies can be registered:
from dissmodel.geo.vector.fill import register_strategy
@register_strategy("my_strategy")
def fill_my_strategy(gdf, attr, **kwargs):
...
Neighbourhood
Spatial neighbourhoods are built via attach_neighbors() or directly through
create_neighborhood() on any CellularAutomaton or SpatialModel.
from libpysal.weights import Queen, Rook, KNN
from dissmodel.geo.vector.neighborhood import attach_neighbors
# topological (Queen — edge or vertex contact)
gdf = attach_neighbors(gdf, strategy=Queen)
# topological (Rook — edge contact only)
gdf = attach_neighbors(gdf, strategy=Rook)
# distance-based (k nearest neighbours)
gdf = attach_neighbors(gdf, strategy=KNN, k=4)
# precomputed (from dict or JSON file — faster for large grids)
gdf = attach_neighbors(gdf, neighbors_dict="neighborhood.json")
| Strategy | Use case |
|---|---|
Queen |
Standard CA — cells share an edge or vertex |
Rook |
Von Neumann-style — edge contact only |
KNN |
Point data, non-contiguous polygons |
neighbors_dict |
Precomputed — skip recomputation on repeated runs |