Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

raster_map_frames
testes

# Byte-compiled / optimized / DLL files
Expand All @@ -9,6 +9,8 @@ __pycache__/
# C extensions
*.so

.ruff_cache

# Distribution / packaging
.Python
build/
Expand Down
275 changes: 275 additions & 0 deletions benchmarks/benchmark_raster_vs_vector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
"""
benchmarks/benchmark_raster_vs_vector.py
==========================================
Benchmark: RasterBackend (NumPy) vs GeoDataFrame (libpysal)

Autocontido — não depende de projetos externos ao dissmodel.
Define um modelo de inundação mínimo inline para isolar a medição
do overhead de cada backend.

Execução
--------
python benchmarks/benchmark_raster_vs_vector.py
python benchmarks/benchmark_raster_vs_vector.py --steps 5 --sizes 10 20 50
python benchmarks/benchmark_raster_vs_vector.py --no-validation
"""
from __future__ import annotations

import argparse
import time
from dataclasses import dataclass, field

import numpy as np
import geopandas as gpd
import pandas as pd
from shapely.geometry import box
from libpysal.weights import Queen

from dissmodel.core import Environment
from dissmodel.geo.raster.backend import RasterBackend
from dissmodel.geo.raster.model import RasterModel
from dissmodel.geo.vector.model import SpatialModel


# ══════════════════════════════════════════════════════════════════════════════
# CONSTANTES MÍNIMAS (inline — sem dependência externa)
# ══════════════════════════════════════════════════════════════════════════════

MAR = 3
SOLO_INUNDADO = 6
AREA_ANTROPIZADA_INUNDADA = 7
MANGUE_INUNDADO = 9
VEG_TERRESTRE_INUNDADA = 10

USOS_INUNDADOS = [MAR, SOLO_INUNDADO, AREA_ANTROPIZADA_INUNDADA,
MANGUE_INUNDADO, VEG_TERRESTRE_INUNDADA]

REGRAS_INUNDACAO = {
1: MANGUE_INUNDADO, # MANGUE
8: MANGUE_INUNDADO, # MANGUE_MIGRADO
2: VEG_TERRESTRE_INUNDADA, # VEGETACAO_TERRESTRE
4: AREA_ANTROPIZADA_INUNDADA,# AREA_ANTROPIZADA
5: SOLO_INUNDADO, # SOLO_DESCOBERTO
}

TAXA = 0.011


# ══════════════════════════════════════════════════════════════════════════════
# MODELOS INLINE (mínimos, sem importar projetos externos)
# ══════════════════════════════════════════════════════════════════════════════

class _FloodRaster(RasterModel):
def setup(self, backend):
super().setup(backend)

def execute(self):
nivel_mar = self.env.now() * TAXA
rows, cols = self.shape
uso_past = self.backend.get("uso").copy()
alt_past = self.backend.get("alt").copy()

eh_fonte = np.isin(uso_past, USOS_INUNDADOS) & (alt_past >= 0)
viz_baixos = np.ones((rows, cols), dtype=float)
for dr, dc in self.dirs:
viz_baixos += (self.shift(alt_past, dr, dc) <= alt_past).astype(float)

fluxo = np.where(eh_fonte, TAXA / viz_baixos, 0.0)
delta_alt = fluxo.copy()
uso_novo = uso_past.copy()

for dr, dc in self.dirs:
fonte_viz = self.shift(eh_fonte.astype(float), dr, dc) > 0
delta_alt += np.where(
fonte_viz & (alt_past <= self.shift(alt_past, dr, dc)),
self.shift(fluxo, dr, dc), 0.0,
)
for uso_seco, uso_inund in REGRAS_INUNDACAO.items():
pode = fonte_viz & (uso_past == uso_seco) & (alt_past <= nivel_mar)
uso_novo = np.where(pode, uso_inund, uso_novo)

self.backend.arrays["alt"] = alt_past + delta_alt
self.backend.arrays["uso"] = uso_novo


class _FloodVector(SpatialModel):
def setup(self):
self.create_neighborhood(strategy=Queen, silence_warnings=True)

def execute(self):
nivel_mar = self.env.now() * TAXA
uso_past = self.gdf["uso"].copy()
alt_past = self.gdf["alt"].copy()

fontes = set(uso_past.index[uso_past.isin(USOS_INUNDADOS) & (alt_past >= 0)])
alt_nova = alt_past.copy()

for idx in fontes:
alt_atual = alt_past[idx]
vizinhos = self.neighs_id(idx)
viz_baixos = 1 + sum(1 for n in vizinhos if alt_past[n] <= alt_atual)
fluxo = TAXA / viz_baixos
alt_nova[idx] += fluxo
for n in vizinhos:
if alt_past[n] <= alt_atual:
alt_nova[n] += fluxo

self.gdf["alt"] = alt_nova
uso_novo = uso_past.copy()

for idx in self.gdf.index:
uso_atual = uso_past[idx]
if uso_atual not in REGRAS_INUNDACAO:
continue
if alt_past[idx] > nivel_mar:
continue
if any(n in fontes for n in self.neighs_id(idx)):
uso_novo[idx] = REGRAS_INUNDACAO[uso_atual]

self.gdf["uso"] = uso_novo


# ══════════════════════════════════════════════════════════════════════════════
# GERAÇÃO DE GRADE SINTÉTICA
# ══════════════════════════════════════════════════════════════════════════════

def _arrays(rows: int, cols: int, seed: int = 42) -> dict[str, np.ndarray]:
rng = np.random.default_rng(seed)
uso = np.full((rows, cols), 2, dtype=np.int16) # VEG_TERRESTRE
alt = np.zeros((rows, cols), dtype=np.float32)
c_mar = int(cols * 0.20)
uso[:, :c_mar] = MAR
for c in range(cols):
alt[:, c] = max(0.0, (c - c_mar) * 0.1)
alt += rng.uniform(0, 0.05, (rows, cols)).astype(np.float32)
alt[:, :c_mar] = 0.0
return {"uso": uso, "alt": alt}


def _make_backend(rows: int, cols: int) -> RasterBackend:
b = RasterBackend(shape=(rows, cols))
for name, arr in _arrays(rows, cols).items():
b.set(name, arr)
return b


def _make_gdf(rows: int, cols: int, cell_size: float = 100.0) -> gpd.GeoDataFrame:
arrs = _arrays(rows, cols)
geoms = [
box(c * cell_size, (rows-1-r) * cell_size,
(c+1) * cell_size, (rows-r) * cell_size)
for r in range(rows) for c in range(cols)
]
return gpd.GeoDataFrame(
{"uso": arrs["uso"].ravel().astype(int),
"alt": arrs["alt"].ravel().astype(float)},
geometry=geoms, crs="EPSG:31984",
)


# ══════════════════════════════════════════════════════════════════════════════
# RUNNERS
# ══════════════════════════════════════════════════════════════════════════════

@dataclass
class Result:
label: str
rows: int
cols: int
steps: int
total_s: float
uso_final: np.ndarray = field(repr=False)

@property
def ms_per_step(self) -> float:
return self.total_s / self.steps * 1000


def run_raster(n: int, steps: int) -> Result:
backend = _make_backend(n, n)
env = Environment(start_time=1, end_time=steps)
_FloodRaster(backend=backend)
t0 = time.perf_counter()
env.run()
return Result("raster", n, n, steps,
time.perf_counter() - t0,
backend.get("uso").copy())


def run_vector(n: int, steps: int) -> Result:
gdf = _make_gdf(n, n)
env = Environment(start_time=1, end_time=steps)
_FloodVector(gdf=gdf)
t0 = time.perf_counter()
env.run()
return Result("vector", n, n, steps,
time.perf_counter() - t0,
gdf["uso"].values.reshape(n, n).astype(np.int16))


# ══════════════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════════════

VECTOR_MAX_CELLS = 10_000 # acima disso vetor é impraticável

def benchmark(sizes: list[int], steps: int, validate: bool) -> None:
rows_data = []

for n in sizes:
cells = n * n
print(f"\n── {n}×{n} ({cells:,} células, {steps} passos) ──")

print(" raster ... ", end="", flush=True)
r = run_raster(n, steps)
print(f"{r.total_s:.3f}s ({r.ms_per_step:.1f} ms/passo)")

row = {
"grid": f"{n}×{n}",
"cells": cells,
"raster_ms": round(r.ms_per_step, 2),
"vector_ms": "—",
"speedup": "—",
"identical": "—",
}

if cells <= VECTOR_MAX_CELLS:
print(" vector ... ", end="", flush=True)
v = run_vector(n, steps)
print(f"{v.total_s:.3f}s ({v.ms_per_step:.1f} ms/passo)")

speedup = v.total_s / r.total_s if r.total_s > 0 else float("inf")
print(f" speedup: {speedup:.0f}×")

if validate:
match = r.uso_final == v.uso_final
pct = float(match.mean()) * 100
ok = bool(match.all())
print(f" validação: {'✅ idêntico' if ok else f'⚠️ {pct:.1f}% iguais'}")
row["identical"] = "yes" if ok else f"{pct:.1f}%"

row["vector_ms"] = round(v.ms_per_step, 2)
row["speedup"] = f"{speedup:.0f}×"
else:
print(f" vector: skipped (>{VECTOR_MAX_CELLS:,} células)")

rows_data.append(row)

print("\n" + "═" * 60)
print("RESUMO")
print("═" * 60)
print(pd.DataFrame(rows_data).to_string(index=False))
print()


def _parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Benchmark RasterBackend vs GeoDataFrame")
p.add_argument("--sizes", nargs="+", type=int, default=[10, 50, 100, 200, 500])
p.add_argument("--steps", type=int, default=10)
p.add_argument("--no-validation", dest="validate", action="store_false")
return p.parse_args()


if __name__ == "__main__":
args = _parse_args()
benchmark(sizes=args.sizes, steps=args.steps, validate=args.validate)
4 changes: 2 additions & 2 deletions benchmarks/ca_game_of_life.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from dissmodel.core import Environment
from dissmodel.geo import FillStrategy, fill, regular_grid
from dissmodel.geo.celullar_automaton import CellularAutomaton
from dissmodel.geo.raster.celullar_automaton import CellularAutomaton


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -111,7 +111,7 @@ def run_benchmark(
t_orig = run_benchmark(GameOfLifeOriginal, dim=(50, 50), steps=5, name="Original")
t_opt = run_benchmark(GameOfLifeOptimized, dim=(50, 50), steps=5, name="Optimized")

print(f"\nResults:")
print("\nResults:")
print(f" Original: {t_orig:.4f}s")
print(f" Optimized: {t_opt:.4f}s")
print(f" Speedup: {t_orig / t_opt:.2f}x")
21 changes: 17 additions & 4 deletions dissmodel/geo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# dissmodel/geo/__init__.py

from .neighborhood import attach_neighbors
from .regular_grid import regular_grid, parse_idx
from .fill import fill, FillStrategy
from .celullar_automaton import CellularAutomaton
# vector substrate
from .vector.neighborhood import attach_neighbors
from .vector.regular_grid import regular_grid, parse_idx
from .vector.fill import fill, FillStrategy
from .vector.cellular_automaton import CellularAutomaton
from .vector.model import SpatialModel

# raster substrate
from .raster.backend import RasterBackend, DIRS_MOORE, DIRS_VON_NEUMANN
from .raster.model import RasterModel
from .raster.cellular_automaton import RasterCellularAutomaton
from .raster.regular_grid import make_raster_grid
from .raster.band_spec import BandSpec # se tiver classe exportável

# raster io — opcional, não importa por padrão (requer rasterio)
# from .raster.io import load_geotiff, save_geotiff
Empty file.
Loading
Loading