From a686826b341f2dacd4a14f15abdb63ca6c009798 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 21 Sep 2024 13:11:34 +0200 Subject: [PATCH] Deprecate Time module and all its Schedulers (#2306) As concluded in #2231, this PR deprecates the whole Time module and all its Schedulers. This change aims to provide more flexibility and control in agent activation and time management, inspired by the NetLogo framework. ### Changes 1. Deprecated the entire `mesa.time` module. 2. Added a module-level deprecation warning to `mesa.time`. 3. Updated the migration guide to include instructions for replacing each scheduler with AgentSet functionality. 4. Removed the `tests/test_time.py` file as it's no longer relevant. ### Migration Guide The migration guide has been updated to include detailed instructions on how to replace each scheduler with AgentSet functionality. This includes examples for: - BaseScheduler - RandomActivation - SimultaneousActivation - StagedActivation - RandomActivationByType The guide also includes general notes on changes to related functionality, such as the automatic incrementing of `Model.steps` and the replacement of scheduler-specific methods with AgentSet methods. --- docs/migration_guide.md | 99 ++++++++++++- mesa/time.py | 13 ++ tests/test_time.py | 301 ---------------------------------------- 3 files changed, 111 insertions(+), 302 deletions(-) delete mode 100644 tests/test_time.py diff --git a/docs/migration_guide.md b/docs/migration_guide.md index c8c06860055..8eca2e398e5 100644 --- a/docs/migration_guide.md +++ b/docs/migration_guide.md @@ -99,8 +99,105 @@ You can access it by `Model.steps`, and it's internally in the datacollector, ba #### Removal of `Model._advance_time()` - The `Model._advance_time()` method is removed. This now happens automatically. - +### Replacing Schedulers with AgentSet functionality +The whole Time module in Mesa is deprecated, and all schedulers are being replaced with AgentSet functionality and the internal `Model.steps` counter. This allows much more flexibility in how to activate Agents and makes it explicit what's done exactly. +Here's how to replace each scheduler: + +#### BaseScheduler +Replace: +```python +self.schedule = BaseScheduler(self) +self.schedule.step() +``` +With: +```python +self.agents.do("step") +``` + +#### RandomActivation +Replace: +```python +self.schedule = RandomActivation(self) +self.schedule.step() +``` +With: +```python +self.agents.shuffle_do("step") +``` + +#### SimultaneousActivation +Replace: +```python +self.schedule = SimultaneousActivation(self) +self.schedule.step() +``` +With: +```python +self.agents.do("step") +self.agents.do("advance") +``` + +#### StagedActivation +Replace: +```python +self.schedule = StagedActivation(self, ["stage1", "stage2", "stage3"]) +self.schedule.step() +``` +With: +```python +for stage in ["stage1", "stage2", "stage3"]: + self.agents.do(stage) +``` + +If you were using the `shuffle` and/or `shuffle_between_stages` options: +```python +stages = ["stage1", "stage2", "stage3"] +if shuffle: + self.random.shuffle(stages) +for stage in stages: + if shuffle_between_stages: + self.agents.shuffle_do(stage) + else: + self.agents.do(stage) +``` + +#### RandomActivationByType +Replace: +```python +self.schedule = RandomActivationByType(self) +self.schedule.step() +``` +With: +```python +for agent_class in self.agent_types: + self.agents_by_type[agent_class].shuffle_do("step") +``` + +##### Replacing `step_type` +The `RandomActivationByType` scheduler had a `step_type` method that allowed stepping only agents of a specific type. To replicate this functionality using AgentSet: + +Replace: +```python +self.schedule.step_type(AgentType) +``` + +With: +```python +self.agents_by_type[AgentType].shuffle_do("step") +``` + +#### General Notes + +1. The `Model.steps` counter is now automatically incremented. You don't need to manage it manually. +2. If you were using `self.schedule.agents`, replace it with `self.agents`. +3. If you were using `self.schedule.get_agent_count()`, replace it with `len(self.agents)`. +4. If you were using `self.schedule.agents_by_type`, replace it with `self.agents_by_type`. +5. Instead of `self.schedule.add()` and `self.schedule.remove()`, agents are now automatically added to and removed from the model's AgentSet when they are created or removed. + +From now on you're now not bound by 5 distinct schedulers, but can mix and match any combination of AgentSet methods (`do`, `shuffle`, `select`, etc.) to get the desired Agent activation. + +Ref: Original discussion [#1912](https://github.com/projectmesa/mesa/discussions/1912), decision discussion [#2231](https://github.com/projectmesa/mesa/discussions/2231), example updates [#183](https://github.com/projectmesa/mesa-examples/pull/183) and [#201](https://github.com/projectmesa/mesa-examples/pull/201), PR [#2306](https://github.com/projectmesa/mesa/pull/2306) ### Visualisation diff --git a/mesa/time.py b/mesa/time.py index 82de81859ac..5ce5f017fd8 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -1,5 +1,10 @@ """Mesa Time Module. +.. warning:: + The time module and all its Schedulers are deprecated and will be removed in a future version. + They can be replaced with AgentSet functionality. See the migration guide for details: + https://mesa.readthedocs.io/en/latest/migration_guide.html#time-and-schedulers + Objects for handling the time component of a model. In particular, this module contains Schedulers, which handle agent activation. A Scheduler is an object which controls when agents are called upon to act, and when. @@ -57,6 +62,14 @@ def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None: agents (Iterable[Agent], None, optional): An iterable of agents who are controlled by the schedule """ + warnings.warn( + "The time module and all its Schedulers are deprecated and will be removed in a future version. " + "They can be replaced with AgentSet functionality. See the migration guide for details. " + "https://mesa.readthedocs.io/en/latest/migration_guide.html#time-and-schedulers", + DeprecationWarning, + stacklevel=2, + ) + self.model = model self.steps = 0 self.time: TimeT = 0 diff --git a/tests/test_time.py b/tests/test_time.py deleted file mode 100644 index 3d0b1f38f11..00000000000 --- a/tests/test_time.py +++ /dev/null @@ -1,301 +0,0 @@ -"""Test the advanced schedulers.""" - -import unittest -from unittest import TestCase, mock - -from mesa.agent import Agent -from mesa.model import Model -from mesa.time import ( - BaseScheduler, - RandomActivation, - RandomActivationByType, - SimultaneousActivation, - StagedActivation, -) - -RANDOM = "random" -STAGED = "staged" -SIMULTANEOUS = "simultaneous" -RANDOM_BY_TYPE = "random_by_type" - - -class MockAgent(Agent): - """Minimalistic agent for testing purposes.""" - - def __init__(self, model): # noqa: D107 - super().__init__(model) - self.steps = 0 - self.advances = 0 - - def kill_other_agent(self): # noqa: D102 - for agent in self.model.schedule.agents: - if agent is not self: - agent.remove() - - def stage_one(self): # noqa: D102 - if self.model.enable_kill_other_agent: - self.kill_other_agent() - self.model.log.append(f"{self.unique_id}_1") - - def stage_two(self): # noqa: D102 - self.model.log.append(f"{self.unique_id}_2") - - def advance(self): # noqa: D102 - self.advances += 1 - - def step(self): # noqa: D102 - if self.model.enable_kill_other_agent: - self.kill_other_agent() - self.steps += 1 - self.model.log.append(self.unique_id) - - -class MockModel(Model): # noqa: D101 - def __init__(self, shuffle=False, activation=STAGED, enable_kill_other_agent=False): - """Creates a Model instance with a schedule. - - Args: - shuffle (Bool): whether to instantiate a scheduler - with shuffling. This option is only used for - StagedActivation schedulers. - activation (str): which kind of scheduler to use. - 'random' creates a RandomActivation scheduler. - 'staged' creates a StagedActivation scheduler. - The default scheduler is a BaseScheduler. - enable_kill_other_agent (bool): whether to enable killing of other agents - """ - super().__init__() - self.log = [] - self.enable_kill_other_agent = enable_kill_other_agent - - # Make scheduler - if activation == STAGED: - model_stages = ["stage_one", "model.model_stage", "stage_two"] - self.schedule = StagedActivation( - self, stage_list=model_stages, shuffle=shuffle - ) - elif activation == RANDOM: - self.schedule = RandomActivation(self) - elif activation == SIMULTANEOUS: - self.schedule = SimultaneousActivation(self) - elif activation == RANDOM_BY_TYPE: - self.schedule = RandomActivationByType(self) - else: - self.schedule = BaseScheduler(self) - - # Make agents - for _ in range(2): - agent = MockAgent(self) - self.schedule.add(agent) - - def step(self): # noqa: D102 - self.schedule.step() - - def model_stage(self): # noqa: D102 - self.log.append("model_stage") - - -class TestStagedActivation(TestCase): - """Test the staged activation.""" - - expected_output = ["1_1", "1_1", "model_stage", "1_2", "1_2"] - - def test_no_shuffle(self): - """Testing the staged activation without shuffling.""" - model = MockModel(shuffle=False) - model.step() - model.step() - assert all(i == j for i, j in zip(model.log[:5], model.log[5:])) - - def test_shuffle(self): - """Test the staged activation with shuffling.""" - model = MockModel(shuffle=True) - model.step() - for output in self.expected_output[:2]: - assert output in model.log[:2] - for output in self.expected_output[3:]: - assert output in model.log[3:] - assert self.expected_output[2] == model.log[2] - - def test_shuffle_shuffles_agents(self): # noqa: D102 - model = MockModel(shuffle=True) - model.random = mock.Mock() - assert model.random.shuffle.call_count == 0 - model.step() - assert model.random.shuffle.call_count == 1 - - def test_remove(self): - """Test the staged activation can remove an agent.""" - model = MockModel(shuffle=True) - agents = list(model.schedule._agents) - agent = agents[0] - model.schedule.remove(agents[0]) - assert agent not in model.schedule.agents - - def test_intrastep_remove(self): - """Test removing an agent in a step of another agent so that the one removed doesn't step.""" - model = MockModel(shuffle=True, enable_kill_other_agent=True) - model.step() - assert len(model.log) == 3 - - def test_add_existing_agent(self): # noqa: D102 - model = MockModel() - agent = model.schedule.agents[0] - with self.assertRaises(Exception): - model.schedule.add(agent) - - -class TestRandomActivation(TestCase): - """Test the random activation.""" - - def test_init(self): # noqa: D102 - model = Model() - agents = [MockAgent(model) for _ in range(10)] - - scheduler = RandomActivation(model, agents) - assert all(agent in scheduler.agents for agent in agents) - - def test_random_activation_step_shuffles(self): - """Test the random activation step.""" - model = MockModel(activation=RANDOM) - model.random = mock.Mock() - model.schedule.step() - assert model.random.shuffle.call_count == 1 - - def test_random_activation_step_increments_step_and_time_counts(self): - """Test the random activation step increments step and time counts.""" - model = MockModel(activation=RANDOM) - assert model.schedule.steps == 0 - assert model.schedule.time == 0 - model.schedule.step() - assert model.schedule.steps == 1 - assert model.schedule.time == 1 - - def test_random_activation_step_steps_each_agent(self): - """Test the random activation step causes each agent to step.""" - model = MockModel(activation=RANDOM) - model.step() - agent_steps = [i.steps for i in model.schedule.agents] - # one step for each of 2 agents - assert all(x == 1 for x in agent_steps) - - def test_intrastep_remove(self): - """Test removal an agent in astep of another agent so that the one removed doesn't step.""" - model = MockModel(activation=RANDOM, enable_kill_other_agent=True) - model.step() - assert len(model.log) == 1 - - def test_get_agent_keys(self): # noqa: D102 - model = MockModel(activation=RANDOM) - - keys = model.schedule.get_agent_keys() - agent_ids = [agent.unique_id for agent in model.agents] - assert all(entry_i == entry_j for entry_i, entry_j in zip(keys, agent_ids)) - - keys = model.schedule.get_agent_keys(shuffle=True) - agent_ids = {agent.unique_id for agent in model.agents} - assert all(entry in agent_ids for entry in keys) - - def test_not_sequential(self): # noqa: D102 - model = MockModel(activation=RANDOM) - # Create 10 agents - for _ in range(10): - model.schedule.add(MockAgent(model)) - # Run 3 steps - for _ in range(3): - model.step() - # Filter out non-integer elements from the log - filtered_log = [item for item in model.log if isinstance(item, int)] - - # Check that there are no 18 consecutive agents id's in the filtered log - total_agents = 10 - assert not any( - all( - (filtered_log[(i + j) % total_agents] - filtered_log[i]) % total_agents - == j % total_agents - for j in range(18) - ) - for i in range(len(filtered_log)) - ), f"Agents are activated sequentially:\n{filtered_log}" - - -class TestSimultaneousActivation(TestCase): - """Test the simultaneous activation.""" - - def test_simultaneous_activation_step_steps_and_advances_each_agent(self): - """Test the simultaneous activation step causes each agent to step.""" - model = MockModel(activation=SIMULTANEOUS) - model.step() - # one step for each of 2 agents - agent_steps = [i.steps for i in model.schedule.agents] - agent_advances = [i.advances for i in model.schedule.agents] - assert all(x == 1 for x in agent_steps) - assert all(x == 1 for x in agent_advances) - - -class TestRandomActivationByType(TestCase): - """Test the random activation by type. - - TODO implement at least 2 types of agents, and test that step_type only - does step for one type of agents, not the entire agents. - """ - - def test_init(self): # noqa: D102 - model = Model() - agents = [MockAgent(model) for _ in range(10)] - agents += [Agent(model) for _ in range(10)] - - scheduler = RandomActivationByType(model, agents) - assert all(agent in scheduler.agents for agent in agents) - - def test_random_activation_step_shuffles(self): - """Test the random activation by type step.""" - model = MockModel(activation=RANDOM_BY_TYPE) - model.random = mock.Mock() - model.schedule.step() - assert model.random.shuffle.call_count == 2 - - def test_random_activation_step_increments_step_and_time_counts(self): - """Test the random activation by type step increments step and time counts.""" - model = MockModel(activation=RANDOM_BY_TYPE) - assert model.schedule.steps == 0 - assert model.schedule.time == 0 - model.schedule.step() - assert model.schedule.steps == 1 - assert model.schedule.time == 1 - - def test_random_activation_step_steps_each_agent(self): - """Test the random activation by type step causes each agent to step.""" - model = MockModel(activation=RANDOM_BY_TYPE) - model.step() - agent_steps = [i.steps for i in model.schedule.agents] - # one step for each of 2 agents - assert all(x == 1 for x in agent_steps) - - def test_random_activation_counts(self): - """Test the random activation by type step causes each agent to step.""" - model = MockModel(activation=RANDOM_BY_TYPE) - - agent_types = model.agent_types - for agent_type in agent_types: - assert model.schedule.get_type_count(agent_type) == len( - model.agents_by_type[agent_type] - ) - - # def test_add_non_unique_ids(self): - # """ - # Test that adding agent with duplicate ids result in an error. - # TODO: we need to run this test on all schedulers, not just - # TODO:: identical IDs is something for the agent, not the scheduler and should be tested there - # RandomActivationByType. - # """ - # model = MockModel(activation=RANDOM_BY_TYPE) - # a = MockAgent(0, model) - # b = MockAgent(0, model) - # model.schedule.add(a) - # with self.assertRaises(Exception): - # model.schedule.add(b) - - -if __name__ == "__main__": - unittest.main()