diff --git a/README.md b/README.md
index fe1ec75..2e5886e 100644
--- a/README.md
+++ b/README.md
@@ -1,335 +1,281 @@
+# DisSModel
-# DisSModel π
+**A Python framework for spatially explicit dynamic modeling, with
+[TerraME](http://www.terrame.org/) as its conceptual basis.**
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/)
[](https://pypi.org/project/dissmodel/)
-[](https://github.com/DisSModel/dissmodel/actions)
-[](https://github.com/DisSModel)
+[](https://pepy.tech/projects/dissmodel)
[](https://doi.org/10.5281/zenodo.23246106)
-[](https://joss.theoj.org/papers/46522bc30d2dbec6b509d2dc487170ec) [](https://pepy.tech/projects/dissmodel)
-
-> *"Science should not need to be rewritten to go into production."*
-> *(A ciΓͺncia nΓ£o deve ser reescrita para ir para a produΓ§Γ£o.)*
-
----
-
-## π Research Trajectory
-
-**DisSModel did not emerge from a blank slate.** It is the current expression of a research agenda that began in 2001 with an undergraduate thesis on geographic data interoperability using XML and open standards β a time when the central question was already forming:
-
-> *How can geospatial models be built so that others can understand, reuse, and trust them?*
-
-| Period | Project | Contribution to the Agenda |
-|--------|---------|---------------------------|
-| **2001β2002** | Terra Translator (XML, ontologies) | Foundation: geographic data needs semantics and open standards |
-| **2005** | TerraHS (Haskell + GIS) | Vision: scientific models as verifiable, executable artifacts |
-| **2007β2010** | TerraME / LuccME (INPE) | Maturity: spatially explicit dynamic models as scientific objects |
-| **2015β2024** | DbCells, Linked Data, QGIS plugins | Infrastructure: reproducibility demands rich metadata and federated access |
-| **2024β2026** | **DisSModel** (Python, FAIR, cloud-native) | Synthesis: same code runs from Jupyter to distributed cluster |
-
-Three principles unite this trajectory:
-1. π **Openness as method** β open source and open data as conditions for scientific validation.
-2. π§© **Interoperability as architecture** β systems designed to communicate, avoiding silos.
-3. β»οΈ **Reproducibility as requirement** β publishing conditions for re-execution, not just results.
-
-DisSModel is the synthesis: a Python-native, FAIR-aligned, cloud-ready simulation framework where the same scientific code runs unchanged from a Jupyter notebook to a distributed production cluster.
-
----
-
-## π― About
-
-**DisSModel** is a modular Python framework for spatially explicit dynamic simulation models. Developed by the [LambdaGeo](https://github.com/DisSModel) group at the Federal University of MaranhΓ£o (UFMA), it provides the simulation layer that connects domain models (LUCC, coastal dynamics) to a reproducible execution environment.
-
-| INPE / TerraME Ecosystem | LambdaGeo Ecosystem | Role |
-|--------------------------|---------------------|------|
-| **TerraME** | `dissmodel` | Generic framework for dynamic spatial modeling |
-| **LUCCME** | `DisSLUCC` | LUCC domain models built on dissmodel |
-| β | `brmangue-dissmodel` | Coastal domain models (BR-MANGUE) built on dissmodel |
-| **TerraLib** | `geopandas` / `rasterio` | Geographic data handling |
-
----
-
-## π Key Features
-
-- **Dual substrate** β same model logic runs on vector (`GeoDataFrame`) and raster (`RasterBackend`/NumPy).
-- **Lightweight scheduler** β pure-Python time-stepped engine; models auto-register at instantiation, run `setup` once and receive each clock tick via `pre_execute / execute / post_execute` lifecycle hooks.
-- **Executor pattern** β strict separation between science (models) and infrastructure (I/O, CLI, reproducible execution).
-- **Experiment tracking** β every run generates an immutable `ExperimentRecord` with SHA-256 checksums, TOML snapshot, and full provenance.
-- **Storage-agnostic I/O** β `dissmodel.io` handles local paths and `s3://` URIs transparently.
-- **Platform-ready contract** β the `ModelExecutor` interface is designed so the same model code can later run on the [DisSModel Platform](#-roadmap-dissmodel-platform), a separate project under development.
-
----
-
-## π Architecture
-
-```mermaid
-flowchart TB
- subgraph science["Science layer: Model"]
- M["FloodModel, MangroveModel, AllocationClueLike, ...
only math, geometry and time"]
- end
- subgraph infra["Infrastructure layer: ModelExecutor"]
- E["BrmangueRasterExecutor, LuccContinuousExecutor, ...
only URIs (local or s3://), column_map, parameters"]
- end
- subgraph core_modules["Core modules"]
- direction LR
- core["dissmodel.core
Environment, Model, SpatialModel"]
- geo["dissmodel.geo
RasterBackend, neighborhoods"]
- exe["dissmodel.executor
ModelExecutor, ExperimentRecord"]
- io["dissmodel.io
load_dataset, save_dataset"]
- viz["dissmodel.visualization
Map, RasterMap, Chart"]
- end
- E -- "loads data, builds and runs" --> M
- M --> core
- M --> geo
- E --> exe
- E --> io
- M -. "optional" .-> viz
-```
-
----
-
-## π Quick Start
-
-### 1. Install
+[](https://joss.theoj.org/papers/46522bc30d2dbec6b509d2dc487170ec)
+
+DisSModel is the core of an ecosystem built at the
+[LambdaGeo](https://github.com/LambdaGeo) group (UFMA, Brazil). It is an
+independent, unofficial Python alternative to TerraME: the concepts are the
+same (a clock, spatial substrates, models that run step by step), and the
+stack is GeoPandas and NumPy instead of TerraLib and Lua. Models for cellular
+automata, system dynamics, agents and land use and cover change (LUCC) live in
+companion packages that build on this one.
+
+Three ideas run through the design:
+
+- **One clock, one lifecycle.** A model is a `Model` registered in an
+ `Environment`. It runs `setup` once and then receives every tick through
+ `pre_execute`, `execute` and `post_execute`.
+- **Two substrates, one way of writing models.** The vector substrate
+ (`GeoDataFrame`) fits irregular geometries and GIS joins; the raster
+ substrate (`RasterBackend`, NumPy) is vectorised and fits large grids.
+- **Reproducibility by design.** Science (the `Model`) is kept apart from
+ infrastructure (the `ModelExecutor`: input and output, command line,
+ provenance). Every run through an executor leaves an `ExperimentRecord` with
+ SHA-256 checksums, the resolved TOML specification and the code version.
+
+Status: version 0.6.6, on PyPI. A paper describing it is under review at the
+[Journal of Open Source Software](https://joss.theoj.org/papers/46522bc30d2dbec6b509d2dc487170ec).
+
+## Installation
```bash
-pip install dissmodel
-
-# Or latest development version
-pip install "git+https://github.com/DisSModel/dissmodel.git@main"
+pip install dissmodel # Python 3.10+
+pip install "dissmodel[s3]" # s3:// URIs (MinIO, AWS S3, ...)
+pip install "dissmodel[xarray]" # NetCDF and Zarr input and output
+pip install "dissmodel[viz]" # ipywidgets, for notebooks
```
-### 2. Write a Model
+## A model in a few lines
+
+A forest fire on a 100 Γ 100 raster, with one ignition at the centre. The
+model reads the state at the start of the step (`state_past`, the equivalent of
+TerraME's `cell.past`) and writes the new one:
```python
-# forest_fire_model.py
+import numpy as np
from dissmodel.core import Environment
-from dissmodel.geo import SpatialModel, vector_grid
+from dissmodel.geo import SyncRasterModel, raster_grid
-# 10x10 grid where every cell starts as forest
-gdf = vector_grid(dimension=(10, 10), resolution=1.0, attrs={"state": "forest"})
+FOREST, BURNING, BURNED = 0, 1, 2
-class ForestFireModel(SpatialModel):
- def setup(self, prob_spread=0.3):
+
+class ForestFire(SyncRasterModel):
+ def setup(self, backend, prob_spread=0.4, seed=42):
+ super().setup(backend)
+ self.land_use_types = ["state"] # bands kept as state_past
self.prob_spread = prob_spread
+ self.rng = np.random.default_rng(seed)
+
+ def execute(self): # once per step: only maths, no I/O
+ past = self.backend.get("state_past")
+ burning_neighbours = self.backend.focal_sum_mask(past == BURNING)
+ catch = (past == FOREST) & (
+ self.rng.random(self.shape) < 1 - (1 - self.prob_spread) ** burning_neighbours
+ )
+ state = past.copy()
+ state[catch] = BURNING
+ state[past == BURNING] = BURNED
+ self.backend.arrays["state"] = state
+
- def execute(self):
- # Called every step β only math here, no I/O
- burning = self.gdf["state"] == "burning"
- # ... apply spread logic ...
+if __name__ == "__main__":
+ grid = raster_grid(rows=100, cols=100, attrs={"state": FOREST})
+ grid.arrays["state"][50, 50] = BURNING
+
+ env = Environment(start_time=0, end_time=60) # the Environment comes first
+ ForestFire(backend=grid, prob_spread=0.4) # models register themselves
+ env.run()
-env = Environment(end_time=50)
-ForestFireModel(gdf=gdf, prob_spread=0.4)
-env.run()
+ print(f"{(grid.get('state') == BURNED).mean():.0%} of the forest burned")
```
-### 3. Wrap an Executor (for CLI + Provenance)
+The same seed gives the same run. To see the model while it runs, add a
+`RasterMap(backend=grid, band="state")` before `env.run()`. The vector version
+of the same idea uses `SpatialModel` and `vector_grid`; see the
+[documentation](https://dissmodel.github.io/dissmodel/) for both.
+
+## From a script to a reproducible experiment
+
+An executor wraps a model with everything that is not science: where the data
+comes from, where the result goes and what is recorded. The model stays
+untouched. Save the model above as `forest_fire_model.py` (the lines that build
+the grid and call `env.run()` go under `if __name__ == "__main__":`), and wrap
+it:
```python
# my_executor.py
+from dissmodel.core import Environment
from dissmodel.executor import ExperimentRecord, ModelExecutor
from dissmodel.executor.cli import run_cli
from dissmodel.io import load_dataset, save_dataset
-from forest_fire_model import ForestFireModel # the model from step 1
+from forest_fire_model import ForestFire
+
class ForestFireExecutor(ModelExecutor):
name = "forest_fire"
- output_suffix = ".gpkg" # file type used when --output is a directory
+ output_suffix = ".tif" # file type used when --output is a directory
def load(self, record: ExperimentRecord):
- gdf, checksum = load_dataset(record.source.uri)
+ (backend, _meta), checksum = load_dataset(record.source.uri)
record.source.checksum = checksum
- return gdf
+ return backend
def run(self, data, record: ExperimentRecord):
- from dissmodel.core import Environment
- env = Environment(end_time=record.parameters.get("end_time", 50))
- ForestFireModel(gdf=data, **record.parameters)
+ env = Environment(end_time=record.parameters.get("end_time", 60))
+ ForestFire(backend=data, prob_spread=record.parameters.get("prob_spread", 0.4))
env.run()
return data
def save(self, result, record: ExperimentRecord) -> ExperimentRecord:
- uri = record.output_path or "output.gpkg"
- checksum = save_dataset(result, uri)
+ uri = record.output_path or "output.tif"
+ record.output_sha256 = save_dataset(result, uri)
record.output_path = uri
- record.output_sha256 = checksum
record.status = "completed"
return record
+
if __name__ == "__main__":
run_cli(ForestFireExecutor)
```
-### 4. Run via CLI
+Parameters come from a TOML file and can be overridden on the command line:
-```bash
-# Execute a simulation
-python my_executor.py run \
- --input data/forest.gpkg \
- --output data/result.gpkg \
- --param end_time=50 \
- --toml model.toml
-
-# Validate data contract without running
-python my_executor.py validate --input data/forest.gpkg
-
-# Show resolved parameters
-python my_executor.py show --toml model.toml
+```toml
+# model.toml
+[model.parameters]
+end_time = 60
+prob_spread = 0.4
```
-### 5. Cloud storage (s3://)
-
-Any `s3://bucket/key` URI works transparently anywhere dissmodel accepts a
-path β the CLI's `--input`/`--output`, `load_dataset`/`save_dataset`,
-`load_geotiff`/`save_geotiff` β so a model can read or write straight from
-object storage with no code changes:
-
```bash
-python my_executor.py run \
- --input s3://forest-data/scenes/forest.gpkg \
- --output s3://forest-data/results/result.gpkg \
- --toml model.toml
+python my_executor.py run --input forest.tif --output result.tif \
+ --toml model.toml --param prob_spread=0.5
+python my_executor.py validate --input forest.tif # check the data, do not run
+python my_executor.py show --toml model.toml # show the resolved parameters
```
-This is a standalone data-access feature, not tied to the rest of the
-`dissmodel-platform` stack β it works against any S3-compatible endpoint:
-a bare MinIO server you run yourself, AWS S3, or anything else that speaks
-the S3 protocol.
-
-Install the extra and configure the endpoint through environment variables:
-
-```bash
-pip install dissmodel[s3]
-
-export MINIO_ENDPOINT=localhost:9000 # host:port, no scheme
-export MINIO_ACCESS_KEY=your-access-key
-export MINIO_SECRET_KEY=your-secret-key
-export MINIO_SECURE=1 # unset/empty for plain HTTP (e.g. local dev)
-```
-
-No further setup is required β `s3://` URIs are resolved lazily, so nothing
-above is needed for runs that only use local paths.
-
----
-
-## π¦ ExperimentRecord: Reproducibility by Design
-
-Every run produces an immutable provenance record:
+The run adds a short experiment ID to the output name (`result_83bc068f.tif`;
+`--preserve-output-name` turns that off) and writes next to it a
+`result_83bc068f.record.json` and a `profiling_83bc068f.md` with the time of
+each phase (validate, load, run, save). The record has what is needed to repeat
+the run:
```json
{
- "experiment_id": "abc123",
- "model_commit": "a3f9c12",
- "code_version": "0.5.0",
- "resolved_spec": { "...TOML snapshot..." },
- "source": { "uri": "s3://...", "checksum": "e3b0c44..." },
- "artifacts": { "output": "sha256...", "profiling": "sha256..." },
- "metrics": { "time_run_sec": 2.15, "time_total_sec": 2.34 },
+ "experiment_id": "83bc068f...",
+ "model_name": "forest_fire",
+ "model_commit": "local-cli",
+ "code_version": "0.6.6",
+ "resolved_spec": { "model": { "parameters": { "end_time": 60, "prob_spread": 0.4 } } },
+ "source": { "uri": "forest.tif", "checksum": "e6079001..." },
+ "parameters": { "end_time": 60, "prob_spread": 0.5 },
+ "output_path": "result_83bc068f.tif",
+ "artifacts": { "output": "ec900273...", "profiling": "d044b85d..." },
+ "metrics": { "time_run_sec": 0.014, "time_total_sec": 0.02 },
"status": "completed"
}
```
-The `record.json` written next to every output contains everything needed to
-re-run the experiment: the input URI and its SHA-256 checksum, the resolved
-parameters (TOML + CLI overrides), per-phase timings, and the output checksum.
-
----
-
-## π Performance Telemetry
-
-Every run via the executor lifecycle generates a `profiling_{id}.md` alongside the output:
-
-| Phase | Time (s) | % Total | Memory Peak (MB) | I/O Ops |
-|-------|----------|---------|-----------------|---------|
-| **Validate** | 0.000 | 0.0% | 142 | 0 |
-| **Load** | 0.306 | 14.7% | 387 | 12 (read) |
-| **Run** | 1.025 | 49.4% | 521 | 0 |
-| **Save** | 0.746 | 35.9% | 498 | 8 (write) |
-| **Total** | **2.077** | **100%** | **521** | **20** |
-
+`model_commit` is `local-cli` for a loose script; for an executor defined in an
+installed package it records that package and its version (for example
+`disslucc==0.5.0`).
----
+### Cloud storage
-## π§© Ecosystem: Models & Examples
+Any `s3://bucket/key` URI works wherever DisSModel accepts a path: the
+`--input` and `--output` options, `load_dataset` and `save_dataset`,
+`load_geotiff` and `save_geotiff`. It talks to any S3-compatible endpoint (a
+MinIO server of your own, AWS S3, ...) and needs nothing else:
-DisSModel is a core framework. To maintain a clean and specialized environment, all simulation models and implementation examples are hosted in separate repositories within the DisSModel ecosystem.
+```bash
+pip install "dissmodel[s3]"
+export MINIO_ENDPOINT=localhost:9000 # host:port, no scheme
+export MINIO_ACCESS_KEY=your-access-key
+export MINIO_SECRET_KEY=your-secret-key
+export MINIO_SECURE=1 # unset for plain HTTP, e.g. local dev
+```
-The map below shows how the pieces fit together, and how each one relates to its counterpart in INPE's TerraME ecosystem, which DisSModel takes as its conceptual basis. It is an early snapshot: the `dissmodel` core is the most mature piece, and the other packages are still taking shape.
+## The ecosystem
-> **πΊ Repository map.** Beyond the packages, the ecosystem includes the TerraME/LuccME reference outputs, the benchmarks that check the packages against them, study cases, an end-to-end workflow and a book, hosted in the [LambdaGeo](https://github.com/LambdaGeo) organization. The **[repository map](https://dissmodel.github.io/repository-map.html)** shows how all of them connect.
+DisSModel is only the core. The models, the data preparation and the land use
+components are separate packages, so each one can be installed, tested and
+cited on its own. The map shows how they relate to their counterparts in
+INPE's TerraME ecosystem.