From 7ce35969ef1e7441b6ac92e7e1e2459a17245eb0 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Wed, 7 Oct 2026 14:35:33 -0300 Subject: [PATCH 01/24] docs(paper): align validation text with disslucc-benchmark - Use benchmark scenario IDs (lab01_md1643, lab15_md10) for the Lab1 and Lab15 results - Point validation to the public disslucc-benchmark instead of test file paths - State the MAE metric (final year; mean over all years 0.0026) and iteration counts - Replace per-step timings with whole-run timings from `make timing` - Describe the correctCellChange guard neutrally (field name mismatch) - Fellowship paragraph in the present tense (four fellows since September 2026) - Add DisSLUCCBenchmark to paper.bib --- paper.bib | 19 +++++++++++++++ paper.md | 71 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/paper.bib b/paper.bib index 0513a37..1c1163f 100644 --- a/paper.bib +++ b/paper.bib @@ -136,6 +136,25 @@ @software{DisSLUCC url = {https://github.com/DisSModel/disslucc} } +@software{DisSCube, + author = {Costa, Sérgio Souza}, + title = {{DisSCube}: Declarative spatial data cubes}, + version = {0.5.0}, + year = {2026}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23172450}, + url = {https://github.com/DisSModel/disscube} +} + +% TODO: tag the benchmark (v0.2.0) before the paper is final and update version/year below. +@software{DisSLUCCBenchmark, + author = {Costa, Sérgio Souza}, + title = {{disslucc-benchmark}: Reproducible validation of {disslucc} against the {LuccME} 3.1 labs}, + year = {2026}, + publisher = {GitHub}, + url = {https://github.com/LambdaGeo/disslucc-benchmark} +} + @software{TerraMEDocker, author = {Costa, Sérgio Souza}, title = {{terrame-docker}: {TerraME} 2.0.1 and {LuccME} in a container, with diff --git a/paper.md b/paper.md index d4542ce..9f5edf4 100644 --- a/paper.md +++ b/paper.md @@ -150,11 +150,11 @@ submitting author — the same researchers responsible for the DisSModel reimple validates the raster implementation against TerraME over the Maranhão Island dataset (50,496 cells, 19 steps): land use and soil match exactly at every checkpoint (MAE 0, max error 0), and elevation on 97.3% of cells within 1 mm -(MAE 0.00068 m) — match percentage being the appropriate metric for categorical +(MAE 0.00068 m; maximum absolute error 0.24 m against elevations of 1–58 m) — match percentage being the appropriate metric for categorical outputs [@PontiusEtAl2011]. In this scenario the flood component triggers no land-use transition and the golden files confirm TerraME does the same, so the -agreement above exercises mangrove migration; flooding is covered separately under -the laboratory parameters. Reproducible via the package's validation executor +agreement above exercises mangrove migration; flooding is only checked separately: a test confirms that it occurs under the +laboratory parameters (2,470 cells by step 11), but a step-by-step comparison with TerraME for that case is still pending. Reproducible via the package's validation executor against its committed golden files. Cross-substrate equivalence (60×60 synthetic grid, 3,600 cells, 10 steps) shows @@ -163,29 +163,29 @@ error 0.024 m), with raster at 2.1 ms/step against 84.2 ms/step for vector (40.1 speedup; the vector port follows TerraME's per-cell loops); the residual elevation divergence is floating-point rounding, not algorithmic disagreement. -**disslucc** [@DisSLUCC] implements the continuous CLUE-like allocation algorithm -[@Veldkamp1996]; MAE is the appropriate metric for its fractional outputs -[@PontiusEtAl2011; @Willmott2005]. Over the Lab1 study area (6,574 cells, 6 steps), -the raster implementation reproduces the TerraME/LUCCME reference at MAE = 0.0036 -(RMSE 0.0062, max error 0.027) in [0,1] scale, at 44.0 ms/step. The residual is a -single, deliberate deviation: LuccME's per-cell consistency correction -(`correctCellChange`) never executes, because its guard tests a misspelled -attribute, while `disslucc` runs it by default. With that step disabled -(`cell_correction=False`), `disslucc` matches TerraME in every simulated year, -including the number of convergence iterations per year (MAE < 1e-7). Reproducible via -`disslucc/tests/test_validation_lab1.py` and `test_goldens_per_year.py`, against -year-by-year reference outputs generated in a containerised TerraME -[@TerraMEDocker]; `disslucc/tests/test_benchmark_discriminance_lab1.py` confirms -that perturbing the regression coefficients breaks the tolerance criterion. - -The discrete CLUE-S-like allocation in `disslucc`, with a logistic-regression -potential, reproduces the Lab15 case study (Mojui, Pará 5,914 cells, 6 steps) -from the reference LuccME implementation [@LuccME] cell for cell — zero quantity and -zero allocation disagreement [@PontiusMillones2011] — at 10.3 ms/step. A shipped -discriminance test shows the final map is also reproduced by a trivial static ranking, -so the map alone validates only coefficient transcription; the convergence loop is -validated separately, as the number of CLUE-S iterations matches TerraME in every -simulated year (0, 67, 56, 56, 61, 61). +**disslucc as a second case.** `disslucc` [@DisSLUCC], a package built on DisSModel, +reimplements the LuccME demand, potential and allocation components (continuous CLUE-like +[@Veldkamp1996] and discrete CLUE-S-like). It is used here to test the `ModelExecutor` contract +and the raster substrate against a second TerraME application, not as a result of its own. Its +checks live in a separate, versioned benchmark [@DisSLUCCBenchmark] that compares every simulated +year, iteration counts included, with per-year outputs generated in a containerised TerraME +[@TerraMEDocker] and checked by SHA-256. Scenarios follow the LuccME test labs (`labNN`); the +`labNN_mdX` variants exercise the convergence loop. MAE suits fractional outputs +[@PontiusEtAl2011; @Willmott2005]. + +`lab01_md1643` (continuous, 6,574 cells, 6 steps) matches the reference with MAE = 0.0036 in the +final year (mean over years 0.0026, [0,1] scale). The residual is one deliberate deviation: +LuccME's per-cell correction (`correctCellChange`) never executes, because its guard reads a +field name that does not match the one defined, while `disslucc` runs it by default. Without it +(`cell_correction=False`) the match holds in every year, with identical iteration counts +(0, 0, 8, 26, 18, 17, 17; MAE < 1e-7). `lab15_md10` (discrete, 5,914 cells) reproduces the +reference cell for cell, with zero quantity and allocation disagreement [@PontiusMillones2011] +and identical iteration counts (0, 67, 56, 56, 61, 61); the final map alone only checks +coefficient transcription, and the iterations check the convergence loop. Overall, ten of the 21 +LuccME test labs and the two variants match with identical iteration counts and differences up to +3e-6 (single-precision paths). A validated lab runs in 0.1 to 0.5 s on the hardware above (median +of five; `make timing`), against 2 to 9 s for one uncontrolled TerraME run: informative, not a +speed ratio. ## Research Impact Statement @@ -205,12 +205,11 @@ INPE), whose doctoral work established BR-MANGUE's scientific foundation program (UFMA projects PVCBS4959-2025 and PVCBS4960-2025), a collaboration predating DisSModel itself [@Bezerra2025BM]. -Starting August 2026, the project receives its first undergraduate research -fellows, funded by UFMA and by CNPq. The 2026 development effort was oriented toward this milestone: -stabilizing the `ModelExecutor` contract so each fellow can own an independent -repository — `disslucc`, `brmangue-dissmodel`, or -`disscube` (a data-cube layer, a Python alternative to TerraME's -`fillCellularSpace`) — without core changes. +Since September 2026, four undergraduate research fellows, funded by UFMA and by CNPq, +are being trained on the project. The 2026 development effort prepared for this by stabilizing +the `ModelExecutor` contract, so that each fellow can own an independent repository — +`disslucc`, `brmangue-dissmodel`, or `disscube` [@DisSCube] (a data-cube layer, a Python alternative to +TerraME's `fillCellularSpace`) — without core changes. Studies such as @Bezerra2022, developed using LuccME, are the class of models `disslucc` aims to reproduce. A roadmap toward @@ -244,7 +243,13 @@ not involve generative AI. Development resumed in February–March 2026 with Cla (chat) used mainly for documentation; from April 2026, Gemini CLI accelerated code generation and refactoring; from June 2026, Claude Code (CLI) was used on newer satellite repositories such as `disslucc`, including the audit of the -validation routines against the original TerraME scripts. AI tools also assisted +validation routines against the original TerraME scripts. In September–October 2026, +in response to the review, Claude (Claude Code and chat) was also used to build the +separate `disslucc-benchmark` repository (scenario definitions, the comparison and +timing scripts, and a differential test against the original Lua code), to reorganize +`disslucc`, and to revise the validation text of this paper so that it matches the +benchmark; the reference results themselves come from the original TerraME/LuccME +code, run unmodified in a container. AI tools also assisted with writing in English, not the submitting author's native language. The scientific design — the TerraME compatibility contract, executor pattern, dual-substrate architecture, and validation methodology — predates and is From 668eb8e9e54eacc9c9d0ec071d093285591c05e3 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Wed, 7 Oct 2026 16:06:50 -0300 Subject: [PATCH 02/24] cite disslucc 0.5.0, brmangue-dissmodel 0.3.0 and disslucc-benchmark with Zenodo DOIs --- paper.bib | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/paper.bib b/paper.bib index 1c1163f..27b5c1c 100644 --- a/paper.bib +++ b/paper.bib @@ -106,8 +106,10 @@ @article{Ferreira2020 @software{BRMangue, author = {Costa, Sérgio Souza and Bezerra, Denilson da Silva and Sousa, Felipe Martins}, title = {{brmangue-dissmodel}: The BR-MANGUE coupled flood and mangrove succession model for DisSModel}, + version = {0.3.0}, year = {2026}, - publisher = {GitHub}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23220431}, url = {https://github.com/DisSModel/brmangue-dissmodel} } @@ -130,9 +132,10 @@ @software{DisSModelSysDyn @software{DisSLUCC, author = {Costa, Sérgio Souza}, title = {{disslucc}: Raster-only continuous and discrete LUCC allocation models for DisSModel}, - version = {0.4.0}, + version = {0.5.0}, year = {2026}, - publisher = {GitHub}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23219338}, url = {https://github.com/DisSModel/disslucc} } @@ -146,12 +149,13 @@ @software{DisSCube url = {https://github.com/DisSModel/disscube} } -% TODO: tag the benchmark (v0.2.0) before the paper is final and update version/year below. +% TODO: if a v0.2.0 release is made before the paper is final, add its version and DOI here. @software{DisSLUCCBenchmark, author = {Costa, Sérgio Souza}, title = {{disslucc-benchmark}: Reproducible validation of {disslucc} against the {LuccME} 3.1 labs}, year = {2026}, - publisher = {GitHub}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23169808}, url = {https://github.com/LambdaGeo/disslucc-benchmark} } From 26561e4e091b1f468d23a9af7f898003e8fc4bd7 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Wed, 7 Oct 2026 16:08:54 -0300 Subject: [PATCH 03/24] docs(paper): BR-MANGUE validation with both TerraME scenarios; cite released versions with DOIs --- paper.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/paper.md b/paper.md index 9f5edf4..4d23245 100644 --- a/paper.md +++ b/paper.md @@ -148,20 +148,21 @@ whose co-authors include Denilson da Silva Bezerra, Felipe Martins Sousa and the submitting author — the same researchers responsible for the DisSModel reimplementation. The `brmangue-dissmodel` package [@BRMangue] validates the raster implementation against TerraME over the Maranhão Island -dataset (50,496 cells, 19 steps): land use and soil match exactly at every -checkpoint (MAE 0, max error 0), and elevation on 97.3% of cells within 1 mm -(MAE 0.00068 m; maximum absolute error 0.24 m against elevations of 1–58 m) — match percentage being the appropriate metric for categorical -outputs [@PontiusEtAl2011]. In this scenario the flood component triggers no -land-use transition and the golden files confirm TerraME does the same, so the -agreement above exercises mangrove migration; flooding is only checked separately: a test confirms that it occurs under the -laboratory parameters (2,470 cells by step 11), but a step-by-step comparison with TerraME for that case is still pending. Reproducible via the package's validation executor -against its committed golden files. +dataset (50,496 cells). In the baseline scenario (19 steps) land use and soil match +exactly at every checkpoint, and elevation matches on 97.4% of cells within 1 mm +(MAE 0.00038 m; maximum absolute error 0.10 m against elevations of 1–58 m); match +percentage is the appropriate metric for categorical outputs [@PontiusEtAl2011]. In the +flooding scenario (the laboratory parameters, 2,469 cells flooded by step 11) soil +matches exactly and land use differs in fewer than 0.05% of cells, with elevation within +1 mm on 94.9% of cells after 10 steps. The residual elevation differences are +floating-point rounding in the order of summation, not a difference in the rules. Both +scenarios are reproducible via the package's validation executor. Cross-substrate equivalence (60×60 synthetic grid, 3,600 cells, 10 steps) shows -100% match for land use, soil, and elevation under tolerance (MAE 0.000959 m, max -error 0.024 m), with raster at 2.1 ms/step against 84.2 ms/step for vector (40.1× -speedup; the vector port follows TerraME's per-cell loops); the residual elevation divergence is floating-point rounding, not -algorithmic disagreement. +100% match for land use, soil, and elevation under tolerance (MAE 0.0011 m, max +error 0.024 m), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4× +speedup; the vector port follows TerraME's per-cell loops); the residual elevation +divergence is floating-point rounding, not algorithmic disagreement. **disslucc as a second case.** `disslucc` [@DisSLUCC], a package built on DisSModel, reimplements the LuccME demand, potential and allocation components (continuous CLUE-like From c253265442dcea3538d0524b6018046f6c8b7aad Mon Sep 17 00:00:00 2001 From: Sergio Costa Date: Wed, 7 Oct 2026 20:27:08 -0300 Subject: [PATCH 04/24] BR-MANGUE residuals, Mesa-Geo, remaining labs, spelling, AI disclosure, trim to 1700 words, final DOIs --- paper.bib | 24 +++-- paper.md | 306 +++++++++++++++++++++++------------------------------- 2 files changed, 148 insertions(+), 182 deletions(-) diff --git a/paper.bib b/paper.bib index 27b5c1c..1deca66 100644 --- a/paper.bib +++ b/paper.bib @@ -38,11 +38,10 @@ @incollection{Bezerra2025BM @software{Jordahl2021, author = {Kelsey Jordahl and Joris Van den Bossche and Martin Fleischmann and James McBride and Jacob Wasserman and others}, - title = {{geopandas/geopandas}: {v0.10.2}}, - month = oct, - year = {2021}, + title = {{geopandas/geopandas}}, + year = {2026}, publisher = {Zenodo}, - doi = {10.5281/zenodo.5573592} + doi = {10.5281/zenodo.2585848} } @@ -109,7 +108,7 @@ @software{BRMangue version = {0.3.0}, year = {2026}, publisher = {Zenodo}, - doi = {10.5281/zenodo.23220431}, + doi = {10.5281/zenodo.23223695}, url = {https://github.com/DisSModel/brmangue-dissmodel} } @@ -135,7 +134,7 @@ @software{DisSLUCC version = {0.5.0}, year = {2026}, publisher = {Zenodo}, - doi = {10.5281/zenodo.23219338}, + doi = {10.5281/zenodo.23224097}, url = {https://github.com/DisSModel/disslucc} } @@ -244,7 +243,8 @@ @inproceedings{Costa2009 author = {Costa, Sérgio Souza and Aguiar, Ana Paula Dutra and Câmara, Gilberto and Moreira, Evaldinolia}, booktitle = {Simpósio Brasileiro de Sensoriamento Remoto (SBSR)}, address = {Natal, RN, Brazil}, - year = {2009} + year = {2009}, + url = {https://www.researchgate.net/publication/228981638_Common_Concepts_to_Development_of_the_Top-Down_Models_of_Land_Changes} } @article{Verburg2002, @@ -271,6 +271,16 @@ @article{SoaresFilho2002 doi = {10.1016/S0304-3800(02)00059-5} } +@inproceedings{Wang2022, + title = {{Mesa-Geo}: A {GIS} Extension for the {Mesa} Agent-Based Modeling Framework in {Python}}, + author = {Wang, Boyu and Hess, Vincent and Crooks, Andrew}, + booktitle = {Proceedings of the 5th ACM SIGSPATIAL International Workshop on GeoSpatial Simulation (GeoSim 2022)}, + publisher = {ACM}, + pages = {1--10}, + year = {2022}, + doi = {10.1145/3557989.3566157} +} + @inproceedings{Kazil2020, title = {Utilizing {Python} for Agent-Based Modeling: The {Mesa} Framework}, author = {Kazil, Jackie and Masad, David and Crooks, Andrew}, diff --git a/paper.md b/paper.md index 4d23245..3b55aee 100644 --- a/paper.md +++ b/paper.md @@ -27,56 +27,49 @@ affiliations: city: São Luís state: MA country: Brazil -date: 24 September 2026 +date: 7 October 2026 bibliography: paper.bib --- ## Summary -DisSModel (Discrete Spatial Modeling) is a modular Python framework for spatially -explicit dynamic modeling, targeting the complexities of Land Use and Land Cover -Change (LUCC). It translates the modeling paradigms of the TerraME framework -[@Carneiro2013] into the Python ecosystem, enabling researchers to simulate -complex socio-environmental systems — forest fires, epidemiological spreads, -coastal dynamics — by coupling a time-stepped clock with the spatial data -structures of GeoPandas [@Jordahl2021]. +DisSModel (Discrete Spatial Modeling) is a modular Python framework for spatially explicit dynamic +modeling, targeting Land Use and Land Cover Change (LUCC). It translates the modeling paradigms of +TerraME [@Carneiro2013] into the Python ecosystem, enabling researchers to simulate socio-environmental +systems — forest fires, epidemics, coastal dynamics — by coupling a time-stepped clock with the +spatial data structures of GeoPandas [@Jordahl2021]. -The framework provides a **dual-substrate architecture**: a vector substrate -backed by GeoDataFrame for spatial expressiveness, and a raster substrate backed -by NumPy 2D arrays for high-performance vectorised computation. DisSModel is -available through the DisSModel GitHub organisation and on PyPI. +Its **dual-substrate architecture** pairs a vector substrate (GeoDataFrame) for spatial expressiveness +with a raster substrate (NumPy 2D arrays) for high-performance vectorized computation. DisSModel is +available through the DisSModel GitHub organization and on PyPI. ## Statement of Need -Python has become the lingua franca for geospatial data science, supported by -libraries such as GeoPandas and PySAL — but these tools target static analysis. -Dynamic spatial modeling, simulating how landscapes evolve over time, has -historically required specialised platforms. In Brazil, TerraME [@Carneiro2013] and -Dinamica EGO [@SoaresFilho2002; @SoaresFilho2013] are the most widely adopted general-purpose frameworks, while -institutions elsewhere rely on narrower allocation models such as CLUE and CLUE-S -[@Veldkamp1996; @Verburg2002]. This fragmentation leaves researchers choosing between a Lua-based +Python has become the lingua franca for geospatial data science, supported by libraries such as +GeoPandas and PySAL — but these tools target static analysis. Dynamic spatial modeling, simulating +how landscapes evolve over time, has historically required specialized platforms. In Brazil, TerraME +[@Carneiro2013] and Dinamica EGO [@SoaresFilho2002; @SoaresFilho2013] are the most widely adopted +general-purpose frameworks, while narrower allocation models such as CLUE and CLUE-S +[@Veldkamp1996; @Verburg2002] are used elsewhere. Researchers must choose between a Lua-based toolchain and single-purpose implementations with no shared contract. -While TerraME is conceptually robust, its reliance on Lua — a language with far -smaller adoption in data science than Python — creates a barrier for data -scientists, and the framework has seen no new release since August 2020. -General-purpose simulation libraries, meanwhile, lack native synchronisation -between a time-stepped clock and the geographical state of a GeoDataFrame. -DisSModel fills this gap with a lightweight scheduler coupled directly to vector -and raster spatial state, providing a Pythonic, actively maintained implementation -of the TerraME paradigm — and, through its satellite packages, of allocation -models like CLUE and CLUE-S. +TerraME is conceptually robust, but its reliance on Lua — far less common in data science than +Python — is a barrier, and the framework has seen no new release since August 2020. General-purpose +simulation libraries, meanwhile, lack native synchronization between a time-stepped clock and the +state of a GeoDataFrame. DisSModel fills this gap with a lightweight scheduler coupled to vector and +raster spatial state: a Pythonic, actively maintained implementation of the TerraME paradigm and, +through its satellite packages, of allocation models like CLUE and CLUE-S. Reproducibility is a first-class concern: the `executor` module provides a -standardised lifecycle — `validate → load → run → save` — capturing provenance +standardized lifecycle — `validate → load → run → save` — capturing provenance metadata (input checksums, parameters, timing, output paths) in an `ExperimentRecord` generated automatically for every run, with no additional -instrumentation by the modeller. +instrumentation by the modeler. ## State of the Field DisSModel occupies a niche between general-purpose agent-based modeling (ABM) -libraries and specialised GIS simulation software: +libraries and specialized GIS simulation software: | Aspect | TerraME | Dinamica EGO | DisSModel | |--------|---------|--------------|-----------| @@ -86,140 +79,105 @@ libraries and specialised GIS simulation software: | GIS Integration | TerraLib | Native Raster | GeoPandas / Rasterio | | Extensibility | Script-based | Block-based | Class Inheritance | | Reproducibility | Manual | Manual | Automated (ExperimentRecord) | -| Neighborhoods | GPM Support | Limited | libpysal weights (Queen, Rook, KNN, custom) | -NetLogo [@Wilensky1999] and Mesa [@Kazil2020] are excellent for ABM but require boilerplate to handle -real-world spatial projections. DisSModel uses GeoPandas as its core engine, -following the discrete spatial modeling approach of @SantosJunior2025. +NetLogo [@Wilensky1999] and Mesa [@Kazil2020] are excellent for ABM; Mesa-Geo [@Wang2022] adds GIS +data to Mesa, whereas DisSModel centers on cellular-space state with a synchronized clock and a +reproducible executor lifecycle. DisSModel builds on GeoPandas, following the discrete spatial modeling approach of @SantosJunior2025. ## Software Design -DisSModel is organised into five modules with strict separation of concerns, -extensible through class inheritance. **Core** manages the simulation clock: the -`Environment` orchestrates time progression, and spatial models auto-register at -instantiation, receiving ticks through `setup / pre_execute / execute / -post_execute` hooks. **Geo** provides the dual-substrate design: a vector -substrate (`SpatialModel`, `CellularAutomaton`) on GeoDataFrame with libpysal -neighbourhoods [@Rey2021], and a raster substrate (`RasterModel`, -`RasterCellularAutomaton`) on NumPy arrays with vectorised operations (`shift2d`, -`focal_sum`, `neighbor_contact`) replacing cell-by-cell loops. **Executor** defines -the `ModelExecutor` four-phase lifecycle (`validate`, `load`, `run`, `save`); -subclasses self-register via `__init_subclass__`, and every run produces an -`ExperimentRecord` with input checksum, parameters, timing, and output paths. -**IO** provides a unified dataset abstraction (`load_dataset` / `save_dataset`) -across GeoDataFrame, GeoTIFF, and Xarray/Zarr, with transparent `s3://` -resolution. **Visualization** integrates Matplotlib, Streamlit-compatible widgets, -and `RasterMap`. - -This extensibility has already produced independent domain packages: -`dissmodel-ca` [@DisSModelCA] (Cellular Automata patterns), `dissmodel-sysdyn` -[@DisSModelSysDyn] (System Dynamics), and `disslucc` [@DisSLUCC], which -implements LUCCME's continuous and discrete components — Demand, Potential, -and Allocation [@Veldkamp1996; @Verburg2002] — on the raster substrate and -the same `ModelExecutor` contract, an explicit Python counterpart to -TerraME/LuccME. +DisSModel is organized into five modules with strict separation of concerns, extensible through +class inheritance. **Core** manages the simulation clock: the `Environment` orchestrates time, and +spatial models auto-register at instantiation, receiving ticks through `setup / pre_execute / +execute / post_execute` hooks. **Geo** provides the dual-substrate design: a vector substrate +(`SpatialModel`, `CellularAutomaton`) on GeoDataFrame with libpysal neighborhoods [@Rey2021], and a +raster substrate (`RasterModel`, `RasterCellularAutomaton`) on NumPy arrays with vectorized +operations (`shift2d`, `focal_sum`, `neighbor_contact`) replacing cell-by-cell loops. **Executor** +defines the `ModelExecutor` four-phase lifecycle (`validate`, `load`, `run`, `save`); subclasses +self-register, and every run produces an `ExperimentRecord` with input checksum, parameters, timing, +and output paths. **IO** provides a unified dataset abstraction across GeoDataFrame, GeoTIFF, and +Xarray/Zarr, with transparent `s3://` resolution. **Visualization** integrates Matplotlib, +Streamlit-compatible widgets, and `RasterMap`. + +This extensibility has produced independent domain packages: `dissmodel-ca` [@DisSModelCA] +(Cellular Automata), `dissmodel-sysdyn` [@DisSModelSysDyn] (System Dynamics), and `disslucc` +[@DisSLUCC], which implements LuccME's continuous and discrete Demand, Potential, and Allocation +components [@Veldkamp1996; @Verburg2002] on the raster substrate and the same `ModelExecutor` +contract, an explicit Python counterpart to TerraME/LuccME. ## Validation and Performance -The vector substrate offers spatial expressiveness; the raster substrate enforces -vectorised rules over NumPy arrays. All benchmarks ran on an Intel Core -i7-7700T @ 2.90GHz, 15 GB RAM (Ubuntu, Python 3.12.3, NumPy 2.4.6, GeoPandas -1.1.3); absolute timings vary by hardware, but the relative speedup is the result -of interest. - -**Conway's Game of Life** confirms mathematical equivalence across substrates with -different throughput: - -| Grid | Cells | Raster, vectorised rule (ms/step) | Vector, per-cell `rule(idx)` (ms/step) | Speedup | -|-----:|------:|-----------------:|-----------------:|--------:| -| 10×10 | 100 | 0.12 | 74.81 | 639× | -| 50×50 | 2,500 | 0.19 | 1,707.76 | 8,809× | -| 100×100 | 10,000 | 0.41 | 7,069.14 | 17,394× | -| 200×200 | 40,000 | 1.21 | — | — | -| 500×500 | 250,000 | 9.74 | — | — | -| 1,000×1,000 | 1,000,000 | 30.60 | — | — | - -The speedup therefore measures a per-cell rule (one Python call per cell) against a -vectorised one, not the substrates themselves; vector runs above 10,000 cells were omitted. - -**BR-MANGUE coastal dynamics.** The foundation for coupled mangrove-flood modeling -was established by Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, -whose co-authors include Denilson da Silva Bezerra, Felipe Martins Sousa and the -submitting author — the same researchers responsible for the DisSModel reimplementation. The -`brmangue-dissmodel` package [@BRMangue] -validates the raster implementation against TerraME over the Maranhão Island -dataset (50,496 cells). In the baseline scenario (19 steps) land use and soil match -exactly at every checkpoint, and elevation matches on 97.4% of cells within 1 mm -(MAE 0.00038 m; maximum absolute error 0.10 m against elevations of 1–58 m); match -percentage is the appropriate metric for categorical outputs [@PontiusEtAl2011]. In the -flooding scenario (the laboratory parameters, 2,469 cells flooded by step 11) soil -matches exactly and land use differs in fewer than 0.05% of cells, with elevation within -1 mm on 94.9% of cells after 10 steps. The residual elevation differences are -floating-point rounding in the order of summation, not a difference in the rules. Both -scenarios are reproducible via the package's validation executor. - -Cross-substrate equivalence (60×60 synthetic grid, 3,600 cells, 10 steps) shows -100% match for land use, soil, and elevation under tolerance (MAE 0.0011 m, max -error 0.024 m), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4× -speedup; the vector port follows TerraME's per-cell loops); the residual elevation -divergence is floating-point rounding, not algorithmic disagreement. - -**disslucc as a second case.** `disslucc` [@DisSLUCC], a package built on DisSModel, -reimplements the LuccME demand, potential and allocation components (continuous CLUE-like -[@Veldkamp1996] and discrete CLUE-S-like). It is used here to test the `ModelExecutor` contract -and the raster substrate against a second TerraME application, not as a result of its own. Its -checks live in a separate, versioned benchmark [@DisSLUCCBenchmark] that compares every simulated -year, iteration counts included, with per-year outputs generated in a containerised TerraME -[@TerraMEDocker] and checked by SHA-256. Scenarios follow the LuccME test labs (`labNN`); the -`labNN_mdX` variants exercise the convergence loop. MAE suits fractional outputs -[@PontiusEtAl2011; @Willmott2005]. - -`lab01_md1643` (continuous, 6,574 cells, 6 steps) matches the reference with MAE = 0.0036 in the -final year (mean over years 0.0026, [0,1] scale). The residual is one deliberate deviation: -LuccME's per-cell correction (`correctCellChange`) never executes, because its guard reads a -field name that does not match the one defined, while `disslucc` runs it by default. Without it -(`cell_correction=False`) the match holds in every year, with identical iteration counts -(0, 0, 8, 26, 18, 17, 17; MAE < 1e-7). `lab15_md10` (discrete, 5,914 cells) reproduces the -reference cell for cell, with zero quantity and allocation disagreement [@PontiusMillones2011] -and identical iteration counts (0, 67, 56, 56, 61, 61); the final map alone only checks -coefficient transcription, and the iterations check the convergence loop. Overall, ten of the 21 -LuccME test labs and the two variants match with identical iteration counts and differences up to -3e-6 (single-precision paths). A validated lab runs in 0.1 to 0.5 s on the hardware above (median -of five; `make timing`), against 2 to 9 s for one uncontrolled TerraME run: informative, not a -speed ratio. +All benchmarks ran on an Intel Core i7-7700T @ 2.90GHz, 15 GB RAM (Ubuntu, Python 3.12.3, +NumPy 2.4.6, GeoPandas 1.1.3); absolute timings vary by hardware. + +**Conway's Game of Life** confirms equivalence across substrates: at 100×100 cells the vectorized +raster rule takes 0.41 ms/step against 7,069 ms/step for a per-cell vector rule, and the raster +scales to 10⁶ cells at 30.6 ms/step (per-cell versus vectorized rule). + +**BR-MANGUE coastal dynamics.** The coupled mangrove–flood model was established by +Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S.B., F.M.S. and the +submitting author, who are also responsible for the DisSModel reimplementation. +`brmangue-dissmodel` [@BRMangue] validates the raster implementation against TerraME over the +Maranhão Island dataset (50,496 cells). In the baseline scenario (19 steps) land use and soil match +exactly at every checkpoint and elevation matches on 97.4% of cells within 1 mm (MAE 0.00038 m; +maximum error 0.10 m); match percentage suits categorical outputs +[@PontiusEtAl2011]. In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea +cells by step 11 and the Python model 2,467; soil matches exactly, land use differs in 4 cells +(0.008%), and elevation matches on 94.4% of cells (MAE 0.008 m, maximum error 1.0 m). The residuals +come from exact ties in elevation: the flux rule compares accumulated elevations with `<=`, so a +~1e-16 difference in summation order (sequential in TerraME, per direction in NumPy) can flip a +comparison and, near the flooding threshold, a cell's class. They are floating-point effects, not a +difference in the rules. Both are reproducible with its validation executor. + +Cross-substrate equivalence (60×60 synthetic grid, 10 steps) gives 100% match for land use, soil +and elevation under tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding, not algorithmic +disagreement), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port +follows TerraME's per-cell loops). + +**disslucc as a second case.** `disslucc` [@DisSLUCC], built on DisSModel, reimplements the LuccME +demand, potential and allocation components (continuous CLUE-like [@Veldkamp1996] and discrete +CLUE-S-like). It tests the `ModelExecutor` contract and the raster substrate against a second +TerraME application, not as a result in itself. Its checks live in a separate, versioned benchmark +[@DisSLUCCBenchmark] that compares every simulated year, iteration counts included, with per-year +outputs generated in a containerized TerraME [@TerraMEDocker] and checked by SHA-256. Scenarios +follow the LuccME test labs (`labNN`); the `labNN_mdX` variants exercise the convergence loop. MAE +suits fractional outputs [@PontiusEtAl2011; @Willmott2005]. + +`lab01_md1643` (continuous, 6,574 cells, 2008–2014) matches the reference with MAE = 0.0036 in the +final year ([0,1] scale). The residual is one deliberate deviation: LuccME's per-cell correction +(`correctCellChange`) never executes, because its guard reads a field name that does not match the +one defined, while `disslucc` runs it by default. Without it (`cell_correction=False`) the match +holds in every year, with identical iteration counts (MAE < 1e-7). +`lab15_md10` (discrete, 5,914 cells) reproduces the reference cell for cell, with zero quantity and +allocation disagreement [@PontiusMillones2011] and identical iteration counts. Overall, ten of 21 LuccME test labs and both variants match with identical iteration +counts and differences up to 3e-6 (single precision); a validated lab runs in 0.1 to 0.5 s on +the hardware above (median of five; `make timing`). The other eleven labs need potential and +allocation components not yet implemented; student fellows are developing them, and each lab joins +the benchmark as its components land. ## Research Impact Statement -DisSModel's scientific lineage is rooted in the TerraME/LuccME research program at -INPE. The submitting author conducted doctoral research at INPE under Prof. -Gilberto Câmara and Dr. Ana Paula Dutra Aguiar — principal architects of -TerraME/LuccME — and has co-authored the modeling program since 2009 -[@Moreira2009; @Costa2009]; `disslucc` reimplements in Python the -continuous and discrete allocation components of that lineage [@LuccME]. On -7 May 2026, DisSModel was presented at INPE's Graduate Program in Applied -Computing seminar series (recording: https://youtu.be/o7pMJt0CvXU). - -The framework is in active use across two UFMA research groups. Within LambdaGeo, -`brmangue-dissmodel` builds on a Master's student's reference implementation. Independently, Prof. Denilson da Silva Bezerra (UFMA, former -INPE), whose doctoral work established BR-MANGUE's scientific foundation -[@Bezerra2013], uses the DisSModel reimplementation in his own coastal dynamics -program (UFMA projects PVCBS4959-2025 and PVCBS4960-2025), a -collaboration predating DisSModel itself [@Bezerra2025BM]. - -Since September 2026, four undergraduate research fellows, funded by UFMA and by CNPq, -are being trained on the project. The 2026 development effort prepared for this by stabilizing -the `ModelExecutor` contract, so that each fellow can own an independent repository — -`disslucc`, `brmangue-dissmodel`, or `disscube` [@DisSCube] (a data-cube layer, a Python alternative to -TerraME's `fillCellularSpace`) — without core changes. - -Studies such as @Bezerra2022, developed using LuccME, are the class -of models `disslucc` aims to reproduce. A roadmap toward -DisSModel 1.0 (May 2027) anchors community outreach including an open textbook, -*Geospatial Modeling with Python* -(https://lambdageo.github.io/geospatial-modeling-python/), already in progress. -This positions DisSModel as the simulation layer in the Brazilian Earth -Observation stack, complementary to SITS [@Simoes2021] and the Brazil Data Cube -[@Ferreira2020]. +DisSModel's scientific lineage is rooted in the TerraME/LuccME research program at INPE. The +submitting author conducted doctoral research there under Prof. Gilberto Câmara and Dr. Ana Paula +Dutra Aguiar — principal architects of TerraME/LuccME — and has co-authored the modeling program +since 2009 [@Moreira2009; @Costa2009]; `disslucc` reimplements in Python the continuous and +discrete allocation components of that lineage [@LuccME]. + +The framework is in use across two UFMA research groups. Within LambdaGeo, `brmangue-dissmodel` +builds on the reference implementation by co-author F.M.S. Independently, Prof. Denilson da Silva +Bezerra (UFMA, former INPE), whose doctoral work founded BR-MANGUE [@Bezerra2013], uses the DisSModel reimplementation in his own coastal dynamics program (UFMA +projects PVCBS4959-2025 and PVCBS4960-2025), a collaboration predating DisSModel itself +[@Bezerra2025BM]. + +Since September 2026, four undergraduate research fellows (UFMA, CNPq) are being trained on the +project; the stabilized `ModelExecutor` contract lets each own an independent repository — +`disslucc`, `brmangue-dissmodel`, or `disscube` [@DisSCube] (a data-cube layer, a Python alternative +to TerraME's `fillCellularSpace`) — without core changes. + +A roadmap toward DisSModel 1.0 (May 2027) includes an open textbook, *Geospatial Modeling with +Python* (https://lambdageo.github.io/geospatial-modeling-python/), already in progress. This +positions DisSModel as a candidate simulation layer in the Brazilian Earth Observation stack, +complementary to SITS [@Simoes2021] and the Brazil Data Cube [@Ferreira2020]. ## Author Contributions @@ -238,25 +196,23 @@ We thank José Magno Pinheiro Alves for early validation testing. ## AI Usage Disclosure -Development followed several phases. The initial prototype (May–June 2025), -corresponding to the second author's undergraduate thesis [@SantosJunior2025], did -not involve generative AI. Development resumed in February–March 2026 with Claude -(chat) used mainly for documentation; from April 2026, Gemini CLI accelerated code -generation and refactoring; from June 2026, Claude Code (CLI) was used on newer -satellite repositories such as `disslucc`, including the audit of the -validation routines against the original TerraME scripts. In September–October 2026, -in response to the review, Claude (Claude Code and chat) was also used to build the -separate `disslucc-benchmark` repository (scenario definitions, the comparison and -timing scripts, and a differential test against the original Lua code), to reorganize -`disslucc`, and to revise the validation text of this paper so that it matches the -benchmark; the reference results themselves come from the original TerraME/LuccME -code, run unmodified in a container. AI tools also assisted -with writing in English, not the submitting author's native language. The -scientific design — the TerraME compatibility contract, executor pattern, -dual-substrate architecture, and validation methodology — predates and is -independent of this AI-assisted phase, tracing to the submitting author's doctoral -research at INPE and the undergraduate thesis cited above. AI assisted with -implementation velocity and language clarity, not scientific or architectural -decisions. All outputs were reviewed and validated by the authors. +Development followed several phases. The initial prototype (May–June 2025), the second author's +undergraduate thesis [@SantosJunior2025], did not involve generative AI. From February 2026 Claude +(chat) was used for documentation, from April Gemini CLI for code generation and refactoring, and +from June Claude Code (CLI) on satellite repositories such as `disslucc`, including auditing its +validation routines against the original TerraME scripts. In +September–October 2026, in response to the review, Claude (Claude Code and chat) wrote and modified +code and text: the `disslucc-benchmark` repository (scenarios, comparison and timing scripts, a +differential test against the original Lua code); the reorganization of `disslucc`; in +`brmangue-dissmodel`, the diagnosis and correction of two discrepancies with TerraME (neighbor +counting at borders; the snapshot shared by the flood and mangrove models) with tests, a headless +driver for regenerating TerraME outputs, and documentation; and the revision of this paper's +validation text. The reference results come from the original TerraME/LuccME code run unmodified in +a container (for BR-MANGUE, unmodified model files with a new headless driver), so they do not +depend on AI-written code. AI also assisted with English writing. The scientific design — TerraME +compatibility contract, executor pattern, dual-substrate architecture, validation methodology — +predates this phase and traces to the submitting author's doctoral research at INPE. The authors +made the design decisions and chose the validation criteria; AI-generated code and text were +reviewed, tested, and run by the authors. ## References From e02c013617d522166d05ff761205669bcce254f9 Mon Sep 17 00:00:00 2001 From: Sergio Costa Date: Wed, 7 Oct 2026 21:21:11 -0300 Subject: [PATCH 05/24] docs(paper): AI disclosure for the BR-MANGUE driver, tie validation numbers to v0.3.0, step 11, Game of Life wording --- paper.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/paper.md b/paper.md index 3b55aee..9b8dc93 100644 --- a/paper.md +++ b/paper.md @@ -110,28 +110,27 @@ contract, an explicit Python counterpart to TerraME/LuccME. All benchmarks ran on an Intel Core i7-7700T @ 2.90GHz, 15 GB RAM (Ubuntu, Python 3.12.3, NumPy 2.4.6, GeoPandas 1.1.3); absolute timings vary by hardware. -**Conway's Game of Life** confirms equivalence across substrates: at 100×100 cells the vectorized +**Conway's Game of Life** compares the two substrates: at 100×100 cells the vectorized raster rule takes 0.41 ms/step against 7,069 ms/step for a per-cell vector rule, and the raster -scales to 10⁶ cells at 30.6 ms/step (per-cell versus vectorized rule). +scales to 10⁶ cells at 30.6 ms/step. **BR-MANGUE coastal dynamics.** The coupled mangrove–flood model was established by Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S.B., F.M.S. and the submitting author, who are also responsible for the DisSModel reimplementation. `brmangue-dissmodel` [@BRMangue] validates the raster implementation against TerraME over the -Maranhão Island dataset (50,496 cells). In the baseline scenario (19 steps) land use and soil match +Maranhão Island dataset (50,496 cells); figures are for v0.3.0, after fixing border neighbor counting and the shared flood/mangrove snapshot. In the baseline scenario (19 steps) land use and soil match exactly at every checkpoint and elevation matches on 97.4% of cells within 1 mm (MAE 0.00038 m; maximum error 0.10 m); match percentage suits categorical outputs [@PontiusEtAl2011]. In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea cells by step 11 and the Python model 2,467; soil matches exactly, land use differs in 4 cells -(0.008%), and elevation matches on 94.4% of cells (MAE 0.008 m, maximum error 1.0 m). The residuals +(0.008%), and at step 11 elevation matches on 94.4% of cells (MAE 0.008 m, maximum error 1.0 m). The residuals come from exact ties in elevation: the flux rule compares accumulated elevations with `<=`, so a ~1e-16 difference in summation order (sequential in TerraME, per direction in NumPy) can flip a comparison and, near the flooding threshold, a cell's class. They are floating-point effects, not a -difference in the rules. Both are reproducible with its validation executor. +difference in the rules; both scenarios are reproducible with the validation executor. Cross-substrate equivalence (60×60 synthetic grid, 10 steps) gives 100% match for land use, soil -and elevation under tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding, not algorithmic -disagreement), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port +and elevation under tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port follows TerraME's per-cell loops). **disslucc as a second case.** `disslucc` [@DisSLUCC], built on DisSModel, reimplements the LuccME @@ -206,13 +205,12 @@ code and text: the `disslucc-benchmark` repository (scenarios, comparison and ti differential test against the original Lua code); the reorganization of `disslucc`; in `brmangue-dissmodel`, the diagnosis and correction of two discrepancies with TerraME (neighbor counting at borders; the snapshot shared by the flood and mangrove models) with tests, a headless -driver for regenerating TerraME outputs, and documentation; and the revision of this paper's -validation text. The reference results come from the original TerraME/LuccME code run unmodified in -a container (for BR-MANGUE, unmodified model files with a new headless driver), so they do not -depend on AI-written code. AI also assisted with English writing. The scientific design — TerraME +driver for regenerating TerraME outputs, and documentation; and this paper's validation text. The reference results come from the original TerraME/LuccME code run unmodified in +a container; the reference model files are unmodified and, for BR-MANGUE, only the headless +driver that runs them is AI-written. AI also assisted with English writing. The scientific design — TerraME compatibility contract, executor pattern, dual-substrate architecture, validation methodology — predates this phase and traces to the submitting author's doctoral research at INPE. The authors -made the design decisions and chose the validation criteria; AI-generated code and text were -reviewed, tested, and run by the authors. +made the design decisions and chose the validation criteria, and reviewed, tested and ran all +AI-generated code and text. ## References From c0c365a11d1bd084f621662060a387b403de6463 Mon Sep 17 00:00:00 2001 From: Sergio Costa Date: Wed, 7 Oct 2026 22:24:50 -0300 Subject: [PATCH 06/24] docs(paper): final revision for JOSS review - BR-MANGUE validation against TerraME (baseline and flooding scenarios), tied to brmangue-dissmodel v0.3.0 - Mesa-Geo in the state of the field; remaining labs explained - AI usage disclosure rewritten (scope, phases, validation references) - INPE seminar (7 May 2026) in the research impact - paper.bib: final Zenodo DOIs (disslucc, brmangue-dissmodel, disslucc-benchmark 0.2.2), Costa2009 URL, seminar entry --- paper.bib | 16 +++++++++++++--- paper.md | 31 ++++++++++++------------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/paper.bib b/paper.bib index 1deca66..85fad66 100644 --- a/paper.bib +++ b/paper.bib @@ -134,7 +134,7 @@ @software{DisSLUCC version = {0.5.0}, year = {2026}, publisher = {Zenodo}, - doi = {10.5281/zenodo.23224097}, + doi = {10.5281/zenodo.23219338}, url = {https://github.com/DisSModel/disslucc} } @@ -148,13 +148,13 @@ @software{DisSCube url = {https://github.com/DisSModel/disscube} } -% TODO: if a v0.2.0 release is made before the paper is final, add its version and DOI here. @software{DisSLUCCBenchmark, author = {Costa, Sérgio Souza}, title = {{disslucc-benchmark}: Reproducible validation of {disslucc} against the {LuccME} 3.1 labs}, + version = {0.2.2}, year = {2026}, publisher = {Zenodo}, - doi = {10.5281/zenodo.23169808}, + doi = {10.5281/zenodo.23224097}, url = {https://github.com/LambdaGeo/disslucc-benchmark} } @@ -311,3 +311,13 @@ @article{SoaresFilho2013 pages = {80--87}, doi = {10.1016/j.envsoft.2013.01.010} } + +@misc{Costa2026INPE, + author = {Costa, Sérgio Souza}, + title = {Building Open and Reproducible Geospatial Simulations with {Python}}, + howpublished = {Seminar at INPE (remote), recorded on the INPE program's YouTube channel}, + year = {2026}, + month = may, + url = {https://lambdageo.github.io/presentations/2026-05-07-inpe-seminar/}, + note = {Video: \url{https://www.youtube.com/watch?v=o7pMJt0CvXU}} +} diff --git a/paper.md b/paper.md index 9b8dc93..a0900a3 100644 --- a/paper.md +++ b/paper.md @@ -118,7 +118,7 @@ scales to 10⁶ cells at 30.6 ms/step. Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S.B., F.M.S. and the submitting author, who are also responsible for the DisSModel reimplementation. `brmangue-dissmodel` [@BRMangue] validates the raster implementation against TerraME over the -Maranhão Island dataset (50,496 cells); figures are for v0.3.0, after fixing border neighbor counting and the shared flood/mangrove snapshot. In the baseline scenario (19 steps) land use and soil match +Maranhão Island dataset (50,496 cells); figures are for v0.3.0, after fixing, in `brmangue-dissmodel`, border neighbor counting and the shared flood/mangrove snapshot. In the baseline scenario (19 steps) land use and soil match exactly at every checkpoint and elevation matches on 97.4% of cells within 1 mm (MAE 0.00038 m; maximum error 0.10 m); match percentage suits categorical outputs [@PontiusEtAl2011]. In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea @@ -160,7 +160,8 @@ DisSModel's scientific lineage is rooted in the TerraME/LuccME research program submitting author conducted doctoral research there under Prof. Gilberto Câmara and Dr. Ana Paula Dutra Aguiar — principal architects of TerraME/LuccME — and has co-authored the modeling program since 2009 [@Moreira2009; @Costa2009]; `disslucc` reimplements in Python the continuous and -discrete allocation components of that lineage [@LuccME]. +discrete allocation components of that lineage [@LuccME]. DisSModel was presented at a seminar of INPE's Applied Computing graduate program (CAP) on 7 May +2026 [@Costa2026INPE]. The framework is in use across two UFMA research groups. Within LambdaGeo, `brmangue-dissmodel` builds on the reference implementation by co-author F.M.S. Independently, Prof. Denilson da Silva @@ -195,22 +196,14 @@ We thank José Magno Pinheiro Alves for early validation testing. ## AI Usage Disclosure -Development followed several phases. The initial prototype (May–June 2025), the second author's -undergraduate thesis [@SantosJunior2025], did not involve generative AI. From February 2026 Claude -(chat) was used for documentation, from April Gemini CLI for code generation and refactoring, and -from June Claude Code (CLI) on satellite repositories such as `disslucc`, including auditing its -validation routines against the original TerraME scripts. In -September–October 2026, in response to the review, Claude (Claude Code and chat) wrote and modified -code and text: the `disslucc-benchmark` repository (scenarios, comparison and timing scripts, a -differential test against the original Lua code); the reorganization of `disslucc`; in -`brmangue-dissmodel`, the diagnosis and correction of two discrepancies with TerraME (neighbor -counting at borders; the snapshot shared by the flood and mangrove models) with tests, a headless -driver for regenerating TerraME outputs, and documentation; and this paper's validation text. The reference results come from the original TerraME/LuccME code run unmodified in -a container; the reference model files are unmodified and, for BR-MANGUE, only the headless -driver that runs them is AI-written. AI also assisted with English writing. The scientific design — TerraME -compatibility contract, executor pattern, dual-substrate architecture, validation methodology — -predates this phase and traces to the submitting author's doctoral research at INPE. The authors -made the design decisions and chose the validation criteria, and reviewed, tested and ran all -AI-generated code and text. +The initial prototype (May–June 2025), the second author's undergraduate thesis +[@SantosJunior2025], did not involve generative AI. From February 2026, Claude and Gemini CLI +supported documentation, tests and refactoring of code that already existed. Since mid-2026, +Claude Code has been used across the whole ecosystem (the core framework, its satellite packages +and their benchmarks) for implementation, validation scripts, documentation and this paper's text +and English. This is what has made it feasible to build the ecosystem envisioned by the +submitting author. The reference outputs used in validation come from the unmodified TerraME/LuccME model files run in a container; for BR-MANGUE, only the headless driver that runs them is AI-written. The scientific design — TerraME compatibility contract, executor pattern, +dual-substrate architecture, validation methodology — is the authors', and traces to the +submitting author's doctoral research at INPE. The authors reviewed, tested and ran all AI-generated code and text. ## References From 2e8e3aff15c43ec95fe99c33f34c0a064e574a24 Mon Sep 17 00:00:00 2001 From: Sergio Costa Date: Wed, 7 Oct 2026 22:33:33 -0300 Subject: [PATCH 07/24] docs(paper): final revision for JOSS review --- paper.bib | 17 +---------------- paper.md | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/paper.bib b/paper.bib index 85fad66..d101612 100644 --- a/paper.bib +++ b/paper.bib @@ -45,21 +45,6 @@ @software{Jordahl2021 } -@article{Bezerra2022, - title = {New land-use change scenarios for Brazil: Refining global SSPs with a regional spatially-explicit allocation model}, - author = {Bezerra, Francisco Gilney Silva and Von Randow, Celso and - Assis, Talita Oliveira and Bezerra, Karine Rocha Aguiar and - Tejada, Graciela and Castro, Aline Anderson and - Gomes, Diego Melo de Paula and Avancini, Rodrigo and - Aguiar, Ana Paula}, - journal = {PLOS ONE}, - volume = {17}, - number = {4}, - pages = {e0256052}, - year = {2022}, - doi = {10.1371/journal.pone.0256052} -} - @mastersthesis{SantosJunior2025, title = {Discrete Spatial Modeling: Uma proposta de arcabouço para construção de modelos dinâmicos espacialmente explícitos baseado no ecossistema Python}, author = {Santos Junior, Nerval de Jesus}, @@ -320,4 +305,4 @@ @misc{Costa2026INPE month = may, url = {https://lambdageo.github.io/presentations/2026-05-07-inpe-seminar/}, note = {Video: \url{https://www.youtube.com/watch?v=o7pMJt0CvXU}} -} +} \ No newline at end of file diff --git a/paper.md b/paper.md index a0900a3..38a2773 100644 --- a/paper.md +++ b/paper.md @@ -110,18 +110,19 @@ contract, an explicit Python counterpart to TerraME/LuccME. All benchmarks ran on an Intel Core i7-7700T @ 2.90GHz, 15 GB RAM (Ubuntu, Python 3.12.3, NumPy 2.4.6, GeoPandas 1.1.3); absolute timings vary by hardware. -**Conway's Game of Life** compares the two substrates: at 100×100 cells the vectorized -raster rule takes 0.41 ms/step against 7,069 ms/step for a per-cell vector rule, and the raster -scales to 10⁶ cells at 30.6 ms/step. +**Conway's Game of Life** compares the two substrates with the same rule and initial state. The +vector rule is a per-cell Python method querying libpysal Queen neighbors; the raster rule is one +whole-grid focal sum; final states are compared cell by cell. At 100×100 cells the raster takes +0.41 ms/step against 7,069 ms/step for the vector (a gap that reflects the per-cell style of the +vector API) and scales to 10⁶ cells at 30.6 ms/step. **BR-MANGUE coastal dynamics.** The coupled mangrove–flood model was established by Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S.B., F.M.S. and the submitting author, who are also responsible for the DisSModel reimplementation. `brmangue-dissmodel` [@BRMangue] validates the raster implementation against TerraME over the -Maranhão Island dataset (50,496 cells); figures are for v0.3.0, after fixing, in `brmangue-dissmodel`, border neighbor counting and the shared flood/mangrove snapshot. In the baseline scenario (19 steps) land use and soil match +Maranhão Island dataset (50,496 cells); figures refer to `brmangue-dissmodel` v0.3.0, which fixed border neighbor counting and the shared flood/mangrove snapshot. Match percentage suits categorical outputs [@PontiusEtAl2011]. In the baseline scenario (19 steps) land use and soil match exactly at every checkpoint and elevation matches on 97.4% of cells within 1 mm (MAE 0.00038 m; -maximum error 0.10 m); match percentage suits categorical outputs -[@PontiusEtAl2011]. In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea +maximum error 0.10 m). In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea cells by step 11 and the Python model 2,467; soil matches exactly, land use differs in 4 cells (0.008%), and at step 11 elevation matches on 94.4% of cells (MAE 0.008 m, maximum error 1.0 m). The residuals come from exact ties in elevation: the flux rule compares accumulated elevations with `<=`, so a @@ -164,10 +165,10 @@ discrete allocation components of that lineage [@LuccME]. DisSModel was presente 2026 [@Costa2026INPE]. The framework is in use across two UFMA research groups. Within LambdaGeo, `brmangue-dissmodel` -builds on the reference implementation by co-author F.M.S. Independently, Prof. Denilson da Silva -Bezerra (UFMA, former INPE), whose doctoral work founded BR-MANGUE [@Bezerra2013], uses the DisSModel reimplementation in his own coastal dynamics program (UFMA -projects PVCBS4959-2025 and PVCBS4960-2025), a collaboration predating DisSModel itself -[@Bezerra2025BM]. +builds on the reference implementation by co-author F.M.S. Co-author D.S.B. (UFMA, former INPE), +whose doctoral work founded BR-MANGUE [@Bezerra2013], also adopts the DisSModel reimplementation in +his coastal dynamics projects (UFMA PVCBS4959-2025 and PVCBS4960-2025), a collaboration predating +DisSModel itself [@Bezerra2025BM]. Since September 2026, four undergraduate research fellows (UFMA, CNPq) are being trained on the project; the stabilized `ModelExecutor` contract lets each own an independent repository — @@ -206,4 +207,4 @@ submitting author. The reference outputs used in validation come from the unmodi dual-substrate architecture, validation methodology — is the authors', and traces to the submitting author's doctoral research at INPE. The authors reviewed, tested and ran all AI-generated code and text. -## References +## References \ No newline at end of file From 07907b480b011c66653c7436f2cd5c87f91c563a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 06:38:31 -0300 Subject: [PATCH 08/24] docs(paper): name the TerraME adaptation used for BR-MANGUE; clarify lab01 and tolerance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Findings from running the validation as a JOSS reviewer would: - BR-MANGUE goldens come from a TerraME 2.0 adaptation of the published model (brmangue-terrame), as brmangue-dissmodel's docs/model-fidelity.md states; say so, cite it, and point to where the flooding goldens are regenerated (they are not shipped with v0.3.0). - The 60×60 cross-substrate check uses a 5 cm tolerance; state it. - In disslucc-benchmark the scenario named lab01_md1643 is the one that matches (cell_correction=False); the MAE 0.0036 belongs to the default run. Reword so the text matches the benchmark table. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- paper.bib | 9 +++++++++ paper.md | 21 ++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/paper.bib b/paper.bib index d101612..69a9466 100644 --- a/paper.bib +++ b/paper.bib @@ -143,6 +143,15 @@ @software{DisSLUCCBenchmark url = {https://github.com/LambdaGeo/disslucc-benchmark} } +@software{BRMangueTerraME, + author = {Costa, Sérgio Souza}, + title = {{brmangue-terrame}: {BR-MANGUE} adapted to {TerraME} 2.0, with a + headless driver for reference outputs}, + year = {2026}, + publisher = {GitHub}, + url = {https://github.com/LambdaGeo/brmangue-terrame} +} + @software{TerraMEDocker, author = {Costa, Sérgio Souza}, title = {{terrame-docker}: {TerraME} 2.0.1 and {LuccME} in a container, with diff --git a/paper.md b/paper.md index 38a2773..c78b252 100644 --- a/paper.md +++ b/paper.md @@ -119,8 +119,9 @@ vector API) and scales to 10⁶ cells at 30.6 ms/step. **BR-MANGUE coastal dynamics.** The coupled mangrove–flood model was established by Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S.B., F.M.S. and the submitting author, who are also responsible for the DisSModel reimplementation. -`brmangue-dissmodel` [@BRMangue] validates the raster implementation against TerraME over the -Maranhão Island dataset (50,496 cells); figures refer to `brmangue-dissmodel` v0.3.0, which fixed border neighbor counting and the shared flood/mangrove snapshot. Match percentage suits categorical outputs [@PontiusEtAl2011]. In the baseline scenario (19 steps) land use and soil match +`brmangue-dissmodel` [@BRMangue] validates the raster implementation against a TerraME 2.0 +adaptation of the published model [@BRMangueTerraME] over the Maranhão Island dataset (50,496 cells); +how each rule relates to the thesis is documented rule by rule in the repository. Figures refer to `brmangue-dissmodel` v0.3.0, which fixed border neighbor counting and the shared flood/mangrove snapshot. Match percentage suits categorical outputs [@PontiusEtAl2011]. In the baseline scenario (19 steps) land use and soil match exactly at every checkpoint and elevation matches on 97.4% of cells within 1 mm (MAE 0.00038 m; maximum error 0.10 m). In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea cells by step 11 and the Python model 2,467; soil matches exactly, land use differs in 4 cells @@ -128,10 +129,11 @@ cells by step 11 and the Python model 2,467; soil matches exactly, land use diff come from exact ties in elevation: the flux rule compares accumulated elevations with `<=`, so a ~1e-16 difference in summation order (sequential in TerraME, per direction in NumPy) can flip a comparison and, near the flooding threshold, a cell's class. They are floating-point effects, not a -difference in the rules; both scenarios are reproducible with the validation executor. +difference in the rules. Both scenarios are reproducible with the validation executor; the TerraME +reference outputs are regenerated by the adaptation's headless driver [@BRMangueTerraME]. Cross-substrate equivalence (60×60 synthetic grid, 10 steps) gives 100% match for land use, soil -and elevation under tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port +and elevation within a 5 cm tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port follows TerraME's per-cell loops). **disslucc as a second case.** `disslucc` [@DisSLUCC], built on DisSModel, reimplements the LuccME @@ -143,11 +145,12 @@ outputs generated in a containerized TerraME [@TerraMEDocker] and checked by SHA follow the LuccME test labs (`labNN`); the `labNN_mdX` variants exercise the convergence loop. MAE suits fractional outputs [@PontiusEtAl2011; @Willmott2005]. -`lab01_md1643` (continuous, 6,574 cells, 2008–2014) matches the reference with MAE = 0.0036 in the -final year ([0,1] scale). The residual is one deliberate deviation: LuccME's per-cell correction -(`correctCellChange`) never executes, because its guard reads a field name that does not match the -one defined, while `disslucc` runs it by default. Without it (`cell_correction=False`) the match -holds in every year, with identical iteration counts (MAE < 1e-7). +`lab01_md1643` (continuous, 6,574 cells, 2008–2014) differs from the reference by MAE = 0.0036 in +the final year ([0,1] scale) with `disslucc`'s defaults. The difference is one deliberate deviation: +LuccME's per-cell correction (`correctCellChange`) never executes, because its guard reads a field +name that does not match the one defined, while `disslucc` runs it by default. Without it +(`cell_correction=False`, the benchmark's `lab01_md1643` scenario) the match holds in every year, +with identical iteration counts (MAE < 1e-7). `lab15_md10` (discrete, 5,914 cells) reproduces the reference cell for cell, with zero quantity and allocation disagreement [@PontiusMillones2011] and identical iteration counts. Overall, ten of 21 LuccME test labs and both variants match with identical iteration counts and differences up to 3e-6 (single precision); a validated lab runs in 0.1 to 0.5 s on From 6a07e3dbd073a72096fa357071a49c04b469d9b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 07:05:47 -0300 Subject: [PATCH 09/24] docs(paper): trim the body back under the 1700-word limit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01S4SjCckAQ8koVsqjYipUHr --- paper.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/paper.md b/paper.md index c78b252..52538e8 100644 --- a/paper.md +++ b/paper.md @@ -121,7 +121,7 @@ Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S submitting author, who are also responsible for the DisSModel reimplementation. `brmangue-dissmodel` [@BRMangue] validates the raster implementation against a TerraME 2.0 adaptation of the published model [@BRMangueTerraME] over the Maranhão Island dataset (50,496 cells); -how each rule relates to the thesis is documented rule by rule in the repository. Figures refer to `brmangue-dissmodel` v0.3.0, which fixed border neighbor counting and the shared flood/mangrove snapshot. Match percentage suits categorical outputs [@PontiusEtAl2011]. In the baseline scenario (19 steps) land use and soil match +fidelity to the thesis is documented in the repository. Figures refer to v0.3.0, which fixed border neighbor counting and the shared flood/mangrove snapshot. Match percentage suits categorical outputs [@PontiusEtAl2011]. In the baseline scenario (19 steps) land use and soil match exactly at every checkpoint and elevation matches on 97.4% of cells within 1 mm (MAE 0.00038 m; maximum error 0.10 m). In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea cells by step 11 and the Python model 2,467; soil matches exactly, land use differs in 4 cells @@ -129,8 +129,8 @@ cells by step 11 and the Python model 2,467; soil matches exactly, land use diff come from exact ties in elevation: the flux rule compares accumulated elevations with `<=`, so a ~1e-16 difference in summation order (sequential in TerraME, per direction in NumPy) can flip a comparison and, near the flooding threshold, a cell's class. They are floating-point effects, not a -difference in the rules. Both scenarios are reproducible with the validation executor; the TerraME -reference outputs are regenerated by the adaptation's headless driver [@BRMangueTerraME]. +difference in the rules. Both scenarios are reproducible with the validation executor and the adaptation's +headless driver [@BRMangueTerraME]. Cross-substrate equivalence (60×60 synthetic grid, 10 steps) gives 100% match for land use, soil and elevation within a 5 cm tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port @@ -146,11 +146,10 @@ follow the LuccME test labs (`labNN`); the `labNN_mdX` variants exercise the con suits fractional outputs [@PontiusEtAl2011; @Willmott2005]. `lab01_md1643` (continuous, 6,574 cells, 2008–2014) differs from the reference by MAE = 0.0036 in -the final year ([0,1] scale) with `disslucc`'s defaults. The difference is one deliberate deviation: -LuccME's per-cell correction (`correctCellChange`) never executes, because its guard reads a field -name that does not match the one defined, while `disslucc` runs it by default. Without it -(`cell_correction=False`, the benchmark's `lab01_md1643` scenario) the match holds in every year, -with identical iteration counts (MAE < 1e-7). +the final year ([0,1] scale) with `disslucc`'s defaults. The difference is one deliberate deviation: LuccME's `correctCellChange` never executes (its guard +reads a mismatched field name), while `disslucc` runs it by default. With `cell_correction=False` +(the benchmark's `lab01_md1643` scenario) the match holds in every year, with identical iteration +counts (MAE < 1e-7). `lab15_md10` (discrete, 5,914 cells) reproduces the reference cell for cell, with zero quantity and allocation disagreement [@PontiusMillones2011] and identical iteration counts. Overall, ten of 21 LuccME test labs and both variants match with identical iteration counts and differences up to 3e-6 (single precision); a validated lab runs in 0.1 to 0.5 s on From 0d92b42f78cf91b1f8b1bedbde0cd882aa3cf678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 06:36:42 -0300 Subject: [PATCH 10/24] fix(cli): exact output name on request; real provenance in ExperimentRecord - New `--preserve-output-name` flag on `run`: write to --output exactly as given instead of injecting the short experiment ID (default unchanged). - Directory output used a hard-coded `.tif` even for vector executors; the extension now comes from the executor's optional `output_suffix` class attribute (default `.tif`, so existing behaviour is kept). - The record written by the CLI had fixed placeholders (model_name="local", model_commit="local-cli", code_version="dev"). It now records the executor's `name`, the installed dissmodel version, and the package that defines the executor (e.g. "brmangue-dissmodel==0.3.0"), also when the executor file is run as a script from inside an installed package. Loose scripts keep "local-cli". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- dissmodel/executor/cli.py | 97 +++++++++++++++++++++++++++---- docs/api/executor/cli.md | 13 +++++ tests/executor/test_cli.py | 113 ++++++++++++++++++++++++++++++++++++- 3 files changed, 211 insertions(+), 12 deletions(-) diff --git a/dissmodel/executor/cli.py b/dissmodel/executor/cli.py index c0a94b0..7dac407 100644 --- a/dissmodel/executor/cli.py +++ b/dissmodel/executor/cli.py @@ -98,9 +98,73 @@ def _load_local_params(toml_path: str | None = None) -> tuple[dict, dict]: return _load_toml(str(path)) +# ── Provenance helpers ──────────────────────────────────────────────────────── + +def _dissmodel_version() -> str: + """Installed dissmodel version, or "unknown" when not installed as a package.""" + from importlib.metadata import PackageNotFoundError, version + try: + return version("dissmodel") + except PackageNotFoundError: + return "unknown" + + +def _executor_package_version(executor_cls) -> str: + """ + "==" of the package that defines the executor + (e.g. "disslucc==0.5.0"), or "local-cli" when the executor is a loose + script or its package is not installed. + """ + from importlib.metadata import PackageNotFoundError, packages_distributions, version + + pkg_dists = packages_distributions() + top_level = (getattr(executor_cls, "__module__", "") or "").split(".")[0] + if top_level == "__main__": + # Executor file run as a script (python path/to/pkg/executor.py): + # recover the package from the file location, but only if that + # directory really is the installed package of that name. + top_level = _installed_package_containing(executor_cls, pkg_dists) + if not top_level: + return "local-cli" + for dist in pkg_dists.get(top_level, []): + try: + return f"{dist}=={version(dist)}" + except PackageNotFoundError: + continue + return "local-cli" + + +def _installed_package_containing(executor_cls, pkg_dists) -> str: + """Name of the installed top-level package whose directory holds the executor's file.""" + import importlib.util + import inspect + + try: + path = Path(inspect.getfile(executor_cls)).resolve() + except (TypeError, OSError): + return "" + for parent in path.parents: + if parent.name not in pkg_dists: + continue + try: + spec = importlib.util.find_spec(parent.name) + except (ImportError, ValueError): + continue + locations = list(getattr(spec, "submodule_search_locations", None) or []) + if any(Path(loc).resolve() == parent for loc in locations): + return parent.name + return "" + + +def _model_name(executor_cls) -> str: + if executor_cls is None: + return "local" + return getattr(executor_cls, "name", None) or executor_cls.__name__ + + # ── Record factory ──────────────────────────────────────────────────────────── -def _build_record(args): +def _build_record(args, executor_cls=None): from dissmodel.executor.schemas import DataSource, ExperimentRecord toml_path = getattr(args, "toml", None) @@ -108,9 +172,9 @@ def _build_record(args): params = {**params, **_parse_params(args.param)} # CLI overrides TOML+spec record = ExperimentRecord( - model_name = "local", - model_commit = "local-cli", - code_version = "dev", + model_name = _model_name(executor_cls), + model_commit = _executor_package_version(executor_cls) if executor_cls else "local-cli", + code_version = _dissmodel_version(), resolved_spec = {"model": spec} if spec else {}, source = DataSource( type = "s3" if args.input.startswith("s3://") else "local", @@ -130,15 +194,19 @@ def _build_record(args): # ── Output path intelligence (CLI-only) ─────────────────────────────────────── -def _apply_output_path_intelligence(record, args) -> None: +def _apply_output_path_intelligence(record, args, executor_cls=None) -> None: """ Inject experiment_id into the output path for traceability. Must run before execute_lifecycle so that save() receives the already-corrected path. - Scenario A: user passed a directory → generate a safe filename. - Scenario B: user passed a file without the experiment ID → inject it. + Scenario A: user passed a directory → generate a safe filename. The + extension comes from the executor's ``output_suffix`` class + attribute (default ``".tif"``). + Scenario B: user passed a file without the experiment ID → inject it, + unless ``--preserve-output-name`` was given, in which case + the file name is used exactly as passed. """ if not record.output_path: return @@ -148,8 +216,12 @@ def _apply_output_path_intelligence(record, args) -> None: if str(record.output_path).endswith(("/", "\\")) or p.is_dir(): # Scenario A — directory only, generate filename - safe_name = f"simulacao_{exp_id_short}.tif" + suffix = getattr(executor_cls, "output_suffix", None) or ".tif" + safe_name = f"simulacao_{exp_id_short}{suffix}" record.output_path = str(p / safe_name) + elif getattr(args, "preserve_output_name", False): + # Explicit opt-out: keep the exact name the user asked for + pass elif exp_id_short not in p.name: # Scenario B — file path without the experiment ID safe_name = f"{p.stem}_{exp_id_short}{p.suffix}" @@ -165,11 +237,11 @@ def _cmd_run(executor_cls, args) -> None: from dissmodel.executor.runner import execute_lifecycle from dissmodel.io._utils import write_text - record = _build_record(args) + record = _build_record(args, executor_cls) executor = executor_cls() # Output path intelligence runs before save() inside execute_lifecycle - _apply_output_path_intelligence(record, args) + _apply_output_path_intelligence(record, args, executor_cls) print("▶ Validating...") print("▶ Loading...") @@ -244,7 +316,7 @@ def _cmd_validate(executor_cls, args) -> None: ok = harness.run_contract_tests() if getattr(args, "input", None): - record = _build_record(args) + record = _build_record(args, executor_cls) harness.run_with_sample_data(record) sys.exit(0 if ok else 1) @@ -299,6 +371,9 @@ def _build_parser(): help="Input URI: local path or s3://bucket/key") run_p.add_argument("--output", "-o", default=None, help="Output path: local file or s3://bucket/key") + run_p.add_argument("--preserve-output-name", action="store_true", + help="Write to --output exactly as given, without " + "injecting the short experiment ID into the file name") run_p.add_argument("--column-map", action="append", metavar="CANONICAL=REAL", help="Column mapping for vector input (repeatable)") run_p.add_argument("--band-map", action="append", metavar="CANONICAL=REAL", diff --git a/docs/api/executor/cli.md b/docs/api/executor/cli.md index 0f98148..fe5c6a0 100644 --- a/docs/api/executor/cli.md +++ b/docs/api/executor/cli.md @@ -146,8 +146,21 @@ accidental overwrites: # File already has ID → unchanged --output results/run_ec17096d.tif → unchanged + +# Exact name requested → unchanged +--output results/run.tif --preserve-output-name → results/run.tif ``` +When `--output` is a directory, the extension of the generated file comes +from the executor's optional `output_suffix` class attribute (default +`.tif`), so a vector executor can declare `output_suffix = ".gpkg"`. + +The record written next to the output (`.record.json`) fills its +provenance fields from the environment: `model_name` is the executor's +`name`, `code_version` the installed dissmodel version, and `model_commit` +the package that defines the executor (e.g. `disslucc==0.5.0`), or +`local-cli` for an executor defined in a loose script. + --- ## API Reference diff --git a/tests/executor/test_cli.py b/tests/executor/test_cli.py index bc09dd4..2d3432c 100644 --- a/tests/executor/test_cli.py +++ b/tests/executor/test_cli.py @@ -287,4 +287,115 @@ def test_no_output_path_is_noop(self, make_record): args = SimpleNamespace(output=None) _apply_output_path_intelligence(record, args) assert record.output_path is None - assert args.output is None \ No newline at end of file + assert args.output is None + +# ── --preserve-output-name and executor-specific suffix ────────────────────── + +class TestPreserveOutputName: + + def test_flag_keeps_exact_file_name(self, make_record): + record = make_record() + record.output_path = "outputs/result.gpkg" + args = SimpleNamespace(output="outputs/result.gpkg", preserve_output_name=True) + _apply_output_path_intelligence(record, args) + assert record.output_path == "outputs/result.gpkg" + assert args.output == "outputs/result.gpkg" + + def test_flag_absent_keeps_previous_behaviour(self, make_record): + record = make_record() + record.output_path = "outputs/result.gpkg" + args = SimpleNamespace(output="outputs/result.gpkg") + _apply_output_path_intelligence(record, args) + assert record.output_path == f"outputs/result_{record.experiment_id[:8]}.gpkg" + + def test_flag_still_generates_name_for_directory(self, make_record): + record = make_record() + record.output_path = "outputs/" + args = SimpleNamespace(output="outputs/", preserve_output_name=True) + _apply_output_path_intelligence(record, args) + assert record.output_path == f"outputs/simulacao_{record.experiment_id[:8]}.tif" + + def test_directory_uses_executor_output_suffix(self, make_record): + class VectorExec: + output_suffix = ".gpkg" + record = make_record() + record.output_path = "outputs/" + args = SimpleNamespace(output="outputs/") + _apply_output_path_intelligence(record, args, VectorExec) + assert record.output_path == f"outputs/simulacao_{record.experiment_id[:8]}.gpkg" + + def test_parser_accepts_flag(self): + from dissmodel.executor.cli import _build_parser + ns = _build_parser().parse_args( + ["run", "--input", "in.gpkg", "--output", "out.gpkg", "--preserve-output-name"] + ) + assert ns.preserve_output_name is True + + +# ── Provenance fields filled from the environment ──────────────────────────── + +class TestRecordProvenance: + + def _args(self): + return SimpleNamespace( + toml=None, param=None, input="data.zip", output=None, + format="auto", column_map=None, band_map=None, + ) + + def test_code_version_is_installed_dissmodel(self, tmp_path, monkeypatch): + from importlib.metadata import version + monkeypatch.chdir(tmp_path) # no stray model.toml + record = _build_record(self._args()) + assert record.code_version == version("dissmodel") + assert record.code_version != "dev" + + def test_model_name_comes_from_executor(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + class Exec: + name = "forest_fire" + assert _build_record(self._args(), Exec).model_name == "forest_fire" + + def test_model_name_falls_back_to_class_name(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + class MyExecutor: + pass + assert _build_record(self._args(), MyExecutor).model_name == "MyExecutor" + + def test_model_commit_records_executor_package(self, tmp_path, monkeypatch): + from importlib.metadata import version + + from dissmodel.executor.model_executor import ModelExecutor + monkeypatch.chdir(tmp_path) + # ModelExecutor lives in the installed dissmodel distribution + record = _build_record(self._args(), ModelExecutor) + assert record.model_commit == f"dissmodel=={version('dissmodel')}" + + def _main_module_from(self, path, monkeypatch): + """Load ``path`` and install it as ``__main__``, as ``python path`` would.""" + import importlib.util + import sys + + spec = importlib.util.spec_from_file_location("__main__", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + monkeypatch.setitem(sys.modules, "__main__", module) + return module + + def test_loose_script_executor_is_local_cli(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + script = tmp_path / "my_executor.py" + script.write_text("class Exec:\n name = 'loose'\n") + module = self._main_module_from(script, monkeypatch) + assert _build_record(self._args(), module.Exec).model_commit == "local-cli" + + def test_script_inside_installed_package_records_package(self, tmp_path, monkeypatch): + # python path/to/dissmodel/executor/some_script.py → dissmodel== + from importlib.metadata import version + from pathlib import Path as _P + + import dissmodel.executor as pkg + monkeypatch.chdir(tmp_path) + script = _P(pkg.__file__).parent / "model_executor.py" + module = self._main_module_from(script, monkeypatch) + record = _build_record(self._args(), module.ModelExecutor) + assert record.model_commit == f"dissmodel=={version('dissmodel')}" From c359e6987bca2cd2f4e975471fa00db82e470851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 06:37:55 -0300 Subject: [PATCH 11/24] docs: replace legacy dissmodel.geo.raster.io guide; fix README executor import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shapefile_to_raster.md (repo root, not in the mkdocs nav) documented `dissmodel.geo.raster.io`, which no longer exists: every import in it raised ImportError. Moved to docs/examples/vector_to_raster.md, rewritten for `dissmodel.io.convert.vector_to_raster_backend` and `dissmodel.io.raster.save_geotiff`, and added to the nav. Dropped the unmeasured "≈ 4,500× faster" claim in favour of a pointer to the benchmarks. All snippets were run against the synthetic 60×60 grid and `mkdocs build --strict` passes. - README step 3 used `ForestFireModel` without importing it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- README.md | 2 + docs/examples/vector_to_raster.md | 141 ++++++++++++++++++++++++++++++ mkdocs.yml | 1 + shapefile_to_raster.md | 141 ------------------------------ 4 files changed, 144 insertions(+), 141 deletions(-) create mode 100644 docs/examples/vector_to_raster.md delete mode 100644 shapefile_to_raster.md diff --git a/README.md b/README.md index 6159a7e..82407c2 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ 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 + class ForestFireExecutor(ModelExecutor): name = "forest_fire" diff --git a/docs/examples/vector_to_raster.md b/docs/examples/vector_to_raster.md new file mode 100644 index 0000000..ba71c8c --- /dev/null +++ b/docs/examples/vector_to_raster.md @@ -0,0 +1,141 @@ +# Loading Vector Data into the Raster Substrate + +DisSModel can load any vector file (Shapefile, GeoJSON, GeoPackage, or a +`.zip` containing a shapefile) and convert it directly into a +`RasterBackend`, so raster models can run on real geographic data without +intermediate GIS steps. + +## How it works + +``` +vector file → GeoDataFrame → rasterize → NumPy arrays → RasterBackend → raster model +``` + +The rasterization step (powered by `rasterio.features.rasterize`) runs +once; after that the model runs entirely on NumPy arrays. + +!!! note "Grid regularity" + This workflow is most accurate when the input already contains a + **regular grid** of equal-area polygons (e.g. 100×100 m cells), the + typical output of spatial homogenization tools. For irregular polygons + (municipalities, watersheds), cell values are burned by centroid or by + touch — inspect the result before running long simulations. + +--- + +## `vector_to_raster_backend` + +```python +from dissmodel.io.convert import vector_to_raster_backend + +b = vector_to_raster_backend( + source = "data/mangue_grid.shp", # path, .zip, or a GeoDataFrame + resolution = 100, # 100 m cells + attrs = ["uso", "alt", "solo"], + crs = "EPSG:31984", # reproject if needed +) + +print(b.shape) # (rows, cols) from bounding box + resolution +print(b.band_names()) # ['mask', 'uso', 'alt', 'solo'] +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `source` | `str`, `Path` or `GeoDataFrame` | Vector file or in-memory GeoDataFrame | +| `resolution` | `float` | Cell size in CRS units (metres for a metric CRS) | +| `attrs` | `list[str]` or `dict[str, default]` | Columns to rasterize | +| `crs` | `str`, `int`, or `None` | Target CRS — reprojects if needed | +| `all_touched` | `bool` | Burn all touched cells (default: centre only) | +| `nodata` | `int` or `float` | Fill value for uncovered cells (default: `0`) | +| `add_mask` | `bool` | Add a `"mask"` band marking covered cells (default: `True`) | + +To set per-column defaults for uncovered cells, pass a dict: + +```python +b = vector_to_raster_backend( + source = "data/mangue_grid.shp", + resolution = 100, + attrs = {"uso": 5, "alt": 0.0, "solo": 1}, + crs = "EPSG:31984", +) +``` + +`shapefile_to_raster_backend` is a deprecated alias of this function and +emits a `FutureWarning`. + +--- + +## Full example — raster model from a vector file + +```python +from dissmodel.core import Environment +from dissmodel.io.convert import vector_to_raster_backend +from dissmodel.visualization.raster_map import RasterMap + +# your own RasterModel subclass, e.g. the BR-MANGUE flood model +from myproject.flood import FloodRasterModel + +# 1. vector file → RasterBackend +b = vector_to_raster_backend( + source = "data/mangue_grid.shp", + resolution = 100, + attrs = {"uso": 5, "alt": 0.0, "solo": 1}, + crs = "EPSG:31984", +) +print(f"Grid: {b.shape[0]} rows × {b.shape[1]} cols = {b.shape[0] * b.shape[1]:,} cells") + +# 2. run the raster model +env = Environment(start_time=2012, end_time=2100) +FloodRasterModel(backend=b, taxa=0.011) +RasterMap(backend=b, band="uso", title="Land Use") +env.run() +``` + +--- + +## Saving results to GeoTIFF + +`vector_to_raster_backend` stores the CRS and affine transform on the +backend, so `save_geotiff` writes a georeferenced file without extra +arguments: + +```python +from dissmodel.io.raster import save_geotiff + +checksum = save_geotiff(b, "output/flood_result.tif") # returns SHA-256 +``` + +--- + +## Same data, two substrates + +```python +# ── vector substrate ───────────────────────────────────────────────────────── +import geopandas as gpd + +gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984") +# → GeoDataFrame with real geometries; use with SpatialModel / CellularAutomaton + +# ── raster substrate ───────────────────────────────────────────────────────── +from dissmodel.io.convert import vector_to_raster_backend + +b = vector_to_raster_backend( + "data/mangue_grid.shp", resolution=100, + attrs=["uso", "alt", "solo"], crs="EPSG:31984", +) +# → RasterBackend with the same attributes; use with RasterModel +``` + +The data source is the same file; only the substrate changes. For measured +run times of the two substrates on the same model, see the benchmarks in +`benchmarks/` and the `brmangue-dissmodel` benchmark executor. + +--- + +## API Reference + +::: dissmodel.io.convert.vector_to_raster_backend + +::: dissmodel.io.raster.save_geotiff diff --git a/mkdocs.yml b/mkdocs.yml index c4b1613..6421c36 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,7 @@ nav: - Overview: examples.md - CLI Examples: examples/cli.md - Streamlit Examples: examples/streamlit.md + - Vector to Raster: examples/vector_to_raster.md - Notebooks: - Game of Life: examples/notebooks/ca_game_of_life.ipynb - SIR Model: examples/notebooks/sysdyn_sir.ipynb diff --git a/shapefile_to_raster.md b/shapefile_to_raster.md deleted file mode 100644 index 6bdc781..0000000 --- a/shapefile_to_raster.md +++ /dev/null @@ -1,141 +0,0 @@ -# Loading Shapefiles into the Raster Substrate - -DisSModel can load any vector file (Shapefile, GeoJSON, GeoPackage) and -convert it directly into a `RasterBackend`, making it possible to run -high-performance raster models on real geographic data without any -intermediate GIS steps. - -## How it works - -``` -Shapefile → GeoDataFrame → rasterize → NumPy arrays → RasterBackend → raster model -``` - -The rasterization step (powered by `rasterio.features.rasterize`) is fast -and happens once — after that, the model runs entirely in NumPy at full -vectorized speed. - -!!! note "Grid regularity" - This workflow is most accurate when the input shapefile already contains - a **regular grid** of equal-area polygons (e.g. 100×100m cells), which is - the typical output of spatial homogenization tools. For irregular polygons - (municipalities, watersheds), cell values are burned by centroid or by - touch — inspect the result before running long simulations. - ---- - -## `shapefile_to_raster_backend` - -```python -from dissmodel.geo.raster.io import shapefile_to_raster_backend - -b = shapefile_to_raster_backend( - path = "data/mangue_grid.shp", - resolution = 100, # 100m cells - attrs = ["uso", "alt", "solo"], - crs = "EPSG:31984", # reproject if needed -) - -print(b.shape) # (rows, cols) derived from bounding box + resolution -print(b.get("uso").dtype) # int32 -``` - -### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `path` | `str` or `Path` | Path to the vector file | -| `resolution` | `float` | Cell size in CRS units (metres for metric CRS) | -| `attrs` | `list[str]` or `dict[str, default]` | Columns to rasterize | -| `crs` | `str`, `int`, or `None` | Target CRS — reprojects if needed | -| `all_touched` | `bool` | Burn all touched cells (default: centre only) | -| `nodata` | `int` or `float` | Fill value for uncovered cells (default: `0`) | - -To set per-column defaults for uncovered cells, pass a dict: - -```python -b = shapefile_to_raster_backend( - path = "data/mangue_grid.shp", - resolution = 100, - attrs = {"uso": 5, "alt": 0.0, "solo": 1}, - crs = "EPSG:31984", -) -``` - ---- - -## Full example — flood model from shapefile - -```python -from dissmodel.core import Environment -from dissmodel.geo.raster.io import shapefile_to_raster_backend -from dissmodel.visualization.raster_map import RasterMap - -# from coastal_dynamics.raster import FloodRasterModel -# (or your own RasterModel subclass) -from myproject.flood import FloodRasterModel - -# 1. load shapefile → RasterBackend -b = shapefile_to_raster_backend( - path = "data/mangue_grid.shp", - resolution = 100, - attrs = {"uso": 5, "alt": 0.0, "solo": 1}, - crs = "EPSG:31984", -) - -print(f"Grid: {b.shape[0]} rows × {b.shape[1]} cols = {b.shape[0]*b.shape[1]:,} cells") - -# 2. run the raster model -env = Environment(start_time=2012, end_time=2100) -FloodRasterModel(backend=b, taxa=0.011) -RasterMap(backend=b, band="uso", title="Land Use") -env.run() -``` - ---- - -## Saving results back to GeoTIFF - -```python -from dissmodel.geo.raster.io import save_raster_backend - -save_raster_backend( - backend = b, - path = "output/flood_result.tif", - bands = ["uso", "alt"], - crs = "EPSG:31984", - transform = transform, # rasterio Affine from original bounds -) -``` - ---- - -## Comparison: vector vs raster workflow from the same shapefile - -```python -# ── vector workflow ─────────────────────────────────────────────────────────── -import geopandas as gpd -from dissmodel.geo.vector.model import SpatialModel - -gdf = gpd.read_file("data/mangue_grid.shp").to_crs("EPSG:31984") -# → GeoDataFrame with real geometries, Queen neighbourhood, ~2 min/step @ 94k cells - -# ── raster workflow ─────────────────────────────────────────────────────────── -from dissmodel.geo.raster.io import shapefile_to_raster_backend - -b = shapefile_to_raster_backend( - "data/mangue_grid.shp", resolution=100, - attrs=["uso", "alt", "solo"], crs="EPSG:31984" -) -# → RasterBackend, same data, ~8 ms/step @ 94k cells (≈ 4,500× faster) -``` - -The data source is the same shapefile. The only difference is the substrate. - ---- - -## API Reference - -::: dissmodel.geo.raster.io.shapefile_to_raster_backend - -::: dissmodel.geo.raster.io.save_raster_backend From 576fcbc54d793dc94d75ba630a4705bce303e5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 06:39:50 -0300 Subject: [PATCH 12/24] =?UTF-8?q?perf(vector):=20MIN=5FDISTANCE=20fill=20v?= =?UTF-8?q?ia=20spatial=20index=20instead=20of=20N=C3=97M=20apply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_fill_min_distance` measured every cell against every target geometry in Python (`apply` + `distance().min()`). It now asks the target's STRtree (`sindex.nearest`) for the nearest geometry of each cell and measures only that pair with `shapely.distance`. Same values as before (tests compare against the brute force for points, lines and polygons, bit for bit); missing/empty geometries and an empty target still give NaN; index and row order are preserved; a CRS mismatch still warns. On a 200×200 grid with 2,000 random lines: 66 s → ~7 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- dissmodel/geo/vector/fill.py | 39 ++++++++++++++-- tests/vector/test_fill.py | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/dissmodel/geo/vector/fill.py b/dissmodel/geo/vector/fill.py index 67ba6e0..806feec 100644 --- a/dissmodel/geo/vector/fill.py +++ b/dissmodel/geo/vector/fill.py @@ -251,10 +251,43 @@ def _fill_min_distance( attr_name : str, optional Name of the column to store the distances, by default ``"min_distance"``. + + Notes + ----- + Uses the spatial index of ``to_gdf`` (an STRtree) to find, for each + row, the nearest target geometry and then measures that one pair, so + the cost grows roughly as N log M instead of N × M. The distances are + the same as a brute-force minimum over all targets. Rows with a + missing or empty geometry, or an empty ``to_gdf``, get ``NaN``. """ - from_gdf[attr_name] = from_gdf.geometry.apply( - lambda geom: to_gdf.geometry.distance(geom).min() - ) + import warnings + + import shapely + + if ( + from_gdf.crs is not None + and to_gdf.crs is not None + and not from_gdf.crs.equals(to_gdf.crs) + ): + warnings.warn( + "CRS mismatch between from_gdf and to_gdf " + f"({from_gdf.crs.to_string()} != {to_gdf.crs.to_string()}); " + "distances are computed in raw coordinates. " + "Use to_crs() to align them.", + UserWarning, + stacklevel=3, + ) + + result = np.full(len(from_gdf), np.nan) + if len(from_gdf) and len(to_gdf): + # (positions in from_gdf, positions in to_gdf) of each nearest pair; + # rows with missing/empty geometries produce no pair and stay NaN. + src_pos, tgt_pos = to_gdf.sindex.nearest(from_gdf.geometry, return_all=False) + result[src_pos] = shapely.distance( + from_gdf.geometry.values[src_pos], + to_gdf.geometry.values[tgt_pos], + ) + from_gdf[attr_name] = result # --------------------------------------------------------------------------- diff --git a/tests/vector/test_fill.py b/tests/vector/test_fill.py index 1e9f1d2..9005870 100644 --- a/tests/vector/test_fill.py +++ b/tests/vector/test_fill.py @@ -57,3 +57,91 @@ def test_parse_idx_roundtrip(): pos = parse_idx("3-4") assert pos.row == 3 assert pos.col == 4 + + +# --------------------------------------------------------------------------- +# MIN_DISTANCE — spatial-index implementation must equal the brute force +# --------------------------------------------------------------------------- + +def _brute_force_min_distance(from_gdf, to_gdf): + return from_gdf.geometry.apply(lambda g: to_gdf.geometry.distance(g).min()).to_numpy() + + +def _targets(kind, rng, n): + import geopandas as gpd + from shapely.geometry import LineString, Point, box + + if kind == "points": + geoms = [Point(*rng.random(2) * 20) for _ in range(n)] + elif kind == "lines": + geoms = [LineString(rng.random((3, 2)) * 20) for _ in range(n)] + else: + geoms = [] + for _ in range(n): + x, y = rng.random(2) * 18 + geoms.append(box(x, y, x + 1.5, y + 1.5)) + return gpd.GeoDataFrame(geometry=geoms) + + +@pytest.mark.parametrize("kind", ["points", "lines", "polygons"]) +def test_min_distance_matches_brute_force(kind): + import numpy as np + + rng = np.random.default_rng(42) + grid = vector_grid(dimension=(20, 20), resolution=1.0) + targets = _targets(kind, rng, 25) + expected = _brute_force_min_distance(grid, targets) + + fill(FillStrategy.MIN_DISTANCE, from_gdf=grid, to_gdf=targets, attr_name="d") + + np.testing.assert_array_equal(grid["d"].to_numpy(), expected) + # cells overlapped by a target are at distance zero + if kind == "polygons": + assert (grid["d"] == 0).any() + + +def test_min_distance_keeps_index_and_row_order(): + import geopandas as gpd + from shapely.geometry import Point + + grid = vector_grid(dimension=(4, 3), resolution=1.0) + shuffled = grid.sample(frac=1.0, random_state=0) + targets = gpd.GeoDataFrame(geometry=[Point(0, 0)]) + fill(FillStrategy.MIN_DISTANCE, from_gdf=shuffled, to_gdf=targets) + + assert list(shuffled.index) == list(grid.sample(frac=1.0, random_state=0).index) + for idx, row in shuffled.iterrows(): + assert row["min_distance"] == row.geometry.distance(Point(0, 0)) + + +def test_min_distance_empty_target_gives_nan(): + import geopandas as gpd + + grid = vector_grid(dimension=(3, 3), resolution=1.0) + fill(FillStrategy.MIN_DISTANCE, from_gdf=grid, to_gdf=gpd.GeoDataFrame(geometry=[])) + assert grid["min_distance"].isna().all() + + +def test_min_distance_missing_geometry_gives_nan(): + import geopandas as gpd + import numpy as np + from shapely.geometry import Point + + grid = vector_grid(dimension=(3, 1), resolution=1.0) + grid.loc[grid.index[1], "geometry"] = None + targets = gpd.GeoDataFrame(geometry=[Point(-1, 0.5)]) + fill(FillStrategy.MIN_DISTANCE, from_gdf=grid, to_gdf=targets) + + values = grid["min_distance"].to_numpy() + assert values[0] == 1.0 and values[2] == 3.0 # cells span x in [0,1] and [2,3] + assert np.isnan(values[1]) + + +def test_min_distance_warns_on_crs_mismatch(): + import geopandas as gpd + from shapely.geometry import Point + + grid = vector_grid(dimension=(2, 2), resolution=1.0, crs="EPSG:31984") + targets = gpd.GeoDataFrame(geometry=[Point(0, 0)], crs="EPSG:4326") + with pytest.warns(UserWarning, match="CRS mismatch"): + fill(FillStrategy.MIN_DISTANCE, from_gdf=grid, to_gdf=targets) From d2e0b0e85d2e266a2509872148f4210a08910a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 06:40:32 -0300 Subject: [PATCH 13/24] perf(vector): build vector_grid cells in one vectorized shapely.box call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two nested Python loops created one `shapely.geometry.box` per cell. `np.meshgrid` + `shapely.box` on arrays now builds all cells at once, in the same column-major order and with the same "row-col" ids, which neighbourhoods and raster conversion depend on. A new test keeps the old loop as reference and checks, for the dimension, bounds+resolution and bounds+dimension forms, that ids and every vertex are identical. 1000×1000 grid: ~20 s → ~4 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- dissmodel/geo/vector/vector_grid.py | 14 +++++------ tests/vector/test_vector_grid.py | 36 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/dissmodel/geo/vector/vector_grid.py b/dissmodel/geo/vector/vector_grid.py index 8b0b73c..6f2ff8d 100644 --- a/dissmodel/geo/vector/vector_grid.py +++ b/dissmodel/geo/vector/vector_grid.py @@ -5,7 +5,7 @@ import geopandas as gpd import numpy as np -from shapely.geometry import box +import shapely # Reusable type aliases Bounds = tuple[float, float, float, float] # (xmin, ymin, xmax, ymax) @@ -144,12 +144,12 @@ def vector_grid( x_edges: np.ndarray = np.arange(xmin, xmax, resolution_x) y_edges: np.ndarray = np.arange(ymin, ymax, resolution_y) - grid_cells = [] - ids = [] - for i, x0 in enumerate(x_edges): - for j, y0 in enumerate(y_edges): - grid_cells.append(box(x0, y0, x0 + resolution_x, y0 + resolution_y)) - ids.append(f"{j}-{i}") + # Column-major order (x outer, y inner) and "row-col" ids, as before: + # neighbourhoods and raster conversions rely on this row order. + x0, y0 = np.meshgrid(x_edges, y_edges, indexing="ij") + x0, y0 = x0.ravel(), y0.ravel() + grid_cells = shapely.box(x0, y0, x0 + resolution_x, y0 + resolution_y) + ids = [f"{j}-{i}" for i in range(len(x_edges)) for j in range(len(y_edges))] data: dict[str, Any] = {"geometry": grid_cells, "id": ids} for key, value in attrs.items(): diff --git a/tests/vector/test_vector_grid.py b/tests/vector/test_vector_grid.py index 1d86cb1..3aa90f9 100644 --- a/tests/vector/test_vector_grid.py +++ b/tests/vector/test_vector_grid.py @@ -178,3 +178,39 @@ def test_consistent_with_grid_index(self): for idx in gdf.index: pos = parse_idx(idx) assert f"{pos.row}-{pos.col}" == idx + + +# ── Vectorized construction must reproduce the original loop exactly ───────── + +def _loop_reference(x_edges, y_edges, rx, ry): + """The pre-vectorization implementation, kept as the reference.""" + from shapely.geometry import box + + cells, ids = [], [] + for i, x0 in enumerate(x_edges): + for j, y0 in enumerate(y_edges): + cells.append(box(x0, y0, x0 + rx, y0 + ry)) + ids.append(f"{j}-{i}") + return cells, ids + + +@pytest.mark.parametrize( + "kwargs, x_edges, y_edges, rx, ry", + [ + (dict(dimension=(4, 3), resolution=1.0), + np.arange(0, 4, 1.0), np.arange(0, 3, 1.0), 1.0, 1.0), + (dict(bounds=(10.0, -5.0, 13.5, -1.0), resolution=0.5), + np.arange(10.0, 13.5, 0.5), np.arange(-5.0, -1.0, 0.5), 0.5, 0.5), + (dict(bounds=(0.0, 0.0, 10.0, 6.0), dimension=(5, 3)), + np.arange(0.0, 10.0, 2.0), np.arange(0.0, 6.0, 2.0), 2.0, 2.0), + ], +) +def test_vectorized_grid_identical_to_loop(kwargs, x_edges, y_edges, rx, ry): + gdf = vector_grid(**kwargs) + cells, ids = _loop_reference(x_edges, y_edges, rx, ry) + + assert list(gdf.index) == ids + assert gdf.index.name == "id" + for got, ref in zip(gdf.geometry, cells, strict=True): + assert got.equals_exact(ref, 0.0) + assert list(got.exterior.coords) == list(ref.exterior.coords) From 6918725b5a44e00410df0dfacc4527f07036d087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 06:42:52 -0300 Subject: [PATCH 14/24] chore(release): 0.6.6 CLI output name and provenance fixes, vector-to-raster docs, faster MIN_DISTANCE fill and vector_grid, plus the raster georeference changes already in Unreleased. Stays in 0.6.x so packages pinned to `dissmodel>=0.6.4,<0.7.0` (disslucc 0.5.0) pick it up unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ CITATION.cff | 4 ++-- pyproject.toml | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3acfb..f083651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.6] — 2026-10-08 + +### Fixed +- CLI: when `--output` is a directory, the generated file name used a + hard-coded `.tif` even for vector executors. The extension now comes from + the executor's optional `output_suffix` class attribute (default `.tif`). +- CLI: the `ExperimentRecord` written by `run` had fixed placeholders + (`model_name="local"`, `model_commit="local-cli"`, `code_version="dev"`). + It now records the executor's `name`, the installed dissmodel version and + the package that defines the executor (e.g. `disslucc==0.5.0`); loose + scripts keep `local-cli`. +- Docs: the vector-to-raster guide documented the removed + `dissmodel.geo.raster.io` module; rewritten for + `dissmodel.io.convert.vector_to_raster_backend`, moved to + `docs/examples/vector_to_raster.md` and added to the site navigation. + README step 3 now imports `ForestFireModel`. + +### Performance +- `fill(FillStrategy.MIN_DISTANCE, ...)` uses the target's spatial index + (`sindex.nearest`) instead of measuring every cell against every target; + same values, ~10× faster on 40,000 cells × 2,000 lines. +- `vector_grid` builds all cells with one vectorized `shapely.box` call; + same geometries, ids and row order, ~5× faster on a 1000×1000 grid. + ### Changed - `load_geotiff` sets `RasterBackend.transform` and `RasterBackend.crs` from the file (they were only in the returned `meta` dict), and `save_geotiff` @@ -20,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `crs` — square metres on the ellipsoid for a geographic CRS (a 1/12° cell is ~86 km² at the equator and ~72 km² at 33° S), the pixel area for a projected one. +- CLI: `run --preserve-output-name` writes to `--output` exactly as given, + without injecting the short experiment ID. ## [0.6.5] — 2026-09-22 diff --git a/CITATION.cff b/CITATION.cff index cd76e3c..ccec115 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -14,8 +14,8 @@ authors: given-names: "Denilson da Silva" orcid: "https://orcid.org/0000-0002-9567-7828" title: "DisSModel: A Python Framework for Spatially Explicit Dynamic Modeling" -version: 0.6.5 -date-released: 2026-09-22 +version: 0.6.6 +date-released: 2026-10-08 url: "https://github.com/DisSModel/dissmodel" repository-code: "https://github.com/DisSModel/dissmodel" license: MIT diff --git a/pyproject.toml b/pyproject.toml index 60ee5b0..81e03f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dissmodel" -version = "0.6.5" +version = "0.6.6" description = "Discrete Spatial Modeling framework for raster and vector simulations" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" From 953a8e32c06954ebc65f817310bc90f9a5d0b489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 08:08:51 -0300 Subject: [PATCH 15/24] fix(cli,io): create output directory; clear error on data/extension mismatch Running the README executor as a reviewer would, with `--output results/`: - the CLI did not create `results/`, so the save failed with FileNotFoundError; `run` now creates the parent of a local output path. - with the directory created, the generated `simulacao_.tif` made save_dataset hand a GeoDataFrame to the GeoTIFF writer, which failed with `AttributeError: 'GeoDataFrame' object has no attribute 'arrays'`. save_dataset now checks the data type against the URI's format first and raises a TypeError that says what to do; the README executor declares `output_suffix = ".gpkg"`, so `--output results/` writes a GeoPackage. Both problems predate 0.6.6 (the `.tif` was hard-coded before). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- CHANGELOG.md | 6 ++++++ README.md | 1 + dissmodel/executor/cli.py | 8 ++++++++ dissmodel/io/_dispatch.py | 27 +++++++++++++++++++++++++++ tests/executor/test_cli.py | 18 ++++++++++++++++++ tests/io/test_dispatch.py | 30 ++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f083651..4fb9cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `dissmodel.io.convert.vector_to_raster_backend`, moved to `docs/examples/vector_to_raster.md` and added to the site navigation. README step 3 now imports `ForestFireModel`. +- CLI: `run` creates the parent directory of a local `--output` path; with + `--output results/` and no `results/` it failed with `FileNotFoundError`. +- `save_dataset` raises a `TypeError` that names the fix when the data cannot + be written to the format of the URI (a GeoDataFrame to `.tif`, a + `RasterBackend` to `.gpkg`), instead of an `AttributeError` from deep inside + the writer. The README executor declares `output_suffix = ".gpkg"`. ### Performance - `fill(FillStrategy.MIN_DISTANCE, ...)` uses the target's spatial index diff --git a/README.md b/README.md index 82407c2..c39cb28 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ from forest_fire_model import ForestFireModel # the model from step 1 class ForestFireExecutor(ModelExecutor): name = "forest_fire" + output_suffix = ".gpkg" # file type used when --output is a directory def load(self, record: ExperimentRecord): gdf, checksum = load_dataset(record.source.uri) diff --git a/dissmodel/executor/cli.py b/dissmodel/executor/cli.py index 7dac407..3597cc7 100644 --- a/dissmodel/executor/cli.py +++ b/dissmodel/executor/cli.py @@ -231,6 +231,13 @@ def _apply_output_path_intelligence(record, args, executor_cls=None) -> None: args.output = record.output_path +def _ensure_local_output_dir(output_path: str | None) -> None: + """Create the parent directory of a local output path (s3:// is left alone).""" + if not output_path or "://" in output_path: + return + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + + # ── Commands ────────────────────────────────────────────────────────────────── def _cmd_run(executor_cls, args) -> None: @@ -242,6 +249,7 @@ def _cmd_run(executor_cls, args) -> None: # Output path intelligence runs before save() inside execute_lifecycle _apply_output_path_intelligence(record, args, executor_cls) + _ensure_local_output_dir(record.output_path) print("▶ Validating...") print("▶ Loading...") diff --git a/dissmodel/io/_dispatch.py b/dissmodel/io/_dispatch.py index 94749d3..f81cc1a 100644 --- a/dissmodel/io/_dispatch.py +++ b/dissmodel/io/_dispatch.py @@ -36,6 +36,32 @@ def load_dataset(uri: str, minio_client=None, fmt: str | None = None, **kwargs): raise ValueError(f"Unsupported format: '{resolved_fmt}'") +def _check_data_matches_format(data, uri: str, fmt: str) -> None: + """ + Fail early, with a readable message, when the data type cannot be + written to the format chosen by the URI (e.g. a GeoDataFrame to .tif). + Without this the writer fails deep inside with an AttributeError. + """ + import geopandas as gpd + + from dissmodel.geo.raster.backend import RasterBackend + + backend = data[0] if isinstance(data, tuple) and data else data + if fmt == "raster" and isinstance(data, gpd.GeoDataFrame): + raise TypeError( + f"Cannot save a GeoDataFrame to raster path '{uri}'. Use a vector " + "extension (.gpkg, .shp, .geojson); for the CLI, pass --output " + "with that extension or set `output_suffix = \".gpkg\"` on the " + "executor class." + ) + if fmt == "vector" and isinstance(backend, RasterBackend): + raise TypeError( + f"Cannot save a RasterBackend to vector path '{uri}'. Use a raster " + "extension (.tif); for the CLI, pass --output with that extension " + "or set `output_suffix = \".tif\"` on the executor class." + ) + + def save_dataset(data, uri: str, minio_client=None, fmt: str | None = None, **kwargs) -> str: """ Save any supported dataset to a URI. @@ -47,6 +73,7 @@ def save_dataset(data, uri: str, minio_client=None, fmt: str | None = None, **kw sha256_checksum of the saved file """ resolved_fmt = fmt or detect_format(uri) + _check_data_matches_format(data, uri, resolved_fmt) if resolved_fmt == "vector": from dissmodel.io.vector import save_gdf diff --git a/tests/executor/test_cli.py b/tests/executor/test_cli.py index 2d3432c..39ca104 100644 --- a/tests/executor/test_cli.py +++ b/tests/executor/test_cli.py @@ -399,3 +399,21 @@ def test_script_inside_installed_package_records_package(self, tmp_path, monkeyp module = self._main_module_from(script, monkeypatch) record = _build_record(self._args(), module.ModelExecutor) assert record.model_commit == f"dissmodel=={version('dissmodel')}" + + +# ── Output directory creation ──────────────────────────────────────────────── + +class TestEnsureLocalOutputDir: + + def test_creates_missing_parent(self, tmp_path): + from dissmodel.executor.cli import _ensure_local_output_dir + target = tmp_path / "a" / "b" / "out.gpkg" + _ensure_local_output_dir(str(target)) + assert target.parent.is_dir() + + def test_s3_and_none_are_ignored(self, tmp_path, monkeypatch): + from dissmodel.executor.cli import _ensure_local_output_dir + monkeypatch.chdir(tmp_path) + _ensure_local_output_dir("s3://bucket/key/out.tif") + _ensure_local_output_dir(None) + assert list(tmp_path.iterdir()) == [] diff --git a/tests/io/test_dispatch.py b/tests/io/test_dispatch.py index 383649f..5ee35c1 100644 --- a/tests/io/test_dispatch.py +++ b/tests/io/test_dispatch.py @@ -164,3 +164,33 @@ def test_missing_minio_package_raises_importerror(self, monkeypatch): monkeypatch.setitem(sys.modules, "minio", None) with pytest.raises(ImportError, match="minio"): _storage.get_default_client() + + +# --------------------------------------------------------------------------- +# save_dataset — data type must match the format of the URI +# --------------------------------------------------------------------------- + +class TestSaveDatasetTypeMismatch: + + def test_geodataframe_to_tif_raises_clear_error(self, tmp_path): + from dissmodel.geo import vector_grid + + gdf = vector_grid(dimension=(2, 2), resolution=1.0, crs="EPSG:31984") + with pytest.raises(TypeError, match="GeoDataFrame to raster path"): + save_dataset(gdf, str(tmp_path / "out.tif")) + assert not (tmp_path / "out.tif").exists() + + def test_raster_backend_to_gpkg_raises_clear_error(self, tmp_path): + from dissmodel.geo import raster_grid + + backend = raster_grid(rows=2, cols=2) + with pytest.raises(TypeError, match="RasterBackend to vector path"): + save_dataset(backend, str(tmp_path / "out.gpkg")) + with pytest.raises(TypeError, match="RasterBackend to vector path"): + save_dataset((backend, {}), str(tmp_path / "out.gpkg")) + + def test_matching_types_still_save(self, tmp_path): + from dissmodel.geo import vector_grid + + gdf = vector_grid(dimension=(2, 2), resolution=1.0, crs="EPSG:31984") + assert len(save_dataset(gdf, str(tmp_path / "out.gpkg"))) == 64 From 44c406593b4f382fcfedb5da01f3fb4a542bd27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Souza=20Costa?= Date: Thu, 8 Oct 2026 11:19:36 -0300 Subject: [PATCH 16/24] fix(widgets): display_inputs no longer exposes framework attributes For a model with no annotation of its own, obj.__annotations__ resolves to Model's annotations, so the sidebar rendered env/start_time/end_time/_step and the env text box replaced the environment with a string. Collect the annotations of the user's classes only (base classes included) and skip private names. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FiTtjpqiqAutXkzYsVzepv --- CHANGELOG.md | 7 +++++ dissmodel/visualization/widgets.py | 27 ++++++++++++++++-- tests/test_widgets.py | 45 ++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb9cf7..2b78104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.6.6] — 2026-10-08 ### Fixed +- `display_inputs` read `obj.__annotations__`, which for a model with no + annotation of its own resolves to the annotations of `Model` itself: the + Streamlit sidebar showed `env`, `start_time`, `end_time`, `_step`… and the + `env` text box replaced the model's environment with a string (models that + call `self.env.now()`, such as `Wolfram` in dissmodel-ca, then failed). + It now renders only the annotations of the user's classes, base classes + included, and skips private names. - CLI: when `--output` is a directory, the generated file name used a hard-coded `.tif` even for vector executors. The extension now comes from the executor's optional `output_suffix` class attribute (default `.tif`). diff --git a/dissmodel/visualization/widgets.py b/dissmodel/visualization/widgets.py index 3b0dbf7..1f3a647 100644 --- a/dissmodel/visualization/widgets.py +++ b/dissmodel/visualization/widgets.py @@ -1,13 +1,36 @@ from __future__ import annotations +import inspect from typing import Any +def _parameter_annotations(obj: Any) -> dict[str, Any]: + """ + Annotations declared by the user's classes, base classes included. + + Classes from the ``dissmodel`` package itself are skipped: their + annotations (``env``, ``start_time``, ``end_time``, ``name``…) are + framework state, not model parameters. Reading ``obj.__annotations__`` + directly would return them for any model that declares no annotation of + its own, and the widget written back would replace ``env`` with a string. + Names starting with ``_`` are skipped as private. + """ + cls = obj if isinstance(obj, type) else type(obj) + found: dict[str, Any] = {} + for klass in reversed(cls.__mro__): + module = getattr(klass, "__module__", "") or "" + if module == "dissmodel" or module.startswith("dissmodel."): + continue + found.update(inspect.get_annotations(klass)) + return {k: v for k, v in found.items() if not k.startswith("_")} + + def display_inputs(obj: Any, st: Any) -> None: """ Render Streamlit input widgets for every annotated attribute on ``obj``. - Iterates over ``obj.__annotations__`` and creates an appropriate widget + Iterates over the annotations of ``obj``'s class (and of its user-defined + base classes; not those of dissmodel's own classes) and creates an appropriate widget for each attribute based on its current value type. The attribute is updated in-place on ``obj`` after each interaction. @@ -36,7 +59,7 @@ def display_inputs(obj: Any, st: Any) -> None: display_inputs(ca_model, st) ``` """ - annotations: dict[str, Any] = getattr(obj, "__annotations__", {}) + annotations = _parameter_annotations(obj) for name in annotations: value: Any = getattr(obj, name, None) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 42c436a..809bf8c 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -72,3 +72,48 @@ def test_object_without_annotations_is_noop(self): st = StubStreamlit() display_inputs(object(), st) assert st.calls == [] + + +class TestFrameworkAttributes: + """Models with no annotation of their own must not expose framework state.""" + + def _models(self): + from dissmodel.core import Environment, Model + + class NoParams(Model): + def execute(self): + pass + + class Base(Model): + rate: float + + def setup(self, rate: float = 0.5): + self.rate = rate + + def execute(self): + pass + + class Child(Base): + steps: int + + def setup(self, rate: float = 0.5, steps: int = 3): + super().setup(rate) + self.steps = steps + + return Environment, NoParams, Child + + def test_model_without_annotations_renders_nothing(self): + Environment, NoParams, _ = self._models() + env = Environment(start_time=0, end_time=3) + model = NoParams() + st = StubStreamlit() + display_inputs(model, st) + assert st.calls == [] + assert model.env is env # was overwritten by a text_input string + + def test_inherited_user_parameters_are_rendered(self): + Environment, _, Child = self._models() + Environment(start_time=0, end_time=3) + st = StubStreamlit() + display_inputs(Child(), st) + assert st.calls == [("slider", "rate"), ("slider", "steps")] From 7c18b08797422ba94d17f7cf1386f31cb2060760 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 13:22:29 -0300 Subject: [PATCH 17/24] docs(readme): architecture diagram in Mermaid Replace the ASCII box diagram (misaligned, and naming executors that no longer exist: CoastalRasterExecutor, LUCCVectorExecutor) with a Mermaid flowchart using current names (BrmangueRasterExecutor, LuccContinuousExecutor) and showing which core module each layer uses. --- paper.bib | 36 +++++++++++++++++++++--------------- paper.md | 8 ++++---- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/paper.bib b/paper.bib index 69a9466..27ff2d4 100644 --- a/paper.bib +++ b/paper.bib @@ -88,28 +88,32 @@ @article{Ferreira2020 } @software{BRMangue, - author = {Costa, Sérgio Souza and Bezerra, Denilson da Silva and Sousa, Felipe Martins}, + author = {Bezerra, Denilson da Silva and Sousa, Felipe Martins and Costa, Sérgio Souza}, title = {{brmangue-dissmodel}: The BR-MANGUE coupled flood and mangrove succession model for DisSModel}, - version = {0.3.0}, + version = {0.5.0}, year = {2026}, publisher = {Zenodo}, - doi = {10.5281/zenodo.23223695}, + doi = {10.5281/zenodo.23240385}, url = {https://github.com/DisSModel/brmangue-dissmodel} } @software{DisSModelCA, - author = {Costa, Sérgio Souza and Santos Junior, Nerval de Jesus}, + author = {Costa, Sérgio Souza and Santos Junior, Nerval de Jesus and Zhao, Samylli Kalei Silva}, title = {{dissmodel-ca}: Cellular Automata extension for DisSModel}, - year = {2025}, - publisher = {GitHub}, + version = {0.1.0}, + year = {2026}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23241654}, url = {https://github.com/DisSModel/dissmodel-ca} } @software{DisSModelSysDyn, - author = {Costa, Sérgio Souza and Santos Junior, Nerval de Jesus}, + author = {Costa, Sérgio Souza and Santos Junior, Nerval de Jesus and Vale, Mateus Dutra}, title = {{dissmodel-sysdyn}: System Dynamics extension for DisSModel}, - year = {2025}, - publisher = {GitHub}, + version = {0.1.1}, + year = {2026}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23242935}, url = {https://github.com/DisSModel/dissmodel-sysdyn} } @@ -144,21 +148,23 @@ @software{DisSLUCCBenchmark } @software{BRMangueTerraME, - author = {Costa, Sérgio Souza}, + author = {Bezerra, Denilson da Silva and Costa, Sérgio Souza}, title = {{brmangue-terrame}: {BR-MANGUE} adapted to {TerraME} 2.0, with a headless driver for reference outputs}, + version = {0.1.0}, year = {2026}, - publisher = {GitHub}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23240318}, url = {https://github.com/LambdaGeo/brmangue-terrame} } @software{TerraMEDocker, author = {Costa, Sérgio Souza}, - title = {{terrame-docker}: {TerraME} 2.0.1 and {LuccME} in a container, with - year-by-year reference outputs of the {LuccME} labs}, - version = {0.1.1}, + title = {{terrame-docker}: {TerraME} and {LuccME} in a {Docker} image}, + version = {0.4.2}, year = {2026}, - publisher = {GitHub}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23160784}, url = {https://github.com/LambdaGeo/terrame-docker} } diff --git a/paper.md b/paper.md index 52538e8..115ebaf 100644 --- a/paper.md +++ b/paper.md @@ -27,7 +27,7 @@ affiliations: city: São Luís state: MA country: Brazil -date: 7 October 2026 +date: 8 October 2026 bibliography: paper.bib --- @@ -121,7 +121,7 @@ Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S submitting author, who are also responsible for the DisSModel reimplementation. `brmangue-dissmodel` [@BRMangue] validates the raster implementation against a TerraME 2.0 adaptation of the published model [@BRMangueTerraME] over the Maranhão Island dataset (50,496 cells); -fidelity to the thesis is documented in the repository. Figures refer to v0.3.0, which fixed border neighbor counting and the shared flood/mangrove snapshot. Match percentage suits categorical outputs [@PontiusEtAl2011]. In the baseline scenario (19 steps) land use and soil match +fidelity to the thesis is documented in the repository. Figures refer to `brmangue-dissmodel` v0.5.0. Match percentage suits categorical outputs [@PontiusEtAl2011]. In the baseline scenario (19 steps) land use and soil match exactly at every checkpoint and elevation matches on 97.4% of cells within 1 mm (MAE 0.00038 m; maximum error 0.10 m). In the flooding scenario (laboratory parameters) TerraME floods 2,469 non-sea cells by step 11 and the Python model 2,467; soil matches exactly, land use differs in 4 cells @@ -129,8 +129,8 @@ cells by step 11 and the Python model 2,467; soil matches exactly, land use diff come from exact ties in elevation: the flux rule compares accumulated elevations with `<=`, so a ~1e-16 difference in summation order (sequential in TerraME, per direction in NumPy) can flip a comparison and, near the flooding threshold, a cell's class. They are floating-point effects, not a -difference in the rules. Both scenarios are reproducible with the validation executor and the adaptation's -headless driver [@BRMangueTerraME]. +difference in the rules. Both scenarios are reproducible with the validation executor against golden +files shipped with the package, which the adaptation's headless driver regenerates [@BRMangueTerraME]. Cross-substrate equivalence (60×60 synthetic grid, 10 steps) gives 100% match for land use, soil and elevation within a 5 cm tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port From 199115a9e48aa0550b7c29b7a4d3fab9474006ea Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 13:23:46 -0300 Subject: [PATCH 18/24] docs: install disslucc and disscube from PyPI - README ecosystem table: disslucc is on PyPI (`pip install disslucc`); add disscube (`pip install "disscube[dissmodel]"`), which the paper cites but the table did not list - executor schemas example: coastal-dynamics was renamed to brmangue-dissmodel; use its `brmangue_raster` executor and the v0.5.0 tag --- README.md | 3 ++- docs/api/executor/schemas.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c39cb28..b82e227 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,8 @@ The map below shows how the pieces fit together, and how each one relates to its | [`dissmodel-ca`](https://github.com/DisSModel/dissmodel-ca) | Classic Cellular Automata (Game of Life, Forest Fire, Growth) | `pip install "git+https://github.com/DisSModel/dissmodel-ca.git"` | | [`dissmodel-sysdyn`](https://github.com/DisSModel/dissmodel-sysdyn) | System Dynamics (SIR, Predator-Prey, Lorenz) | `pip install "git+https://github.com/DisSModel/dissmodel-sysdyn.git"` | | [`brmangue-dissmodel`](https://github.com/DisSModel/brmangue-dissmodel) | BR-MANGUE coastal flooding and mangrove succession model (raster + vector, validated against TerraME) | `pip install "git+https://github.com/DisSModel/brmangue-dissmodel.git"` | -| [`disslucc`](https://github.com/DisSModel/disslucc) | Land Use and Cover Change models, continuous and discrete allocation (CLUE-inspired), raster-only | `pip install "git+https://github.com/DisSModel/disslucc.git"` | +| [`disslucc`](https://github.com/DisSModel/disslucc) | Land Use and Cover Change models, continuous and discrete allocation (CLUE-inspired), raster-only | `pip install disslucc` | +| [`disscube`](https://github.com/DisSModel/disscube) | Declarative spatial data cubes (a Python alternative to TerraME's fillCell): sources, grid and derived variables in TOML; hands the cube to DisSModel models | `pip install "disscube[dissmodel]"` | ### 🛠 Implementation Templates diff --git a/docs/api/executor/schemas.md b/docs/api/executor/schemas.md index f9f2886..3ba4e1f 100644 --- a/docs/api/executor/schemas.md +++ b/docs/api/executor/schemas.md @@ -83,8 +83,8 @@ from dissmodel.executor.schemas import JobRequest job = JobRequest( model = { - "class": "coastal_raster", - "package": "git+https://github.com/DisSModel/coastal-dynamics@main", + "class": "brmangue_raster", + "package": "git+https://github.com/DisSModel/brmangue-dissmodel@v0.5.0", "parameters": {"end_time": 88}, }, source = {"type": "s3", "uri": "s3://dissmodel-inputs/grid.zip"}, From 31e81455388c706ae40e3f1f15609d2fa3933c7f Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 13:25:02 -0300 Subject: [PATCH 19/24] docs(readme): architecture diagram in Mermaid Replace the ASCII box diagram (misaligned, and naming executors that no longer exist: CoastalRasterExecutor, LUCCVectorExecutor) with a Mermaid flowchart using current names (BrmangueRasterExecutor, LuccContinuousExecutor) and showing which core module each layer uses. --- README.md | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b82e227..06e2b41 100644 --- a/README.md +++ b/README.md @@ -62,23 +62,28 @@ DisSModel is the synthesis: a Python-native, FAIR-aligned, cloud-ready simulatio ## 🏗 Architecture -``` -┌──────────────────────────────────────────────────────────┐ -│ Science Layer (Model) │ -│ FloodModel, AllocationClueLike, MangroveModel, ... │ -│ → only knows math, geometry and time │ -├──────────────────────────────────────────────────────────┤ -│ Infrastructure Layer (ModelExecutor) │ -│ CoastalRasterExecutor, LUCCVectorExecutor, ... │ -│ → only knows URIs, local/S3, column_map, parameters │ -├──────────────────────────────────────────────────────────┤ -│ Core modules │ -│ dissmodel.core — Environment, Model, SpatialModel │ -│ dissmodel.geo — RasterBackend, neighborhoods │ -│ dissmodel.executor — ModelExecutor ABC, ExperimentRecord│ -│ dissmodel.io — load_dataset / save_dataset │ -│ dissmodel.visualization — Map, RasterMap, Chart │ -└──────────────────────────────────────────────────────────┘ +```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 ``` --- From ec19843a38dc3a22c88a01afee72a3ef32eb636a Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 13:58:32 -0300 Subject: [PATCH 20/24] ocs: general AI disclosure in the paper; README citation 0.6.6, coverage badge - paper: list AI uses in general terms (coding, bug fixing, refactoring, code translation, tests, validation scripts, documentation, text) and state that reference outputs are produced by TerraME itself, with AI-assisted changes to model files checked to leave outputs unchanged; replaces the claim that only the BR-MANGUE headless driver was AI-written - paper: cite luccme-goldens v1.1.0 (10.5281/zenodo.23161342), the reference outputs the disslucc benchmark compares against - README: citation block cited 0.6.5; coverage badge said 79% (CI: 83%) --- README.md | 4 ++-- paper.bib | 10 ++++++++++ paper.md | 19 +++++++++++-------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 06e2b41..4e62c63 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) [![PyPI version](https://badge.fury.io/py/dissmodel.svg)](https://pypi.org/project/dissmodel/) -[![Coverage](https://img.shields.io/badge/coverage-79%25-orange.svg)](https://github.com/DisSModel/dissmodel/actions) +[![Coverage](https://img.shields.io/badge/coverage-83%25-yellowgreen.svg)](https://github.com/DisSModel/dissmodel/actions) [![DisSModel](https://img.shields.io/badge/LambdaGeo-Research-green.svg)](https://github.com/DisSModel) [![JOSS Status](https://joss.theoj.org/papers/46522bc30d2dbec6b509d2dc487170ec/status.svg)](https://joss.theoj.org/papers/46522bc30d2dbec6b509d2dc487170ec) @@ -320,7 +320,7 @@ Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTIN year = {2026}, publisher = {GitHub}, url = {https://github.com/DisSModel/dissmodel}, - version = {0.6.5} + version = {0.6.6} } ``` diff --git a/paper.bib b/paper.bib index 27ff2d4..6a56e0a 100644 --- a/paper.bib +++ b/paper.bib @@ -158,6 +158,16 @@ @software{BRMangueTerraME url = {https://github.com/LambdaGeo/brmangue-terrame} } +@dataset{LuccMEGoldens, + author = {Costa, Sérgio Souza}, + title = {{luccme-goldens}: Canonical Reference Execution Outputs for {TerraME} 2.0.1 and {LuccME} 3.1}, + version = {1.1.0}, + year = {2026}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.23161342}, + url = {https://github.com/LambdaGeo/luccme-goldens} +} + @software{TerraMEDocker, author = {Costa, Sérgio Souza}, title = {{terrame-docker}: {TerraME} and {LuccME} in a {Docker} image}, diff --git a/paper.md b/paper.md index 115ebaf..59c41a9 100644 --- a/paper.md +++ b/paper.md @@ -141,7 +141,7 @@ demand, potential and allocation components (continuous CLUE-like [@Veldkamp1996 CLUE-S-like). It tests the `ModelExecutor` contract and the raster substrate against a second TerraME application, not as a result in itself. Its checks live in a separate, versioned benchmark [@DisSLUCCBenchmark] that compares every simulated year, iteration counts included, with per-year -outputs generated in a containerized TerraME [@TerraMEDocker] and checked by SHA-256. Scenarios +reference outputs [@LuccMEGoldens] generated in a containerized TerraME [@TerraMEDocker] and checked by SHA-256. Scenarios follow the LuccME test labs (`labNN`); the `labNN_mdX` variants exercise the convergence loop. MAE suits fractional outputs [@PontiusEtAl2011; @Willmott2005]. @@ -201,12 +201,15 @@ We thank José Magno Pinheiro Alves for early validation testing. The initial prototype (May–June 2025), the second author's undergraduate thesis [@SantosJunior2025], did not involve generative AI. From February 2026, Claude and Gemini CLI -supported documentation, tests and refactoring of code that already existed. Since mid-2026, -Claude Code has been used across the whole ecosystem (the core framework, its satellite packages -and their benchmarks) for implementation, validation scripts, documentation and this paper's text -and English. This is what has made it feasible to build the ecosystem envisioned by the -submitting author. The reference outputs used in validation come from the unmodified TerraME/LuccME model files run in a container; for BR-MANGUE, only the headless driver that runs them is AI-written. The scientific design — TerraME compatibility contract, executor pattern, -dual-substrate architecture, validation methodology — is the authors', and traces to the -submitting author's doctoral research at INPE. The authors reviewed, tested and ran all AI-generated code and text. +supported documentation, tests and refactoring of existing code. Since mid-2026, Claude Code has +been used across the whole ecosystem (the core framework, its satellite packages and their +benchmarks) for coding, bug fixing, refactoring, code translation, tests, validation scripts, +documentation, and this paper's text and English. This is what has made it feasible to build the +ecosystem envisioned by the submitting author. The reference outputs used in validation are +produced by TerraME itself, running the model files in a container; AI-assisted changes to those +files were checked to leave the outputs unchanged. The scientific design — TerraME compatibility +contract, executor pattern, dual-substrate architecture, validation methodology — is the +authors', and traces to the submitting author's doctoral research at INPE. The authors reviewed, +tested and ran all AI-generated code and text. ## References \ No newline at end of file From 99d7a6d6a10d2d1ba0a8c2eceaa854a3699d65bc Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 14:00:57 -0300 Subject: [PATCH 21/24] test: run Chart and RasterMap tests in a temporary directory In headless mode Chart and RasterMap write PNG frames to the working directory, so running pytest from the repository root left chart_frames/ and raster_map_frames/ behind. An autouse fixture moves each test of those two modules into tmp_path. --- tests/raster/test_raster_map.py | 7 +++++++ tests/visualization/test_chart.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/tests/raster/test_raster_map.py b/tests/raster/test_raster_map.py index 500f100..1a6a360 100644 --- a/tests/raster/test_raster_map.py +++ b/tests/raster/test_raster_map.py @@ -19,6 +19,13 @@ from dissmodel.visualization.raster_map import RasterMap +@pytest.fixture(autouse=True) +def _run_in_tmp_path(tmp_path, monkeypatch): + """Headless Chart/RasterMap write PNG frames to the working directory; + run every test in a temporary one so the repository stays clean.""" + monkeypatch.chdir(tmp_path) + + # ── force headless for all tests ────────────────────────────────────────────── @pytest.fixture(autouse=True) diff --git a/tests/visualization/test_chart.py b/tests/visualization/test_chart.py index a78d658..6314fb7 100644 --- a/tests/visualization/test_chart.py +++ b/tests/visualization/test_chart.py @@ -22,6 +22,13 @@ from dissmodel.visualization.chart import Chart, track_plot +@pytest.fixture(autouse=True) +def _run_in_tmp_path(tmp_path, monkeypatch): + """Headless Chart/RasterMap write PNG frames to the working directory; + run every test in a temporary one so the repository stays clean.""" + monkeypatch.chdir(tmp_path) + + # ── helper model with tracked attributes ────────────────────────────────────── @track_plot(label="Infected", color="red") From 5d64e72aa88db38baf058046776d99e320cbaf1f Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 14:16:08 -0300 Subject: [PATCH 22/24] docs(paper): trim AI disclosure to stay within 1700 words --- paper.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/paper.md b/paper.md index 59c41a9..00d514f 100644 --- a/paper.md +++ b/paper.md @@ -202,10 +202,8 @@ We thank José Magno Pinheiro Alves for early validation testing. The initial prototype (May–June 2025), the second author's undergraduate thesis [@SantosJunior2025], did not involve generative AI. From February 2026, Claude and Gemini CLI supported documentation, tests and refactoring of existing code. Since mid-2026, Claude Code has -been used across the whole ecosystem (the core framework, its satellite packages and their -benchmarks) for coding, bug fixing, refactoring, code translation, tests, validation scripts, -documentation, and this paper's text and English. This is what has made it feasible to build the -ecosystem envisioned by the submitting author. The reference outputs used in validation are +been used across the whole ecosystem (framework, satellite packages and benchmarks) for coding, bug fixing, refactoring, code translation, tests, validation scripts, +documentation, and this paper's text. This made the ecosystem envisioned by the submitting author feasible. The reference outputs used in validation are produced by TerraME itself, running the model files in a container; AI-assisted changes to those files were checked to leave the outputs unchanged. The scientific design — TerraME compatibility contract, executor pattern, dual-substrate architecture, validation methodology — is the From 36043740002d2265951741e8b6866a148f2a292d Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 14:46:34 -0300 Subject: [PATCH 23/24] docs(paper): soften the statement of need --- README.md | 2 +- paper.md | 39 +++++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4e62c63..65bc46b 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,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). -- **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. +- **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. diff --git a/paper.md b/paper.md index 00d514f..ef5db87 100644 --- a/paper.md +++ b/paper.md @@ -50,15 +50,16 @@ GeoPandas and PySAL — but these tools target static analysis. Dynamic spatial how landscapes evolve over time, has historically required specialized platforms. In Brazil, TerraME [@Carneiro2013] and Dinamica EGO [@SoaresFilho2002; @SoaresFilho2013] are the most widely adopted general-purpose frameworks, while narrower allocation models such as CLUE and CLUE-S -[@Veldkamp1996; @Verburg2002] are used elsewhere. Researchers must choose between a Lua-based -toolchain and single-purpose implementations with no shared contract. +[@Veldkamp1996; @Verburg2002] are used elsewhere. Researchers working in Python thus lack a +general-purpose option: the established frameworks are Lua- or GUI-based, and allocation models +are usually standalone implementations without a shared interface. TerraME is conceptually robust, but its reliance on Lua — far less common in data science than Python — is a barrier, and the framework has seen no new release since August 2020. General-purpose simulation libraries, meanwhile, lack native synchronization between a time-stepped clock and the state of a GeoDataFrame. DisSModel fills this gap with a lightweight scheduler coupled to vector and -raster spatial state: a Pythonic, actively maintained implementation of the TerraME paradigm and, -through its satellite packages, of allocation models like CLUE and CLUE-S. +raster spatial state: an actively maintained Python implementation of the TerraME paradigm and, +through its satellite package `disslucc`, of LuccME's CLUE-like and CLUE-S-like allocation components. Reproducibility is a first-class concern: the `executor` module provides a standardized lifecycle — `validate → load → run → save` — capturing provenance @@ -88,8 +89,8 @@ reproducible executor lifecycle. DisSModel builds on GeoPandas, following the di DisSModel is organized into five modules with strict separation of concerns, extensible through class inheritance. **Core** manages the simulation clock: the `Environment` orchestrates time, and -spatial models auto-register at instantiation, receiving ticks through `setup / pre_execute / -execute / post_execute` hooks. **Geo** provides the dual-substrate design: a vector substrate +models auto-register at instantiation, run `setup` once, and receive each tick through +`pre_execute / execute / post_execute` hooks. **Geo** provides the dual-substrate design: a vector substrate (`SpatialModel`, `CellularAutomaton`) on GeoDataFrame with libpysal neighborhoods [@Rey2021], and a raster substrate (`RasterModel`, `RasterCellularAutomaton`) on NumPy arrays with vectorized operations (`shift2d`, `focal_sum`, `neighbor_contact`) replacing cell-by-cell loops. **Executor** @@ -159,17 +160,18 @@ the benchmark as its components land. ## Research Impact Statement -DisSModel's scientific lineage is rooted in the TerraME/LuccME research program at INPE. The -submitting author conducted doctoral research there under Prof. Gilberto Câmara and Dr. Ana Paula -Dutra Aguiar — principal architects of TerraME/LuccME — and has co-authored the modeling program -since 2009 [@Moreira2009; @Costa2009]; `disslucc` reimplements in Python the continuous and -discrete allocation components of that lineage [@LuccME]. DisSModel was presented at a seminar of INPE's Applied Computing graduate program (CAP) on 7 May -2026 [@Costa2026INPE]. +DisSModel's scientific lineage is rooted in the TerraME/LuccME research program at INPE, where +TerraME was developed by Tiago Carneiro [@Carneiro2013] and LuccME by Ana Paula Dutra Aguiar +[@LuccME], both former doctoral students of Prof. Gilberto Câmara. The submitting author conducted +doctoral research in the same program, under Câmara and Aguiar, and has co-authored its modeling +work since 2009 [@Moreira2009; @Costa2009]; `disslucc` reimplements in Python the continuous and +discrete allocation components of that lineage. DisSModel was presented at a seminar of INPE's +Applied Computing graduate program (CAP) on 7 May 2026 [@Costa2026INPE]. The framework is in use across two UFMA research groups. Within LambdaGeo, `brmangue-dissmodel` -builds on the reference implementation by co-author F.M.S. Co-author D.S.B. (UFMA, former INPE), -whose doctoral work founded BR-MANGUE [@Bezerra2013], also adopts the DisSModel reimplementation in -his coastal dynamics projects (UFMA PVCBS4959-2025 and PVCBS4960-2025), a collaboration predating +builds on the reference implementation by co-author F.M.S. Co-author D.S.B. (former INPE), co-leader +of UFMA's Geotechnologies and Environmental Sciences Laboratory (GEOTAM), whose doctoral work founded +BR-MANGUE [@Bezerra2013], also adopts the DisSModel reimplementation in his coastal dynamics projects (UFMA PVCBS4959-2025 and PVCBS4960-2025), a collaboration predating DisSModel itself [@Bezerra2025BM]. Since September 2026, four undergraduate research fellows (UFMA, CNPq) are being trained on the @@ -178,9 +180,10 @@ project; the stabilized `ModelExecutor` contract lets each own an independent re to TerraME's `fillCellularSpace`) — without core changes. A roadmap toward DisSModel 1.0 (May 2027) includes an open textbook, *Geospatial Modeling with -Python* (https://lambdageo.github.io/geospatial-modeling-python/), already in progress. This -positions DisSModel as a candidate simulation layer in the Brazilian Earth Observation stack, -complementary to SITS [@Simoes2021] and the Brazil Data Cube [@Ferreira2020]. +Python* (https://lambdageo.github.io/geospatial-modeling-python/), already in progress. On the data +side, `disscube` already reads Brazil Data Cube collections through their STAC catalog +[@Ferreira2020], so DisSModel models can run on the same Earth observation data used by SITS +[@Simoes2021]. ## Author Contributions From 3b14e2e805cb4c0d77731c89c864a80f74d1afd3 Mon Sep 17 00:00:00 2001 From: Sergio Souza Costa Date: Thu, 8 Oct 2026 15:34:59 -0300 Subject: [PATCH 24/24] docs(paper): re-run benchmarks on current stack; mention Sync models; 'adapts' TerraME paradigm --- paper.md | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/paper.md b/paper.md index ef5db87..d66f4f2 100644 --- a/paper.md +++ b/paper.md @@ -33,11 +33,7 @@ bibliography: paper.bib ## Summary -DisSModel (Discrete Spatial Modeling) is a modular Python framework for spatially explicit dynamic -modeling, targeting Land Use and Land Cover Change (LUCC). It translates the modeling paradigms of -TerraME [@Carneiro2013] into the Python ecosystem, enabling researchers to simulate socio-environmental -systems — forest fires, epidemics, coastal dynamics — by coupling a time-stepped clock with the -spatial data structures of GeoPandas [@Jordahl2021]. + DisSModel (Discrete Spatial Modeling) is a modular Python framework for spatially explicit dynamic modeling, targeting Land Use and Land Cover Change (LUCC). It adapts the modeling paradigm of TerraME [@Carneiro2013] to the Python ecosystem, enabling researchers to simulate socio-environmental systems — forest fires, epidemics, coastal dynamics — by coupling a time-stepped clock with the spatial data structures of GeoPandas [@Jordahl2021]. Its **dual-substrate architecture** pairs a vector substrate (GeoDataFrame) for spatial expressiveness with a raster substrate (NumPy 2D arrays) for high-performance vectorized computation. DisSModel is @@ -90,15 +86,16 @@ reproducible executor lifecycle. DisSModel builds on GeoPandas, following the di DisSModel is organized into five modules with strict separation of concerns, extensible through class inheritance. **Core** manages the simulation clock: the `Environment` orchestrates time, and models auto-register at instantiation, run `setup` once, and receive each tick through -`pre_execute / execute / post_execute` hooks. **Geo** provides the dual-substrate design: a vector substrate -(`SpatialModel`, `CellularAutomaton`) on GeoDataFrame with libpysal neighborhoods [@Rey2021], and a -raster substrate (`RasterModel`, `RasterCellularAutomaton`) on NumPy arrays with vectorized -operations (`shift2d`, `focal_sum`, `neighbor_contact`) replacing cell-by-cell loops. **Executor** -defines the `ModelExecutor` four-phase lifecycle (`validate`, `load`, `run`, `save`); subclasses -self-register, and every run produces an `ExperimentRecord` with input checksum, parameters, timing, -and output paths. **IO** provides a unified dataset abstraction across GeoDataFrame, GeoTIFF, and -Xarray/Zarr, with transparent `s3://` resolution. **Visualization** integrates Matplotlib, -Streamlit-compatible widgets, and `RasterMap`. +`pre_execute / execute / post_execute` hooks. **Geo** provides the dual-substrate design: a vector +substrate (`SpatialModel`, `CellularAutomaton`) on GeoDataFrame with libpysal neighborhoods +[@Rey2021], and a raster substrate (`RasterModel`, `RasterCellularAutomaton`) on NumPy arrays with +vectorized operations (`shift2d`, `focal_sum`, `neighbor_contact`) replacing cell-by-cell loops. +`SyncSpatialModel` and `SyncRasterModel` mirror TerraME's `synchronize()`, so coupled models sharing +one grid read the same start-of-step state. **Executor** defines the `ModelExecutor` four-phase +lifecycle (`validate`, `load`, `run`, `save`); subclasses self-register, and every run produces an +`ExperimentRecord` with input checksum, parameters, timing, and output paths. **IO** provides a +unified dataset abstraction across GeoDataFrame, GeoTIFF, and Xarray/Zarr, with transparent `s3://` +resolution. **Visualization** integrates Matplotlib, Streamlit-compatible widgets, and `RasterMap`. This extensibility has produced independent domain packages: `dissmodel-ca` [@DisSModelCA] (Cellular Automata), `dissmodel-sysdyn` [@DisSModelSysDyn] (System Dynamics), and `disslucc` @@ -108,14 +105,14 @@ contract, an explicit Python counterpart to TerraME/LuccME. ## Validation and Performance -All benchmarks ran on an Intel Core i7-7700T @ 2.90GHz, 15 GB RAM (Ubuntu, Python 3.12.3, -NumPy 2.4.6, GeoPandas 1.1.3); absolute timings vary by hardware. +All benchmarks ran on an Intel Core i7-7700T @ 2.90GHz, 15 GB RAM (Linux Mint 22.2, Python 3.12.3, +NumPy 2.5.3, GeoPandas 1.2.0); absolute timings vary by hardware. **Conway's Game of Life** compares the two substrates with the same rule and initial state. The vector rule is a per-cell Python method querying libpysal Queen neighbors; the raster rule is one whole-grid focal sum; final states are compared cell by cell. At 100×100 cells the raster takes -0.41 ms/step against 7,069 ms/step for the vector (a gap that reflects the per-cell style of the -vector API) and scales to 10⁶ cells at 30.6 ms/step. +0.34 ms/step against 3,783 ms/step for the vector (a gap that reflects the per-cell style of the +vector API) and scales to 10⁶ cells at 26.6 ms/step. **BR-MANGUE coastal dynamics.** The coupled mangrove–flood model was established by Bezerra et al. [@Bezerra2013] and extended in @Bezerra2025BM, co-authored by D.S.B., F.M.S. and the @@ -134,8 +131,9 @@ difference in the rules. Both scenarios are reproducible with the validation exe files shipped with the package, which the adaptation's headless driver regenerates [@BRMangueTerraME]. Cross-substrate equivalence (60×60 synthetic grid, 10 steps) gives 100% match for land use, soil -and elevation within a 5 cm tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding), with raster at 2.0 ms/step against 76.4 ms/step for vector (37.4×; the vector port -follows TerraME's per-cell loops). +and elevation within a 5 cm tolerance (MAE 0.0011 m, max 0.024 m; floating-point rounding), with +raster at 2.5 ms/step against 69.1 ms/step for vector (28.1×; the vector port follows TerraME's +per-cell loops). **disslucc as a second case.** `disslucc` [@DisSLUCC], built on DisSModel, reimplements the LuccME demand, potential and allocation components (continuous CLUE-like [@Veldkamp1996] and discrete @@ -153,7 +151,7 @@ reads a mismatched field name), while `disslucc` runs it by default. With `cell_ counts (MAE < 1e-7). `lab15_md10` (discrete, 5,914 cells) reproduces the reference cell for cell, with zero quantity and allocation disagreement [@PontiusMillones2011] and identical iteration counts. Overall, ten of 21 LuccME test labs and both variants match with identical iteration -counts and differences up to 3e-6 (single precision); a validated lab runs in 0.1 to 0.5 s on +counts and differences up to 3e-6 (single precision); a validated lab runs in 0.11 to 0.55 s on the hardware above (median of five; `make timing`). The other eleven labs need potential and allocation components not yet implemented; student fellows are developing them, and each lab joins the benchmark as its components land.