Skip to content

Commit 1139ed4

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 6ea6d6a + 9d36047 commit 1139ed4

File tree

6 files changed

+450
-5
lines changed

6 files changed

+450
-5
lines changed

docs/apis/visualization.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Visualization
22

3+
4+
⚠️ **Important note for SolaraViz users**
5+
6+
When using **SolaraViz**, Mesa models must be instantiated **using keyword arguments only**.
7+
SolaraViz creates model instances internally via keyword-based parameters, and positional arguments are **not supported**.
8+
9+
**Not supported:**
10+
11+
```python
12+
MyModel(10, 10)
13+
```
14+
15+
**Supported:**
16+
17+
```python
18+
MyModel(width=10, height=10)
19+
```
20+
21+
To avoid errors, it is recommended to define your model constructor with keyword-only arguments, for example:
22+
23+
```python
24+
class MyModel(Model):
25+
def __init__(self, *, width, height, seed=None):
26+
...
27+
```
28+
29+
330
For detailed tutorials, please refer to:
431

532
- [Basic Visualization](../tutorials/4_visualization_basic)

docs/migration_guide.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,23 @@ def propertylayer_portrayal(layer):
8383

8484
* Ref: [PR #2786](https://github.com/mesa/mesa/pull/2786)
8585

86+
### Passing portrayal arguments to draw methods
87+
Passing portrayal arguments directly to `draw_agents()` and `draw_propertylayer()` is deprecated. Use the `setup_agents()` and `setup_propertylayer()` methods before calling the draw methods.
88+
89+
```python
90+
# Old
91+
renderer.draw_agents(agent_portrayal=agent_portrayal)
92+
renderer.draw_propertylayer(propertylayer_portrayal)
93+
94+
# New
95+
renderer.setup_agents(agent_portrayal).draw_agents()
96+
renderer.setup_propertylayer(propertylayer_portrayal).draw_propertylayer()
97+
```
98+
99+
This change allows for better method chaining and separates the configuration phase from the rendering phase.
100+
101+
* Ref: [PR #2893](https://github.com/mesa/mesa/pull/2893)
102+
86103
### Default Space Visualization
87104
While the visualization methods from Mesa versions before 3.3.0 still work, version 3.3.0 introduces `SpaceRenderer`, which changes how space visualizations are rendered. Check out the updated [Mesa documentation](https://mesa.readthedocs.io/latest/tutorials/4_visualization_basic.html) for guidance on upgrading your model’s visualization using `SpaceRenderer`.
88105

docs/tutorials/4_visualization_basic.ipynb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,52 @@
162162
" self.datacollector.collect(self)"
163163
]
164164
},
165+
{
166+
"cell_type": "markdown",
167+
"metadata": {},
168+
"source": [
169+
"### Important note for SolaraViz users\n",
170+
"\n",
171+
"When using **SolaraViz**, Mesa models must be instantiated **using keyword arguments only**. SolaraViz creates model instances internally via keyword-based parameters, and positional arguments are **not supported**.\n",
172+
"\n",
173+
"**Not supported:**\n",
174+
"```python\n",
175+
"MyModel(10, 10)\n",
176+
"```\n",
177+
"\n",
178+
"**Supported:**\n",
179+
"```python\n",
180+
"MyModel(width=10, height=10)\n",
181+
"```\n",
182+
"**Common Pitfall:**\n",
183+
"\n",
184+
"When converting from positional to keyword arguments, make sure to include ALL parameters. For example:\n",
185+
"```python\n",
186+
"model=MoneyModel(100,10,10) #n=100, width=10, height=10\n",
187+
"\n",
188+
"# Correct conversion (keyword)\n",
189+
"model = MoneyModel(n=100, width=10, height=10) # All parameters included\n",
190+
"\n",
191+
"# Incorrect conversion (missing n)\n",
192+
"model = MoneyModel(width=10, height=10) # Defaults to n=10\n",
193+
"```\n",
194+
"\n",
195+
"To avoid errors, it is recommended to define your model constructor with keyword-only arguments, for example:\n",
196+
"```python\n",
197+
"class MyModel(Model):\n",
198+
" def __init__(self, *, width, height, seed=None):\n",
199+
" ...\n",
200+
"```\n"
201+
]
202+
},
165203
{
166204
"cell_type": "code",
167205
"execution_count": null,
168206
"metadata": {},
169207
"outputs": [],
170208
"source": [
171209
"# Lets make sure the model works\n",
172-
"model = MoneyModel(100, 10, 10)\n",
210+
"model = MoneyModel(n=100, width=100, height=10, seed=10)\n",
173211
"for _ in range(20):\n",
174212
" model.step()\n",
175213
"\n",

mesa/experimental/devs/eventlist.py

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@
2525
from enum import IntEnum
2626
from heapq import heapify, heappop, heappush, nsmallest
2727
from types import MethodType
28-
from typing import Any
28+
from typing import TYPE_CHECKING, Any
2929
from weakref import WeakMethod, ref
3030

31+
if TYPE_CHECKING:
32+
from mesa import Model
33+
3134

3235
class Priority(IntEnum):
3336
"""Enumeration of priority levels."""
@@ -123,6 +126,181 @@ def __lt__(self, other): # noqa
123126
)
124127

125128

129+
class EventGenerator:
130+
"""A generator that creates recurring events at specified intervals.
131+
132+
EventGenerator represents a pattern for when things should happen repeatedly.
133+
Unlike a single SimulationEvent, an EventGenerator is persistent and can be
134+
stopped or configured with stop conditions.
135+
136+
Attributes:
137+
model: The model this generator belongs to
138+
function: The callable to execute for each generated event
139+
interval: Time between events (fixed value or callable returning value)
140+
priority: Priority level for generated events
141+
142+
"""
143+
144+
def __init__(
145+
self,
146+
model: Model,
147+
function: Callable,
148+
interval: float | int | Callable[[Model], float | int],
149+
priority: Priority = Priority.DEFAULT,
150+
) -> None:
151+
"""Initialize an EventGenerator.
152+
153+
Args:
154+
model: The model this generator belongs to
155+
function: The callable to execute for each generated event.
156+
Use functools.partial to bind arguments.
157+
interval: Time between events. Can be a fixed value or a callable
158+
that takes the model and returns the interval.
159+
priority: Priority level for generated events
160+
161+
"""
162+
self.model = model
163+
self.function = function
164+
self.interval = interval
165+
self.priority = priority
166+
167+
self._active: bool = False
168+
self._current_event: SimulationEvent | None = None
169+
self._execution_count: int = 0
170+
171+
# Stop conditions (mutually exclusive)
172+
self._max_count: int | None = None
173+
self._end_time: float | None = None
174+
175+
@property
176+
def is_active(self) -> bool:
177+
"""Return whether the generator is currently active."""
178+
return self._active
179+
180+
@property
181+
def execution_count(self) -> int:
182+
"""Return the number of times this generator has executed."""
183+
return self._execution_count
184+
185+
def _get_interval(self) -> float | int:
186+
"""Get the next interval value."""
187+
if callable(self.interval):
188+
return self.interval(self.model)
189+
return self.interval
190+
191+
def _should_stop(self, next_time: float) -> bool:
192+
"""Check if the generator should stop before scheduling the next event."""
193+
return (
194+
self._max_count is not None and self._execution_count >= self._max_count
195+
) or (self._end_time is not None and next_time > self._end_time)
196+
197+
def _execute_and_reschedule(self) -> None:
198+
"""Execute the function and schedule the next event."""
199+
if not self._active:
200+
return
201+
202+
self.function()
203+
self._execution_count += 1
204+
205+
# Schedule next event if we shouldn't stop
206+
next_time = self.model.time + self._get_interval()
207+
if not self._should_stop(next_time):
208+
self._schedule_next(next_time)
209+
else:
210+
self._active = False
211+
self._current_event = None
212+
213+
def _schedule_next(self, time: float) -> None:
214+
"""Schedule the next event at the given time."""
215+
self._current_event = SimulationEvent(
216+
time,
217+
self._execute_and_reschedule,
218+
priority=self.priority,
219+
)
220+
self.model._simulator.event_list.add_event(self._current_event)
221+
222+
def start(
223+
self,
224+
at: float | None = None,
225+
after: float | None = None,
226+
) -> EventGenerator:
227+
"""Start the event generator.
228+
229+
Args:
230+
at: Absolute time to start generating events
231+
after: Relative time from now to start generating events
232+
233+
Returns:
234+
Self for method chaining
235+
236+
Raises:
237+
ValueError: If both `at` and `after` are specified
238+
239+
"""
240+
if self._active:
241+
return self
242+
243+
if at is not None and after is not None:
244+
raise ValueError("Cannot specify both 'at' and 'after'")
245+
246+
if at is None and after is None:
247+
# Default: start at next interval from now
248+
start_time = self.model.time + self._get_interval()
249+
elif at is not None:
250+
if at < self.model.time:
251+
raise ValueError(f"Cannot start in the past: {at} < {self.model.time}")
252+
start_time = at
253+
else: # after is not None
254+
if after < 0:
255+
raise ValueError(f"Cannot start in the past: after={after}")
256+
start_time = self.model.time + after
257+
258+
self._active = True
259+
self._schedule_next(start_time)
260+
return self
261+
262+
def stop(
263+
self,
264+
at: float | None = None,
265+
after: float | None = None,
266+
count: int | None = None,
267+
) -> EventGenerator:
268+
"""Stop the event generator.
269+
270+
Args:
271+
at: Absolute time to stop generating events
272+
after: Relative time from now to stop generating events
273+
count: Number of additional executions before stopping
274+
275+
Returns:
276+
Self for method chaining
277+
278+
Raises:
279+
ValueError: If more than one stop condition is specified
280+
281+
"""
282+
conditions = sum(x is not None for x in [at, after, count])
283+
if conditions > 1:
284+
raise ValueError("Can only specify one of 'at', 'after', or 'count'")
285+
286+
if conditions == 0:
287+
# Immediate stop
288+
self._active = False
289+
if self._current_event is not None:
290+
self._current_event.cancel()
291+
self._current_event = None
292+
return self
293+
294+
if at is not None:
295+
self._end_time = at
296+
elif after is not None:
297+
self._end_time = self.model.time + after
298+
elif count is not None:
299+
self._max_count = self._execution_count + count
300+
301+
return self
302+
303+
126304
class EventList:
127305
"""An event list.
128306

mesa/visualization/space_renderer.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,8 @@ def draw_agents(self, agent_portrayal=None, **kwargs):
272272
if agent_portrayal is not None:
273273
warnings.warn(
274274
"Passing agent_portrayal to draw_agents() is deprecated and will be removed in Mesa 4.0. "
275-
"Use setup_agents(agent_portrayal, **kwargs) before calling draw_agents().",
275+
"Use setup_agents(agent_portrayal, **kwargs) before calling draw_agents()."
276+
"See https://mesa.readthedocs.io/latest/migration_guide.html#passing-portrayal-arguments-to-draw-methods",
276277
FutureWarning,
277278
stacklevel=2,
278279
)
@@ -314,7 +315,8 @@ def draw_propertylayer(self, propertylayer_portrayal=None):
314315
if propertylayer_portrayal is not None:
315316
warnings.warn(
316317
"Passing propertylayer_portrayal to draw_propertylayer() is deprecated and will be removed in Mesa 4.0. "
317-
"Use setup_propertylayer(propertylayer_portrayal) before calling draw_propertylayer().",
318+
"Use setup_propertylayer(propertylayer_portrayal) before calling draw_propertylayer()."
319+
"See https://mesa.readthedocs.io/latest/migration_guide.html#passing-portrayal-arguments-to-draw-methods",
318320
FutureWarning,
319321
stacklevel=2,
320322
)

0 commit comments

Comments
 (0)