Skip to content
Merged

Sync #91

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 99 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,107 @@
# Changelog

All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [0.2.1] - 2026-03

### Changed
- `parse_idx` now returns `GridPos(row, col)` namedtuple instead of a plain tuple,
eliminating the `(col, row)` vs `(row, col)` ambiguity. Tuple unpacking remains
fully compatible — no breaking change for existing callers.
- `regular_grid()` renamed to `vector_grid()`. Old name kept as a `DeprecationWarning`
alias and will be removed in v0.3.0.

### Added
- `GridPos` namedtuple exported from `dissmodel.geo.vector.regular_grid`.

### Fixed
- `RasterModel.setup()` and `RasterCellularAutomaton.setup()` no longer accept
`**kwargs`, resolving a salabim incompatibility that raised
`TypeError: parameter 'kwargs' not allowed`.

---

## [0.2.0] - 2026-03

### Added
- Raster backend architecture
- Examples for raster simulations
- CLI examples

### Improved
- Package organization
- Documentation
#### Raster substrate
- `RasterBackend` — named NumPy array store with vectorized spatial operations:
`shift2d`, `focal_sum`, `focal_sum_mask`, `neighbor_contact`, `snapshot`.
- `RasterModel` — base class for raster push models, providing `self.backend`,
`self.shape`, `self.shift`, and `self.dirs` (Moore neighbourhood).
- `RasterCellularAutomaton` — vectorized CA base class; `rule(arrays) → dict`
replaces per-cell iteration.
- `raster_grid()` / `make_raster_grid()` — `RasterBackend` factory.
- `DIRS_MOORE` and `DIRS_VON_NEUMANN` — neighbourhood direction constants.
- `RasterMap` — visualization component supporting categorical (`color_map`) and
continuous (`cmap`) modes; renders to Streamlit, Jupyter, interactive window,
or headless PNG frames.

#### Vector substrate
- `SpatialModel` — GeoDataFrame-based push model with `create_neighborhood()`,
`neighs_id()`, `neighs()`, and `neighbor_values()`.
- `vector_grid()` — replaces `regular_grid()` as the canonical grid factory name.

#### Examples and benchmarks
- `GameOfLifeRaster` — Conway's Game of Life on raster substrate.
- `FireModelRaster` — forest fire spread on raster substrate.
- `benchmark_game_of_life.py` — vector vs raster benchmark with exact cell-by-cell
validation; supports `--steps`, `--sizes`, `--no-validation` flags.
- `benchmark_raster_vs_vector.py` — flood model benchmark at realistic workload.
- CLI examples updated.

#### Tests
- Full test suite: `tests/vector/`, `tests/raster/`, `tests/integration/`.
- `tests/integration/test_game_of_life.py` — exact cell-by-cell validation across
5×5, 10×10, 20×20 grids and 5 seeds.
- `tests/integration/test_flood_model.py` — cross-substrate equivalence with 95%
match threshold.

### Changed
- Package reorganized: `dissmodel.geo.vector.*` and `dissmodel.geo.raster.*`
replace the flat `dissmodel.geo.*` layout.
- Documentation migrated to MkDocs Material with separate API reference pages for
vector and raster substrates.

### Fixed
- Import paths in examples
- Import paths corrected across all examples after package reorganization.
- `celullar_automaton.py` filename typo fixed → `cellular_automaton.py`.

### Performance
Benchmarks on Conway's Game of Life (10 steps, Python 3.12, NumPy):

| Grid | Cells | Raster (ms/step) | Vector (ms/step) | Speedup |
|-----:|------:|-----------------:|-----------------:|--------:|
| 10×10 | 100 | 0.15 | 30.11 | 206× |
| 50×50 | 2,500 | 0.20 | 647.22 | 3,164× |
| 100×100 | 10,000 | 0.60 | 2,715.16 | 4,491× |
| 1,000×1,000 | 1,000,000 | 25.85 | — | — |

---

## [0.1.5] - 2026-02

### Added
- JOSS submission at this version.

### Fixed
- Minor documentation and packaging fixes.

---

## [0.1.0] - 2025

### Added
- Initial release.
- `CellularAutomaton` and `SpatialModel` on GeoDataFrame substrate.
- `regular_grid()`, `fill()`, `FillStrategy`.
- System Dynamics models: `SIR`, `PredatorPrey`, `PopulationGrowth`, `Lorenz`, `Coffee`.
- Cellular Automata models: `GameOfLife`, `FireModel`, `FireModelProb`, `Snow`, `Growth`,
`Propagation`, `Anneal`.
- Salabim integration via `dissmodel.core.Environment` and `Model`.
- Streamlit examples.
1 change: 1 addition & 0 deletions dissmodel/visualization/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dissmodel.visualization.map import Map
from dissmodel.visualization.raster_map import RasterMap
from dissmodel.visualization.chart import Chart, track_plot
from dissmodel.visualization.widgets import display_inputs
143 changes: 142 additions & 1 deletion docs/api/core.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,145 @@
# Core

The `dissmodel.core` module provides the simulation clock and execution lifecycle,
built on top of [Salabim](https://www.salabim.org/)'s discrete event engine.

All models and visualization components must be instantiated **after** the
`Environment` — they register themselves automatically on creation.

```
Environment → Model → Visualization → env.run()
↑ ↑ ↑ ↑
first second third fourth
```

## Usage

```python
from dissmodel.core import Environment, Model

env = Environment(start_time=1, end_time=10)

class MyModel(Model):
def setup(self):
pass

def execute(self):
print(f"step {self.env.now()}")

MyModel()
env.run()
```

## Object-Oriented Modeling

Object-oriented modeling is a core feature of DisSModel, inherited directly from
Python's class system. Just as TerraME defines agents as objects with encapsulated
attributes and behaviours, DisSModel uses class inheritance to build structured,
reusable, and modular models.

Every model is a subclass of `Model`, which guarantees automatic registration with
the active `Environment`. This means the simulation clock, the execution lifecycle,
and any visualization components are wired together without any boilerplate.

```python
from dissmodel.core import Model, Environment

class SIR(Model):

def setup(self, susceptible=9998, infected=2, recovered=0,
duration=2, contacts=6, probability=0.25):
self.susceptible = susceptible
self.infected = infected
self.recovered = recovered
self.duration = duration
self.contacts = contacts
self.probability = probability

def execute(self):
total = self.susceptible + self.infected + self.recovered
alpha = self.contacts * self.probability
new_inf = self.infected * alpha * (self.susceptible / total)
new_rec = self.infected / self.duration
self.susceptible -= new_inf
self.infected += new_inf - new_rec
self.recovered += new_rec
```

Instantiation is clean and parametric:

```python
env = Environment(end_time=30)
SIR(susceptible=9998, infected=2, recovered=0,
duration=2, contacts=6, probability=0.25)
env.run()
```

!!! tip "Why subclass Model?"
- **Automatic clock integration** — `self.env.now()` is always available inside `execute()`.
- **Encapsulation** — each model owns its state; multiple instances can run in the same environment independently.
- **Extensibility** — override `setup()` to add parameters, `execute()` to define the transition rule. Nothing else is required.
- **Composability** — models can read each other's state, enabling coupled CA + SysDyn simulations within a single `env.run()`.



Each model can define its own `start_time` and `end_time`, independent of the
environment interval. This allows different parts of a simulation to be active
at different periods within the same run.

```python
from dissmodel.core import Model, Environment

class ModelA(Model):
def execute(self):
print(f"[A] t={self.env.now()}")

class ModelB(Model):
def execute(self):
print(f"[B] t={self.env.now()}")

class ModelC(Model):
def execute(self):
print(f"[C] t={self.env.now()}")

env = Environment(start_time=2010, end_time=2016)

ModelA(start_time=2012) # active from 2012 to end
ModelB(end_time=2013) # active from start to 2013
ModelC() # active throughout

env.run()
```

Expected output:

```
Running from 2010 to 2016 (duration: 6)
[B] t=2010.0
[C] t=2010.0
[B] t=2011.0
[C] t=2011.0
[A] t=2012.0
[B] t=2012.0
[C] t=2012.0
[A] t=2013.0
[C] t=2013.0
[A] t=2014.0
[C] t=2014.0
[A] t=2015.0
[C] t=2015.0
[A] t=2016.0
[C] t=2016.0
```

!!! note
Models with no `start_time` / `end_time` inherit the environment's interval.
Models are synchronised — all active models execute at each time step before
the clock advances.

---

## API Reference

::: dissmodel.core.Environment
::: dissmodel.core.Model

::: dissmodel.core.Model
Loading
Loading