From 9f33c5d9e216cab8bfa78791e2c5ad389d852858 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Sat, 2 May 2026 20:10:29 -0300 Subject: [PATCH 1/3] refactor(core): replace salabim with lightweight time-stepped scheduler --- dissmodel/core/environment.py | 143 ++++++++++++++------- dissmodel/core/model.py | 100 ++++++++------ dissmodel/geo/vector/cellular_automaton.py | 1 + pyproject.toml | 3 - 4 files changed, 160 insertions(+), 87 deletions(-) diff --git a/dissmodel/core/environment.py b/dissmodel/core/environment.py index 5981890..e34736d 100644 --- a/dissmodel/core/environment.py +++ b/dissmodel/core/environment.py @@ -1,16 +1,14 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any, ClassVar, Optional -import salabim as sim - -class Environment(sim.Environment): +class Environment: """ Simulation environment with support for a custom time window. - Extends :class:`salabim.Environment` with ``start_time`` and ``end_time`` - to define the simulation boundaries explicitly. + Manages the simulation clock and coordinates the execution of all + registered :class:`~dissmodel.core.Model` instances. Parameters ---------- @@ -18,10 +16,6 @@ class Environment(sim.Environment): Simulation start time, by default 0. end_time : float, optional Simulation end time. Can also be set via ``till`` in :meth:`run`. - *args : - Extra positional arguments forwarded to :class:`salabim.Environment`. - **kwargs : - Extra keyword arguments forwarded to :class:`salabim.Environment`. Examples -------- @@ -32,23 +26,71 @@ class Environment(sim.Environment): 10 """ + _current: ClassVar[Optional[Environment]] = None + def __init__( self, start_time: float = 0, end_time: Optional[float] = None, - *args: Any, - **kwargs: Any, ) -> None: - kwargs.pop("animation", None) - kwargs.pop("trace", False) - super().__init__(*args, trace=False, **kwargs) self.start_time = start_time self.end_time = end_time + self._now: float = start_time + self._models: list[Any] = [] + self._plot_metadata: dict[str, Any] = {} + Environment._current = self + + # ------------------------------------------------------------------ + # Clock + # ------------------------------------------------------------------ + + def now(self) -> float: + """ + Return the current simulation time. + + Returns + ------- + float + Current simulation time. + + Examples + -------- + >>> env = Environment(start_time=5) + >>> env.now() + 5 + """ + return self._now + + # ------------------------------------------------------------------ + # Model registration + # ------------------------------------------------------------------ + + def _register(self, model: Any) -> None: + """ + Register a model to be executed during the simulation. + + Called automatically by :class:`~dissmodel.core.Model.__init__`. + + Parameters + ---------- + model : Model + The model instance to register. + """ + self._models.append(model) + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ def run(self, till: Optional[float] = None) -> None: """ Run the simulation over the configured time window. + Executes all registered models in time-step order. On each tick, + every model whose next scheduled time is less than or equal to the + current simulation time has its :meth:`~dissmodel.core.Model.execute` + method called. The clock then advances to the nearest pending event. + Parameters ---------- till : float, optional @@ -71,45 +113,58 @@ def run(self, till: Optional[float] = None) -> None: if till is not None: self.end_time = self.start_time + till elif self.end_time is not None: - till = self.end_time - self.start_time + pass else: - raise ValueError("Provide 'till' or set 'end_time' before calling run().") - - print(f"Running from {self.start_time} to {self.end_time} (duration: {till})") - super().run(till=till) + raise ValueError( + "Provide 'till' or set 'end_time' before calling run()." + ) + + duration = self.end_time - self.start_time + print( + f"Running from {self.start_time} to {self.end_time} " + f"(duration: {duration})" + ) + + # Initialise next-execution time for every registered model + for model in self._models: + model._next_time = model.start_time + + self._now = self.start_time + + while self._now < self.end_time: + for model in self._models: + if ( + model._next_time <= self._now + and self._now < model.end_time + ): + model.execute() + model._next_time = self._now + model._step + + # Advance clock to the nearest pending event + pending = [ + m._next_time + for m in self._models + if m._next_time < self.end_time + ] + if not pending: + break + self._now = min(pending) def reset(self) -> None: """ - Clear accumulated plot data. + Reset the clock and clear accumulated plot data. - This method is called automatically at the start of :meth:`run` to - ensure charts start fresh on each simulation run. + Called automatically at the start of :meth:`run` to ensure the + environment starts fresh on each simulation run. Examples -------- - >>> env = Environment() + >>> env = Environment(start_time=0, end_time=10) >>> env._plot_metadata = {"x": {"data": [1, 2, 3]}} >>> env.reset() >>> env._plot_metadata["x"]["data"] [] """ - if hasattr(self, "_plot_metadata"): - for item in self._plot_metadata.values(): - item["data"].clear() - - def now(self) -> float: - """ - Return the current simulation time adjusted by ``start_time``. - - Returns - ------- - float - Current time as ``salabim.now() + start_time``. - - Examples - -------- - >>> env = Environment(start_time=5) - >>> env.now() - 5.0 - """ - return super().now() + self.start_time + self._now = self.start_time + for item in self._plot_metadata.values(): + item["data"].clear() diff --git a/dissmodel/core/model.py b/dissmodel/core/model.py index c8f17af..cccb5cd 100644 --- a/dissmodel/core/model.py +++ b/dissmodel/core/model.py @@ -3,16 +3,20 @@ import math from typing import Any -import salabim as sim +from .environment import Environment -class Model(sim.Component): +class Model: """ - Base class for simulation models backed by a salabim Component. + Base class for simulation models. Provides a time-stepped execution loop and automatic tracking of - attributes marked for plotting via the :func:`~dissmodel.visualization.track_plot` - decorator. + attributes marked for plotting via the + :func:`~dissmodel.visualization.track_plot` decorator. + + Every ``Model`` instance auto-registers with the currently active + :class:`~dissmodel.core.Environment` at construction time. An active + environment must exist before instantiating any model. Parameters ---------- @@ -23,26 +27,30 @@ class Model(sim.Component): end_time : float, optional Time at which the model stops executing, by default ``math.inf``. name : str, optional - Component name, by default ``""``. - *args : - Extra positional arguments forwarded to :class:`salabim.Component`. + Human-readable model name, by default ``""``. **kwargs : - Extra keyword arguments forwarded to :class:`salabim.Component`. + Extra keyword arguments (ignored; kept for subclass compatibility). + + Raises + ------ + RuntimeError + If no active :class:`~dissmodel.core.Environment` exists when the + model is instantiated. Examples -------- >>> class MyModel(Model): ... def execute(self): - ... print (self.env.now()) - >>> env = Environment() - >>> model = MyModel(step=1, start_time=0, end_time=5) - >>> env.run(5) + ... print(self.env.now()) + >>> env = Environment(start_time=0, end_time=5) + >>> model = MyModel(step=1) + >>> env.run() Running from 0 to 5 (duration: 5) - 0.0 - 1.0 - 2.0 - 3.0 - 4.0 + 0 + 1 + 2 + 3 + 4 """ def __init__( @@ -51,27 +59,37 @@ def __init__( start_time: float = 0, end_time: float = math.inf, name: str = "", - *args: Any, **kwargs: Any, ) -> None: - super().__init__(*args, **kwargs) - self._step = step - self.start_time = start_time - self.end_time = end_time - - def process(self) -> None: + env = Environment._current + if env is None: + raise RuntimeError( + "No active Environment found. " + "Create an Environment before instantiating a Model." + ) + + # Set internal attributes directly to avoid triggering __setattr__ + # plot-tracking logic before _plot_info is available. + object.__setattr__(self, "name", name) + object.__setattr__(self, "_step", step) + object.__setattr__(self, "start_time", start_time) + object.__setattr__(self, "end_time", end_time) + object.__setattr__(self, "_next_time", start_time) + object.__setattr__(self, "env", env) + + env._register(self) + self.setup(**kwargs) + + def setup(self, **kwargs: Any) -> None: """ - salabim process loop. + Called once after instantiation, receiving any extra keyword + arguments not consumed by ``__init__``. - Waits until ``start_time``, then calls :meth:`execute` every - ``step`` time units until ``end_time``. + Override in subclasses to perform one-time setup such as building + neighborhoods or initializing visualization state. Mirrors the + salabim ``Component.setup()`` contract. """ - if self.env.now() < self.start_time: - self.hold(self.start_time - self.env.now()) - - while self.env.now() < self.end_time: - self.execute() - self.hold(self._step) + pass def execute(self) -> None: """ @@ -81,11 +99,15 @@ def execute(self) -> None: """ pass + # ------------------------------------------------------------------ + # Plot tracking + # ------------------------------------------------------------------ + def __setattr__(self, name: str, value: Any) -> None: """ Intercept attribute assignment to record values marked for plotting. - If the class defines ``_plot_info`` (via the + If the class defines ``_plot_info`` (populated by the :func:`~dissmodel.visualization.track_plot` decorator) and ``name`` matches a tracked attribute, the value is appended to the plot data buffer and registered in ``env._plot_metadata``. @@ -103,10 +125,8 @@ def __setattr__(self, name: str, value: Any) -> None: plot_info: dict[str, Any] = cls._plot_info[name.lower()] plot_info["data"].append(value) - if not hasattr(self.env, "_plot_metadata"): - self.env._plot_metadata = {} - - if plot_info["label"] not in self.env._plot_metadata: - self.env._plot_metadata[plot_info["label"]] = plot_info + env = Environment._current + if env is not None and plot_info["label"] not in env._plot_metadata: + env._plot_metadata[plot_info["label"]] = plot_info super().__setattr__(name, value) diff --git a/dissmodel/geo/vector/cellular_automaton.py b/dissmodel/geo/vector/cellular_automaton.py index f013a6f..f02bc4d 100644 --- a/dissmodel/geo/vector/cellular_automaton.py +++ b/dissmodel/geo/vector/cellular_automaton.py @@ -76,6 +76,7 @@ def __init__( name=name, **kwargs, ) + def initialize(self) -> None: """ diff --git a/pyproject.toml b/pyproject.toml index b2af91a..c3e8e04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,8 +50,6 @@ dependencies = [ "matplotlib>=3.7.0", "rasterstats>=0.19.0", "affine>=2.4.0", - "greenlet>=3.0.0", - "salabim>=25.0.0", "streamlit>=1.40.0", "ipywidgets>=8.0.0", # Colab/Jupyter visualization — Output widget ] @@ -111,7 +109,6 @@ exclude = ["tests/", "docs/", "examples/"] [[tool.mypy.overrides]] module = [ - "salabim.*", "geopandas.*", "shapely.*", "matplotlib.*", From df5685fec0159db6fd141fa4dbe51453af307f1c Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Sat, 2 May 2026 20:19:14 -0300 Subject: [PATCH 2/3] refactor(core): replace salabim with lightweight time-stepped scheduler - Remove salabim and greenlet dependencies from pyproject.toml - Rewrite Environment as a pure Python event scheduler - Rewrite Model with setup(**kwargs)/pre_execute/execute/post_execute hooks - Replace SyncRasterModel.process() with pre_execute/post_execute - Replace SyncSpatialModel.process() with pre_execute/post_execute Public API unchanged. setup(**kwargs) mirrors salabim Component.setup() contract. pre/post_execute hooks replace salabim process() loop. --- dissmodel/core/environment.py | 2 + dissmodel/core/model.py | 18 ++++++++ dissmodel/geo/raster/sync_model.py | 72 +++++++++++++++--------------- dissmodel/geo/vector/sync_model.py | 57 ++++++++++++----------- 4 files changed, 87 insertions(+), 62 deletions(-) diff --git a/dissmodel/core/environment.py b/dissmodel/core/environment.py index e34736d..da60f68 100644 --- a/dissmodel/core/environment.py +++ b/dissmodel/core/environment.py @@ -137,7 +137,9 @@ def run(self, till: Optional[float] = None) -> None: model._next_time <= self._now and self._now < model.end_time ): + model.pre_execute() model.execute() + model.post_execute() model._next_time = self._now + model._step # Advance clock to the nearest pending event diff --git a/dissmodel/core/model.py b/dissmodel/core/model.py index cccb5cd..8d0e62b 100644 --- a/dissmodel/core/model.py +++ b/dissmodel/core/model.py @@ -91,6 +91,15 @@ def setup(self, **kwargs: Any) -> None: """ pass + def pre_execute(self) -> None: + """ + Called once before each :meth:`execute`. + + Override in subclasses to perform per-step setup, such as + snapshotting state arrays before the transition rule runs. + """ + pass + def execute(self) -> None: """ Called once per time step. @@ -99,6 +108,15 @@ def execute(self) -> None: """ pass + def post_execute(self) -> None: + """ + Called once after each :meth:`execute`. + + Override in subclasses to perform per-step cleanup or snapshotting + after the transition rule runs. + """ + pass + # ------------------------------------------------------------------ # Plot tracking # ------------------------------------------------------------------ diff --git a/dissmodel/geo/raster/sync_model.py b/dissmodel/geo/raster/sync_model.py index 865c116..afb5338 100644 --- a/dissmodel/geo/raster/sync_model.py +++ b/dissmodel/geo/raster/sync_model.py @@ -9,11 +9,14 @@ 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. +Before the first ``execute()``, ``pre_execute()`` calls ``synchronize()`` +once to capture the initial state. After each ``execute()``, +``post_execute()`` calls ``synchronize()`` again to freeze the current +state as ``_past`` for the next step. + +Models can call ``self.backend.get("f_past")`` inside ``execute()`` to +access the state at the beginning of the current step — equivalent to +TerraME's ``cell.past[attr]``. Usage ----- @@ -21,19 +24,15 @@ ``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 setup(self, backend, rate=0.01): + super().setup(backend) + self.land_use_types = ["forest", "defor"] + self.rate = rate 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. + forest_past = self.backend.get("forest_past") + gain = forest_past * self.rate + self.backend.arrays["forest"] = forest_past + gain Relationship to domain libraries ---------------------------------- @@ -45,8 +44,6 @@ def execute(self): """ from __future__ import annotations -import numpy as np - from dissmodel.geo import RasterModel @@ -54,9 +51,9 @@ 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 + Extends :class:`~dissmodel.geo.raster.model.RasterModel` with + ``pre_execute()`` / ``post_execute()`` hooks that copy 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. @@ -91,31 +88,36 @@ class SyncRasterModel(RasterModel): ... self.backend.arrays["forest"] = forest_past + gain """ - def process(self) -> None: + def pre_execute(self) -> None: """ - Simulation loop with automatic snapshot management. + Snapshot arrays before the first step. - Overrides :meth:`~dissmodel.core.Model.process` to insert - :meth:`synchronize` calls before the first step and after each step. + On the first call, freezes the initial state into ``_past`` + arrays so that ``execute()`` can read them. Subsequent calls are + no-ops — post_execute() handles ongoing snapshots. """ - if self.env.now() < self.start_time: - self.hold(self.start_time - self.env.now()) + if not getattr(self, "_first_sync_done", False): + self.synchronize() + self._first_sync_done = True - # initial snapshot — captures state at t=0 before any execution - self.synchronize() + def post_execute(self) -> None: + """ + Snapshot arrays after each step. - while self.env.now() < self.end_time: - self.execute() - self.synchronize() # update snapshot for the next step - self.hold(self._step) + Freezes the current state into ``_past`` arrays so that + the next ``execute()`` call reads the state at step start — + equivalent to TerraME's ``cs:synchronize()``. + """ + self.synchronize() 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. + via ``pre_execute()`` before the first step and via ``post_execute()`` + 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). diff --git a/dissmodel/geo/vector/sync_model.py b/dissmodel/geo/vector/sync_model.py index 9539271..bed451e 100644 --- a/dissmodel/geo/vector/sync_model.py +++ b/dissmodel/geo/vector/sync_model.py @@ -9,10 +9,14 @@ 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. +Before the first ``execute()``, ``pre_execute()`` calls ``synchronize()`` +once to capture the initial state. After each ``execute()``, +``post_execute()`` calls ``synchronize()`` again to freeze the current +state as ``_past`` for the next step. + +Models can read ``gdf["f_past"]`` inside ``execute()`` to access the state +at the beginning of the current step — equivalent to TerraME's +``cell.past[attr]``. Usage ----- @@ -28,12 +32,6 @@ 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, @@ -52,9 +50,9 @@ 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. + ``pre_execute()`` / ``post_execute()`` hooks that copy 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 @@ -88,31 +86,36 @@ class SyncSpatialModel(SpatialModel): ... self.gdf["forest"] = self.gdf["forest_past"] + gain """ - def process(self) -> None: + def pre_execute(self) -> None: """ - Simulation loop with automatic snapshot management. + Snapshot columns before the first step. - Overrides :meth:`~dissmodel.core.Model.process` to insert - :meth:`synchronize` calls before the first step and after each step. + On the first call, freezes the initial state into ``_past`` + columns so that ``execute()`` can read them. Subsequent calls are + no-ops — post_execute() handles ongoing snapshots. """ - if self.env.now() < self.start_time: - self.hold(self.start_time - self.env.now()) + if not getattr(self, "_first_sync_done", False): + self.synchronize() + self._first_sync_done = True - # initial snapshot — captures state at t=0 before any execution - self.synchronize() + def post_execute(self) -> None: + """ + Snapshot columns after each step. - while self.env.now() < self.end_time: - self.execute() - self.synchronize() # update snapshot for the next step - self.hold(self._step) + Freezes the current state into ``_past`` columns so that + the next ``execute()`` call reads the state at step start — + equivalent to TerraME's ``cs:synchronize()``. + """ + self.synchronize() 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. + via ``pre_execute()`` before the first step and via ``post_execute()`` + 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). From 285af12dbca1f2a0685b0da36703d44de95a87d6 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Sat, 2 May 2026 20:51:22 -0300 Subject: [PATCH 3/3] chore(release): bump version to 0.5.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ README.md | 34 ++++++++++++++-------------------- paper.md | 18 +++++++++--------- pyproject.toml | 2 +- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ecc6e..df1d966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.5.0] — 2026-05-02 + +### Breaking Changes +- Removed `salabim` and `greenlet` dependencies entirely +- `Model.process()` no longer exists — replace with `pre_execute()` / `post_execute()` hooks + +### Added +- `Environment`: lightweight pure-Python time-stepped scheduler +- `Model.setup(**kwargs)`: called automatically at instantiation, mirrors salabim `Component.setup()` contract +- `Model.pre_execute()`: hook called before each `execute()` +- `Model.post_execute()`: hook called after each `execute()` + +### Changed +- `SyncRasterModel`: replaced `process()` with `pre_execute()` / `post_execute()` +- `SyncSpatialModel`: replaced `process()` with `pre_execute()` / `post_execute()` +- `Environment.run()`: loop now calls `pre_execute → execute → post_execute` per model per tick + +### Removed +- `salabim>=25.0.0` from dependencies +- `greenlet>=3.0.0` from dependencies +- `salabim.*` from mypy overrides + +### Internal +- Public API unchanged — existing models implementing only `execute()` require no modification +--- + ## [0.3.0] - 2026-04 ### Added diff --git a/README.md b/README.md index a48bf3a..c241d34 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ DisSModel is the synthesis: a Python-native, FAIR-aligned, cloud-ready simulatio ## 🌟 Key Features - **Dual substrate** — same model logic runs on vector (`GeoDataFrame`) and raster (`RasterBackend`/NumPy). -- **Discrete Event Simulation** — built on [Salabim](https://salabim.org/); time advances to the next relevant event, not millisecond by millisecond. +- **Lightweight scheduler** — pure-Python time-stepped engine; models auto-register at instantiation and receive clock ticks via `setup / 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. @@ -63,7 +63,7 @@ DisSModel is the synthesis: a Python-native, FAIR-aligned, cloud-ready simulatio ``` ┌──────────────────────────────────────────────────────────┐ -│ Science Layer (Model / Salabim) │ +│ Science Layer (Model) │ │ FloodModel, AllocationClueLike, MangroveModel, ... │ │ → only knows math, geometry and time │ ├──────────────────────────────────────────────────────────┤ @@ -72,7 +72,7 @@ DisSModel is the synthesis: a Python-native, FAIR-aligned, cloud-ready simulatio │ → only knows URIs, local/S3, column_map, parameters │ ├──────────────────────────────────────────────────────────┤ │ Core modules │ -│ dissmodel.core — Environment, SpatialModel │ +│ dissmodel.core — Environment, Model, SpatialModel │ │ dissmodel.geo — RasterBackend, neighborhoods │ │ dissmodel.executor — ModelExecutor ABC, ExperimentRecord│ │ dissmodel.io — load_dataset / save_dataset │ @@ -104,10 +104,9 @@ class ForestFireModel(SpatialModel): self.prob_spread = prob_spread def execute(self): - # Called every step by Salabim — only math here, no I/O + # Called every step — only math here, no I/O burning = self.gdf["state"] == "burning" # ... apply spread logic ... - return self.gdf env = Environment(end_time=50) ForestFireModel(gdf=gdf, prob_spread=0.4) @@ -130,13 +129,12 @@ class ForestFireExecutor(ModelExecutor): record.source.checksum = checksum return gdf - def run(self, record: ExperimentRecord): + def run(self, data, record: ExperimentRecord): from dissmodel.core import Environment - gdf = self.load(record) env = Environment(end_time=record.parameters.get("end_time", 50)) - ForestFireModel(gdf=gdf, **record.parameters) + ForestFireModel(gdf=data, **record.parameters) env.run() - return gdf + return data def save(self, result, record: ExperimentRecord) -> ExperimentRecord: uri = record.output_path or "output.gpkg" @@ -177,7 +175,7 @@ Every run produces an immutable provenance record: { "experiment_id": "abc123", "model_commit": "a3f9c12", - "code_version": "0.4.0", + "code_version": "0.5.0", "resolved_spec": { "...TOML snapshot..." }, "source": { "uri": "s3://...", "checksum": "e3b0c44..." }, "artifacts": { "output": "sha256...", "profiling": "sha256..." }, @@ -212,7 +210,7 @@ Every run via the executor lifecycle generates a `profiling_{id}.md` alongside t ## 🧩 Ecosystem: Models & Examples -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 LambdaGeo ecosystem. +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. ### 🔬 Specialized Model Libraries @@ -234,9 +232,9 @@ Each repository demonstrates how to: ## 📚 Documentation -- 📘 **User Guide**: [https://lambdageo.github.io/dissmodel/](https://lambdageo.github.io/dissmodel/) -- 🧪 **API Reference**: [https://lambdageo.github.io/dissmodel/api/](https://lambdageo.github.io/dissmodel/api/) -- 🎓 **Tutorials**: [https://lambdageo.github.io/dissmodel/tutorials/](https://lambdageo.github.io/dissmodel/tutorials/) +- 📘 **User Guide**: [https://dissmodel.github.io/dissmodel/](https://dissmodel.github.io/dissmodel/) +- 🧪 **API Reference**: [https://dissmodel.github.io/dissmodel/api/](https://dissmodel.github.io/dissmodel/api/) +- 🎓 **Tutorials**: [https://dissmodel.github.io/dissmodel/tutorials/](https://dissmodel.github.io/dissmodel/tutorials/) --- @@ -259,7 +257,7 @@ Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTIN year = {2026}, publisher = {LambdaGeo, Federal University of Maranhão (UFMA)}, url = {https://github.com/DisSModel/dissmodel}, - version = {0.4.0} + version = {0.5.0} } ``` @@ -267,9 +265,5 @@ Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTIN ## ⚖️ License -MIT © [LambdaGeo — UFMA](https://github.com/DisSModel) +MIT © [DisSModel — UFMA](https://github.com/DisSModel) See [LICENSE](LICENSE) for details. - - ---- - diff --git a/paper.md b/paper.md index 611b16c..43a01bc 100644 --- a/paper.md +++ b/paper.md @@ -44,13 +44,13 @@ geospatial analysis and high-level dynamic simulations, DisSModel translates the modeling paradigms of the TerraME framework [@Carneiro2013] into the Python ecosystem. It enables researchers to simulate complex socio-environmental systems — including forest fires, epidemiological spreads, and coastal dynamics — -by integrating the simulation clock of discrete-event engines with the spatial -data structures of GeoPandas [@Jordahl2021]. +by coupling a time-stepped simulation clock with the spatial data structures of +GeoPandas [@Jordahl2021]. The framework provides a **dual-substrate architecture**: a vector substrate backed by GeoDataFrame for flexibility and spatial expressiveness, and a raster substrate backed by NumPy 2D arrays for high-performance vectorised computation. DisSModel is -available through the LambdaGeo GitHub repository and on PyPI. +available through the DisSModel GitHub organisation and on PyPI. ## Statement of Need @@ -87,7 +87,7 @@ its positioning: | Aspect | TerraME | Dinamica EGO | DisSModel | |--------|---------|--------------|-----------| | Language | Lua | Visual/Internal | Python | -| Simulation Engine | Discrete Event | Cellular Automata | Integrated Salabim (DES) | +| Simulation Engine | Discrete Event | Cellular Automata | Time-stepped scheduler | | Spatial Structure | CellularSpace (Fixed) | Cellular Grid | GeoDataFrame + NumPy (Dual) | | GIS Integration | TerraLib | Native Raster | GeoPandas / Rasterio | | Extensibility | Script-based | Block-based | Class Inheritance | @@ -104,10 +104,10 @@ spatial modeling approach proposed by @SantosJunior2025. DisSModel is organised into five modules, following a strict separation of concerns that allows researchers to extend the framework through class inheritance. -**Core** manages the simulation clock and discrete-event execution via Salabim -integration. The `Environment` class orchestrates time progression, and all spatial -models register themselves as Salabim components, receiving clock ticks -automatically. +**Core** manages the simulation clock and time-stepped execution. The `Environment` +class orchestrates time progression via a lightweight pure-Python scheduler, and all +spatial models auto-register at instantiation, receiving clock ticks automatically +through `setup / pre_execute / execute / post_execute` lifecycle hooks. **Geo** manages spatial representations through a dual-substrate design. The vector substrate (`vector_grid`, `SpatialModel`, `CellularAutomaton`) operates on @@ -250,4 +250,4 @@ Jules) to assist with structuring documentation, synthesising prior work, and generating submission checklists. All outputs were reviewed and validated by the human authors. -## References \ No newline at end of file +## References diff --git a/pyproject.toml b/pyproject.toml index c3e8e04..a26e01c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dissmodel" -version = "0.4.1" +version = "0.5.0" description = "Discrete Spatial Modeling framework for raster and vector simulations" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10"