From bc1718ce3c48cfa31ae5f660a1a53112e6a24d08 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 15 Jan 2024 11:23:43 +0100 Subject: [PATCH 01/20] Make RandomActivationByType.agents_by_type backward compatible --- mesa/time.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/mesa/time.py b/mesa/time.py index b93b7863d29..633813024fa 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -28,6 +28,7 @@ import heapq import warnings import weakref +from collections import defaultdict from collections.abc import Iterable # mypy @@ -287,6 +288,22 @@ class RandomActivationByType(BaseScheduler): - get_type_count: Returns the count of agents of a specific type. """ + @property + def agents_by_type(self): + warnings.warn( + "Because of the shift to using AgentSet, in the future this attribute will return a dict with" + "type as key as AgentSet as value", + DeprecationWarning, + stacklevel=2, + ) + + agentsbytype = defaultdict(dict) + for k, v in self._agents_by_type.items(): + agentsbytype[k] = {agent: agent.unique_id for agent in v} + + return agentsbytype + + def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None: super().__init__(model, agents) """ @@ -297,14 +314,14 @@ def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None: """ # can't be a defaultdict because we need to pass model to AgentSet - self.agents_by_type: [type, AgentSet] = {} + self._agents_by_type: [type, AgentSet] = {} if agents is not None: for agent in agents: try: - self.agents_by_type[type(agent)].add(agent) + self._agents_by_type[type(agent)].add(agent) except KeyError: - self.agents_by_type[type(agent)] = AgentSet([agent], self.model) + self._agents_by_type[type(agent)] = AgentSet([agent], self.model) def add(self, agent: Agent) -> None: """ @@ -316,16 +333,16 @@ def add(self, agent: Agent) -> None: super().add(agent) try: - self.agents_by_type[type(agent)].add(agent) + self._agents_by_type[type(agent)].add(agent) except KeyError: - self.agents_by_type[type(agent)] = AgentSet([agent], self.model) + self._agents_by_type[type(agent)] = AgentSet([agent], self.model) def remove(self, agent: Agent) -> None: """ Remove all instances of a given agent from the schedule. """ super().remove(agent) - self.agents_by_type[type(agent)].remove(agent) + self._agents_by_type[type(agent)].remove(agent) def step(self, shuffle_types: bool = True, shuffle_agents: bool = True) -> None: """ @@ -339,7 +356,7 @@ def step(self, shuffle_types: bool = True, shuffle_agents: bool = True) -> None: """ # To be able to remove and/or add agents during stepping # it's necessary to cast the keys view to a list. - type_keys: list[type[Agent]] = list(self.agents_by_type.keys()) + type_keys: list[type[Agent]] = list(self._agents_by_type.keys()) if shuffle_types: self.model.random.shuffle(type_keys) for agent_class in type_keys: @@ -355,7 +372,7 @@ def step_type(self, agenttype: type[Agent], shuffle_agents: bool = True) -> None Args: agenttype: Class object of the type to run. """ - agents = self.agents_by_type[agenttype] + agents = self._agents_by_type[agenttype] if shuffle_agents: agents.shuffle(inplace=True) @@ -365,7 +382,7 @@ def get_type_count(self, agenttype: type[Agent]) -> int: """ Returns the current number of agents of certain type in the queue. """ - return len(self.agents_by_type[agenttype]) + return len(self._agents_by_type[agenttype]) class DiscreteEventScheduler(BaseScheduler): From a7ddad55f53c58e1f84a1f08fdfc56e6d1c45453 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:48:57 +0000 Subject: [PATCH 02/20] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/time.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mesa/time.py b/mesa/time.py index 633813024fa..bb6c55883a9 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -303,7 +303,6 @@ def agents_by_type(self): return agentsbytype - def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None: super().__init__(model, agents) """ From 1f9af4925070856959176688e585fe812778011f Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 15 Jan 2024 13:07:04 +0100 Subject: [PATCH 03/20] updated warning --- mesa/time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/time.py b/mesa/time.py index bb6c55883a9..c56fa638236 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -292,7 +292,7 @@ class RandomActivationByType(BaseScheduler): def agents_by_type(self): warnings.warn( "Because of the shift to using AgentSet, in the future this attribute will return a dict with" - "type as key as AgentSet as value", + "type as key as AgentSet as value. Future behavior is available via RandomActivationByType._agents_by_type", DeprecationWarning, stacklevel=2, ) From 2076b6d73f19c361fc96d6bff2a08e6d99f7b438 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 11:05:06 +0100 Subject: [PATCH 04/20] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e2819243943..5dd232f192b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ pythonenv*/ # JS dependencies mesa/visualization/templates/external/ mesa/visualization/templates/js/external/ +*.pclprof +*.pickle From c83179d106cdaab0413645fce133d8031877536f Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 11:18:27 +0100 Subject: [PATCH 05/20] Update global_benchmark.py --- benchmarks/global_benchmark.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index 9547d94a674..38aa2ff7913 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -5,6 +5,10 @@ import time import timeit +# making sure we use this version of mesa and not one +# also installed in site_packages or so. +sys.path.insert(0, os.path.abspath('..')) + from configurations import configurations @@ -56,7 +60,7 @@ def run_experiments(model_class, config): mean_run = sum(results[1]) / len(results[1]) print( - f"{time.strftime("%H:%M:%S", time.localtime())} {model.__name__:<14} ({size}) timings: Init {mean_init:.5f} s; Run {mean_run:.4f} s" + f"{time.strftime('%H:%M:%S', time.localtime())} {model.__name__:<14} ({size}) timings: Init {mean_init:.5f} s; Run {mean_run:.4f} s" ) results_dict[model, size] = results From 4c8cb8b74c12b200b08190241f970c9dd723c3c0 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 14:37:25 +0100 Subject: [PATCH 06/20] pep8 rename --- benchmarks/Schelling/{Schelling.py => schelling.py} | 9 ++++----- benchmarks/WolfSheep/{WolfSheep.py => wolf_sheep.py} | 0 benchmarks/configurations.py | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) rename benchmarks/Schelling/{Schelling.py => schelling.py} (91%) rename benchmarks/WolfSheep/{WolfSheep.py => wolf_sheep.py} (100%) diff --git a/benchmarks/Schelling/Schelling.py b/benchmarks/Schelling/schelling.py similarity index 91% rename from benchmarks/Schelling/Schelling.py rename to benchmarks/Schelling/schelling.py index eb1379edbfa..f505ab094c0 100644 --- a/benchmarks/Schelling/Schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -10,7 +10,7 @@ class SchellingAgent(Agent): Schelling segregation agent """ - def __init__(self, pos, model, agent_type): + def __init__(self, unique_id, model, agent_type): """ Create a new Schelling agent. Args: @@ -18,8 +18,7 @@ def __init__(self, pos, model, agent_type): x, y: Agent initial location. agent_type: Indicator for the agent's type (minority=1, majority=0) """ - super().__init__(pos, model) - self.pos = pos + super().__init__(unique_id, model) self.type = agent_type def step(self): @@ -36,7 +35,7 @@ def step(self): self.model.happy += 1 -class SchellingModel(Model): +class Schelling(Model): """ Model class for the Schelling segregation model. """ @@ -65,7 +64,7 @@ def __init__( for _cont, pos in self.grid.coord_iter(): if random.random() < self.density: # noqa: S311 agent_type = 1 if random.random() < self.minority_pc else 0 # noqa: S311 - agent = SchellingAgent(pos, self, agent_type) + agent = SchellingAgent(self.next_id(), self, agent_type) self.grid.place_agent(agent, pos) self.schedule.add(agent) diff --git a/benchmarks/WolfSheep/WolfSheep.py b/benchmarks/WolfSheep/wolf_sheep.py similarity index 100% rename from benchmarks/WolfSheep/WolfSheep.py rename to benchmarks/WolfSheep/wolf_sheep.py diff --git a/benchmarks/configurations.py b/benchmarks/configurations.py index ab8960408d4..c3e4b362985 100644 --- a/benchmarks/configurations.py +++ b/benchmarks/configurations.py @@ -1,10 +1,10 @@ from Flocking.Flocking import BoidFlockers -from Schelling.Schelling import SchellingModel -from WolfSheep.WolfSheep import WolfSheep +from Schelling.schelling import Schelling +from WolfSheep.wolf_sheep import WolfSheep configurations = { # Schelling Model Configurations - SchellingModel: { + Schelling: { "small": { "seeds": 50, "replications": 5, From 7bccdd163d9bf14492c5356619db5f05b2eb6930 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 14:46:38 +0100 Subject: [PATCH 07/20] Update schelling.py --- benchmarks/Schelling/schelling.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index f505ab094c0..dd4865d764a 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -74,3 +74,15 @@ def step(self): """ self.happy = 0 # Reset counter of happy agents self.schedule.step() + + +if __name__ == '__main__': + import time + + model = Schelling + + start_time = time.perf_counter() + for _ in range(100): + model.step() + + print(time.perf_counter() - start_time) \ No newline at end of file From 64d8c463c0b1a5a96763364c7c869e9409dcb8d3 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 14:48:20 +0100 Subject: [PATCH 08/20] Update schelling.py --- benchmarks/Schelling/schelling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index dd4865d764a..09a79178c8c 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -79,7 +79,8 @@ def step(self): if __name__ == '__main__': import time - model = Schelling + # model = Schelling(15, 40, 40, 3, 1, 0.625) + model = Schelling(15, 100, 100, 8, 2, 0.8) start_time = time.perf_counter() for _ in range(100): From 1e794d3b2cb14f57c4cfc34328d5fd023160cd20 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 14:57:39 +0100 Subject: [PATCH 09/20] Update schelling.py --- benchmarks/Schelling/schelling.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 09a79178c8c..d87a169d6d6 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -23,8 +23,7 @@ def __init__(self, unique_id, model, agent_type): def step(self): similar = 0 - r = self.model.radius - for neighbor in self.model.grid.iter_neighbors(self.pos, moore=True, radius=r): + for neighbor in self.model.grid.iter_neighbors(self.pos, moore=True, radius=self.model.radius): if neighbor.type == self.type: similar += 1 From 4bc5fac94146a839d671d2aa2178d14a960fa9ad Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 15:00:26 +0100 Subject: [PATCH 10/20] Update schelling.py --- benchmarks/Schelling/schelling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index d87a169d6d6..5ff7674ac9b 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -61,8 +61,8 @@ def __init__( # the coordinates of a cell as well as # its contents. (coord_iter) for _cont, pos in self.grid.coord_iter(): - if random.random() < self.density: # noqa: S311 - agent_type = 1 if random.random() < self.minority_pc else 0 # noqa: S311 + if self.random.random() < self.density: # noqa: S311 + agent_type = 1 if self.random.random() < self.minority_pc else 0 # noqa: S311 agent = SchellingAgent(self.next_id(), self, agent_type) self.grid.place_agent(agent, pos) self.schedule.add(agent) From 948508754eaabb2312cef6863c467ea905069551 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 17:53:39 +0100 Subject: [PATCH 11/20] Squashed commit of the following: commit c0de4a1c4dbb586f26084764d6f34818ce1e5dfb Author: Ewout ter Hoeven Date: Sun Jan 21 14:43:08 2024 +0100 Add CI workflow for performance benchmarks (#1983) This commit introduces a new GitHub Actions workflow for performance benchmarking. The workflow is triggered on `pull_request_target` events and can be triggered with a "/rerun-benchmarks" comment in the PR. It should be compatible with PRs from both forks and the main repository. It includes steps for setting up the environment, checking out the PR and main branches, installing dependencies, and running benchmarks on both branches. The results are then compared, encoded, and posted as a comment on the PR. --- .github/workflows/benchmarks.yml | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/benchmarks.yml diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 00000000000..09eb1f95c57 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,77 @@ +name: Performance benchmarks + +on: + pull_request_target: + types: [opened, ready_for_review] + branches: + - main + issue_comment: + types: [created] + +permissions: + issues: write + pull-requests: write + +jobs: + run-benchmarks: + if: > + github.event_name == 'pull_request_target' || + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/rerun-benchmarks')) + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Add project directory to PYTHONPATH + run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV + - name: Install dependencies + run: pip install numpy pandas tqdm tabulate + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + repository: projectmesa/mesa + - name: Run benchmarks on main branch + working-directory: benchmarks + run: python global_benchmark.py + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: timings-main + path: benchmarks/timings_1.pickle + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + persist-credentials: false + clean: false + - name: Download benchmark results + uses: actions/download-artifact@v4 + with: + name: timings-main + path: benchmarks + - name: Run benchmarks on PR branch + working-directory: benchmarks + run: python global_benchmark.py + - name: Run compare timings and encode output + working-directory: benchmarks + run: | + TIMING_COMPARISON=$(python compare_timings.py | base64 -w 0) # Base64 encode the output + echo "TIMING_COMPARISON=$TIMING_COMPARISON" >> $GITHUB_ENV + - name: Comment PR + uses: actions/github-script@v7 + with: + script: | + const output = Buffer.from(process.env.TIMING_COMPARISON, 'base64').toString('utf-8'); + const issue_number = context.issue.number; + github.rest.issues.createComment({ + issue_number: issue_number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Performance benchmarks:\n\n' + output + }); From 5cd0e748dfabd9b6dd0fef9b638b8ee8e4feb753 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 18:03:05 +0100 Subject: [PATCH 12/20] Revert "Squashed commit of the following:" This reverts commit 948508754eaabb2312cef6863c467ea905069551. --- .github/workflows/benchmarks.yml | 77 -------------------------------- 1 file changed, 77 deletions(-) delete mode 100644 .github/workflows/benchmarks.yml diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml deleted file mode 100644 index 09eb1f95c57..00000000000 --- a/.github/workflows/benchmarks.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Performance benchmarks - -on: - pull_request_target: - types: [opened, ready_for_review] - branches: - - main - issue_comment: - types: [created] - -permissions: - issues: write - pull-requests: write - -jobs: - run-benchmarks: - if: > - github.event_name == 'pull_request_target' || - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/rerun-benchmarks')) - runs-on: ubuntu-latest - steps: - - name: Checkout PR branch - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Add project directory to PYTHONPATH - run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV - - name: Install dependencies - run: pip install numpy pandas tqdm tabulate - - name: Checkout main branch - uses: actions/checkout@v4 - with: - ref: main - repository: projectmesa/mesa - - name: Run benchmarks on main branch - working-directory: benchmarks - run: python global_benchmark.py - - name: Upload benchmark results - uses: actions/upload-artifact@v4 - with: - name: timings-main - path: benchmarks/timings_1.pickle - - name: Checkout PR branch - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - fetch-depth: 0 - persist-credentials: false - clean: false - - name: Download benchmark results - uses: actions/download-artifact@v4 - with: - name: timings-main - path: benchmarks - - name: Run benchmarks on PR branch - working-directory: benchmarks - run: python global_benchmark.py - - name: Run compare timings and encode output - working-directory: benchmarks - run: | - TIMING_COMPARISON=$(python compare_timings.py | base64 -w 0) # Base64 encode the output - echo "TIMING_COMPARISON=$TIMING_COMPARISON" >> $GITHUB_ENV - - name: Comment PR - uses: actions/github-script@v7 - with: - script: | - const output = Buffer.from(process.env.TIMING_COMPARISON, 'base64').toString('utf-8'); - const issue_number = context.issue.number; - github.rest.issues.createComment({ - issue_number: issue_number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Performance benchmarks:\n\n' + output - }); From 63eec5e6faa8b8cb94f4d151e82cb1b21ac550dd Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 18:04:57 +0100 Subject: [PATCH 13/20] Update .gitignore --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a8eb64039fd..814f6a5daf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Benchmarking -benchmarking/**/*.pickle +benchmarks/**/*.pickle # Byte-compiled / optimized / DLL files __pycache__/ @@ -69,6 +69,7 @@ target/ # PyCharm environment files .idea/ +*.pclprof # VSCode environment files .vscode/ @@ -88,5 +89,4 @@ pythonenv*/ # JS dependencies mesa/visualization/templates/external/ mesa/visualization/templates/js/external/ -*.pclprof -*.pickle + From 2a4459c3176e68438566b453f567b6c9cc7fc51b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 18:57:57 +0100 Subject: [PATCH 14/20] further updates --- benchmarks/Flocking/Flocking.py | 80 ----------------- benchmarks/Flocking/{boid.py => flocking.py} | 90 +++++++++++++++++++- benchmarks/Schelling/schelling.py | 9 +- benchmarks/WolfSheep/__init__.py | 14 +++ benchmarks/configurations.py | 2 +- benchmarks/global_benchmark.py | 2 +- 6 files changed, 109 insertions(+), 88 deletions(-) delete mode 100644 benchmarks/Flocking/Flocking.py rename benchmarks/Flocking/{boid.py => flocking.py} (52%) diff --git a/benchmarks/Flocking/Flocking.py b/benchmarks/Flocking/Flocking.py deleted file mode 100644 index 4a5e8529cdd..00000000000 --- a/benchmarks/Flocking/Flocking.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Flockers -============================================================= -A Mesa implementation of Craig Reynolds's Boids flocker model. -Uses numpy arrays to represent vectors. -""" - -import numpy as np - -from mesa import Model -from mesa.space import ContinuousSpace -from mesa.time import RandomActivation - -from .boid import Boid - - -class BoidFlockers(Model): - """ - Flocker model class. Handles agent creation, placement and scheduling. - """ - - def __init__( - self, - seed, - population, - width, - height, - vision, - speed=1, - separation=1, - cohere=0.03, - separate=0.015, - match=0.05, - ): - """ - Create a new Flockers model. - - Args: - population: Number of Boids - width, height: Size of the space. - speed: How fast should the Boids move. - vision: How far around should each Boid look for its neighbors - separation: What's the minimum distance each Boid will attempt to - keep from any other - cohere, separate, match: factors for the relative importance of - the three drives.""" - super().__init__(seed=seed) - self.population = population - self.vision = vision - self.speed = speed - self.separation = separation - self.schedule = RandomActivation(self) - self.space = ContinuousSpace(width, height, True) - self.factors = {"cohere": cohere, "separate": separate, "match": match} - self.make_agents() - - def make_agents(self): - """ - Create self.population agents, with random positions and starting headings. - """ - for i in range(self.population): - x = self.random.random() * self.space.x_max - y = self.random.random() * self.space.y_max - pos = np.array((x, y)) - velocity = np.random.random(2) * 2 - 1 - boid = Boid( - i, - self, - pos, - self.speed, - velocity, - self.vision, - self.separation, - **self.factors, - ) - self.space.place_agent(boid, pos) - self.schedule.add(boid) - - def step(self): - self.schedule.step() diff --git a/benchmarks/Flocking/boid.py b/benchmarks/Flocking/flocking.py similarity index 52% rename from benchmarks/Flocking/boid.py rename to benchmarks/Flocking/flocking.py index 598ea32b84f..0331d356e69 100644 --- a/benchmarks/Flocking/boid.py +++ b/benchmarks/Flocking/flocking.py @@ -1,6 +1,15 @@ +""" +Flockers +============================================================= +A Mesa implementation of Craig Reynolds's Boids flocker model. +Uses numpy arrays to represent vectors. +""" + import numpy as np -from mesa import Agent +from mesa import Agent, Model +from mesa.space import ContinuousSpace +from mesa.time import RandomActivation class Boid(Agent): @@ -79,3 +88,82 @@ def step(self): self.velocity /= np.linalg.norm(self.velocity) new_pos = self.pos + self.velocity * self.speed self.model.space.move_agent(self, new_pos) + + +class BoidFlockers(Model): + """ + Flocker model class. Handles agent creation, placement and scheduling. + """ + + def __init__( + self, + seed, + population, + width, + height, + vision, + speed=1, + separation=1, + cohere=0.03, + separate=0.015, + match=0.05, + ): + """ + Create a new Flockers model. + + Args: + population: Number of Boids + width, height: Size of the space. + speed: How fast should the Boids move. + vision: How far around should each Boid look for its neighbors + separation: What's the minimum distance each Boid will attempt to + keep from any other + cohere, separate, match: factors for the relative importance of + the three drives.""" + super().__init__(seed=seed) + self.population = population + self.vision = vision + self.speed = speed + self.separation = separation + self.schedule = RandomActivation(self) + self.space = ContinuousSpace(width, height, True) + self.factors = {"cohere": cohere, "separate": separate, "match": match} + self.make_agents() + + def make_agents(self): + """ + Create self.population agents, with random positions and starting headings. + """ + for i in range(self.population): + x = self.random.random() * self.space.x_max + y = self.random.random() * self.space.y_max + pos = np.array((x, y)) + velocity = np.random.random(2) * 2 - 1 + boid = Boid( + i, + self, + pos, + self.speed, + velocity, + self.vision, + self.separation, + **self.factors, + ) + self.space.place_agent(boid, pos) + self.schedule.add(boid) + + def step(self): + self.schedule.step() + + +if __name__ == "__main__": + import time + + # model = BoidFlockers(15, 200, 100, 100, 5) + model = BoidFlockers(15, 400, 100, 100, 15) + + start_time = time.perf_counter() + for _ in range(100): + model.step() + + print(time.perf_counter() - start_time) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 5ff7674ac9b..624dd0b3b9a 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,4 +1,3 @@ -import random from mesa import Agent, Model from mesa.space import SingleGrid @@ -61,8 +60,8 @@ def __init__( # the coordinates of a cell as well as # its contents. (coord_iter) for _cont, pos in self.grid.coord_iter(): - if self.random.random() < self.density: # noqa: S311 - agent_type = 1 if self.random.random() < self.minority_pc else 0 # noqa: S311 + if self.random.random() < self.density: + agent_type = 1 if self.random.random() < self.minority_pc else 0 agent = SchellingAgent(self.next_id(), self, agent_type) self.grid.place_agent(agent, pos) self.schedule.add(agent) @@ -75,7 +74,7 @@ def step(self): self.schedule.step() -if __name__ == '__main__': +if __name__ == "__main__": import time # model = Schelling(15, 40, 40, 3, 1, 0.625) @@ -85,4 +84,4 @@ def step(self): for _ in range(100): model.step() - print(time.perf_counter() - start_time) \ No newline at end of file + print(time.perf_counter() - start_time) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index e69de29bb2d..18b86ab19ba 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -0,0 +1,14 @@ +from wolf_sheep import WolfSheep + +if __name__ == "__main__": + # for profiling this benchmark model + import time + + # model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20) + model = WolfSheep(15, 100, 100, 1000, 500, 0.4, 0.2, 20) + + start_time = time.perf_counter() + for _ in range(100): + model.step() + + print(time.perf_counter() - start_time) diff --git a/benchmarks/configurations.py b/benchmarks/configurations.py index c3e4b362985..c42f69adfb6 100644 --- a/benchmarks/configurations.py +++ b/benchmarks/configurations.py @@ -1,4 +1,4 @@ -from Flocking.Flocking import BoidFlockers +from Flocking.flocking import BoidFlockers from Schelling.schelling import Schelling from WolfSheep.wolf_sheep import WolfSheep diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index 38aa2ff7913..0f8d5379732 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -7,7 +7,7 @@ # making sure we use this version of mesa and not one # also installed in site_packages or so. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) from configurations import configurations From 1b5661e8843567b749fb78f93812117794185aac Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 19:13:00 +0100 Subject: [PATCH 15/20] Update benchmarks/WolfSheep/__init__.py --- benchmarks/WolfSheep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index 18b86ab19ba..98b1e9fdfed 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -1,4 +1,4 @@ -from wolf_sheep import WolfSheep +from .wolf_sheep import WolfSheep if __name__ == "__main__": # for profiling this benchmark model From dcf1e50fa631ce4f942f8c903afc89a50ebd076b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 26 Jan 2024 09:16:10 +0100 Subject: [PATCH 16/20] shuffle update --- mesa/agent.py | 27 ++++++++++++++++++--------- tests/test_agent.py | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index fcf643d3289..7ca00b04a78 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -178,7 +178,7 @@ def agent_generator(filter_func=None, agent_type=None, n=0): return AgentSet(agents, self.model) if not inplace else self._update(agents) - def shuffle(self, inplace: bool = False) -> AgentSet: + def shuffle(self, inplace: bool = True) -> AgentSet: """ Randomly shuffle the order of agents in the AgentSet. @@ -188,14 +188,23 @@ def shuffle(self, inplace: bool = False) -> AgentSet: Returns: AgentSet: A shuffled AgentSet. Returns the current AgentSet if inplace is True. """ - shuffled_agents = list(self) - self.random.shuffle(shuffled_agents) - - return ( - AgentSet(shuffled_agents, self.model) - if not inplace - else self._update(shuffled_agents) - ) + weakrefs = list(self._agents.keyrefs()) + self.random.shuffle(weakrefs) + + if inplace: + self._agents.data = {entry:None for entry in weakrefs} + return self + else: + return AgentSet((agent for ref in weakrefs if (agent:=ref()) is not None), self.model) + + # shuffled_agents = list(self) + # self.random.shuffle(shuffled_agents) + # + # return ( + # AgentSet(shuffled_agents, self.model) + # if not inplace + # else self._update(shuffled_agents) + # ) def sort( self, diff --git a/tests/test_agent.py b/tests/test_agent.py index 5861038d793..eb282308a64 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -66,7 +66,7 @@ def test_function(agent): assert all(a1 == a2 for a1, a2 in zip(agentset.select(), agentset)) assert all(a1 == a2 for a1, a2 in zip(agentset.select(n=5), agentset[:5])) - assert len(agentset.shuffle().select(n=5)) == 5 + assert len(agentset.shuffle(inplace=False).select(n=5)) == 5 def test_function(agent): return agent.unique_id From d1963863d5c0948103e9b74845cb96344eedd118 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 10:33:22 +0100 Subject: [PATCH 17/20] switch to walrus operator in do --- mesa/agent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index c5c2d9550fb..2461cb2d247 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -196,7 +196,7 @@ def shuffle(self, inplace: bool = True) -> AgentSet: self._agents.data = {entry:None for entry in weakrefs} return self else: - return AgentSet((agent for ref in weakrefs if (agent:=ref()) is not None), self.model) + return AgentSet((agent for ref in weakrefs if (agent := ref()) is not None), self.model) # shuffled_agents = list(self) # self.random.shuffle(shuffled_agents) @@ -260,9 +260,9 @@ def do( """ # we iterate over the actual weakref keys and check if weakref is alive before calling the method res = [ - getattr(agentref(), method_name)(*args, **kwargs) + getattr(agent, method_name)(*args, **kwargs) for agentref in self._agents.keyrefs() - if agentref() + if (agent := agentref()) is not None ] return res if return_results else self From 4ef66debb2bd0f4585d1fd50ad237b4b2c4d8762 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 27 Jan 2024 09:43:59 +0000 Subject: [PATCH 18/20] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Schelling/schelling.py | 5 +++-- mesa/agent.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 624dd0b3b9a..3f9e7567e45 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,4 +1,3 @@ - from mesa import Agent, Model from mesa.space import SingleGrid from mesa.time import RandomActivation @@ -22,7 +21,9 @@ def __init__(self, unique_id, model, agent_type): def step(self): similar = 0 - for neighbor in self.model.grid.iter_neighbors(self.pos, moore=True, radius=self.model.radius): + for neighbor in self.model.grid.iter_neighbors( + self.pos, moore=True, radius=self.model.radius + ): if neighbor.type == self.type: similar += 1 diff --git a/mesa/agent.py b/mesa/agent.py index 2461cb2d247..0d2deeb8c45 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -193,10 +193,12 @@ def shuffle(self, inplace: bool = True) -> AgentSet: self.random.shuffle(weakrefs) if inplace: - self._agents.data = {entry:None for entry in weakrefs} + self._agents.data = {entry: None for entry in weakrefs} return self else: - return AgentSet((agent for ref in weakrefs if (agent := ref()) is not None), self.model) + return AgentSet( + (agent for ref in weakrefs if (agent := ref()) is not None), self.model + ) # shuffled_agents = list(self) # self.random.shuffle(shuffled_agents) From f910106ad6c48268a424fe3fe83aac7bb1f23396 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 11:53:36 +0100 Subject: [PATCH 19/20] revert inplace to False --- mesa/agent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mesa/agent.py b/mesa/agent.py index 2461cb2d247..e272d1b52c5 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -179,7 +179,7 @@ def agent_generator(filter_func=None, agent_type=None, n=0): return AgentSet(agents, self.model) if not inplace else self._update(agents) - def shuffle(self, inplace: bool = True) -> AgentSet: + def shuffle(self, inplace: bool = False) -> AgentSet: """ Randomly shuffle the order of agents in the AgentSet. @@ -188,6 +188,10 @@ def shuffle(self, inplace: bool = True) -> AgentSet: Returns: AgentSet: A shuffled AgentSet. Returns the current AgentSet if inplace is True. + + Note: + Using inplace = True is more performant + """ weakrefs = list(self._agents.keyrefs()) self.random.shuffle(weakrefs) From 9710914f2fc1cc89b8a2512485e598b6a0d0d53c Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 14:07:14 +0100 Subject: [PATCH 20/20] remove old code --- mesa/agent.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index bab16aeabb1..243a187337b 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -204,15 +204,6 @@ def shuffle(self, inplace: bool = False) -> AgentSet: (agent for ref in weakrefs if (agent := ref()) is not None), self.model ) - # shuffled_agents = list(self) - # self.random.shuffle(shuffled_agents) - # - # return ( - # AgentSet(shuffled_agents, self.model) - # if not inplace - # else self._update(shuffled_agents) - # ) - def sort( self, key: Callable[[Agent], Any] | str,