Skip to content

Commit 8973d56

Browse files
EwoutHrht
andauthoredJan 20, 2024
Add performance benchmarking scripts (projectmesa#1979)
* benchmarks: Upload initial models from JuliaDynamics https://github.com/JuliaDynamics/ABMFrameworksComparison/tree/5551d7abf1611d377b3b32346c7774f176af4c65 * benchmarks: Add dictionary with configurations * benchmarks: Update configurations, use relative imports * benchmarks: Add single script to run all benchmarks * benchmarks: Update configurations Few less replications, some more seeds. Every benchmark now takes between 10 and 20 seconds (on my machine). * benchmarks: Add generated pickle files to gitignore That allows switching branches without benchmarks results disappearing * benchmarks: Update global script to calculate and save stuff Prints some stuff when running and saves a pickle file after running. * benchmarks: Write a script to statistically compare runs - The bootstrap_speedup_confidence_interval function calculates the mean speedup and its confidence interval using bootstrapping, which is more suitable for paired data. - The mean speedup and confidence interval are calculated for both initialization and run times. - Positive values indicate an increase in time (longer duration), and negative values indicate a decrease (shorter duration). - The results are displayed in a DataFrame with the percentage changes and their confidence intervals. * benchmarks: Remove seperate benchmark scripts * benchmarks: Black and ruff * benchmarks: Use old f-string formatting to work with Python 3.11 and older * benchmarks: Add fancy colored performance indicators 🟢🔴🔵 If the 95% confidence interval is: - fully below -3%: 🟢 - fully above +3%: 🔴 - else: 🔵 * Fix typos caught by codespell * Apply ruff format benchmarks/ * Apply ruff --fix benchmarks/ * Apply ruff --fix --unsafe-fixes benchmarks/ * Apply manual Ruff fixes * codecov: Ignore benchmarks/ --------- Co-authored-by: rht <[email protected]>
1 parent 0bd481d commit 8973d56

14 files changed

+739
-1
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Benchmarking
2+
benchmarking/**/*.pickle
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

‎benchmarks/Flocking/Flocking.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Flockers
3+
=============================================================
4+
A Mesa implementation of Craig Reynolds's Boids flocker model.
5+
Uses numpy arrays to represent vectors.
6+
"""
7+
8+
import numpy as np
9+
10+
from mesa import Model
11+
from mesa.space import ContinuousSpace
12+
from mesa.time import RandomActivation
13+
14+
from .boid import Boid
15+
16+
17+
class BoidFlockers(Model):
18+
"""
19+
Flocker model class. Handles agent creation, placement and scheduling.
20+
"""
21+
22+
def __init__(
23+
self,
24+
seed,
25+
population,
26+
width,
27+
height,
28+
vision,
29+
speed=1,
30+
separation=1,
31+
cohere=0.03,
32+
separate=0.015,
33+
match=0.05,
34+
):
35+
"""
36+
Create a new Flockers model.
37+
38+
Args:
39+
population: Number of Boids
40+
width, height: Size of the space.
41+
speed: How fast should the Boids move.
42+
vision: How far around should each Boid look for its neighbors
43+
separation: What's the minimum distance each Boid will attempt to
44+
keep from any other
45+
cohere, separate, match: factors for the relative importance of
46+
the three drives."""
47+
super().__init__(seed=seed)
48+
self.population = population
49+
self.vision = vision
50+
self.speed = speed
51+
self.separation = separation
52+
self.schedule = RandomActivation(self)
53+
self.space = ContinuousSpace(width, height, True)
54+
self.factors = {"cohere": cohere, "separate": separate, "match": match}
55+
self.make_agents()
56+
57+
def make_agents(self):
58+
"""
59+
Create self.population agents, with random positions and starting headings.
60+
"""
61+
for i in range(self.population):
62+
x = self.random.random() * self.space.x_max
63+
y = self.random.random() * self.space.y_max
64+
pos = np.array((x, y))
65+
velocity = np.random.random(2) * 2 - 1
66+
boid = Boid(
67+
i,
68+
self,
69+
pos,
70+
self.speed,
71+
velocity,
72+
self.vision,
73+
self.separation,
74+
**self.factors,
75+
)
76+
self.space.place_agent(boid, pos)
77+
self.schedule.add(boid)
78+
79+
def step(self):
80+
self.schedule.step()

‎benchmarks/Flocking/__init__.py

Whitespace-only changes.

‎benchmarks/Flocking/boid.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import numpy as np
2+
3+
from mesa import Agent
4+
5+
6+
class Boid(Agent):
7+
"""
8+
A Boid-style flocker agent.
9+
10+
The agent follows three behaviors to flock:
11+
- Cohesion: steering towards neighboring agents.
12+
- Separation: avoiding getting too close to any other agent.
13+
- Alignment: try to fly in the same direction as the neighbors.
14+
15+
Boids have a vision that defines the radius in which they look for their
16+
neighbors to flock with. Their speed (a scalar) and velocity (a vector)
17+
define their movement. Separation is their desired minimum distance from
18+
any other Boid.
19+
"""
20+
21+
def __init__(
22+
self,
23+
unique_id,
24+
model,
25+
pos,
26+
speed,
27+
velocity,
28+
vision,
29+
separation,
30+
cohere=0.03,
31+
separate=0.015,
32+
match=0.05,
33+
):
34+
"""
35+
Create a new Boid flocker agent.
36+
37+
Args:
38+
unique_id: Unique agent identifier.
39+
pos: Starting position
40+
speed: Distance to move per step.
41+
heading: numpy vector for the Boid's direction of movement.
42+
vision: Radius to look around for nearby Boids.
43+
separation: Minimum distance to maintain from other Boids.
44+
cohere: the relative importance of matching neighbors' positions
45+
separate: the relative importance of avoiding close neighbors
46+
match: the relative importance of matching neighbors' headings
47+
48+
"""
49+
super().__init__(unique_id, model)
50+
self.pos = np.array(pos)
51+
self.speed = speed
52+
self.velocity = velocity
53+
self.vision = vision
54+
self.separation = separation
55+
self.cohere_factor = cohere
56+
self.separate_factor = separate
57+
self.match_factor = match
58+
59+
def step(self):
60+
"""
61+
Get the Boid's neighbors, compute the new vector, and move accordingly.
62+
"""
63+
64+
neighbors = self.model.space.get_neighbors(self.pos, self.vision, False)
65+
n = 0
66+
match_vector, separation_vector, cohere = np.zeros((3, 2))
67+
for neighbor in neighbors:
68+
n += 1
69+
heading = self.model.space.get_heading(self.pos, neighbor.pos)
70+
cohere += heading
71+
if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation:
72+
separation_vector -= heading
73+
match_vector += neighbor.velocity
74+
n = max(n, 1)
75+
cohere = cohere * self.cohere_factor
76+
separation_vector = separation_vector * self.separate_factor
77+
match_vector = match_vector * self.match_factor
78+
self.velocity += (cohere + separation_vector + match_vector) / n
79+
self.velocity /= np.linalg.norm(self.velocity)
80+
new_pos = self.pos + self.velocity * self.speed
81+
self.model.space.move_agent(self, new_pos)

‎benchmarks/Schelling/Schelling.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import random
2+
3+
from mesa import Agent, Model
4+
from mesa.space import SingleGrid
5+
from mesa.time import RandomActivation
6+
7+
8+
class SchellingAgent(Agent):
9+
"""
10+
Schelling segregation agent
11+
"""
12+
13+
def __init__(self, pos, model, agent_type):
14+
"""
15+
Create a new Schelling agent.
16+
Args:
17+
unique_id: Unique identifier for the agent.
18+
x, y: Agent initial location.
19+
agent_type: Indicator for the agent's type (minority=1, majority=0)
20+
"""
21+
super().__init__(pos, model)
22+
self.pos = pos
23+
self.type = agent_type
24+
25+
def step(self):
26+
similar = 0
27+
r = self.model.radius
28+
for neighbor in self.model.grid.iter_neighbors(self.pos, moore=True, radius=r):
29+
if neighbor.type == self.type:
30+
similar += 1
31+
32+
# If unhappy, move:
33+
if similar < self.model.homophily:
34+
self.model.grid.move_to_empty(self)
35+
else:
36+
self.model.happy += 1
37+
38+
39+
class SchellingModel(Model):
40+
"""
41+
Model class for the Schelling segregation model.
42+
"""
43+
44+
def __init__(
45+
self, seed, height, width, homophily, radius, density, minority_pc=0.5
46+
):
47+
""" """
48+
super().__init__(seed=seed)
49+
self.height = height
50+
self.width = width
51+
self.density = density
52+
self.minority_pc = minority_pc
53+
self.homophily = homophily
54+
self.radius = radius
55+
56+
self.schedule = RandomActivation(self)
57+
self.grid = SingleGrid(height, width, torus=True)
58+
59+
self.happy = 0
60+
61+
# Set up agents
62+
# We use a grid iterator that returns
63+
# the coordinates of a cell as well as
64+
# its contents. (coord_iter)
65+
for _cont, pos in self.grid.coord_iter():
66+
if random.random() < self.density: # noqa: S311
67+
agent_type = 1 if random.random() < self.minority_pc else 0 # noqa: S311
68+
agent = SchellingAgent(pos, self, agent_type)
69+
self.grid.place_agent(agent, pos)
70+
self.schedule.add(agent)
71+
72+
def step(self):
73+
"""
74+
Run one step of the model.
75+
"""
76+
self.happy = 0 # Reset counter of happy agents
77+
self.schedule.step()

‎benchmarks/Schelling/__init__.py

Whitespace-only changes.

‎benchmarks/WolfSheep/WolfSheep.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Wolf-Sheep Predation Model
3+
================================
4+
5+
Replication of the model found in NetLogo:
6+
Wilensky, U. (1997). NetLogo Wolf Sheep Predation model.
7+
http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation.
8+
Center for Connected Learning and Computer-Based Modeling,
9+
Northwestern University, Evanston, IL.
10+
"""
11+
12+
import mesa
13+
from mesa.space import MultiGrid
14+
from mesa.time import RandomActivationByType
15+
16+
from .agents import GrassPatch, Sheep, Wolf
17+
18+
19+
class WolfSheep(mesa.Model):
20+
"""
21+
Wolf-Sheep Predation Model
22+
23+
A model for simulating wolf and sheep (predator-prey) ecosystem modelling.
24+
"""
25+
26+
def __init__(
27+
self,
28+
seed,
29+
height,
30+
width,
31+
initial_sheep,
32+
initial_wolves,
33+
sheep_reproduce,
34+
wolf_reproduce,
35+
grass_regrowth_time,
36+
wolf_gain_from_food=13,
37+
sheep_gain_from_food=5,
38+
):
39+
"""
40+
Create a new Wolf-Sheep model with the given parameters.
41+
42+
Args:
43+
initial_sheep: Number of sheep to start with
44+
initial_wolves: Number of wolves to start with
45+
sheep_reproduce: Probability of each sheep reproducing each step
46+
wolf_reproduce: Probability of each wolf reproducing each step
47+
wolf_gain_from_food: Energy a wolf gains from eating a sheep
48+
grass: Whether to have the sheep eat grass for energy
49+
grass_regrowth_time: How long it takes for a grass patch to regrow
50+
once it is eaten
51+
sheep_gain_from_food: Energy sheep gain from grass, if enabled.
52+
"""
53+
super().__init__(seed=seed)
54+
# Set parameters
55+
self.height = height
56+
self.width = width
57+
self.initial_sheep = initial_sheep
58+
self.initial_wolves = initial_wolves
59+
self.sheep_reproduce = sheep_reproduce
60+
self.wolf_reproduce = wolf_reproduce
61+
self.wolf_gain_from_food = wolf_gain_from_food
62+
self.grass_regrowth_time = grass_regrowth_time
63+
self.sheep_gain_from_food = sheep_gain_from_food
64+
65+
self.schedule = RandomActivationByType(self)
66+
self.grid = MultiGrid(self.height, self.width, torus=False)
67+
68+
# Create sheep:
69+
for _i in range(self.initial_sheep):
70+
pos = (
71+
self.random.randrange(self.width),
72+
self.random.randrange(self.height),
73+
)
74+
energy = self.random.randrange(2 * self.sheep_gain_from_food)
75+
sheep = Sheep(self.next_id(), pos, self, True, energy)
76+
self.grid.place_agent(sheep, pos)
77+
self.schedule.add(sheep)
78+
79+
# Create wolves
80+
for _i in range(self.initial_wolves):
81+
pos = (
82+
self.random.randrange(self.width),
83+
self.random.randrange(self.height),
84+
)
85+
energy = self.random.randrange(2 * self.wolf_gain_from_food)
86+
wolf = Wolf(self.next_id(), pos, self, True, energy)
87+
self.grid.place_agent(wolf, pos)
88+
self.schedule.add(wolf)
89+
90+
# Create grass patches
91+
possibly_fully_grown = [True, False]
92+
for _agent, pos in self.grid.coord_iter():
93+
fully_grown = self.random.choice(possibly_fully_grown)
94+
if fully_grown:
95+
countdown = self.grass_regrowth_time
96+
else:
97+
countdown = self.random.randrange(self.grass_regrowth_time)
98+
patch = GrassPatch(self.next_id(), pos, self, fully_grown, countdown)
99+
self.grid.place_agent(patch, pos)
100+
self.schedule.add(patch)
101+
102+
def step(self):
103+
self.schedule.step()

‎benchmarks/WolfSheep/__init__.py

Whitespace-only changes.

‎benchmarks/WolfSheep/agents.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from mesa import Agent
2+
3+
from .random_walk import RandomWalker
4+
5+
6+
class Sheep(RandomWalker):
7+
"""
8+
A sheep that walks around, reproduces (asexually) and gets eaten.
9+
10+
The init is the same as the RandomWalker.
11+
"""
12+
13+
def __init__(self, unique_id, pos, model, moore, energy=None):
14+
super().__init__(unique_id, pos, model, moore=moore)
15+
self.energy = energy
16+
17+
def step(self):
18+
"""
19+
A model step. Move, then eat grass and reproduce.
20+
"""
21+
self.random_move()
22+
23+
# Reduce energy
24+
self.energy -= 1
25+
26+
# If there is grass available, eat it
27+
this_cell = self.model.grid.get_cell_list_contents(self.pos)
28+
grass_patch = next(obj for obj in this_cell if isinstance(obj, GrassPatch))
29+
if grass_patch.fully_grown:
30+
self.energy += self.model.sheep_gain_from_food
31+
grass_patch.fully_grown = False
32+
33+
# Death
34+
if self.energy < 0:
35+
self.model.grid.remove_agent(self)
36+
self.model.schedule.remove(self)
37+
elif self.random.random() < self.model.sheep_reproduce:
38+
# Create a new sheep:
39+
self.energy /= 2
40+
lamb = Sheep(
41+
self.model.next_id(), self.pos, self.model, self.moore, self.energy
42+
)
43+
self.model.grid.place_agent(lamb, self.pos)
44+
self.model.schedule.add(lamb)
45+
46+
47+
class Wolf(RandomWalker):
48+
"""
49+
A wolf that walks around, reproduces (asexually) and eats sheep.
50+
"""
51+
52+
def __init__(self, unique_id, pos, model, moore, energy=None):
53+
super().__init__(unique_id, pos, model, moore=moore)
54+
self.energy = energy
55+
56+
def step(self):
57+
self.random_move()
58+
self.energy -= 1
59+
60+
# If there are sheep present, eat one
61+
x, y = self.pos
62+
this_cell = self.model.grid.get_cell_list_contents([self.pos])
63+
sheep = [obj for obj in this_cell if isinstance(obj, Sheep)]
64+
if len(sheep) > 0:
65+
sheep_to_eat = self.random.choice(sheep)
66+
self.energy += self.model.wolf_gain_from_food
67+
68+
# Kill the sheep
69+
self.model.grid.remove_agent(sheep_to_eat)
70+
self.model.schedule.remove(sheep_to_eat)
71+
72+
# Death or reproduction
73+
if self.energy < 0:
74+
self.model.grid.remove_agent(self)
75+
self.model.schedule.remove(self)
76+
elif self.random.random() < self.model.wolf_reproduce:
77+
# Create a new wolf cub
78+
self.energy /= 2
79+
cub = Wolf(
80+
self.model.next_id(), self.pos, self.model, self.moore, self.energy
81+
)
82+
self.model.grid.place_agent(cub, cub.pos)
83+
self.model.schedule.add(cub)
84+
85+
86+
class GrassPatch(Agent):
87+
"""
88+
A patch of grass that grows at a fixed rate and it is eaten by sheep
89+
"""
90+
91+
def __init__(self, unique_id, pos, model, fully_grown, countdown):
92+
"""
93+
Creates a new patch of grass
94+
95+
Args:
96+
grown: (boolean) Whether the patch of grass is fully grown or not
97+
countdown: Time for the patch of grass to be fully grown again
98+
"""
99+
super().__init__(unique_id, model)
100+
self.fully_grown = fully_grown
101+
self.countdown = countdown
102+
self.pos = pos
103+
104+
def step(self):
105+
if not self.fully_grown:
106+
if self.countdown <= 0:
107+
# Set as fully grown
108+
self.fully_grown = True
109+
self.countdown = self.model.grass_regrowth_time
110+
else:
111+
self.countdown -= 1

‎benchmarks/WolfSheep/random_walk.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Generalized behavior for random walking, one grid cell at a time.
3+
"""
4+
5+
from mesa import Agent
6+
7+
8+
class RandomWalker(Agent):
9+
"""
10+
Class implementing random walker methods in a generalized manner.
11+
12+
Not intended to be used on its own, but to inherit its methods to multiple
13+
other agents.
14+
15+
"""
16+
17+
def __init__(self, unique_id, pos, model, moore=True):
18+
"""
19+
grid: The MultiGrid object in which the agent lives.
20+
x: The agent's current x coordinate
21+
y: The agent's current y coordinate
22+
moore: If True, may move in all 8 directions.
23+
Otherwise, only up, down, left, right.
24+
"""
25+
super().__init__(unique_id, model)
26+
self.pos = pos
27+
self.moore = moore
28+
29+
def random_move(self):
30+
"""
31+
Step one cell in any allowable direction.
32+
"""
33+
# Pick the next cell from the adjacent cells.
34+
next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True)
35+
next_move = self.random.choice(next_moves)
36+
# Now move:
37+
self.model.grid.move_agent(self, next_move)

‎benchmarks/compare_timings.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import pickle
2+
3+
import numpy as np
4+
import pandas as pd
5+
6+
filename1 = "timings_1"
7+
filename2 = "timings_2"
8+
9+
with open(f"{filename1}.pickle", "rb") as handle:
10+
timings_1 = pickle.load(handle) # noqa: S301
11+
12+
with open(f"{filename2}.pickle", "rb") as handle:
13+
timings_2 = pickle.load(handle) # noqa: S301
14+
15+
16+
# Function to calculate the percentage change and perform bootstrap to estimate the confidence interval
17+
def bootstrap_percentage_change_confidence_interval(data1, data2, n=1000):
18+
change_samples = []
19+
for _ in range(n):
20+
sampled_indices = np.random.choice(
21+
range(len(data1)), size=len(data1), replace=True
22+
)
23+
sampled_data1 = np.array(data1)[sampled_indices]
24+
sampled_data2 = np.array(data2)[sampled_indices]
25+
change = 100 * (sampled_data2 - sampled_data1) / sampled_data1
26+
change_samples.append(np.mean(change))
27+
lower, upper = np.percentile(change_samples, [2.5, 97.5])
28+
return np.mean(change_samples), lower, upper
29+
30+
31+
# DataFrame to store the results
32+
results_df = pd.DataFrame()
33+
34+
35+
# Function to determine the emoji based on change and confidence interval
36+
def performance_emoji(lower, upper):
37+
if upper < -3:
38+
return "🟢" # Emoji for faster performance
39+
elif lower > 3:
40+
return "🔴" # Emoji for slower performance
41+
else:
42+
return "🔵" # Emoji for insignificant change
43+
44+
45+
# Iterate over the models and sizes, perform analysis, and populate the DataFrame
46+
for model, size in timings_1:
47+
model_name = model.__name__
48+
49+
# Calculate percentage change and confidence interval for init times
50+
(
51+
init_change,
52+
init_lower,
53+
init_upper,
54+
) = bootstrap_percentage_change_confidence_interval(
55+
timings_1[(model, size)][0], timings_2[(model, size)][0]
56+
)
57+
init_emoji = performance_emoji(init_lower, init_upper)
58+
init_summary = (
59+
f"{init_emoji} {init_change:+.1f}% [{init_lower:+.1f}%, {init_upper:+.1f}%]"
60+
)
61+
62+
# Calculate percentage change and confidence interval for run times
63+
run_change, run_lower, run_upper = bootstrap_percentage_change_confidence_interval(
64+
timings_1[(model, size)][1], timings_2[(model, size)][1]
65+
)
66+
run_emoji = performance_emoji(run_lower, run_upper)
67+
run_summary = (
68+
f"{run_emoji} {run_change:+.1f}% [{run_lower:+.1f}%, {run_upper:+.1f}%]"
69+
)
70+
71+
# Append results to DataFrame
72+
row = pd.DataFrame(
73+
{
74+
"Model": [model_name],
75+
"Size": [size],
76+
"Init time [95% CI]": [init_summary],
77+
"Run time [95% CI]": [run_summary],
78+
}
79+
)
80+
81+
results_df = pd.concat([results_df, row], ignore_index=True)
82+
83+
# Convert DataFrame to markdown with specified alignments
84+
markdown_representation = results_df.to_markdown(index=False, tablefmt="github")
85+
86+
# Display the markdown representation
87+
print(markdown_representation)

‎benchmarks/configurations.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from Flocking.Flocking import BoidFlockers
2+
from Schelling.Schelling import SchellingModel
3+
from WolfSheep.WolfSheep import WolfSheep
4+
5+
configurations = {
6+
# Schelling Model Configurations
7+
SchellingModel: {
8+
"small": {
9+
"seeds": 50,
10+
"replications": 5,
11+
"steps": 20,
12+
"parameters": {
13+
"height": 40,
14+
"width": 40,
15+
"homophily": 3,
16+
"radius": 1,
17+
"density": 0.625,
18+
},
19+
},
20+
"large": {
21+
"seeds": 10,
22+
"replications": 3,
23+
"steps": 10,
24+
"parameters": {
25+
"height": 100,
26+
"width": 100,
27+
"homophily": 8,
28+
"radius": 2,
29+
"density": 0.8,
30+
},
31+
},
32+
},
33+
# WolfSheep Model Configurations
34+
WolfSheep: {
35+
"small": {
36+
"seeds": 50,
37+
"replications": 5,
38+
"steps": 40,
39+
"parameters": {
40+
"height": 25,
41+
"width": 25,
42+
"initial_sheep": 60,
43+
"initial_wolves": 40,
44+
"sheep_reproduce": 0.2,
45+
"wolf_reproduce": 0.1,
46+
"grass_regrowth_time": 20,
47+
},
48+
},
49+
"large": {
50+
"seeds": 10,
51+
"replications": 3,
52+
"steps": 20,
53+
"parameters": {
54+
"height": 100,
55+
"width": 100,
56+
"initial_sheep": 1000,
57+
"initial_wolves": 500,
58+
"sheep_reproduce": 0.4,
59+
"wolf_reproduce": 0.2,
60+
"grass_regrowth_time": 10,
61+
},
62+
},
63+
},
64+
# BoidFlockers Model Configurations
65+
BoidFlockers: {
66+
"small": {
67+
"seeds": 25,
68+
"replications": 3,
69+
"steps": 20,
70+
"parameters": {"population": 200, "width": 100, "height": 100, "vision": 5},
71+
},
72+
"large": {
73+
"seeds": 10,
74+
"replications": 3,
75+
"steps": 10,
76+
"parameters": {
77+
"population": 400,
78+
"width": 150,
79+
"height": 150,
80+
"vision": 15,
81+
},
82+
},
83+
},
84+
}

‎benchmarks/global_benchmark.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import gc
2+
import os
3+
import pickle
4+
import sys
5+
import time
6+
import timeit
7+
8+
from configurations import configurations
9+
10+
11+
# Generic function to initialize and run a model
12+
def run_model(model_class, seed, parameters):
13+
start_init = timeit.default_timer()
14+
model = model_class(seed=seed, **parameters)
15+
# time.sleep(0.001)
16+
17+
end_init_start_run = timeit.default_timer()
18+
19+
for _ in range(config["steps"]):
20+
model.step()
21+
# time.sleep(0.0001)
22+
end_run = timeit.default_timer()
23+
24+
return (end_init_start_run - start_init), (end_run - end_init_start_run)
25+
26+
27+
# Function to run experiments and save the fastest replication for each seed
28+
def run_experiments(model_class, config):
29+
gc.enable()
30+
sys.path.insert(0, os.path.abspath("."))
31+
32+
init_times = []
33+
run_times = []
34+
for seed in range(1, config["seeds"] + 1):
35+
fastest_init = float("inf")
36+
fastest_run = float("inf")
37+
for _replication in range(1, config["replications"] + 1):
38+
init_time, run_time = run_model(model_class, seed, config["parameters"])
39+
if init_time < fastest_init:
40+
fastest_init = init_time
41+
if run_time < fastest_run:
42+
fastest_run = run_time
43+
init_times.append(fastest_init)
44+
run_times.append(fastest_run)
45+
46+
return init_times, run_times
47+
48+
49+
print(f"{time.strftime('%H:%M:%S', time.localtime())} starting benchmarks.")
50+
results_dict = {}
51+
for model, model_config in configurations.items():
52+
for size, config in model_config.items():
53+
results = run_experiments(model, config)
54+
55+
mean_init = sum(results[0]) / len(results[0])
56+
mean_run = sum(results[1]) / len(results[1])
57+
58+
print(
59+
f"{time.strftime("%H:%M:%S", time.localtime())} {model.__name__:<14} ({size}) timings: Init {mean_init:.5f} s; Run {mean_run:.4f} s"
60+
)
61+
62+
results_dict[model, size] = results
63+
64+
# Change this name to anything you like
65+
save_name = "timings"
66+
67+
i = 1
68+
while os.path.exists(f"{save_name}_{i}.pickle"):
69+
i += 1
70+
71+
with open(f"{save_name}_{i}.pickle", "wb") as handle:
72+
pickle.dump(results_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
73+
74+
print(f"Done benchmarking. Saved results to {save_name}_{i}.pickle.")

‎codecov.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ coverage:
77

88
ignore:
99
- "**experimental/"
10+
- "benchmarks/**"
1011

11-
comment: off
12+
comment: off

0 commit comments

Comments
 (0)
Please sign in to comment.