Mesa 3.1.1__py3-none-any.whl → 3.1.3__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/examples/advanced/pd_grid/analysis.ipynb +44 -89
- mesa/examples/advanced/wolf_sheep/agents.py +33 -1
- mesa/examples/basic/boid_flockers/agents.py +26 -38
- mesa/examples/basic/boid_flockers/app.py +6 -1
- mesa/examples/basic/boid_flockers/model.py +30 -37
- mesa/examples/basic/schelling/analysis.ipynb +42 -36
- mesa/examples/basic/schelling/app.py +1 -1
- mesa/examples/basic/schelling/model.py +3 -3
- mesa/experimental/__init__.py +2 -2
- mesa/experimental/cell_space/voronoi.py +1 -4
- mesa/experimental/continuous_space/__init__.py +8 -0
- mesa/experimental/continuous_space/continuous_space.py +273 -0
- mesa/experimental/continuous_space/continuous_space_agents.py +101 -0
- mesa/model.py +1 -1
- mesa/space.py +7 -0
- mesa/visualization/mpl_space_drawing.py +22 -13
- mesa/visualization/solara_viz.py +30 -7
- {mesa-3.1.1.dist-info → mesa-3.1.3.dist-info}/METADATA +5 -2
- {mesa-3.1.1.dist-info → mesa-3.1.3.dist-info}/RECORD +24 -21
- {mesa-3.1.1.dist-info → mesa-3.1.3.dist-info}/WHEEL +1 -1
- {mesa-3.1.1.dist-info → mesa-3.1.3.dist-info}/entry_points.txt +0 -0
- {mesa-3.1.1.dist-info → mesa-3.1.3.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.1.1.dist-info → mesa-3.1.3.dist-info}/licenses/NOTICE +0 -0
|
@@ -5,11 +5,17 @@ A Mesa implementation of Craig Reynolds's Boids flocker model.
|
|
|
5
5
|
Uses numpy arrays to represent vectors.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, os.path.abspath("../../../.."))
|
|
12
|
+
|
|
13
|
+
|
|
8
14
|
import numpy as np
|
|
9
15
|
|
|
10
16
|
from mesa import Model
|
|
11
17
|
from mesa.examples.basic.boid_flockers.agents import Boid
|
|
12
|
-
from mesa.
|
|
18
|
+
from mesa.experimental.continuous_space import ContinuousSpace
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
class BoidFlockers(Model):
|
|
@@ -17,7 +23,7 @@ class BoidFlockers(Model):
|
|
|
17
23
|
|
|
18
24
|
def __init__(
|
|
19
25
|
self,
|
|
20
|
-
|
|
26
|
+
population_size=100,
|
|
21
27
|
width=100,
|
|
22
28
|
height=100,
|
|
23
29
|
speed=1,
|
|
@@ -31,7 +37,7 @@ class BoidFlockers(Model):
|
|
|
31
37
|
"""Create a new Boids Flocking model.
|
|
32
38
|
|
|
33
39
|
Args:
|
|
34
|
-
|
|
40
|
+
population_size: Number of Boids in the simulation (default: 100)
|
|
35
41
|
width: Width of the space (default: 100)
|
|
36
42
|
height: Height of the space (default: 100)
|
|
37
43
|
speed: How fast the Boids move (default: 1)
|
|
@@ -44,48 +50,35 @@ class BoidFlockers(Model):
|
|
|
44
50
|
"""
|
|
45
51
|
super().__init__(seed=seed)
|
|
46
52
|
|
|
47
|
-
# Model Parameters
|
|
48
|
-
self.population = population
|
|
49
|
-
self.vision = vision
|
|
50
|
-
self.speed = speed
|
|
51
|
-
self.separation = separation
|
|
52
|
-
|
|
53
53
|
# Set up the space
|
|
54
|
-
self.space = ContinuousSpace(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
self.space = ContinuousSpace(
|
|
55
|
+
[[0, width], [0, height]],
|
|
56
|
+
torus=True,
|
|
57
|
+
random=self.random,
|
|
58
|
+
n_agents=population_size,
|
|
59
|
+
)
|
|
58
60
|
|
|
59
61
|
# Create and place the Boid agents
|
|
60
|
-
self.
|
|
62
|
+
positions = self.rng.random(size=(population_size, 2)) * self.space.size
|
|
63
|
+
directions = self.rng.uniform(-1, 1, size=(population_size, 2))
|
|
64
|
+
Boid.create_agents(
|
|
65
|
+
self,
|
|
66
|
+
population_size,
|
|
67
|
+
self.space,
|
|
68
|
+
position=positions,
|
|
69
|
+
direction=directions,
|
|
70
|
+
cohere=cohere,
|
|
71
|
+
separate=separate,
|
|
72
|
+
match=match,
|
|
73
|
+
speed=speed,
|
|
74
|
+
vision=vision,
|
|
75
|
+
separation=separation,
|
|
76
|
+
)
|
|
61
77
|
|
|
62
78
|
# For tracking statistics
|
|
63
79
|
self.average_heading = None
|
|
64
80
|
self.update_average_heading()
|
|
65
81
|
|
|
66
|
-
def make_agents(self):
|
|
67
|
-
"""Create and place all Boid agents randomly in the space."""
|
|
68
|
-
for _ in range(self.population):
|
|
69
|
-
# Random position
|
|
70
|
-
x = self.random.random() * self.space.x_max
|
|
71
|
-
y = self.random.random() * self.space.y_max
|
|
72
|
-
pos = np.array((x, y))
|
|
73
|
-
|
|
74
|
-
# Random initial direction
|
|
75
|
-
direction = np.random.random(2) * 2 - 1 # Random vector between -1 and 1
|
|
76
|
-
direction /= np.linalg.norm(direction) # Normalize
|
|
77
|
-
|
|
78
|
-
# Create and place the Boid
|
|
79
|
-
boid = Boid(
|
|
80
|
-
model=self,
|
|
81
|
-
speed=self.speed,
|
|
82
|
-
direction=direction,
|
|
83
|
-
vision=self.vision,
|
|
84
|
-
separation=self.separation,
|
|
85
|
-
**self.factors,
|
|
86
|
-
)
|
|
87
|
-
self.space.place_agent(boid, pos)
|
|
88
|
-
|
|
89
82
|
def update_average_heading(self):
|
|
90
83
|
"""Calculate the average heading (direction) of all Boids."""
|
|
91
84
|
if not self.agents:
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"cell_type": "code",
|
|
20
|
+
"execution_count": null,
|
|
20
21
|
"metadata": {},
|
|
22
|
+
"outputs": [],
|
|
21
23
|
"source": [
|
|
22
24
|
"import matplotlib.pyplot as plt\n",
|
|
23
25
|
"import pandas as pd\n",
|
|
@@ -25,9 +27,7 @@
|
|
|
25
27
|
"%matplotlib inline\n",
|
|
26
28
|
"\n",
|
|
27
29
|
"from model import Schelling"
|
|
28
|
-
]
|
|
29
|
-
"outputs": [],
|
|
30
|
-
"execution_count": null
|
|
30
|
+
]
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"cell_type": "markdown",
|
|
@@ -38,10 +38,14 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"cell_type": "code",
|
|
41
|
+
"execution_count": null,
|
|
41
42
|
"metadata": {},
|
|
42
|
-
"source": "model = Schelling(height=10, width=10, homophily=3, density=0.8, minority_pc=0.2)",
|
|
43
43
|
"outputs": [],
|
|
44
|
-
"
|
|
44
|
+
"source": [
|
|
45
|
+
"schelling_model = Schelling(\n",
|
|
46
|
+
" height=10, width=10, homophily=3, density=0.8, minority_pc=0.2\n",
|
|
47
|
+
")"
|
|
48
|
+
]
|
|
45
49
|
},
|
|
46
50
|
{
|
|
47
51
|
"cell_type": "markdown",
|
|
@@ -52,14 +56,14 @@
|
|
|
52
56
|
},
|
|
53
57
|
{
|
|
54
58
|
"cell_type": "code",
|
|
59
|
+
"execution_count": null,
|
|
55
60
|
"metadata": {},
|
|
56
|
-
"source": [
|
|
57
|
-
"while model.running and model.steps < 100:\n",
|
|
58
|
-
" model.step()\n",
|
|
59
|
-
"print(model.steps) # Show how many steps have actually run"
|
|
60
|
-
],
|
|
61
61
|
"outputs": [],
|
|
62
|
-
"
|
|
62
|
+
"source": [
|
|
63
|
+
"while schelling_model.running and schelling_model.steps < 100:\n",
|
|
64
|
+
" schelling_model.step()\n",
|
|
65
|
+
"print(schelling_model.steps) # Show how many steps have actually run"
|
|
66
|
+
]
|
|
63
67
|
},
|
|
64
68
|
{
|
|
65
69
|
"cell_type": "markdown",
|
|
@@ -70,21 +74,21 @@
|
|
|
70
74
|
},
|
|
71
75
|
{
|
|
72
76
|
"cell_type": "code",
|
|
77
|
+
"execution_count": null,
|
|
73
78
|
"metadata": {},
|
|
74
|
-
"source": [
|
|
75
|
-
"model_out = model.datacollector.get_model_vars_dataframe()"
|
|
76
|
-
],
|
|
77
79
|
"outputs": [],
|
|
78
|
-
"
|
|
80
|
+
"source": [
|
|
81
|
+
"model_out = schelling_model.datacollector.get_model_vars_dataframe()"
|
|
82
|
+
]
|
|
79
83
|
},
|
|
80
84
|
{
|
|
81
85
|
"cell_type": "code",
|
|
86
|
+
"execution_count": null,
|
|
82
87
|
"metadata": {},
|
|
88
|
+
"outputs": [],
|
|
83
89
|
"source": [
|
|
84
90
|
"model_out.head()"
|
|
85
|
-
]
|
|
86
|
-
"outputs": [],
|
|
87
|
-
"execution_count": null
|
|
91
|
+
]
|
|
88
92
|
},
|
|
89
93
|
{
|
|
90
94
|
"cell_type": "markdown",
|
|
@@ -95,12 +99,12 @@
|
|
|
95
99
|
},
|
|
96
100
|
{
|
|
97
101
|
"cell_type": "code",
|
|
102
|
+
"execution_count": null,
|
|
98
103
|
"metadata": {},
|
|
104
|
+
"outputs": [],
|
|
99
105
|
"source": [
|
|
100
106
|
"model_out.happy.plot()"
|
|
101
|
-
]
|
|
102
|
-
"outputs": [],
|
|
103
|
-
"execution_count": null
|
|
107
|
+
]
|
|
104
108
|
},
|
|
105
109
|
{
|
|
106
110
|
"cell_type": "markdown",
|
|
@@ -115,10 +119,12 @@
|
|
|
115
119
|
},
|
|
116
120
|
{
|
|
117
121
|
"cell_type": "code",
|
|
122
|
+
"execution_count": null,
|
|
118
123
|
"metadata": {},
|
|
119
|
-
"source": "from mesa.batchrunner import batch_run",
|
|
120
124
|
"outputs": [],
|
|
121
|
-
"
|
|
125
|
+
"source": [
|
|
126
|
+
"from mesa.batchrunner import batch_run"
|
|
127
|
+
]
|
|
122
128
|
},
|
|
123
129
|
{
|
|
124
130
|
"cell_type": "markdown",
|
|
@@ -129,18 +135,20 @@
|
|
|
129
135
|
},
|
|
130
136
|
{
|
|
131
137
|
"cell_type": "code",
|
|
138
|
+
"execution_count": null,
|
|
132
139
|
"metadata": {},
|
|
140
|
+
"outputs": [],
|
|
133
141
|
"source": [
|
|
134
142
|
"fixed_params = {\"height\": 10, \"width\": 10, \"density\": 0.8, \"minority_pc\": 0.2}\n",
|
|
135
143
|
"variable_parms = {\"homophily\": range(1, 9)}\n",
|
|
136
144
|
"all_params = fixed_params | variable_parms"
|
|
137
|
-
]
|
|
138
|
-
"outputs": [],
|
|
139
|
-
"execution_count": null
|
|
145
|
+
]
|
|
140
146
|
},
|
|
141
147
|
{
|
|
142
148
|
"cell_type": "code",
|
|
149
|
+
"execution_count": null,
|
|
143
150
|
"metadata": {},
|
|
151
|
+
"outputs": [],
|
|
144
152
|
"source": [
|
|
145
153
|
"results = batch_run(\n",
|
|
146
154
|
" Schelling,\n",
|
|
@@ -148,23 +156,23 @@
|
|
|
148
156
|
" iterations=10,\n",
|
|
149
157
|
" max_steps=200,\n",
|
|
150
158
|
")"
|
|
151
|
-
]
|
|
152
|
-
"outputs": [],
|
|
153
|
-
"execution_count": null
|
|
159
|
+
]
|
|
154
160
|
},
|
|
155
161
|
{
|
|
156
|
-
"metadata": {},
|
|
157
162
|
"cell_type": "code",
|
|
163
|
+
"execution_count": null,
|
|
164
|
+
"metadata": {},
|
|
165
|
+
"outputs": [],
|
|
158
166
|
"source": [
|
|
159
167
|
"df = pd.DataFrame(results)\n",
|
|
160
168
|
"df"
|
|
161
|
-
]
|
|
162
|
-
"outputs": [],
|
|
163
|
-
"execution_count": null
|
|
169
|
+
]
|
|
164
170
|
},
|
|
165
171
|
{
|
|
166
172
|
"cell_type": "code",
|
|
173
|
+
"execution_count": null,
|
|
167
174
|
"metadata": {},
|
|
175
|
+
"outputs": [],
|
|
168
176
|
"source": [
|
|
169
177
|
"plt.scatter(df.homophily, df.happy)\n",
|
|
170
178
|
"plt.xlabel(\"Homophily\")\n",
|
|
@@ -172,9 +180,7 @@
|
|
|
172
180
|
"plt.grid()\n",
|
|
173
181
|
"plt.title(\"Effect of Homophily on segregation\")\n",
|
|
174
182
|
"plt.show()"
|
|
175
|
-
]
|
|
176
|
-
"outputs": [],
|
|
177
|
-
"execution_count": null
|
|
183
|
+
]
|
|
178
184
|
}
|
|
179
185
|
],
|
|
180
186
|
"metadata": {
|
|
@@ -26,7 +26,7 @@ model_params = {
|
|
|
26
26
|
},
|
|
27
27
|
"density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1),
|
|
28
28
|
"minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05),
|
|
29
|
-
"homophily": Slider("Homophily", 0.
|
|
29
|
+
"homophily": Slider("Homophily", 0.4, 0.0, 1.0, 0.125),
|
|
30
30
|
"width": 20,
|
|
31
31
|
"height": 20,
|
|
32
32
|
}
|
|
@@ -9,11 +9,11 @@ class Schelling(Model):
|
|
|
9
9
|
|
|
10
10
|
def __init__(
|
|
11
11
|
self,
|
|
12
|
-
height: int =
|
|
13
|
-
width: int =
|
|
12
|
+
height: int = 20,
|
|
13
|
+
width: int = 20,
|
|
14
14
|
density: float = 0.8,
|
|
15
15
|
minority_pc: float = 0.5,
|
|
16
|
-
homophily:
|
|
16
|
+
homophily: float = 0.4,
|
|
17
17
|
radius: int = 1,
|
|
18
18
|
seed=None,
|
|
19
19
|
):
|
mesa/experimental/__init__.py
CHANGED
|
@@ -15,6 +15,6 @@ Notes:
|
|
|
15
15
|
- Features graduate from experimental status once their APIs are stabilized
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from mesa.experimental import cell_space, devs, mesa_signals
|
|
18
|
+
from mesa.experimental import cell_space, continuous_space, devs, mesa_signals
|
|
19
19
|
|
|
20
|
-
__all__ = ["cell_space", "devs", "mesa_signals"]
|
|
20
|
+
__all__ = ["cell_space", "continuous_space", "devs", "mesa_signals"]
|
|
@@ -186,7 +186,6 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
186
186
|
random: Random | None = None,
|
|
187
187
|
cell_klass: type[Cell] = Cell,
|
|
188
188
|
capacity_function: callable = round_float,
|
|
189
|
-
cell_coloring_property: str | None = None,
|
|
190
189
|
) -> None:
|
|
191
190
|
"""A Voronoi Tessellation Grid.
|
|
192
191
|
|
|
@@ -200,7 +199,7 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
200
199
|
random (Random): random number generator
|
|
201
200
|
cell_klass (type[Cell]): type of cell class
|
|
202
201
|
capacity_function (Callable): function to compute (int) capacity according to (float) area
|
|
203
|
-
|
|
202
|
+
|
|
204
203
|
"""
|
|
205
204
|
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
206
205
|
self.centroids_coordinates = centroids_coordinates
|
|
@@ -215,7 +214,6 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
215
214
|
self.triangulation = None
|
|
216
215
|
self.voronoi_coordinates = None
|
|
217
216
|
self.capacity_function = capacity_function
|
|
218
|
-
self.cell_coloring_property = cell_coloring_property
|
|
219
217
|
|
|
220
218
|
self._connect_cells()
|
|
221
219
|
self._build_cell_polygons()
|
|
@@ -266,4 +264,3 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
266
264
|
polygon_area = self._compute_polygon_area(polygon)
|
|
267
265
|
self._cells[region].properties["area"] = polygon_area
|
|
268
266
|
self._cells[region].capacity = self.capacity_function(polygon_area)
|
|
269
|
-
self._cells[region].properties[self.cell_coloring_property] = 0
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Continuous space support."""
|
|
2
|
+
|
|
3
|
+
from mesa.experimental.continuous_space.continuous_space import ContinuousSpace
|
|
4
|
+
from mesa.experimental.continuous_space.continuous_space_agents import (
|
|
5
|
+
ContinuousSpaceAgent,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = ["ContinuousSpace", "ContinuousSpaceAgent"]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""A Continuous Space class."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from itertools import compress
|
|
6
|
+
from random import Random
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import ArrayLike
|
|
10
|
+
from scipy.spatial.distance import cdist
|
|
11
|
+
|
|
12
|
+
from mesa.agent import Agent, AgentSet
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContinuousSpace:
|
|
16
|
+
"""Continuous space where each agent can have an arbitrary position."""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def x_min(self): # noqa: D102
|
|
20
|
+
# compatibility with solara_viz
|
|
21
|
+
return self.dimensions[0, 0]
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def x_max(self): # noqa: D102
|
|
25
|
+
# compatibility with solara_viz
|
|
26
|
+
return self.dimensions[0, 1]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def y_min(self): # noqa: D102
|
|
30
|
+
# compatibility with solara_viz
|
|
31
|
+
return self.dimensions[1, 0]
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def y_max(self): # noqa: D102
|
|
35
|
+
# compatibility with solara_viz
|
|
36
|
+
return self.dimensions[1, 1]
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def width(self): # noqa: D102
|
|
40
|
+
# compatibility with solara_viz
|
|
41
|
+
return self.size[0]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def height(self): # noqa: D102
|
|
45
|
+
# compatibility with solara_viz
|
|
46
|
+
return self.size[1]
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
dimensions: ArrayLike,
|
|
51
|
+
torus: bool = False,
|
|
52
|
+
random: Random | None = None,
|
|
53
|
+
n_agents: int = 100,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Create a new continuous space.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
dimensions: a numpy array like object where each row specifies the minimum and maximum value of that dimension.
|
|
59
|
+
torus: boolean for whether the space wraps around or not
|
|
60
|
+
random: a seeded stdlib random.Random instance
|
|
61
|
+
n_agents: the expected number of agents in the space
|
|
62
|
+
|
|
63
|
+
Internally, a numpy array is used to store the positions of all agents. This is resized if needed,
|
|
64
|
+
but you can control the initial size explicitly by passing n_agents.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
if random is None:
|
|
69
|
+
warnings.warn(
|
|
70
|
+
"Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly",
|
|
71
|
+
UserWarning,
|
|
72
|
+
stacklevel=2,
|
|
73
|
+
)
|
|
74
|
+
random = Random()
|
|
75
|
+
self.random = random
|
|
76
|
+
|
|
77
|
+
self.dimensions: np.array = np.asanyarray(dimensions)
|
|
78
|
+
self.ndims: int = self.dimensions.shape[0]
|
|
79
|
+
self.size: np.array = self.dimensions[:, 1] - self.dimensions[:, 0]
|
|
80
|
+
self.center: np.array = np.sum(self.dimensions, axis=1) / 2
|
|
81
|
+
|
|
82
|
+
self.torus: bool = torus
|
|
83
|
+
|
|
84
|
+
# self._agent_positions is the array containing all agent positions
|
|
85
|
+
# plus potential extra empty rows
|
|
86
|
+
# agent_positions is a view into _agent_positions containing only the filled rows
|
|
87
|
+
self._agent_positions: np.array = np.empty(
|
|
88
|
+
(n_agents, self.dimensions.shape[0]), dtype=float
|
|
89
|
+
)
|
|
90
|
+
self.agent_positions: (
|
|
91
|
+
np.array
|
|
92
|
+
) # a view on _agent_positions containing all active positions
|
|
93
|
+
|
|
94
|
+
# the list of agents in the space
|
|
95
|
+
self.active_agents = []
|
|
96
|
+
self._n_agents = 0 # the number of active agents in the space
|
|
97
|
+
|
|
98
|
+
# a mapping from agents to index and vice versa
|
|
99
|
+
self._index_to_agent: dict[int, Agent] = {}
|
|
100
|
+
self._agent_to_index: dict[Agent, int | None] = {}
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def agents(self) -> AgentSet:
|
|
104
|
+
"""Return an AgentSet with the agents in the space."""
|
|
105
|
+
return AgentSet(self.active_agents, random=self.random)
|
|
106
|
+
|
|
107
|
+
def _add_agent(self, agent: Agent) -> int:
|
|
108
|
+
"""Helper method for adding an agent to the space.
|
|
109
|
+
|
|
110
|
+
This method manages the numpy array with the agent positions and ensuring it is
|
|
111
|
+
enlarged if and when needed. It is called automatically by ContinousSpaceAgent when created.
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
index = self._n_agents
|
|
115
|
+
self._n_agents += 1
|
|
116
|
+
|
|
117
|
+
if self._agent_positions.shape[0] <= index:
|
|
118
|
+
# we are out of space
|
|
119
|
+
fraction = 0.2 # we add 20% Fixme
|
|
120
|
+
n = int(round(fraction * self._n_agents))
|
|
121
|
+
self._agent_positions = np.vstack(
|
|
122
|
+
[
|
|
123
|
+
self._agent_positions,
|
|
124
|
+
np.empty(
|
|
125
|
+
(n, self.dimensions.shape[0]),
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
self._agent_to_index[agent] = index
|
|
131
|
+
self._index_to_agent[index] = agent
|
|
132
|
+
|
|
133
|
+
# we want to maintain a view rather than a copy on the active agents and positions
|
|
134
|
+
# this is essential for the performance of the rest of this code
|
|
135
|
+
self.active_agents.append(agent)
|
|
136
|
+
self.agent_positions = self._agent_positions[0 : self._n_agents]
|
|
137
|
+
|
|
138
|
+
return index
|
|
139
|
+
|
|
140
|
+
def _remove_agent(self, agent: Agent) -> None:
|
|
141
|
+
"""Remove an agent from the space.
|
|
142
|
+
|
|
143
|
+
This method is automatically called by ContinuousSpaceAgent.remove.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
index = self._agent_to_index[agent]
|
|
147
|
+
self._agent_to_index.pop(agent, None)
|
|
148
|
+
self._index_to_agent.pop(index, None)
|
|
149
|
+
del self.active_agents[index]
|
|
150
|
+
|
|
151
|
+
# we update all indices
|
|
152
|
+
for agent in self.active_agents[index::]:
|
|
153
|
+
old_index = self._agent_to_index[agent]
|
|
154
|
+
self._agent_to_index[agent] = old_index - 1
|
|
155
|
+
self._index_to_agent[old_index - 1] = agent
|
|
156
|
+
|
|
157
|
+
# we move all data below the removed agent one row up
|
|
158
|
+
self._agent_positions[index : self._n_agents - 1] = self._agent_positions[
|
|
159
|
+
index + 1 : self._n_agents
|
|
160
|
+
]
|
|
161
|
+
self._n_agents -= 1
|
|
162
|
+
self.agent_positions = self._agent_positions[0 : self._n_agents]
|
|
163
|
+
|
|
164
|
+
def calculate_difference_vector(self, point: np.ndarray, agents=None) -> np.ndarray:
|
|
165
|
+
"""Calculate the difference vector between the point and all agenents.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
point: the point to calculate the difference vector for
|
|
169
|
+
agents: the agents to calculate the difference vector of point with. By default,
|
|
170
|
+
all agents are considered.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
point = np.asanyarray(point)
|
|
175
|
+
positions = (
|
|
176
|
+
self.agent_positions
|
|
177
|
+
if agents is None
|
|
178
|
+
else self._agent_positions[[self._agent_to_index[a] for a in agents]]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
delta = positions - point
|
|
182
|
+
|
|
183
|
+
if self.torus:
|
|
184
|
+
inverse_delta = delta - np.sign(delta) * self.size
|
|
185
|
+
|
|
186
|
+
# we need to use the lowest absolute value from delta and inverse delta
|
|
187
|
+
logical = np.abs(delta) < np.abs(inverse_delta)
|
|
188
|
+
|
|
189
|
+
out = np.zeros(delta.shape)
|
|
190
|
+
out[logical] = delta[logical]
|
|
191
|
+
out[~logical] = inverse_delta[~logical]
|
|
192
|
+
|
|
193
|
+
delta = out
|
|
194
|
+
|
|
195
|
+
return delta
|
|
196
|
+
|
|
197
|
+
def calculate_distances(
|
|
198
|
+
self, point: ArrayLike, agents: Iterable[Agent] | None = None, **kwargs
|
|
199
|
+
) -> tuple[np.ndarray, list]:
|
|
200
|
+
"""Calculate the distance between the point and all agents.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
point: the point to calculate the difference vector for
|
|
204
|
+
agents: the agents to calculate the difference vector of point with. By default,
|
|
205
|
+
all agents are considered.
|
|
206
|
+
kwargs: any additional keyword arguments are passed to scipy's cdist, which is used
|
|
207
|
+
only if torus is False. This allows for non-Euclidian distance measures.
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
point = np.asanyarray(point)
|
|
211
|
+
|
|
212
|
+
if agents is None:
|
|
213
|
+
positions = self.agent_positions
|
|
214
|
+
agents = self.active_agents
|
|
215
|
+
else:
|
|
216
|
+
positions = self._agent_positions[[self._agent_to_index[a] for a in agents]]
|
|
217
|
+
agents = np.asarray(agents)
|
|
218
|
+
|
|
219
|
+
if self.torus:
|
|
220
|
+
delta = np.abs(point - positions)
|
|
221
|
+
delta = np.minimum(delta, self.size - delta, out=delta)
|
|
222
|
+
|
|
223
|
+
# + is much faster than np.sum or array.sum
|
|
224
|
+
dists = delta[:, 0] ** 2
|
|
225
|
+
for i in range(1, self.ndims):
|
|
226
|
+
dists += delta[:, i] ** 2
|
|
227
|
+
dists = np.sqrt(dists)
|
|
228
|
+
else:
|
|
229
|
+
dists = cdist(point[np.newaxis, :], positions, **kwargs)[0, :]
|
|
230
|
+
return dists, agents
|
|
231
|
+
|
|
232
|
+
def get_agents_in_radius(
|
|
233
|
+
self, point: ArrayLike, radius: float | int = 1
|
|
234
|
+
) -> tuple[list, np.ndarray]:
|
|
235
|
+
"""Return the agents and their distances within a radius for the point."""
|
|
236
|
+
distances, agents = self.calculate_distances(point)
|
|
237
|
+
logical = distances <= radius
|
|
238
|
+
agents = list(compress(agents, logical))
|
|
239
|
+
return (
|
|
240
|
+
agents,
|
|
241
|
+
distances[logical],
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def get_k_nearest_agents(
|
|
245
|
+
self, point: ArrayLike, k: int = 1
|
|
246
|
+
) -> tuple[list, np.ndarray]:
|
|
247
|
+
"""Return the k nearest agents and their distances to the point.
|
|
248
|
+
|
|
249
|
+
Notes:
|
|
250
|
+
This method returns exactly k agents, ignoring ties. In case of ties, the
|
|
251
|
+
earlier an agent is inserted the higher it will rank.
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
dists, agents = self.calculate_distances(point)
|
|
255
|
+
|
|
256
|
+
indices = np.argpartition(dists, k)[:k]
|
|
257
|
+
agents = [agents[i] for i in indices]
|
|
258
|
+
return agents, dists[indices]
|
|
259
|
+
|
|
260
|
+
def in_bounds(self, point: ArrayLike) -> bool:
|
|
261
|
+
"""Check if point is inside the bounds of the space."""
|
|
262
|
+
return bool(
|
|
263
|
+
(
|
|
264
|
+
(np.asanyarray(point) >= self.dimensions[:, 0])
|
|
265
|
+
& (point <= self.dimensions[:, 1])
|
|
266
|
+
).all()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def torus_correct(self, point: ArrayLike) -> np.ndarray:
|
|
270
|
+
"""Apply a torus correction to the point."""
|
|
271
|
+
return self.dimensions[:, 0] + np.mod(
|
|
272
|
+
np.asanyarray(point) - self.dimensions[:, 0], self.size
|
|
273
|
+
)
|