Skip to content

Commit

Permalink
refactor(project): ♻️ Completely compatible to Mesa 3.0 framework
Browse files Browse the repository at this point in the history
  • Loading branch information
SongshGeo committed Nov 17, 2024
1 parent 2c32b18 commit 4c7522d
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 46 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@

<a id='changelog-0.7.0'></a>
# 0.7.0 — 2024-11-17

## Refactoring

- [x] #refactor♻️ API compatible with Mesa 3.x version.
- [x] #refactor♻️ Better experiment class

## New Features

- [x] #feat✨ Improve compatibility for visualisation with Mesa 3.x version
- [x] #feat✨ Add Solara function for Mesa 3.x visualisation

<a id='changelog-0.7.0.alpha'></a>
# 0.7.0.alpha — 2024-11-08

Expand Down
2 changes: 1 addition & 1 deletion abses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"Experiment",
"load_data",
]
__version__ = "v0.7.0.alpha"
__version__ = "v0.7.0"

from .actor import Actor, alive_required, perception
from .decision import Decision
Expand Down
9 changes: 9 additions & 0 deletions abses/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

from abses._bases.errors import ABSESpyError
from abses._bases.modules import Module, _ModuleFactory
from abses.actor import Actor
from abses.cells import PatchCell
from abses.random import ListRandom
from abses.sequences import ActorsList
Expand Down Expand Up @@ -430,6 +431,14 @@ def coords(self) -> Coordinate:
"x": x_coord,
}

@property
def agents(self) -> ActorsList[Actor]:
"""Return a list of all agents in the module."""
agents = []
for c in self.cells_lst:
agents.extend(list(c.agents))
return ActorsList(self.model, agents)

def transform_coord(self, row: int, col: int) -> Coordinate:
"""Converts grid indices to real-world coordinates.
Expand Down
175 changes: 175 additions & 0 deletions abses/viz/solara.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/env python 3.11.0
# -*-coding:utf-8 -*-
# @Author : Shuang (Twist) Song
# @Contact : [email protected]
# GitHub : https://github.com/SongshGeo
# Website: https://cv.songshgeo.com/

from typing import Any, Callable

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from matplotlib.cm import ScalarMappable
from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
from matplotlib.figure import Figure
from mesa.visualization.mpl_space_drawing import draw_orthogonal_grid
from mesa.visualization.utils import update_counter
from xarray import DataArray

from abses.main import MainModel
from abses.patch import PatchModule

try:
import solara
except ImportError as e:
raise ImportError(
"`solara` is not installed, please install it via `pip install solara`"
) from e


def draw_property_layers(
space: PatchModule,
propertylayer_portrayal: dict[str, dict[str, Any]],
ax: Axes,
):
"""Draw PropertyLayers on the given axes.
Args:
space (mesa.space._Grid): The space containing the PropertyLayers.
propertylayer_portrayal (dict): the key is the name of the layer, the value is a dict with
fields specifying how the layer is to be portrayed
ax (matplotlib.axes.Axes): The axes to draw on.
Notes:
valid fields in in the inner dict of propertylayer_portrayal are "alpha", "vmin", "vmax", "color" or "colormap", and "colorbar"
so you can do `{"some_layer":{"colormap":'viridis', 'alpha':.25, "colorbar":False}}`
"""
for layer_name, portrayal in propertylayer_portrayal.items():
layer: DataArray = space.get_xarray(layer_name)

data = (
layer.data.astype(float)
if layer.data.dtype == bool
else layer.data
)

# Get portrayal properties, or use defaults
alpha = portrayal.get("alpha", 1)
vmin = portrayal.get("vmin", np.min(data))
vmax = portrayal.get("vmax", np.max(data))
colorbar = portrayal.get("colorbar", True)

# Draw the layer
if "color" in portrayal:
rgba_color = to_rgba(portrayal["color"])
normalized_data = (data - vmin) / (vmax - vmin)
rgba_data = np.full((*data.shape, 4), rgba_color)
rgba_data[..., 3] *= normalized_data * alpha
rgba_data = np.clip(rgba_data, 0, 1)
cmap = LinearSegmentedColormap.from_list(
layer_name, [(0, 0, 0, 0), (*rgba_color[:3], alpha)]
)
im = ax.imshow(
rgba_data.transpose(1, 0, 2),
origin="lower",
)
if colorbar:
norm = Normalize(vmin=vmin, vmax=vmax)
sm = ScalarMappable(norm=norm, cmap=cmap)
sm.set_array([])
ax.figure.colorbar(sm, ax=ax, orientation="vertical")

elif "colormap" in portrayal:
cmap = portrayal.get("colormap", "viridis")
if isinstance(cmap, list):
cmap = LinearSegmentedColormap.from_list(layer_name, cmap)
im = ax.imshow(
data.T,
cmap=cmap,
alpha=alpha,
vmin=vmin,
vmax=vmax,
origin="lower",
)
if colorbar:
plt.colorbar(im, ax=ax, label=layer_name)
else:
raise ValueError(
f"PropertyLayer {layer_name} portrayal must include 'color' or 'colormap'."
)


@solara.component
def SpaceMatplotlib(
model: MainModel,
agent_portrayal,
propertylayer_portrayal,
dependencies: list[Any] | None = None,
post_process: Callable | None = None,
**space_drawing_kwargs,
):
"""Create a Matplotlib-based space visualization component."""
update_counter.get()
if model.nature.major_layer is None:
raise ValueError("Major layer is not set for the nature.")
space = model.nature.major_layer

fig = Figure()
ax = fig.add_subplot()

draw_orthogonal_grid(
space,
agent_portrayal,
ax=ax,
**space_drawing_kwargs,
)

if post_process is not None:
post_process(ax)

solara.FigureMatplotlib(
fig, format="png", bbox_inches="tight", dependencies=dependencies
)

if propertylayer_portrayal:
draw_property_layers(space, propertylayer_portrayal, ax=ax)


def make_mpl_space_component(
agent_portrayal: Callable | None = None,
propertylayer_portrayal: dict | None = None,
post_process: Callable | None = None,
**space_drawing_kwargs,
) -> Callable:
"""Create a Matplotlib-based space visualization component.
Args:
agent_portrayal: Function to portray agents.
propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications
post_process : a callable that will be called with the Axes instance. Allows for fine tuning plots (e.g., control ticks)
space_drawing_kwargs : additional keyword arguments to be passed on to the underlying space drawer function. See
the functions for drawing the various spaces for further details.
``agent_portrayal`` is called with an agent and should return a dict. Valid fields in this dict are "color",
"size", "marker", "zorder", alpha, linewidths, and edgecolors. Other field are ignored and will result in a user warning.
Returns:
function: A function that creates a SpaceMatplotlib component
"""
if agent_portrayal is None:

def agent_portrayal(a):
return {}

def MakeSpaceMatplotlib(model):
return SpaceMatplotlib(
model,
agent_portrayal,
propertylayer_portrayal,
post_process=post_process,
**space_drawing_kwargs,
)

return MakeSpaceMatplotlib
91 changes: 46 additions & 45 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ line_length = 79

[tool.poetry]
name = "abses"
version = "0.7.0.alpha"
version = "0.7.0"
description = "ABSESpy makes it easier to build artificial Social-ecological systems with real GeoSpatial datasets and fully incorporate human behaviour."
authors = ["Shuang Song <[email protected]>"]
license = "Apache 2.0 License"
Expand All @@ -51,8 +51,8 @@ abses = "hydra_plugins.abses_searchpath_plugin:ABSESpySearchPathPlugin"
python = ">=3.10,<3.12"
netcdf4 = ">=1.6"
hydra-core = "~1.3"
mesa = {version = ">=3.0.0a4", allow-prereleases = true}
mesa_geo = {git = "https://github.com/SongshGeo/mesa-geo.git", branch = "dev", develop = true}
mesa = {version = ">=3.0.0", allow-prereleases = true}
mesa_geo = {git = "https://github.com/SongshGeo/mesa-geo.git", branch = "dev"}
xarray = ">=2023"
fiona = ">1.8"
loguru = ">=0.7"
Expand All @@ -62,53 +62,54 @@ geopandas = "~0"
typing-extensions = "~4"
fontawesome = ">=5"
seaborn = ">=0.13"
geocube = "^0.5.2"
geocube = ">=0.5.0"
solara = "*"

[tool.poetry.group.dev.dependencies]
pytest-clarity = "^1.0.1"
pre-commit = "^3.0.1"
scriv = "^1.2.0"
pytest = "^7.2.1"
sourcery = "^1.0.6"
allure-pytest = "^2.13.2"
pytest-sugar = "^0.9.7"
ipykernel = "^6.25.1"
jupyterlab = "^4.0.5"
jupyterlab-execute-time = "^3.0.1"
matplotlib = "^3.7.2"
pytest-cov = "^4.1.0"
flake8 = "^6.1.0"
isort = "^5.12.0"
nbstripout = "^0.6.2"
pydocstyle = "^6.3.0"
pre-commit-hooks = "^4.4.0"
interrogate = "^1.5.0"
mypy = "^1.6.1"
bandit = "^1.7.5"
black = "^23.9.1"
pylint = "^3.0.1"
tox = "^4.11.3"
lxml = "^5.2.1"
pytest-clarity = "*"
pre-commit = ">=3.0.1"
scriv = ">=1.2.0"
pytest = ">=7.2.1"
sourcery = ">=1.0.6"
allure-pytest = ">=2.13.2"
pytest-sugar = ">=0.9.7"
ipykernel = ">=6.25.1"
jupyterlab = ">=4.0.5"
jupyterlab-execute-time = ">=3.0.1"
matplotlib = ">=3.7.2"
pytest-cov = ">=4.1.0"
flake8 = ">=6.1.0"
isort = ">=5.12.0"
nbstripout = ">=0.6.2"
pydocstyle = ">=6.3.0"
pre-commit-hooks = ">=4.4.0"
interrogate = ">=1.5.0"
mypy = ">=1.6.1"
bandit = ">=1.7.5"
black = ">=23.9.1"
pylint = ">=3.0.1"
tox = ">=4.11.3"
lxml = ">=5.2.1"


[tool.poetry.group.docs.dependencies]
mkdocs = "^1.5.3"
mkdocs-material = "^9.4.2"
mkdocs-git-revision-date-localized-plugin = "^1.2.0"
mkdocs-minify-plugin = "^0.7.1"
mkdocs-redirects = "^1.2.1"
mkdocs-awesome-pages-plugin = "^2.9.2"
mkdocs-git-authors-plugin = "^0.7.2"
mkdocstrings = {extras = ["python"], version = "^0.24.0"}
mkdocs-bibtex = "^2.11.0"
mkdocs-macros-plugin = "^1.0.4"
mkdocs-jupyter = "^0.24.5"
mkdocs-callouts = "^1.9.1"
mkdocs-glightbox = "^0.3.4"
mike = "^2.0.0"
mkdocs-exclude = "^1.0.2"
mkdocs-simple-hooks = "^0.1.5"
pymdown-extensions = "^10.7"
mkdocs = ">=1.5.3"
mkdocs-material = ">=9.4.2"
mkdocs-git-revision-date-localized-plugin = ">=1.2.0"
mkdocs-minify-plugin = ">=0.7.1"
mkdocs-redirects = ">=1.2.1"
mkdocs-awesome-pages-plugin = ">=2.9.2"
mkdocs-git-authors-plugin = ">=0.7.2"
mkdocstrings = {extras = ["python"], version = ">=0.24.0"}
mkdocs-bibtex = ">=2.11.0"
mkdocs-macros-plugin = ">=1.0.4"
mkdocs-jupyter = ">=0.24.5"
mkdocs-callouts = ">=1.9.1"
mkdocs-glightbox = ">=0.3.4"
mike = ">=2.0.0"
mkdocs-exclude = ">=1.0.2"
mkdocs-simple-hooks = ">=0.1.5"
pymdown-extensions = ">=10.7"

[build-system]
requires = ["poetry-core"]
Expand Down

0 comments on commit 4c7522d

Please sign in to comment.