Mesa 3.0.0a2__py3-none-any.whl → 3.0.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Mesa might be problematic. Click here for more details.
- mesa/__init__.py +1 -1
- mesa/agent.py +202 -25
- mesa/batchrunner.py +4 -4
- mesa/datacollection.py +2 -2
- mesa/experimental/cell_space/__init__.py +2 -0
- mesa/experimental/cell_space/voronoi.py +264 -0
- mesa/model.py +96 -31
- mesa/time.py +0 -7
- mesa/visualization/components/matplotlib.py +53 -0
- mesa/visualization/solara_viz.py +6 -2
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a3.dist-info}/METADATA +7 -1
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a3.dist-info}/RECORD +15 -14
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a3.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a3.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a3.dist-info}/licenses/LICENSE +0 -0
mesa/__init__.py
CHANGED
|
@@ -24,7 +24,7 @@ __all__ = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
__title__ = "mesa"
|
|
27
|
-
__version__ = "3.0.
|
|
27
|
+
__version__ = "3.0.0a3"
|
|
28
28
|
__license__ = "Apache 2.0"
|
|
29
29
|
_this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
|
|
30
30
|
__copyright__ = f"Copyright {_this_year} Project Mesa Team"
|
mesa/agent.py
CHANGED
|
@@ -11,7 +11,9 @@ from __future__ import annotations
|
|
|
11
11
|
import contextlib
|
|
12
12
|
import copy
|
|
13
13
|
import operator
|
|
14
|
+
import warnings
|
|
14
15
|
import weakref
|
|
16
|
+
from collections import defaultdict
|
|
15
17
|
from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence
|
|
16
18
|
from random import Random
|
|
17
19
|
|
|
@@ -47,19 +49,12 @@ class Agent:
|
|
|
47
49
|
self.model = model
|
|
48
50
|
self.pos: Position | None = None
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
self.model.agents_[type(self)][self] = None
|
|
53
|
-
except AttributeError as err:
|
|
54
|
-
# model super has not been called
|
|
55
|
-
raise RuntimeError(
|
|
56
|
-
"The Mesa Model class was not initialized. You must explicitly initialize the Model by calling super().__init__() on initialization."
|
|
57
|
-
) from err
|
|
52
|
+
self.model.register_agent(self)
|
|
58
53
|
|
|
59
54
|
def remove(self) -> None:
|
|
60
55
|
"""Remove and delete the agent from the model."""
|
|
61
56
|
with contextlib.suppress(KeyError):
|
|
62
|
-
self.model.
|
|
57
|
+
self.model.deregister_agent(self)
|
|
63
58
|
|
|
64
59
|
def step(self) -> None:
|
|
65
60
|
"""A single step of the agent."""
|
|
@@ -119,9 +114,10 @@ class AgentSet(MutableSet, Sequence):
|
|
|
119
114
|
def select(
|
|
120
115
|
self,
|
|
121
116
|
filter_func: Callable[[Agent], bool] | None = None,
|
|
122
|
-
|
|
117
|
+
at_most: int | float = float("inf"),
|
|
123
118
|
inplace: bool = False,
|
|
124
119
|
agent_type: type[Agent] | None = None,
|
|
120
|
+
n: int | None = None,
|
|
125
121
|
) -> AgentSet:
|
|
126
122
|
"""
|
|
127
123
|
Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
|
|
@@ -129,29 +125,47 @@ class AgentSet(MutableSet, Sequence):
|
|
|
129
125
|
Args:
|
|
130
126
|
filter_func (Callable[[Agent], bool], optional): A function that takes an Agent and returns True if the
|
|
131
127
|
agent should be included in the result. Defaults to None, meaning no filtering is applied.
|
|
132
|
-
|
|
128
|
+
at_most (int | float, optional): The maximum amount of agents to select. Defaults to infinity.
|
|
129
|
+
- If an integer, at most the first number of matching agents are selected.
|
|
130
|
+
- If a float between 0 and 1, at most that fraction of original the agents are selected.
|
|
133
131
|
inplace (bool, optional): If True, modifies the current AgentSet; otherwise, returns a new AgentSet. Defaults to False.
|
|
134
132
|
agent_type (type[Agent], optional): The class type of the agents to select. Defaults to None, meaning no type filtering is applied.
|
|
135
133
|
|
|
136
134
|
Returns:
|
|
137
135
|
AgentSet: A new AgentSet containing the selected agents, unless inplace is True, in which case the current AgentSet is updated.
|
|
136
|
+
|
|
137
|
+
Notes:
|
|
138
|
+
- at_most just return the first n or fraction of agents. To take a random sample, shuffle() beforehand.
|
|
139
|
+
- at_most is an upper limit. When specifying other criteria, the number of agents returned can be smaller.
|
|
138
140
|
"""
|
|
141
|
+
if n is not None:
|
|
142
|
+
warnings.warn(
|
|
143
|
+
"The parameter 'n' is deprecated. Use 'at_most' instead.",
|
|
144
|
+
DeprecationWarning,
|
|
145
|
+
stacklevel=2,
|
|
146
|
+
)
|
|
147
|
+
at_most = n
|
|
139
148
|
|
|
140
|
-
|
|
149
|
+
inf = float("inf")
|
|
150
|
+
if filter_func is None and agent_type is None and at_most == inf:
|
|
141
151
|
return self if inplace else copy.copy(self)
|
|
142
152
|
|
|
143
|
-
|
|
153
|
+
# Check if at_most is of type float
|
|
154
|
+
if at_most <= 1.0 and isinstance(at_most, float):
|
|
155
|
+
at_most = int(len(self) * at_most) # Note that it rounds down (floor)
|
|
156
|
+
|
|
157
|
+
def agent_generator(filter_func, agent_type, at_most):
|
|
144
158
|
count = 0
|
|
145
159
|
for agent in self:
|
|
160
|
+
if count >= at_most:
|
|
161
|
+
break
|
|
146
162
|
if (not filter_func or filter_func(agent)) and (
|
|
147
163
|
not agent_type or isinstance(agent, agent_type)
|
|
148
164
|
):
|
|
149
165
|
yield agent
|
|
150
166
|
count += 1
|
|
151
|
-
if 0 < n <= count:
|
|
152
|
-
break
|
|
153
167
|
|
|
154
|
-
agents = agent_generator(filter_func, agent_type,
|
|
168
|
+
agents = agent_generator(filter_func, agent_type, at_most)
|
|
155
169
|
|
|
156
170
|
return AgentSet(agents, self.model) if not inplace else self._update(agents)
|
|
157
171
|
|
|
@@ -216,25 +230,64 @@ class AgentSet(MutableSet, Sequence):
|
|
|
216
230
|
self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
|
|
217
231
|
return self
|
|
218
232
|
|
|
219
|
-
def do(
|
|
220
|
-
self, method: str | Callable, *args, return_results: bool = False, **kwargs
|
|
221
|
-
) -> AgentSet | list[Any]:
|
|
233
|
+
def do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
|
|
222
234
|
"""
|
|
223
235
|
Invoke a method or function on each agent in the AgentSet.
|
|
224
236
|
|
|
225
237
|
Args:
|
|
226
|
-
method (str, callable): the callable to do on each
|
|
238
|
+
method (str, callable): the callable to do on each agent
|
|
227
239
|
|
|
228
240
|
* in case of str, the name of the method to call on each agent.
|
|
229
241
|
* in case of callable, the function to be called with each agent as first argument
|
|
230
242
|
|
|
231
|
-
return_results (bool, optional): If True, returns the results of the method calls; otherwise, returns the AgentSet itself. Defaults to False, so you can chain method calls.
|
|
232
243
|
*args: Variable length argument list passed to the callable being called.
|
|
233
244
|
**kwargs: Arbitrary keyword arguments passed to the callable being called.
|
|
234
245
|
|
|
235
246
|
Returns:
|
|
236
247
|
AgentSet | list[Any]: The results of the callable calls if return_results is True, otherwise the AgentSet itself.
|
|
237
248
|
"""
|
|
249
|
+
try:
|
|
250
|
+
return_results = kwargs.pop("return_results")
|
|
251
|
+
except KeyError:
|
|
252
|
+
return_results = False
|
|
253
|
+
else:
|
|
254
|
+
warnings.warn(
|
|
255
|
+
"Using return_results is deprecated. Use AgenSet.do in case of return_results=False, and "
|
|
256
|
+
"AgentSet.map in case of return_results=True",
|
|
257
|
+
stacklevel=2,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if return_results:
|
|
261
|
+
return self.map(method, *args, **kwargs)
|
|
262
|
+
|
|
263
|
+
# we iterate over the actual weakref keys and check if weakref is alive before calling the method
|
|
264
|
+
if isinstance(method, str):
|
|
265
|
+
for agentref in self._agents.keyrefs():
|
|
266
|
+
if (agent := agentref()) is not None:
|
|
267
|
+
getattr(agent, method)(*args, **kwargs)
|
|
268
|
+
else:
|
|
269
|
+
for agentref in self._agents.keyrefs():
|
|
270
|
+
if (agent := agentref()) is not None:
|
|
271
|
+
method(agent, *args, **kwargs)
|
|
272
|
+
|
|
273
|
+
return self
|
|
274
|
+
|
|
275
|
+
def map(self, method: str | Callable, *args, **kwargs) -> list[Any]:
|
|
276
|
+
"""
|
|
277
|
+
Invoke a method or function on each agent in the AgentSet and return the results.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
method (str, callable): the callable to apply on each agent
|
|
281
|
+
|
|
282
|
+
* in case of str, the name of the method to call on each agent.
|
|
283
|
+
* in case of callable, the function to be called with each agent as first argument
|
|
284
|
+
|
|
285
|
+
*args: Variable length argument list passed to the callable being called.
|
|
286
|
+
**kwargs: Arbitrary keyword arguments passed to the callable being called.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
list[Any]: The results of the callable calls
|
|
290
|
+
"""
|
|
238
291
|
# we iterate over the actual weakref keys and check if weakref is alive before calling the method
|
|
239
292
|
if isinstance(method, str):
|
|
240
293
|
res = [
|
|
@@ -249,7 +302,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
249
302
|
if (agent := agentref()) is not None
|
|
250
303
|
]
|
|
251
304
|
|
|
252
|
-
return res
|
|
305
|
+
return res
|
|
253
306
|
|
|
254
307
|
def get(self, attr_names: str | list[str]) -> list[Any]:
|
|
255
308
|
"""
|
|
@@ -275,6 +328,21 @@ class AgentSet(MutableSet, Sequence):
|
|
|
275
328
|
for agent in self._agents
|
|
276
329
|
]
|
|
277
330
|
|
|
331
|
+
def set(self, attr_name: str, value: Any) -> AgentSet:
|
|
332
|
+
"""
|
|
333
|
+
Set a specified attribute to a given value for all agents in the AgentSet.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
attr_name (str): The name of the attribute to set.
|
|
337
|
+
value (Any): The value to set the attribute to.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
AgentSet: The AgentSet instance itself, after setting the attribute.
|
|
341
|
+
"""
|
|
342
|
+
for agent in self:
|
|
343
|
+
setattr(agent, attr_name, value)
|
|
344
|
+
return self
|
|
345
|
+
|
|
278
346
|
def __getitem__(self, item: int | slice) -> Agent:
|
|
279
347
|
"""
|
|
280
348
|
Retrieve an agent or a slice of agents from the AgentSet.
|
|
@@ -357,7 +425,116 @@ class AgentSet(MutableSet, Sequence):
|
|
|
357
425
|
"""
|
|
358
426
|
return self.model.random
|
|
359
427
|
|
|
428
|
+
def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
|
|
429
|
+
"""
|
|
430
|
+
Group agents by the specified attribute or return from the callable
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
by (Callable, str): used to determine what to group agents by
|
|
434
|
+
|
|
435
|
+
* if ``by`` is a callable, it will be called for each agent and the return is used
|
|
436
|
+
for grouping
|
|
437
|
+
* if ``by`` is a str, it should refer to an attribute on the agent and the value
|
|
438
|
+
of this attribute will be used for grouping
|
|
439
|
+
result_type (str, optional): The datatype for the resulting groups {"agentset", "list"}
|
|
440
|
+
Returns:
|
|
441
|
+
GroupBy
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
Notes:
|
|
445
|
+
There might be performance benefits to using `result_type='list'` if you don't need the advanced functionality
|
|
446
|
+
of an AgentSet.
|
|
447
|
+
|
|
448
|
+
"""
|
|
449
|
+
groups = defaultdict(list)
|
|
450
|
+
|
|
451
|
+
if isinstance(by, Callable):
|
|
452
|
+
for agent in self:
|
|
453
|
+
groups[by(agent)].append(agent)
|
|
454
|
+
else:
|
|
455
|
+
for agent in self:
|
|
456
|
+
groups[getattr(agent, by)].append(agent)
|
|
457
|
+
|
|
458
|
+
if result_type == "agentset":
|
|
459
|
+
return GroupBy(
|
|
460
|
+
{k: AgentSet(v, model=self.model) for k, v in groups.items()}
|
|
461
|
+
)
|
|
462
|
+
else:
|
|
463
|
+
return GroupBy(groups)
|
|
464
|
+
|
|
465
|
+
# consider adding for performance reasons
|
|
466
|
+
# for Sequence: __reversed__, index, and count
|
|
467
|
+
# for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class GroupBy:
|
|
471
|
+
"""Helper class for AgentSet.groupby
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
Attributes:
|
|
475
|
+
groups (dict): A dictionary with the group_name as key and group as values
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
def __init__(self, groups: dict[Any, list | AgentSet]):
|
|
480
|
+
self.groups: dict[Any, list | AgentSet] = groups
|
|
481
|
+
|
|
482
|
+
def map(self, method: Callable | str, *args, **kwargs) -> dict[Any, Any]:
|
|
483
|
+
"""Apply the specified callable to each group and return the results.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
method (Callable, str): The callable to apply to each group,
|
|
487
|
+
|
|
488
|
+
* if ``method`` is a callable, it will be called it will be called with the group as first argument
|
|
489
|
+
* if ``method`` is a str, it should refer to a method on the group
|
|
490
|
+
|
|
491
|
+
Additional arguments and keyword arguments will be passed on to the callable.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
dict with group_name as key and the return of the method as value
|
|
495
|
+
|
|
496
|
+
Notes:
|
|
497
|
+
this method is useful for methods or functions that do return something. It
|
|
498
|
+
will break method chaining. For that, use ``do`` instead.
|
|
499
|
+
|
|
500
|
+
"""
|
|
501
|
+
if isinstance(method, str):
|
|
502
|
+
return {
|
|
503
|
+
k: getattr(v, method)(*args, **kwargs) for k, v in self.groups.items()
|
|
504
|
+
}
|
|
505
|
+
else:
|
|
506
|
+
return {k: method(v, *args, **kwargs) for k, v in self.groups.items()}
|
|
507
|
+
|
|
508
|
+
def do(self, method: Callable | str, *args, **kwargs) -> GroupBy:
|
|
509
|
+
"""Apply the specified callable to each group
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
method (Callable, str): The callable to apply to each group,
|
|
513
|
+
|
|
514
|
+
* if ``method`` is a callable, it will be called it will be called with the group as first argument
|
|
515
|
+
* if ``method`` is a str, it should refer to a method on the group
|
|
516
|
+
|
|
517
|
+
Additional arguments and keyword arguments will be passed on to the callable.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
the original GroupBy instance
|
|
521
|
+
|
|
522
|
+
Notes:
|
|
523
|
+
this method is useful for methods or functions that don't return anything and/or
|
|
524
|
+
if you want to chain multiple do calls
|
|
525
|
+
|
|
526
|
+
"""
|
|
527
|
+
if isinstance(method, str):
|
|
528
|
+
for v in self.groups.values():
|
|
529
|
+
getattr(v, method)(*args, **kwargs)
|
|
530
|
+
else:
|
|
531
|
+
for v in self.groups.values():
|
|
532
|
+
method(v, *args, **kwargs)
|
|
533
|
+
|
|
534
|
+
return self
|
|
535
|
+
|
|
536
|
+
def __iter__(self):
|
|
537
|
+
return iter(self.groups.items())
|
|
360
538
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
# for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__
|
|
539
|
+
def __len__(self):
|
|
540
|
+
return len(self.groups)
|
mesa/batchrunner.py
CHANGED
|
@@ -132,14 +132,14 @@ def _model_run_func(
|
|
|
132
132
|
"""
|
|
133
133
|
run_id, iteration, kwargs = run
|
|
134
134
|
model = model_cls(**kwargs)
|
|
135
|
-
while model.running and model.
|
|
135
|
+
while model.running and model.steps <= max_steps:
|
|
136
136
|
model.step()
|
|
137
137
|
|
|
138
138
|
data = []
|
|
139
139
|
|
|
140
|
-
steps = list(range(0, model.
|
|
141
|
-
if not steps or steps[-1] != model.
|
|
142
|
-
steps.append(model.
|
|
140
|
+
steps = list(range(0, model.steps, data_collection_period))
|
|
141
|
+
if not steps or steps[-1] != model.steps - 1:
|
|
142
|
+
steps.append(model.steps - 1)
|
|
143
143
|
|
|
144
144
|
for step in steps:
|
|
145
145
|
model_data, all_agents_data = _collect_data(model, step)
|
mesa/datacollection.py
CHANGED
|
@@ -180,7 +180,7 @@ class DataCollector:
|
|
|
180
180
|
rep_funcs = self.agent_reporters.values()
|
|
181
181
|
|
|
182
182
|
def get_reports(agent):
|
|
183
|
-
_prefix = (agent.model.
|
|
183
|
+
_prefix = (agent.model.steps, agent.unique_id)
|
|
184
184
|
reports = tuple(rep(agent) for rep in rep_funcs)
|
|
185
185
|
return _prefix + reports
|
|
186
186
|
|
|
@@ -216,7 +216,7 @@ class DataCollector:
|
|
|
216
216
|
|
|
217
217
|
if self.agent_reporters:
|
|
218
218
|
agent_records = self._record_agents(model)
|
|
219
|
-
self._agent_records[model.
|
|
219
|
+
self._agent_records[model.steps] = list(agent_records)
|
|
220
220
|
|
|
221
221
|
def add_table_row(self, table_name, row, ignore_missing=False):
|
|
222
222
|
"""Add a row dictionary to a specific table.
|
|
@@ -9,6 +9,7 @@ from mesa.experimental.cell_space.grid import (
|
|
|
9
9
|
OrthogonalVonNeumannGrid,
|
|
10
10
|
)
|
|
11
11
|
from mesa.experimental.cell_space.network import Network
|
|
12
|
+
from mesa.experimental.cell_space.voronoi import VoronoiGrid
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"CellCollection",
|
|
@@ -20,4 +21,5 @@ __all__ = [
|
|
|
20
21
|
"OrthogonalMooreGrid",
|
|
21
22
|
"OrthogonalVonNeumannGrid",
|
|
22
23
|
"Network",
|
|
24
|
+
"VoronoiGrid",
|
|
23
25
|
]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
from random import Random
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
8
|
+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Delaunay:
|
|
12
|
+
"""
|
|
13
|
+
Class to compute a Delaunay triangulation in 2D
|
|
14
|
+
ref: http://github.com/jmespadero/pyDelaunay2D
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Init and create a new frame to contain the triangulation
|
|
20
|
+
center: Optional position for the center of the frame. Default (0,0)
|
|
21
|
+
radius: Optional distance from corners to the center.
|
|
22
|
+
"""
|
|
23
|
+
center = np.asarray(center)
|
|
24
|
+
# Create coordinates for the corners of the frame
|
|
25
|
+
self.coords = [
|
|
26
|
+
center + radius * np.array((-1, -1)),
|
|
27
|
+
center + radius * np.array((+1, -1)),
|
|
28
|
+
center + radius * np.array((+1, +1)),
|
|
29
|
+
center + radius * np.array((-1, +1)),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Create two dicts to store triangle neighbours and circumcircles.
|
|
33
|
+
self.triangles = {}
|
|
34
|
+
self.circles = {}
|
|
35
|
+
|
|
36
|
+
# Create two CCW triangles for the frame
|
|
37
|
+
triangle1 = (0, 1, 3)
|
|
38
|
+
triangle2 = (2, 3, 1)
|
|
39
|
+
self.triangles[triangle1] = [triangle2, None, None]
|
|
40
|
+
self.triangles[triangle2] = [triangle1, None, None]
|
|
41
|
+
|
|
42
|
+
# Compute circumcenters and circumradius for each triangle
|
|
43
|
+
for t in self.triangles:
|
|
44
|
+
self.circles[t] = self._circumcenter(t)
|
|
45
|
+
|
|
46
|
+
def _circumcenter(self, triangle: list) -> tuple:
|
|
47
|
+
"""
|
|
48
|
+
Compute circumcenter and circumradius of a triangle in 2D.
|
|
49
|
+
"""
|
|
50
|
+
points = np.asarray([self.coords[v] for v in triangle])
|
|
51
|
+
points2 = np.dot(points, points.T)
|
|
52
|
+
a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
|
|
53
|
+
|
|
54
|
+
b = np.hstack((np.sum(points * points, axis=1), [1]))
|
|
55
|
+
x = np.linalg.solve(a, b)
|
|
56
|
+
bary_coords = x[:-1]
|
|
57
|
+
center = np.dot(bary_coords, points)
|
|
58
|
+
|
|
59
|
+
radius = np.sum(np.square(points[0] - center)) # squared distance
|
|
60
|
+
return (center, radius)
|
|
61
|
+
|
|
62
|
+
def _in_circle(self, triangle: list, point: list) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Check if point p is inside of precomputed circumcircle of triangle.
|
|
65
|
+
"""
|
|
66
|
+
center, radius = self.circles[triangle]
|
|
67
|
+
return np.sum(np.square(center - point)) <= radius
|
|
68
|
+
|
|
69
|
+
def add_point(self, point: Sequence) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Add a point to the current DT, and refine it using Bowyer-Watson.
|
|
72
|
+
"""
|
|
73
|
+
point_index = len(self.coords)
|
|
74
|
+
self.coords.append(np.asarray(point))
|
|
75
|
+
|
|
76
|
+
bad_triangles = []
|
|
77
|
+
for triangle in self.triangles:
|
|
78
|
+
if self._in_circle(triangle, point):
|
|
79
|
+
bad_triangles.append(triangle)
|
|
80
|
+
|
|
81
|
+
boundary = []
|
|
82
|
+
triangle = bad_triangles[0]
|
|
83
|
+
edge = 0
|
|
84
|
+
|
|
85
|
+
while True:
|
|
86
|
+
opposite_triangle = self.triangles[triangle][edge]
|
|
87
|
+
if opposite_triangle not in bad_triangles:
|
|
88
|
+
boundary.append(
|
|
89
|
+
(
|
|
90
|
+
triangle[(edge + 1) % 3],
|
|
91
|
+
triangle[(edge - 1) % 3],
|
|
92
|
+
opposite_triangle,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
edge = (edge + 1) % 3
|
|
96
|
+
if boundary[0][0] == boundary[-1][1]:
|
|
97
|
+
break
|
|
98
|
+
else:
|
|
99
|
+
edge = (self.triangles[opposite_triangle].index(triangle) + 1) % 3
|
|
100
|
+
triangle = opposite_triangle
|
|
101
|
+
|
|
102
|
+
for triangle in bad_triangles:
|
|
103
|
+
del self.triangles[triangle]
|
|
104
|
+
del self.circles[triangle]
|
|
105
|
+
|
|
106
|
+
new_triangles = []
|
|
107
|
+
for e0, e1, opposite_triangle in boundary:
|
|
108
|
+
triangle = (point_index, e0, e1)
|
|
109
|
+
self.circles[triangle] = self._circumcenter(triangle)
|
|
110
|
+
self.triangles[triangle] = [opposite_triangle, None, None]
|
|
111
|
+
if opposite_triangle:
|
|
112
|
+
for i, neighbor in enumerate(self.triangles[opposite_triangle]):
|
|
113
|
+
if neighbor and e1 in neighbor and e0 in neighbor:
|
|
114
|
+
self.triangles[opposite_triangle][i] = triangle
|
|
115
|
+
|
|
116
|
+
new_triangles.append(triangle)
|
|
117
|
+
|
|
118
|
+
n = len(new_triangles)
|
|
119
|
+
for i, triangle in enumerate(new_triangles):
|
|
120
|
+
self.triangles[triangle][1] = new_triangles[(i + 1) % n] # next
|
|
121
|
+
self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
|
|
122
|
+
|
|
123
|
+
def export_triangles(self) -> list:
|
|
124
|
+
"""
|
|
125
|
+
Export the current list of Delaunay triangles
|
|
126
|
+
"""
|
|
127
|
+
triangles_list = [
|
|
128
|
+
(a - 4, b - 4, c - 4)
|
|
129
|
+
for (a, b, c) in self.triangles
|
|
130
|
+
if a > 3 and b > 3 and c > 3
|
|
131
|
+
]
|
|
132
|
+
return triangles_list
|
|
133
|
+
|
|
134
|
+
def export_voronoi_regions(self):
|
|
135
|
+
"""
|
|
136
|
+
Export coordinates and regions of Voronoi diagram as indexed data.
|
|
137
|
+
"""
|
|
138
|
+
use_vertex = {i: [] for i in range(len(self.coords))}
|
|
139
|
+
vor_coors = []
|
|
140
|
+
index = {}
|
|
141
|
+
for triangle_index, (a, b, c) in enumerate(sorted(self.triangles)):
|
|
142
|
+
vor_coors.append(self.circles[(a, b, c)][0])
|
|
143
|
+
use_vertex[a] += [(b, c, a)]
|
|
144
|
+
use_vertex[b] += [(c, a, b)]
|
|
145
|
+
use_vertex[c] += [(a, b, c)]
|
|
146
|
+
|
|
147
|
+
index[(a, b, c)] = triangle_index
|
|
148
|
+
index[(c, a, b)] = triangle_index
|
|
149
|
+
index[(b, c, a)] = triangle_index
|
|
150
|
+
|
|
151
|
+
regions = {}
|
|
152
|
+
for i in range(4, len(self.coords)):
|
|
153
|
+
vertex = use_vertex[i][0][0]
|
|
154
|
+
region = []
|
|
155
|
+
for _ in range(len(use_vertex[i])):
|
|
156
|
+
triangle = next(
|
|
157
|
+
triangle for triangle in use_vertex[i] if triangle[0] == vertex
|
|
158
|
+
)
|
|
159
|
+
region.append(index[triangle])
|
|
160
|
+
vertex = triangle[1]
|
|
161
|
+
regions[i - 4] = region
|
|
162
|
+
|
|
163
|
+
return vor_coors, regions
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def round_float(x: float) -> int:
|
|
167
|
+
return int(x * 500)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class VoronoiGrid(DiscreteSpace):
|
|
171
|
+
triangulation: Delaunay
|
|
172
|
+
voronoi_coordinates: list
|
|
173
|
+
regions: list
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
centroids_coordinates: Sequence[Sequence[float]],
|
|
178
|
+
capacity: float | None = None,
|
|
179
|
+
random: Random | None = None,
|
|
180
|
+
cell_klass: type[Cell] = Cell,
|
|
181
|
+
capacity_function: callable = round_float,
|
|
182
|
+
cell_coloring_property: str | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""
|
|
185
|
+
A Voronoi Tessellation Grid.
|
|
186
|
+
|
|
187
|
+
Given a set of points, this class creates a grid where a cell is centered in each point,
|
|
188
|
+
its neighbors are given by Voronoi Tessellation cells neighbors
|
|
189
|
+
and the capacity by the polygon area.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
centroids_coordinates: coordinates of centroids to build the tessellation space
|
|
193
|
+
capacity (int) : capacity of the cells in the discrete space
|
|
194
|
+
random (Random): random number generator
|
|
195
|
+
CellKlass (type[Cell]): type of cell class
|
|
196
|
+
capacity_function (Callable): function to compute (int) capacity according to (float) area
|
|
197
|
+
cell_coloring_property (str): voronoi visualization polygon fill property
|
|
198
|
+
"""
|
|
199
|
+
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
200
|
+
self.centroids_coordinates = centroids_coordinates
|
|
201
|
+
self._validate_parameters()
|
|
202
|
+
|
|
203
|
+
self._cells = {
|
|
204
|
+
i: cell_klass(self.centroids_coordinates[i], capacity, random=self.random)
|
|
205
|
+
for i in range(len(self.centroids_coordinates))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
self.regions = None
|
|
209
|
+
self.triangulation = None
|
|
210
|
+
self.voronoi_coordinates = None
|
|
211
|
+
self.capacity_function = capacity_function
|
|
212
|
+
self.cell_coloring_property = cell_coloring_property
|
|
213
|
+
|
|
214
|
+
self._connect_cells()
|
|
215
|
+
self._build_cell_polygons()
|
|
216
|
+
|
|
217
|
+
def _connect_cells(self) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Connect cells to neighbors based on given centroids and using Delaunay Triangulation
|
|
220
|
+
"""
|
|
221
|
+
self.triangulation = Delaunay()
|
|
222
|
+
for centroid in self.centroids_coordinates:
|
|
223
|
+
self.triangulation.add_point(centroid)
|
|
224
|
+
|
|
225
|
+
for point in self.triangulation.export_triangles():
|
|
226
|
+
for i, j in combinations(point, 2):
|
|
227
|
+
self._cells[i].connect(self._cells[j])
|
|
228
|
+
self._cells[j].connect(self._cells[i])
|
|
229
|
+
|
|
230
|
+
def _validate_parameters(self) -> None:
|
|
231
|
+
if self.capacity is not None and not isinstance(self.capacity, float | int):
|
|
232
|
+
raise ValueError("Capacity must be a number or None.")
|
|
233
|
+
if not isinstance(self.centroids_coordinates, Sequence) or not isinstance(
|
|
234
|
+
self.centroids_coordinates[0], Sequence
|
|
235
|
+
):
|
|
236
|
+
raise ValueError("Centroids should be a list of lists")
|
|
237
|
+
dimension_1 = len(self.centroids_coordinates[0])
|
|
238
|
+
for coordinate in self.centroids_coordinates:
|
|
239
|
+
if dimension_1 != len(coordinate):
|
|
240
|
+
raise ValueError("Centroid coordinates should be a homogeneous array")
|
|
241
|
+
|
|
242
|
+
def _get_voronoi_regions(self) -> tuple:
|
|
243
|
+
if self.voronoi_coordinates is None or self.regions is None:
|
|
244
|
+
self.voronoi_coordinates, self.regions = (
|
|
245
|
+
self.triangulation.export_voronoi_regions()
|
|
246
|
+
)
|
|
247
|
+
return self.voronoi_coordinates, self.regions
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def _compute_polygon_area(polygon: list) -> float:
|
|
251
|
+
polygon = np.array(polygon)
|
|
252
|
+
x = polygon[:, 0]
|
|
253
|
+
y = polygon[:, 1]
|
|
254
|
+
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
|
|
255
|
+
|
|
256
|
+
def _build_cell_polygons(self):
|
|
257
|
+
coordinates, regions = self._get_voronoi_regions()
|
|
258
|
+
for region in regions:
|
|
259
|
+
polygon = [coordinates[i] for i in regions[region]]
|
|
260
|
+
self._cells[region].properties["polygon"] = polygon
|
|
261
|
+
polygon_area = self._compute_polygon_area(polygon)
|
|
262
|
+
self._cells[region].properties["area"] = polygon_area
|
|
263
|
+
self._cells[region].capacity = self.capacity_function(polygon_area)
|
|
264
|
+
self._cells[region].properties[self.cell_coloring_property] = 0
|
mesa/model.py
CHANGED
|
@@ -8,9 +8,8 @@ Core Objects: Model
|
|
|
8
8
|
# Remove this __future__ import once the oldest supported Python is 3.10
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import itertools
|
|
12
11
|
import random
|
|
13
|
-
|
|
12
|
+
import warnings
|
|
14
13
|
|
|
15
14
|
# mypy
|
|
16
15
|
from typing import Any
|
|
@@ -18,8 +17,6 @@ from typing import Any
|
|
|
18
17
|
from mesa.agent import Agent, AgentSet
|
|
19
18
|
from mesa.datacollection import DataCollector
|
|
20
19
|
|
|
21
|
-
TimeT = float | int
|
|
22
|
-
|
|
23
20
|
|
|
24
21
|
class Model:
|
|
25
22
|
"""Base class for models in the Mesa ABM library.
|
|
@@ -32,12 +29,12 @@ class Model:
|
|
|
32
29
|
running: A boolean indicating if the model should continue running.
|
|
33
30
|
schedule: An object to manage the order and execution of agent steps.
|
|
34
31
|
current_id: A counter for assigning unique IDs to agents.
|
|
35
|
-
agents_: A defaultdict mapping each agent type to a dict of its instances.
|
|
36
|
-
This private attribute is used internally to manage agents.
|
|
37
32
|
|
|
38
33
|
Properties:
|
|
39
|
-
agents: An AgentSet containing all agents in the model
|
|
34
|
+
agents: An AgentSet containing all agents in the model
|
|
40
35
|
agent_types: A list of different agent types present in the model.
|
|
36
|
+
steps: An integer representing the number of steps the model has taken.
|
|
37
|
+
It increases automatically at the start of each step() call.
|
|
41
38
|
|
|
42
39
|
Methods:
|
|
43
40
|
get_agents_of_type: Returns an AgentSet of agents of the specified type.
|
|
@@ -46,6 +43,14 @@ class Model:
|
|
|
46
43
|
next_id: Generates and returns the next unique identifier for an agent.
|
|
47
44
|
reset_randomizer: Resets the model's random number generator with a new or existing seed.
|
|
48
45
|
initialize_data_collector: Sets up the data collector for the model, requiring an initialized scheduler and agents.
|
|
46
|
+
register_agent : register an agent with the model
|
|
47
|
+
deregister_agent : remove an agent from the model
|
|
48
|
+
|
|
49
|
+
Notes:
|
|
50
|
+
Model.agents returns the AgentSet containing all agents registered with the model. Changing
|
|
51
|
+
the content of the AgentSet directly can result in strange behavior. If you want change the
|
|
52
|
+
composition of this AgentSet, ensure you operate on a copy.
|
|
53
|
+
|
|
49
54
|
"""
|
|
50
55
|
|
|
51
56
|
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
@@ -57,9 +62,6 @@ class Model:
|
|
|
57
62
|
# advance.
|
|
58
63
|
obj._seed = random.random()
|
|
59
64
|
obj.random = random.Random(obj._seed)
|
|
60
|
-
# TODO: Remove these 2 lines just before Mesa 3.0
|
|
61
|
-
obj._steps = 0
|
|
62
|
-
obj._time = 0
|
|
63
65
|
return obj
|
|
64
66
|
|
|
65
67
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -71,39 +73,107 @@ class Model:
|
|
|
71
73
|
self.running = True
|
|
72
74
|
self.schedule = None
|
|
73
75
|
self.current_id = 0
|
|
74
|
-
self.
|
|
76
|
+
self.steps: int = 0
|
|
77
|
+
|
|
78
|
+
self._setup_agent_registration()
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
self.
|
|
80
|
+
# Wrap the user-defined step method
|
|
81
|
+
self._user_step = self.step
|
|
82
|
+
self.step = self._wrapped_step
|
|
83
|
+
|
|
84
|
+
def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
|
|
85
|
+
"""Automatically increments time and steps after calling the user's step method."""
|
|
86
|
+
# Automatically increment time and step counters
|
|
87
|
+
self.steps += 1
|
|
88
|
+
# Call the original user-defined step method
|
|
89
|
+
self._user_step(*args, **kwargs)
|
|
78
90
|
|
|
79
91
|
@property
|
|
80
92
|
def agents(self) -> AgentSet:
|
|
81
93
|
"""Provides an AgentSet of all agents in the model, combining agents from all types."""
|
|
82
|
-
|
|
83
|
-
if hasattr(self, "_agents"):
|
|
84
|
-
return self._agents
|
|
85
|
-
else:
|
|
86
|
-
all_agents = itertools.chain.from_iterable(self.agents_.values())
|
|
87
|
-
return AgentSet(all_agents, self)
|
|
94
|
+
return self._all_agents
|
|
88
95
|
|
|
89
96
|
@agents.setter
|
|
90
97
|
def agents(self, agents: Any) -> None:
|
|
91
98
|
raise AttributeError(
|
|
92
|
-
"You are trying to set model.agents. In Mesa 3.0 and higher, this attribute
|
|
99
|
+
"You are trying to set model.agents. In Mesa 3.0 and higher, this attribute is "
|
|
93
100
|
"used by Mesa itself, so you cannot use it directly anymore."
|
|
94
101
|
"Please adjust your code to use a different attribute name for custom agent storage."
|
|
95
102
|
)
|
|
96
103
|
|
|
97
|
-
self._agents = agents
|
|
98
|
-
|
|
99
104
|
@property
|
|
100
105
|
def agent_types(self) -> list[type]:
|
|
101
|
-
"""Return a list of
|
|
102
|
-
return list(self.
|
|
106
|
+
"""Return a list of all unique agent types registered with the model."""
|
|
107
|
+
return list(self._agents_by_type.keys())
|
|
103
108
|
|
|
104
109
|
def get_agents_of_type(self, agenttype: type[Agent]) -> AgentSet:
|
|
105
|
-
"""Retrieves an AgentSet containing all agents of the specified type.
|
|
106
|
-
|
|
110
|
+
"""Retrieves an AgentSet containing all agents of the specified type.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
agenttype: The type of agent to retrieve.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
KeyError: If agenttype does not exist
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
return self._agents_by_type[agenttype]
|
|
121
|
+
|
|
122
|
+
def _setup_agent_registration(self):
|
|
123
|
+
"""helper method to initialize the agent registration datastructures"""
|
|
124
|
+
self._agents = {} # the hard references to all agents in the model
|
|
125
|
+
self._agents_by_type: dict[
|
|
126
|
+
type, AgentSet
|
|
127
|
+
] = {} # a dict with an agentset for each class of agents
|
|
128
|
+
self._all_agents = AgentSet([], self) # an agenset with all agents
|
|
129
|
+
|
|
130
|
+
def register_agent(self, agent):
|
|
131
|
+
"""Register the agent with the model
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
agent: The agent to register.
|
|
135
|
+
|
|
136
|
+
Notes:
|
|
137
|
+
This method is called automatically by ``Agent.__init__``, so there is no need to use this
|
|
138
|
+
if you are subclassing Agent and calling its super in the ``__init__`` method.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
if not hasattr(self, "_agents"):
|
|
142
|
+
self._setup_agent_registration()
|
|
143
|
+
|
|
144
|
+
warnings.warn(
|
|
145
|
+
"The Mesa Model class was not initialized. In the future, you need to explicitly initialize "
|
|
146
|
+
"the Model by calling super().__init__() on initialization.",
|
|
147
|
+
FutureWarning,
|
|
148
|
+
stacklevel=2,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
self._agents[agent] = None
|
|
152
|
+
|
|
153
|
+
# because AgentSet requires model, we cannot use defaultdict
|
|
154
|
+
# tricks with a function won't work because model then cannot be pickled
|
|
155
|
+
try:
|
|
156
|
+
self._agents_by_type[type(agent)].add(agent)
|
|
157
|
+
except KeyError:
|
|
158
|
+
self._agents_by_type[type(agent)] = AgentSet(
|
|
159
|
+
[
|
|
160
|
+
agent,
|
|
161
|
+
],
|
|
162
|
+
self,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self._all_agents.add(agent)
|
|
166
|
+
|
|
167
|
+
def deregister_agent(self, agent):
|
|
168
|
+
"""Deregister the agent with the model
|
|
169
|
+
|
|
170
|
+
Notes::
|
|
171
|
+
This method is called automatically by ``Agent.remove``
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
del self._agents[agent]
|
|
175
|
+
self._agents_by_type[type(agent)].remove(agent)
|
|
176
|
+
self._all_agents.remove(agent)
|
|
107
177
|
|
|
108
178
|
def run_model(self) -> None:
|
|
109
179
|
"""Run the model until the end condition is reached. Overload as
|
|
@@ -115,11 +185,6 @@ class Model:
|
|
|
115
185
|
def step(self) -> None:
|
|
116
186
|
"""A single step. Fill in here."""
|
|
117
187
|
|
|
118
|
-
def _advance_time(self, deltat: TimeT = 1):
|
|
119
|
-
"""Increment the model's steps counter and clock."""
|
|
120
|
-
self._steps += 1
|
|
121
|
-
self._time += deltat
|
|
122
|
-
|
|
123
188
|
def next_id(self) -> int:
|
|
124
189
|
"""Return the next unique ID for agents, increment current_id"""
|
|
125
190
|
self.current_id += 1
|
mesa/time.py
CHANGED
|
@@ -69,8 +69,6 @@ class BaseScheduler:
|
|
|
69
69
|
self.model = model
|
|
70
70
|
self.steps = 0
|
|
71
71
|
self.time: TimeT = 0
|
|
72
|
-
self._original_step = self.step
|
|
73
|
-
self.step = self._wrapped_step
|
|
74
72
|
|
|
75
73
|
if agents is None:
|
|
76
74
|
agents = []
|
|
@@ -113,11 +111,6 @@ class BaseScheduler:
|
|
|
113
111
|
self.steps += 1
|
|
114
112
|
self.time += 1
|
|
115
113
|
|
|
116
|
-
def _wrapped_step(self):
|
|
117
|
-
"""Wrapper for the step method to include time and step updating."""
|
|
118
|
-
self._original_step()
|
|
119
|
-
self.model._advance_time()
|
|
120
|
-
|
|
121
114
|
def get_agent_count(self) -> int:
|
|
122
115
|
"""Returns the current number of agents in the queue."""
|
|
123
116
|
return len(self._agents)
|
|
@@ -6,6 +6,7 @@ from matplotlib.figure import Figure
|
|
|
6
6
|
from matplotlib.ticker import MaxNLocator
|
|
7
7
|
|
|
8
8
|
import mesa
|
|
9
|
+
from mesa.experimental.cell_space import VoronoiGrid
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@solara.component
|
|
@@ -20,6 +21,8 @@ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = Non
|
|
|
20
21
|
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
21
22
|
elif isinstance(space, mesa.space.ContinuousSpace):
|
|
22
23
|
_draw_continuous_space(space, space_ax, agent_portrayal)
|
|
24
|
+
elif isinstance(space, VoronoiGrid):
|
|
25
|
+
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
23
26
|
else:
|
|
24
27
|
_draw_grid(space, space_ax, agent_portrayal)
|
|
25
28
|
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
|
@@ -150,6 +153,56 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
|
150
153
|
_split_and_scatter(portray(space), space_ax)
|
|
151
154
|
|
|
152
155
|
|
|
156
|
+
def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
157
|
+
def portray(g):
|
|
158
|
+
x = []
|
|
159
|
+
y = []
|
|
160
|
+
s = [] # size
|
|
161
|
+
c = [] # color
|
|
162
|
+
|
|
163
|
+
for cell in g.all_cells:
|
|
164
|
+
for agent in cell.agents:
|
|
165
|
+
data = agent_portrayal(agent)
|
|
166
|
+
x.append(cell.coordinate[0])
|
|
167
|
+
y.append(cell.coordinate[1])
|
|
168
|
+
if "size" in data:
|
|
169
|
+
s.append(data["size"])
|
|
170
|
+
if "color" in data:
|
|
171
|
+
c.append(data["color"])
|
|
172
|
+
out = {"x": x, "y": y}
|
|
173
|
+
# This is the default value for the marker size, which auto-scales
|
|
174
|
+
# according to the grid area.
|
|
175
|
+
out["s"] = s
|
|
176
|
+
if len(c) > 0:
|
|
177
|
+
out["c"] = c
|
|
178
|
+
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
x_list = [i[0] for i in space.centroids_coordinates]
|
|
182
|
+
y_list = [i[1] for i in space.centroids_coordinates]
|
|
183
|
+
x_max = max(x_list)
|
|
184
|
+
x_min = min(x_list)
|
|
185
|
+
y_max = max(y_list)
|
|
186
|
+
y_min = min(y_list)
|
|
187
|
+
|
|
188
|
+
width = x_max - x_min
|
|
189
|
+
x_padding = width / 20
|
|
190
|
+
height = y_max - y_min
|
|
191
|
+
y_padding = height / 20
|
|
192
|
+
space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
|
|
193
|
+
space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
|
|
194
|
+
space_ax.scatter(**portray(space))
|
|
195
|
+
|
|
196
|
+
for cell in space.all_cells:
|
|
197
|
+
polygon = cell.properties["polygon"]
|
|
198
|
+
space_ax.fill(
|
|
199
|
+
*zip(*polygon),
|
|
200
|
+
alpha=min(1, cell.properties[space.cell_coloring_property]),
|
|
201
|
+
c="red",
|
|
202
|
+
) # Plot filled polygon
|
|
203
|
+
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in red
|
|
204
|
+
|
|
205
|
+
|
|
153
206
|
@solara.component
|
|
154
207
|
def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
155
208
|
fig = Figure()
|
mesa/visualization/solara_viz.py
CHANGED
|
@@ -158,7 +158,11 @@ def SolaraViz(
|
|
|
158
158
|
"""Update the random seed for the model."""
|
|
159
159
|
reactive_seed.value = model.random.random()
|
|
160
160
|
|
|
161
|
-
dependencies = [
|
|
161
|
+
dependencies = [
|
|
162
|
+
*list(model_parameters.values()),
|
|
163
|
+
current_step.value,
|
|
164
|
+
reactive_seed.value,
|
|
165
|
+
]
|
|
162
166
|
|
|
163
167
|
# if space drawer is disabled, do not include it
|
|
164
168
|
layout_types = [{"Space": "default"}] if space_drawer else []
|
|
@@ -240,7 +244,7 @@ def ModelController(model, play_interval, current_step, reset_counter):
|
|
|
240
244
|
"""Advance the model by one step."""
|
|
241
245
|
model.step()
|
|
242
246
|
previous_step.value = current_step.value
|
|
243
|
-
current_step.value = model.
|
|
247
|
+
current_step.value = model.steps
|
|
244
248
|
|
|
245
249
|
def do_play():
|
|
246
250
|
"""Run the model continuously."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: Mesa
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0a3
|
|
4
4
|
Summary: Agent-based modeling (ABM) in Python
|
|
5
5
|
Project-URL: homepage, https://github.com/projectmesa/mesa
|
|
6
6
|
Project-URL: repository, https://github.com/projectmesa/mesa
|
|
@@ -42,6 +42,9 @@ Requires-Dist: myst-parser; extra == 'docs'
|
|
|
42
42
|
Requires-Dist: pydata-sphinx-theme; extra == 'docs'
|
|
43
43
|
Requires-Dist: seaborn; extra == 'docs'
|
|
44
44
|
Requires-Dist: sphinx; extra == 'docs'
|
|
45
|
+
Provides-Extra: examples
|
|
46
|
+
Requires-Dist: pytest>=4.6; extra == 'examples'
|
|
47
|
+
Requires-Dist: scipy; extra == 'examples'
|
|
45
48
|
Description-Content-Type: text/markdown
|
|
46
49
|
|
|
47
50
|
# Mesa: Agent-based modeling in Python
|
|
@@ -98,15 +101,18 @@ Or any other (development) branch on this repo or your own fork:
|
|
|
98
101
|
pip install -U -e git+https://github.com/YOUR_FORK/mesa@YOUR_BRANCH#egg=mesa
|
|
99
102
|
```
|
|
100
103
|
|
|
104
|
+
## Resources
|
|
101
105
|
For resources or help on using Mesa, check out the following:
|
|
102
106
|
|
|
103
107
|
- [Intro to Mesa Tutorial](http://mesa.readthedocs.org/en/stable/tutorials/intro_tutorial.html) (An introductory model, the Boltzmann
|
|
104
108
|
Wealth Model, for beginners or those new to Mesa.)
|
|
109
|
+
- [Visualization Tutorial](https://mesa.readthedocs.io/en/stable/tutorials/visualization_tutorial.html) (An introduction into our Solara visualization)
|
|
105
110
|
- [Complexity Explorer Tutorial](https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa) (An advanced-beginner model,
|
|
106
111
|
SugarScape with Traders, with instructional videos)
|
|
107
112
|
- [Mesa Examples](https://github.com/projectmesa/mesa-examples/tree/main/examples) (A repository of seminal ABMs using Mesa and
|
|
108
113
|
examples of employing specific Mesa Features)
|
|
109
114
|
- [Docs](http://mesa.readthedocs.org/) (Mesa's documentation, API and useful snippets)
|
|
115
|
+
- [Development version docs](https://mesa.readthedocs.io/en/latest/) (the latest version docs if you're using a pre-release Mesa version)
|
|
110
116
|
- [Discussions](https://github.com/projectmesa/mesa/discussions) (GitHub threaded discussions about Mesa)
|
|
111
117
|
- [Matrix Chat](https://matrix.to/#/#project-mesa:matrix.org) (Chat Forum via Matrix to talk about Mesa)
|
|
112
118
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
mesa/__init__.py,sha256=
|
|
2
|
-
mesa/agent.py,sha256=
|
|
3
|
-
mesa/batchrunner.py,sha256=
|
|
4
|
-
mesa/datacollection.py,sha256=
|
|
1
|
+
mesa/__init__.py,sha256=fHbxauJqwzvD_Y--JHDC7qj52vdMSltmq1EdOf8963E,618
|
|
2
|
+
mesa/agent.py,sha256=0V10JPHfgKmwduPi8eX6CrtJ2ergcT6Lw8uSLn-IcY8,19647
|
|
3
|
+
mesa/batchrunner.py,sha256=iwsfitWFo-m7dm2hNFB8xFOlnXa43URypbxF-zebnws,6019
|
|
4
|
+
mesa/datacollection.py,sha256=WUpZoFC2ZdLtKZ0oTwZTqraoP_yNx_yQY9pxO0TR8y0,11442
|
|
5
5
|
mesa/main.py,sha256=7MovfNz88VWNnfXP0kcERB6C3GfkVOh0hb0o32hM9LU,1602
|
|
6
|
-
mesa/model.py,sha256=
|
|
6
|
+
mesa/model.py,sha256=kyWzDnnV_RI4Kef_p70Zz5ikxlFD7NeJl1OaS9hjh2w,8049
|
|
7
7
|
mesa/space.py,sha256=zC96qoNjhLmBXY2ZaEQmF7w30WZAtPpEY4mwcet-Dio,62462
|
|
8
|
-
mesa/time.py,sha256=
|
|
8
|
+
mesa/time.py,sha256=53VX0x8zujaq32R6w_aBv1NmLpWO_h5K5BQTaK4mO3Q,14960
|
|
9
9
|
mesa/cookiecutter-mesa/cookiecutter.json,sha256=tBSWli39fOWUXGfiDCTKd92M7uKaBIswXbkOdbUufYY,337
|
|
10
10
|
mesa/cookiecutter-mesa/hooks/post_gen_project.py,sha256=8JoXZKIioRYEWJURC0udj8WS3rg0c4So62sOZSGbrMY,294
|
|
11
11
|
mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md,sha256=Yji4lGY-NtQSnW-oBj0_Jhs-XhCfZA8R1mBBM_IllGs,80
|
|
@@ -14,13 +14,14 @@ mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate,sha256=UtRpLM_Cke
|
|
|
14
14
|
mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate,sha256=Aml4Z6E1yj7E7DtHNSUqnKNRUdkxG9WWtJyW8fkxCng,1870
|
|
16
16
|
mesa/experimental/__init__.py,sha256=MaSRE9cTFIWwMZsbRKfnCiCBkhvtzJdgWlg3Dls7Unw,67
|
|
17
|
-
mesa/experimental/cell_space/__init__.py,sha256=
|
|
17
|
+
mesa/experimental/cell_space/__init__.py,sha256=gXntv_Ie13SIegBumJNaIPcTUmTQLb4LlGd1JxoIn9M,708
|
|
18
18
|
mesa/experimental/cell_space/cell.py,sha256=AUnvVnXWhdgzr0bLKDRDO9c93v22Zkw6W-tWxhEhGdQ,4578
|
|
19
19
|
mesa/experimental/cell_space/cell_agent.py,sha256=G4u9ht4gW9ns1y2L7pFumF3K4HiP6ROuxwrxHZ-mL1M,1107
|
|
20
20
|
mesa/experimental/cell_space/cell_collection.py,sha256=4FmfDEg9LoFiJ0mF_nC8KUt9fCJ7Q21erjWPeBTQ_lw,2293
|
|
21
21
|
mesa/experimental/cell_space/discrete_space.py,sha256=ta__YojsrrhWL4DgMzUqZpSgbeexKMrA6bxlYPJGfK0,1921
|
|
22
22
|
mesa/experimental/cell_space/grid.py,sha256=gYDExuFBMF3OThUkhbXmolQFKBOqTukcibjfgXicP00,6948
|
|
23
23
|
mesa/experimental/cell_space/network.py,sha256=mAaFHBdd4s9kxUWHbViovLW2-pU2yXH0dtY_vF8sCJg,1179
|
|
24
|
+
mesa/experimental/cell_space/voronoi.py,sha256=swwfV1Hfi7wp3XfM-rb9Lq5H0yAPH9zohZnXzU8SiHM,9997
|
|
24
25
|
mesa/experimental/devs/__init__.py,sha256=CWam15vCj-RD_biMyqv4sJfos1fsL823P7MDEGrbwW8,174
|
|
25
26
|
mesa/experimental/devs/eventlist.py,sha256=nyUFNDWnnSPQnrMtj7Qj1PexxKyOwSJuIGBoxtSwVI0,5269
|
|
26
27
|
mesa/experimental/devs/simulator.py,sha256=0SMC7daIOyL2rYfoQOOTaTOYDos0gLeBUbU1Krd42HA,9557
|
|
@@ -28,11 +29,11 @@ mesa/experimental/devs/examples/epstein_civil_violence.py,sha256=KqH9KI-A_BYt7oW
|
|
|
28
29
|
mesa/experimental/devs/examples/wolf_sheep.py,sha256=h5z-eDqMpYeOjrq293N2BcQbs_LDVsgtg9vblXJM7XQ,7697
|
|
29
30
|
mesa/visualization/UserParam.py,sha256=WgnY3Q0padtGqUCaezgYzd6cZ7LziuIQnGKP3DBuHZY,1641
|
|
30
31
|
mesa/visualization/__init__.py,sha256=zsAzEY3-0O9CZUfiUL6p8zCR1mvvL5Sai2WzoiQ2pmY,127
|
|
31
|
-
mesa/visualization/solara_viz.py,sha256=
|
|
32
|
+
mesa/visualization/solara_viz.py,sha256=hT-w4N32x5HUkk7AVYN6Ps_5JWrcaQWiyIo5ZyvYoJA,15256
|
|
32
33
|
mesa/visualization/components/altair.py,sha256=V2CQ-Zr7PeijgWtYBNH3VklGVfrf1ee70XVh0DBBONQ,2366
|
|
33
|
-
mesa/visualization/components/matplotlib.py,sha256=
|
|
34
|
-
mesa-3.0.
|
|
35
|
-
mesa-3.0.
|
|
36
|
-
mesa-3.0.
|
|
37
|
-
mesa-3.0.
|
|
38
|
-
mesa-3.0.
|
|
34
|
+
mesa/visualization/components/matplotlib.py,sha256=J61gHkXd37XyZiNLGF1NaQPOYqDdh9tpSfbOzMqK3dI,7570
|
|
35
|
+
mesa-3.0.0a3.dist-info/METADATA,sha256=r4qETS44Ho4RD37R5yxmUDeTE0LZXfQHrAmT2hRNioI,8288
|
|
36
|
+
mesa-3.0.0a3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
37
|
+
mesa-3.0.0a3.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
|
|
38
|
+
mesa-3.0.0a3.dist-info/licenses/LICENSE,sha256=OGUgret9fRrm8J3pdsPXETIjf0H8puK_Nmy970ZzT78,572
|
|
39
|
+
mesa-3.0.0a3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|