Skip to content

Commit

Permalink
Merge branch 'projectmesa:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
tpike3 authored Jan 11, 2025
2 parents 01a1788 + cfb1925 commit 6f4d127
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 23 deletions.
54 changes: 54 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,60 @@
---
title: Release History
---
# 3.1.3 (2025-01-11)
## Highlights
Mesa 3.1.3 introduces a major experimental reimplementation of Mesa's continuous space, providing an intuitive agent-centric API and significant performance improvements. The new implementation supports n-dimensional spaces and offers streamlined methods for agent movement and neighbor calculations.

### New Continuous Space Features
- Agent-centric movement API similar to cell spaces
- Efficient neighbor calculations and position updates
- Support for n-dimensional spaces
- Improved memory management with dynamic array resizing

Here's a quick look at the new API:

```python
# Create a 2D continuous space
space = ContinuousSpace(
dimensions=[[0, 1], [0, 1]],
torus=True,
random=model.random
)

# Create and position an agent
agent = ContinuousSpaceAgent(space, model)
agent.position = [0.5, 0.5]

# Move agent using vector arithmetic
agent.position += [0.1, 0.1]

# Get neighbors within radius
neighbors, distances = agent.get_neighbors_in_radius(radius=0.2)

# Find k nearest neighbors
nearest, distances = agent.get_nearest_neighbors(k=5)
```

The new implementation particularly benefits models requiring frequent position updates and neighbor queries, such as flocking simulations or particle systems. See [#2584](https://github.com/projectmesa/mesa/pull/2584) for more details. We would love to get feedback on the new Continuous Space in [#2611](https://github.com/projectmesa/mesa/discussions/2611).

Other improvements in this release include consistent visualization behavior across space types with the reimplementation of `draw_voronoi` [#2608](https://github.com/projectmesa/mesa/pull/2608), and a new render interval slider for controlling visualization update frequency in SolaraViz, which helps improve performance when working with complex visualizations [#2596](https://github.com/projectmesa/mesa/pull/2596). We've also fixed a bug affecting random number generation determinism when using `Model(seed=something)`, ensuring both `model.random` and `model.rng` now behave consistently when seeded with the same initial value [#2598](https://github.com/projectmesa/mesa/pull/2598).

## What's Changed
### 🧪 Experimental features
* Reimplementation of Continuous Space by @quaquel in https://github.com/projectmesa/mesa/pull/2584
### 🛠 Enhancements made
* reimplementation of draw_voroinoi by @quaquel in https://github.com/projectmesa/mesa/pull/2608
* Add render interval slider to control visualization update frequency by @HMNS19 in https://github.com/projectmesa/mesa/pull/2596
### 🐛 Bugs fixed
* Bugfix for non deterministic rng behavior by @quaquel in https://github.com/projectmesa/mesa/pull/2598
### 🔍 Examples updated
* Clarify ContinuousSpace.get_neighbors behavior with multiple agents at same position by @quaquel in https://github.com/projectmesa/mesa/pull/2599

## New Contributors
* @HMNS19 made their first contribution in https://github.com/projectmesa/mesa/pull/2596

**Full Changelog**: https://github.com/projectmesa/mesa/compare/v3.1.2...v3.1.3

# 3.1.2 (2025-01-04)
## Highlights
Mesa v3.1.2 is a patch release containing updates to our wolf-sheep, shelling and prisoner's dilemma example models and improving documentation in the tutorials and visualisation docstring. No functional changes to the core library were made.
Expand Down
12 changes: 12 additions & 0 deletions docs/apis/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,15 @@ This namespace contains experimental features. These are under development, and
.. automodule:: experimental.devs.simulator
:members:
```

## Continuous Space

```{eval-rst}
.. automodule:: experimental.continuous_space.continuous_space
:members:
```

```{eval-rst}
.. automodule:: experimental.continuous_space.continuous_space_agents
:members:
```
2 changes: 1 addition & 1 deletion mesa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
]

__title__ = "mesa"
__version__ = "3.1.2"
__version__ = "3.1.3"
__license__ = "Apache 2.0"
_this_year = datetime.datetime.now(tz=datetime.UTC).date().year
__copyright__ = f"Copyright {_this_year} Project Mesa Team"
5 changes: 1 addition & 4 deletions mesa/experimental/cell_space/voronoi.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ def __init__(
random: Random | None = None,
cell_klass: type[Cell] = Cell,
capacity_function: callable = round_float,
cell_coloring_property: str | None = None,
) -> None:
"""A Voronoi Tessellation Grid.
Expand All @@ -200,7 +199,7 @@ def __init__(
random (Random): random number generator
cell_klass (type[Cell]): type of cell class
capacity_function (Callable): function to compute (int) capacity according to (float) area
cell_coloring_property (str): voronoi visualization polygon fill property
"""
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
self.centroids_coordinates = centroids_coordinates
Expand All @@ -215,7 +214,6 @@ def __init__(
self.triangulation = None
self.voronoi_coordinates = None
self.capacity_function = capacity_function
self.cell_coloring_property = cell_coloring_property

self._connect_cells()
self._build_cell_polygons()
Expand Down Expand Up @@ -266,4 +264,3 @@ def _build_cell_polygons(self):
polygon_area = self._compute_polygon_area(polygon)
self._cells[region].properties["area"] = polygon_area
self._cells[region].capacity = self.capacity_function(polygon_area)
self._cells[region].properties[self.cell_coloring_property] = 0
29 changes: 18 additions & 11 deletions mesa/visualization/mpl_space_drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from matplotlib.cm import ScalarMappable
from matplotlib.collections import PatchCollection
from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
from matplotlib.patches import RegularPolygon
from matplotlib.patches import Polygon, RegularPolygon

import mesa
from mesa.experimental.cell_space import (
Expand Down Expand Up @@ -501,14 +501,19 @@ def draw_continuous_space(


def draw_voronoi_grid(
space: VoronoiGrid, agent_portrayal: Callable, ax: Axes | None = None, **kwargs
space: VoronoiGrid,
agent_portrayal: Callable,
ax: Axes | None = None,
draw_grid: bool = True,
**kwargs,
):
"""Visualize a voronoi grid.
Args:
space: the space to visualize
agent_portrayal: a callable that is called with the agent and returns a dict
ax: a Matplotlib Axes instance. If none is provided a new figure and ax will be created using plt.subplots
draw_grid: whether to draw the grid or not
kwargs: additional keyword arguments passed to ax.scatter
Returns:
Expand Down Expand Up @@ -541,16 +546,18 @@ def draw_voronoi_grid(

_scatter(ax, arguments, **kwargs)

for cell in space.all_cells:
polygon = cell.properties["polygon"]
ax.fill(
*zip(*polygon),
alpha=min(1, cell.properties[space.cell_coloring_property]),
c="red",
zorder=0,
) # Plot filled polygon
ax.plot(*zip(*polygon), color="black") # Plot polygon edges in black
def setup_voroinoimesh(cells):
patches = []
for cell in cells:
patch = Polygon(cell.properties["polygon"])
patches.append(patch)
mesh = PatchCollection(
patches, edgecolor="k", facecolor=(1, 1, 1, 0), linestyle="dotted", lw=1
)
return mesh

if draw_grid:
ax.add_collection(setup_voroinoimesh(space.all_cells.cells))
return ax


Expand Down
37 changes: 30 additions & 7 deletions mesa/visualization/solara_viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def SolaraViz(
| Literal["default"] = "default",
*,
play_interval: int = 100,
render_interval: int = 1,
simulator: Simulator | None = None,
model_params=None,
name: str | None = None,
Expand All @@ -72,6 +73,8 @@ def SolaraViz(
Defaults to "default", which uses the default Altair space visualization.
play_interval (int, optional): Interval for playing the model steps in milliseconds.
This controls the speed of the model's automatic stepping. Defaults to 100 ms.
render_interval (int, optional): Controls how often plots are updated during a simulation,
allowing users to skip intermediate steps and update graphs less frequently.
simulator: A simulator that controls the model (optional)
model_params (dict, optional): Parameters for (re-)instantiating a model.
Can include user-adjustable parameters and fixed parameters. Defaults to None.
Expand All @@ -90,6 +93,8 @@ def SolaraViz(
model instance is provided, it will be converted to a reactive model using `solara.use_reactive`.
- The `play_interval` argument controls the speed of the model's automatic stepping. A lower
value results in faster stepping, while a higher value results in slower stepping.
- The `render_interval` argument determines how often plots are updated during simulation. Higher values
reduce update frequency,resulting in faster execution.
"""
if components == "default":
components = [components_altair.make_altair_space()]
Expand All @@ -103,7 +108,7 @@ def SolaraViz(
# set up reactive model_parameters shared by ModelCreator and ModelController
reactive_model_parameters = solara.use_reactive({})
reactive_play_interval = solara.use_reactive(play_interval)

reactive_render_interval = solara.use_reactive(render_interval)
with solara.AppBar():
solara.AppBarTitle(name if name else model.value.__class__.__name__)

Expand All @@ -117,18 +122,28 @@ def SolaraViz(
max=500,
step=10,
)
solara.SliderInt(
label="Render Interval (steps)",
value=reactive_render_interval,
on_value=lambda v: reactive_render_interval.set(v),
min=1,
max=100,
step=2,
)
if not isinstance(simulator, Simulator):
ModelController(
model,
model_parameters=reactive_model_parameters,
play_interval=reactive_play_interval,
render_interval=reactive_render_interval,
)
else:
SimulatorController(
model,
simulator,
model_parameters=reactive_model_parameters,
play_interval=reactive_play_interval,
render_interval=reactive_render_interval,
)
with solara.Card("Model Parameters"):
ModelCreator(
Expand Down Expand Up @@ -189,14 +204,15 @@ def ModelController(
*,
model_parameters: dict | solara.Reactive[dict] = None,
play_interval: int | solara.Reactive[int] = 100,
render_interval: int | solara.Reactive[int] = 1,
):
"""Create controls for model execution (step, play, pause, reset).
Args:
model: Reactive model instance
model_parameters: Reactive parameters for (re-)instantiating a model.
play_interval: Interval for playing the model steps in milliseconds.
render_interval: Controls how often the plots are updated during simulation steps.Higher value reduce update frequency.
"""
playing = solara.use_reactive(False)
running = solara.use_reactive(True)
Expand All @@ -215,9 +231,12 @@ async def step():

@function_logger(__name__)
def do_step():
"""Advance the model by one step."""
model.value.step()
"""Advance the model by the number of steps specified by the render_interval slider."""
for _ in range(render_interval.value):
model.value.step()

running.value = model.value.running

force_update()

@function_logger(__name__)
Expand Down Expand Up @@ -259,6 +278,7 @@ def SimulatorController(
*,
model_parameters: dict | solara.Reactive[dict] = None,
play_interval: int | solara.Reactive[int] = 100,
render_interval: int | solara.Reactive[int] = 1,
):
"""Create controls for model execution (step, play, pause, reset).
Expand All @@ -267,7 +287,11 @@ def SimulatorController(
simulator: Simulator instance
model_parameters: Reactive parameters for (re-)instantiating a model.
play_interval: Interval for playing the model steps in milliseconds.
render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency.
Notes:
The `step button` increments the step by the value specified in the `render_interval` slider.
This behavior ensures synchronization between simulation steps and plot updates.
"""
playing = solara.use_reactive(False)
running = solara.use_reactive(True)
Expand All @@ -285,8 +309,8 @@ async def step():
)

def do_step():
"""Advance the model by one step."""
simulator.run_for(1)
"""Advance the model by the number of steps specified by the render_interval slider."""
simulator.run_for(render_interval.value)
running.value = model.value.running
force_update()

Expand Down Expand Up @@ -390,7 +414,6 @@ def ModelCreator(
or are dictionaries containing parameter details such as type, value, min, and max.
- The `seed` argument ensures reproducibility by setting the initial seed for the model's random number generator.
- The component provides an interface for adjusting user-defined parameters and reseeding the model.
"""
if model_parameters is None:
model_parameters = {}
Expand Down

0 comments on commit 6f4d127

Please sign in to comment.