Skip to content

Commit 1d7b3ed

Browse files
examples: Adding Sugarscape IG - polars with numba vectorized loop (#91)
* adding numba to project.toml * splitting agent_type * splitting antpolars and antpolarsloop * adding py-spy to dev tools * splitting between numba, loop with DF and loop non vectorized implementations * performance comparison between loop and numba * fix: cuda target, not gpu * adding polars comparison * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * correcting dtypes * adding writable_args (see https://numba.readthedocs.io/en/stable/user/vectorize.html#overwriting-input-values) * processed_agents is of type bool * fix: occupied cells sorted by agent_order, assertion to verify best moves aren't duplicated * adding fixed initial positions (to assert equality between simulations) * adding complete reproducibility via seed * fix: taking into account potential (max) sugar when creating the neighborhood. For both numba and completely vectorized it's easier to reason this way then update the current sugar and "best moves" ranking when agents move * fix: considering priority (if there are previous order agents that might make the same move and haven't found the optimal move yet). This avoids race conditions. * fix: formatting * clarifying with comments * updating actual sugar before executing the step (NOTE: might be unncessary since we prepare the neighborhood looking at potential/max sugar anyway) * whitespace fixes * adding initial_positions and seed to mesa model * renaming grid to space for mesa_models * fix: best_moves only uses neighborhood and not agent_order * adding documentation and type hints * fix: logic for the priority condition (right order of parentheses) * removing assertion (testing purposes only) * adding equality_check on model state * changing n_range to reflect million of agents * fix: changing callable to typing.Callable * removing extra requirement * removing flame_graph (it was just a one-off, we can add memory-profiling in the future) * fix: comparing DFs * removing outdated picture * adding mesa_comparison.png * adding polars_cocmparison.png * updating kernels for comparison --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 04714fa commit 1d7b3ed

File tree

11 files changed

+521
-86
lines changed

11 files changed

+521
-86
lines changed

docs/general/user-guide/4_benchmarks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ mesa-frames offers significant performance improvements over the original mesa f
1818

1919
[View the benchmark script](https://github.com/projectmesa/mesa-frames/blob/main/examples/sugarscape_ig/performance_comparison.py)
2020

21-
![Performance Graph SS IG](https://github.com/projectmesa/mesa-frames/raw/main/examples/sugarscape_ig/benchmark_plot_0.png)
21+
![Performance Graph SS IG](https://github.com/projectmesa/mesa-frames/raw/main/examples/sugarscape_ig/mesa_comparison.png)
-30.5 KB
Binary file not shown.
-31.3 KB
Binary file not shown.
31 KB
Loading

examples/sugarscape_ig/performance_comparison.py

Lines changed: 180 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@
33
import matplotlib.pyplot as plt
44
import numpy as np
55
import perfplot
6+
import polars as pl
7+
import seaborn as sns
8+
from polars.testing import assert_frame_equal
69
from ss_mesa.model import SugarscapeMesa
710
from ss_pandas.model import SugarscapePandas
11+
from ss_polars.agents import (
12+
AntPolarsLoopDF,
13+
AntPolarsLoopNoVec,
14+
AntPolarsNumbaCPU,
15+
AntPolarsNumbaGPU,
16+
AntPolarsNumbaParallel,
17+
)
818
from ss_polars.model import SugarscapePolars
9-
19+
from typing_extensions import Callable
1020

1121
class SugarScapeSetup:
1222
def __init__(self, n: int):
@@ -15,39 +25,152 @@ def __init__(self, n: int):
1525
else:
1626
density = 0.04 # mesa
1727
self.n = n
28+
self.seed = 42
1829
dimension = math.ceil(math.sqrt(n / density))
19-
self.sugar_grid = np.random.randint(0, 4, (dimension, dimension))
20-
self.initial_sugar = np.random.randint(6, 25, n)
21-
self.metabolism = np.random.randint(2, 4, n)
22-
self.vision = np.random.randint(1, 6, n)
30+
random_gen = np.random.default_rng(self.seed)
31+
self.sugar_grid = random_gen.integers(0, 4, (dimension, dimension))
32+
self.initial_sugar = random_gen.integers(6, 25, n)
33+
self.metabolism = random_gen.integers(2, 4, n)
34+
self.vision = random_gen.integers(1, 6, n)
35+
self.initial_positions = pl.DataFrame(
36+
schema={"dim_0": pl.Int64, "dim_1": pl.Int64}
37+
)
38+
while self.initial_positions.shape[0] < n:
39+
initial_pos_0 = random_gen.integers(
40+
0, dimension, n - self.initial_positions.shape[0]
41+
)
42+
initial_pos_1 = random_gen.integers(
43+
0, dimension, n - self.initial_positions.shape[0]
44+
)
45+
self.initial_positions = self.initial_positions.vstack(
46+
pl.DataFrame(
47+
{
48+
"dim_0": initial_pos_0,
49+
"dim_1": initial_pos_1,
50+
}
51+
)
52+
).unique(maintain_order=True)
53+
return
2354

2455

2556
def mesa_implementation(setup: SugarScapeSetup):
26-
return SugarscapeMesa(
27-
setup.n, setup.sugar_grid, setup.initial_sugar, setup.metabolism, setup.vision
28-
).run_model(100)
57+
model = SugarscapeMesa(
58+
setup.n,
59+
setup.sugar_grid,
60+
setup.initial_sugar,
61+
setup.metabolism,
62+
setup.vision,
63+
setup.initial_positions,
64+
setup.seed,
65+
)
66+
model.run_model(100)
67+
return model
2968

3069

3170
def mesa_frames_pandas_concise(setup: SugarScapeSetup):
32-
return SugarscapePandas(
33-
setup.n, setup.sugar_grid, setup.initial_sugar, setup.metabolism, setup.vision
34-
).run_model(100)
35-
36-
37-
def mesa_frames_polars_concise(setup: SugarScapeSetup):
38-
return SugarscapePolars(
39-
setup.n, setup.sugar_grid, setup.initial_sugar, setup.metabolism, setup.vision
40-
).run_model(100)
41-
42-
43-
def plot_and_print_benchmark(labels, kernels, n_range, title, image_path):
71+
model = SugarscapePandas(
72+
setup.n,
73+
setup.sugar_grid,
74+
setup.initial_sugar,
75+
setup.metabolism,
76+
setup.vision,
77+
setup.initial_positions,
78+
setup.seed,
79+
)
80+
model.run_model(100)
81+
return model
82+
83+
84+
def mesa_frames_polars_loop_DF(setup: SugarScapeSetup):
85+
model = SugarscapePolars(
86+
AntPolarsLoopDF,
87+
setup.n,
88+
setup.sugar_grid,
89+
setup.initial_sugar,
90+
setup.metabolism,
91+
setup.vision,
92+
setup.initial_positions,
93+
setup.seed,
94+
)
95+
model.run_model(100)
96+
return model
97+
98+
99+
def mesa_frames_polars_loop_no_vec(setup: SugarScapeSetup):
100+
model = SugarscapePolars(
101+
AntPolarsLoopNoVec,
102+
setup.n,
103+
setup.sugar_grid,
104+
setup.initial_sugar,
105+
setup.metabolism,
106+
setup.vision,
107+
setup.initial_positions,
108+
setup.seed,
109+
)
110+
model.run_model(100)
111+
return model
112+
113+
114+
def mesa_frames_polars_numba_cpu(setup: SugarScapeSetup):
115+
model = SugarscapePolars(
116+
AntPolarsNumbaCPU,
117+
setup.n,
118+
setup.sugar_grid,
119+
setup.initial_sugar,
120+
setup.metabolism,
121+
setup.vision,
122+
setup.initial_positions,
123+
setup.seed,
124+
)
125+
model.run_model(100)
126+
return model
127+
128+
129+
def mesa_frames_polars_numba_gpu(setup: SugarScapeSetup):
130+
model = SugarscapePolars(
131+
AntPolarsNumbaGPU,
132+
setup.n,
133+
setup.sugar_grid,
134+
setup.initial_sugar,
135+
setup.metabolism,
136+
setup.vision,
137+
setup.initial_positions,
138+
setup.seed,
139+
)
140+
model.run_model(100)
141+
return model
142+
143+
144+
def mesa_frames_polars_numba_parallel(setup: SugarScapeSetup):
145+
model = SugarscapePolars(
146+
AntPolarsNumbaParallel,
147+
setup.n,
148+
setup.sugar_grid,
149+
setup.initial_sugar,
150+
setup.metabolism,
151+
setup.vision,
152+
setup.initial_positions,
153+
setup.seed,
154+
)
155+
model.run_model(100)
156+
return model
157+
158+
159+
def plot_and_print_benchmark(
160+
labels: list[str],
161+
kernels: list[Callable],
162+
n_range: list[int],
163+
title: str,
164+
image_path: str,
165+
equality_check: Callable | None = None,
166+
):
44167
out = perfplot.bench(
45168
setup=SugarScapeSetup,
46169
kernels=kernels,
47170
labels=labels,
48171
n_range=n_range,
49172
xlabel="Number of agents",
50-
equality_check=None,
173+
equality_check=equality_check,
51174
title=title,
52175
)
53176
plt.ylabel("Execution time (s)")
@@ -60,37 +183,58 @@ def plot_and_print_benchmark(labels, kernels, n_range, title, image_path):
60183
print("---------------")
61184

62185

186+
def polars_equality_check(a: SugarscapePolars, b: SugarscapePolars):
187+
assert_frame_equal(a.space.agents, b.space.agents, check_row_order=False)
188+
assert_frame_equal(a.space.cells, b.space.cells, check_row_order=False)
189+
return True
190+
191+
63192
def main():
64-
"""# Mesa comparison
193+
# Mesa comparison
65194
sns.set_theme(style="whitegrid")
66195
labels_0 = [
196+
# "mesa-frames (pd concise)", # Pandas to be removed because of performance
197+
"mesa-frames (pl numba parallel)",
67198
"mesa",
68-
# "mesa-frames (pd concise)",
69-
"mesa-frames (pl concise)",
70199
]
71200
kernels_0 = [
72-
mesa_implementation,
73201
# mesa_frames_pandas_concise,
74-
mesa_frames_polars_concise,
202+
mesa_frames_polars_numba_parallel,
203+
mesa_implementation,
75204
]
76-
n_range_0 = [k for k in range(1, 100002, 10000)]
205+
n_range_0 = [k for k in range(10**5, 5*10**5 + 2, 10**5)]
77206
title_0 = "100 steps of the SugarScape IG model:\n" + " vs ".join(labels_0)
78-
image_path_0 = "benchmark_plot_0.png"
79-
plot_and_print_benchmark(labels_0, kernels_0, n_range_0, title_0, image_path_0)"""
207+
image_path_0 = "mesa_comparison.png"
208+
plot_and_print_benchmark(labels_0, kernels_0, n_range_0, title_0, image_path_0)
80209

81-
# FLAME2-GPU comparison
210+
# mesa-frames comparison
82211
labels_1 = [
83212
# "mesa-frames (pd concise)",
84-
"mesa-frames (pl concise)",
213+
"mesa-frames (pl loop DF)",
214+
"mesa-frames (pl loop no vec)",
215+
"mesa-frames (pl numba CPU)",
216+
"mesa-frames (pl numba parallel)",
217+
"mesa-frames (pl numba GPU)",
85218
]
219+
# Polars best_moves (non-vectorized loop vs DF loop vs numba loop)
86220
kernels_1 = [
87-
# mesa_frames_pandas_concise,
88-
mesa_frames_polars_concise,
221+
mesa_frames_polars_loop_DF,
222+
mesa_frames_polars_loop_no_vec,
223+
mesa_frames_polars_numba_cpu,
224+
mesa_frames_polars_numba_parallel,
225+
mesa_frames_polars_numba_gpu,
89226
]
90-
n_range_1 = [k for k in range(1, 3 * 10**6 + 2, 10**6)]
227+
n_range_1 = [k for k in range(10**6, 3 * 10**6 + 2, 10**6)]
91228
title_1 = "100 steps of the SugarScape IG model:\n" + " vs ".join(labels_1)
92-
image_path_1 = "benchmark_plot_1.png"
93-
plot_and_print_benchmark(labels_1, kernels_1, n_range_1, title_1, image_path_1)
229+
image_path_1 = "polars_comparison.png"
230+
plot_and_print_benchmark(
231+
labels_1,
232+
kernels_1,
233+
n_range_1,
234+
title_1,
235+
image_path_1,
236+
equality_check=polars_equality_check,
237+
)
94238

95239

96240
if __name__ == "__main__":
68.6 KB
Loading

examples/sugarscape_ig/ss_mesa/agents.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ def __init__(self, unique_id, model, moore=False, sugar=0, metabolism=0, vision=
2525
self.vision = vision
2626

2727
def get_sugar(self, pos):
28-
this_cell = self.model.grid.get_cell_list_contents([pos])
28+
this_cell = self.model.space.get_cell_list_contents([pos])
2929
for agent in this_cell:
3030
if type(agent) is Sugar:
3131
return agent
3232

3333
def is_occupied(self, pos):
34-
this_cell = self.model.grid.get_cell_list_contents([pos])
34+
this_cell = self.model.space.get_cell_list_contents([pos])
3535
return any(isinstance(agent, AntMesa) for agent in this_cell)
3636

3737
def move(self):
3838
# Get neighborhood within vision
3939
neighbors = [
4040
i
41-
for i in self.model.grid.get_neighborhood(
41+
for i in self.model.space.get_neighborhood(
4242
self.pos, self.moore, False, radius=self.vision
4343
)
4444
if not self.is_occupied(i)
@@ -55,7 +55,7 @@ def move(self):
5555
pos for pos in candidates if get_distance(self.pos, pos) == min_dist
5656
]
5757
self.random.shuffle(final_candidates)
58-
self.model.grid.move_agent(self, final_candidates[0])
58+
self.model.space.move_agent(self, final_candidates[0])
5959

6060
def eat(self):
6161
sugar_patch = self.get_sugar(self.pos)
@@ -66,7 +66,7 @@ def step(self):
6666
self.move()
6767
self.eat()
6868
if self.sugar <= 0:
69-
self.model.grid.remove_agent(self)
69+
self.model.space.remove_agent(self)
7070
self.model.agents.remove(self)
7171

7272

@@ -77,7 +77,7 @@ def __init__(self, unique_id, model, max_sugar):
7777
self.max_sugar = max_sugar
7878

7979
def step(self):
80-
if self.model.grid.is_cell_empty(self.pos):
80+
if self.model.space.is_cell_empty(self.pos):
8181
self.amount = self.max_sugar
8282
else:
8383
self.amount = 0

examples/sugarscape_ig/ss_mesa/model.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import mesa
22
import numpy as np
3+
import polars as pl
34

45
from .agents import AntMesa, Sugar
56

@@ -16,6 +17,8 @@ def __init__(
1617
initial_sugar: np.ndarray | None = None,
1718
metabolism: np.ndarray | None = None,
1819
vision: np.ndarray | None = None,
20+
initial_positions: pl.DataFrame | None = None,
21+
seed: int | None = None,
1922
width: int | None = None,
2023
height: int | None = None,
2124
):
@@ -34,30 +37,37 @@ def __init__(
3437
metabolism = np.random.randint(2, 4, n_agents)
3538
if vision is None:
3639
vision = np.random.randint(1, 6, n_agents)
40+
if seed is not None:
41+
self.reset_randomizer(seed)
3742

3843
self.width, self.height = sugar_grid.shape
3944
self.n_agents = n_agents
40-
self.grid = mesa.space.MultiGrid(self.width, self.height, torus=False)
45+
self.space = mesa.space.MultiGrid(self.width, self.height, torus=False)
4146
self.agents: list = []
4247

4348
agent_id = 0
4449
self.sugars = []
45-
for _, (x, y) in self.grid.coord_iter():
50+
51+
for _, (x, y) in self.space.coord_iter():
4652
max_sugar = sugar_grid[x, y]
4753
sugar = Sugar(agent_id, self, max_sugar)
4854
agent_id += 1
49-
self.grid.place_agent(sugar, (x, y))
55+
self.space.place_agent(sugar, (x, y))
5056
self.sugars.append(sugar)
5157

5258
# Create agent:
5359
for i in range(self.n_agents):
54-
x = self.random.randrange(self.width)
55-
y = self.random.randrange(self.height)
60+
if initial_positions is not None:
61+
x = initial_positions["dim_0"][i]
62+
y = initial_positions["dim_1"][i]
63+
else:
64+
x = self.random.randrange(self.width)
65+
y = self.random.randrange(self.height)
5666
ssa = AntMesa(
5767
agent_id, self, False, initial_sugar[i], metabolism[i], vision[i]
5868
)
5969
agent_id += 1
60-
self.grid.place_agent(ssa, (x, y))
70+
self.space.place_agent(ssa, (x, y))
6171
self.agents.append(ssa)
6272

6373
self.running = True

0 commit comments

Comments
 (0)