From 3270602ab9c0df2a1c6c04a6125948467d8cd09a Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 9 Nov 2024 16:34:04 +0100 Subject: [PATCH] Remove deprecated time module The time module was already deprecated, this PR removes it. --- docs/apis/time.md | 7 - docs/mesa.md | 10 - mesa/__init__.py | 2 - mesa/time.py | 391 --------------------------------- tests/test_import_namespace.py | 5 - tests/test_time.py | 317 -------------------------- 6 files changed, 732 deletions(-) delete mode 100644 docs/apis/time.md delete mode 100644 mesa/time.py delete mode 100644 tests/test_time.py diff --git a/docs/apis/time.md b/docs/apis/time.md deleted file mode 100644 index afcbfa3f1be..00000000000 --- a/docs/apis/time.md +++ /dev/null @@ -1,7 +0,0 @@ -# Time - -```{eval-rst} -.. automodule:: mesa.time - :members: - :inherited-members: -``` diff --git a/docs/mesa.md b/docs/mesa.md index 512bb23f208..ab363b89126 100644 --- a/docs/mesa.md +++ b/docs/mesa.md @@ -56,16 +56,6 @@ :show-inheritance: ``` -## mesa.time module - -```{eval-rst} -.. automodule:: mesa.time - :members: - :undoc-members: - :show-inheritance: - -``` - ## Module contents ```{eval-rst} diff --git a/mesa/__init__.py b/mesa/__init__.py index 966c5838255..db794ed40a6 100644 --- a/mesa/__init__.py +++ b/mesa/__init__.py @@ -7,7 +7,6 @@ import mesa.experimental as experimental import mesa.space as space -import mesa.time as time from mesa.agent import Agent from mesa.batchrunner import batch_run from mesa.datacollection import DataCollector @@ -16,7 +15,6 @@ __all__ = [ "Model", "Agent", - "time", "space", "DataCollector", "batch_run", diff --git a/mesa/time.py b/mesa/time.py deleted file mode 100644 index 271dcf9f394..00000000000 --- a/mesa/time.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Mesa Time Module. - -.. warning:: - The time module and all its Schedulers are deprecated and will be removed in Mesa 3.1. - They can be replaced with AgentSet functionality. See the migration guide for details: - https://mesa.readthedocs.io/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. - -The activation order can have a serious impact on model behavior, so it's -important to specify it explicitly. Example simple activation regimes include -activating all agents in the same order every step, shuffling the activation -order every time, activating each agent *on average* once per step, and more. - -Key concepts: - Step: Many models advance in 'steps'. A step may involve the activation of - all agents, or a random (or selected) subset of them. Each agent in turn - may have their own step() method. - - Time: Some models may simulate a continuous 'clock' instead of discrete - steps. However, by default, the Time is equal to the number of steps the - model has taken. -""" - -# Mypy; for the `|` operator purpose -# Remove this __future__ import once the oldest supported Python is 3.10 -from __future__ import annotations - -import warnings -from collections import defaultdict -from collections.abc import Iterable - -# mypy -from mesa.agent import Agent, AgentSet -from mesa.model import Model - -# BaseScheduler has a self.time of int, while -# StagedActivation has a self.time of float -TimeT = float | int - - -class BaseScheduler: - """A simple scheduler that activates agents one at a time, in the order they were added. - - This scheduler is designed to replicate the behavior of the scheduler in MASON, a multi-agent simulation toolkit. - It assumes that each agent added has a `step` method which takes no arguments and executes the agent's actions. - - Attributes: - model (Model): The model instance associated with the scheduler. - steps (int): The number of steps the scheduler has taken. - time (TimeT): The current time in the simulation. Can be an integer or a float. - - """ - - def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None: - """Create a new BaseScheduler. - - Args: - model (Model): The model to which the schedule belongs - 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 Mesa 3.1. " - "They can be replaced with AgentSet functionality. See the migration guide for details. " - "https://mesa.readthedocs.io/latest/migration_guide.html#time-and-schedulers", - DeprecationWarning, - stacklevel=2, - ) - - self.model = model - self.steps = 0 - self.time: TimeT = 0 - - if agents is None: - agents = [] - - self._agents: AgentSet = AgentSet(agents, model.random) - - self._remove_warning_given = False - self._agents_key_warning_given = False - - def add(self, agent: Agent) -> None: - """Add an Agent object to the schedule. - - Args: - agent (Agent): An Agent to be added to the schedule. - """ - if agent not in self._agents: - self._agents.add(agent) - else: - raise ValueError("agent already added to scheduler") - - def remove(self, agent: Agent) -> None: - """Remove all instances of a given agent from the schedule. - - Args: - agent: An `Agent` instance. - - Note: - It is only necessary to explicitly remove agents from the schedule if - the agent is not removed from the model. - - """ - self._agents.remove(agent) - - def step(self) -> None: - """Execute the step of all the agents, one at a time.""" - # To be able to remove and/or add agents during stepping - # it's necessary for the keys view to be a list. - self.do_each("step") - self.steps += 1 - self.time += 1 - - def get_agent_count(self) -> int: - """Returns the current number of agents in the queue.""" - return len(self._agents) - - @property - def agents(self) -> AgentSet: - """Return agents in the scheduler.""" - # a bit dirty, but returns a copy of the internal agent set - return self._agents.select() - - def get_agent_keys(self, shuffle: bool = False) -> list[int]: - """Deprecated.""" - # To be able to remove and/or add agents during stepping - # it's necessary to cast the keys view to a list. - - if not self._agents_key_warning_given: - self._agents_key_warning_given = True - warnings.warn( - "Because of the shift to using weakrefs, this method will be removed in a future version", - DeprecationWarning, - stacklevel=2, - ) - - agent_keys = [agent.unique_id for agent in self._agents] - if shuffle: - self.model.random.shuffle(agent_keys) - return agent_keys - - def do_each(self, method, shuffle=False): - """Perform `method` on each agent. - - Args: - method: method to call - shuffle: shuffle the agents or not prior to calling method - - - """ - if shuffle: - self._agents.shuffle(inplace=True) - self._agents.do(method) - - -class RandomActivation(BaseScheduler): - """A scheduler that activates each agent once per step, in a random order, with the order reshuffled each step. - - This scheduler is equivalent to the NetLogo 'ask agents...' behavior and is a common default for ABMs. - It assumes that all agents have a `step` method. - - The random activation ensures that no single agent or sequence of agents consistently influences the model due - to ordering effects, which is crucial for certain types of simulations. - - Inherits all attributes and methods from BaseScheduler. - - """ - - def step(self) -> None: - """Executes the step of all agents, one at a time, in random order.""" - self.do_each("step", shuffle=True) - self.steps += 1 - self.time += 1 - - -class SimultaneousActivation(BaseScheduler): - """A scheduler that simulates the simultaneous activation of all agents. - - This scheduler is unique in that it requires agents to have both `step` and `advance` methods. - - The `step` method is for activating the agent and staging any changes without applying them immediately. - - The `advance` method then applies these changes, simulating simultaneous action. - - This scheduler is useful in scenarios where the interactions between agents are sensitive to the order - of execution, and a quasi-simultaneous execution is more realistic. - - Inherits all attributes and methods from BaseScheduler. - - """ - - def step(self) -> None: - """Step all agents, then advance them.""" - self.do_each("step") - # do_each recomputes the agent_keys from scratch whenever it is called. - # It can handle the case when some agents might have been removed in - # the previous loop. - self.do_each("advance") - self.steps += 1 - self.time += 1 - - -class StagedActivation(BaseScheduler): - """A scheduler allowing agent activation to be divided into several stages. - - All agents executing one stage before moving on to the next. This class is a generalization of SimultaneousActivation. - - This scheduler is useful for complex models where actions need to be broken down into distinct phases - for each agent in each time step. Agents must implement methods for each defined stage. - - The scheduler also tracks steps and time separately, allowing fractional time increments based on the number - of stages. Time advances in fractional increments of 1 / (# of stages), meaning that 1 step = 1 unit of time. - - Inherits all attributes and methods from BaseScheduler. - - Attributes: - - stage_list (list[str]): A list of stage names that define the order of execution. - - shuffle (bool): Determines whether to shuffle the order of agents each step. - - shuffle_between_stages (bool): Determines whether to shuffle agents between each stage. - - """ - - def __init__( - self, - model: Model, - agents: Iterable[Agent] | None = None, - stage_list: list[str] | None = None, - shuffle: bool = False, - shuffle_between_stages: bool = False, - ) -> None: - """Create an empty Staged Activation schedule. - - Args: - model (Model): The model to which the schedule belongs - agents (Iterable[Agent], None, optional): An iterable of agents who are controlled by the schedule - stage_list (:obj:`list` of :obj:`str`): List of strings of names of stages to run, in the - order to run them in. - shuffle (bool, optional): If True, shuffle the order of agents each step. - shuffle_between_stages (bool, optional): If True, shuffle the agents after each - stage; otherwise, only shuffle at the start - of each step. - """ - super().__init__(model, agents) - self.stage_list = stage_list if stage_list else ["step"] - self.shuffle = shuffle - self.shuffle_between_stages = shuffle_between_stages - self.stage_time = 1 / len(self.stage_list) - - def step(self) -> None: - """Executes all the stages for all agents.""" - shuffle = self.shuffle - for stage in self.stage_list: - if stage.startswith("model."): - getattr(self.model, stage[6:])() - else: - self.do_each(stage, shuffle=shuffle) - - shuffle = self.shuffle_between_stages - self.time += self.stage_time - - self.steps += 1 - - -class RandomActivationByType(BaseScheduler): - """A scheduler that activates each type of agent once per step, in random order, with the order reshuffled every step. - - This scheduler is useful for models with multiple types of agents, ensuring that each type is treated - equitably in terms of activation order. The randomness in activation order helps in reducing biases - due to ordering effects. - - Inherits all attributes and methods from BaseScheduler. - - If you want to do some computations / data collections specific to an agent - type, you can either: - - loop through all agents, and filter by their type - - access via `your_model.scheduler.agents_by_type[your_type_class]` - - Attributes: - - agents_by_type (defaultdict): A dictionary mapping agent types to dictionaries of agents. - - """ - - @property - def agents_by_type(self): # noqa: D102 - 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. Future behavior is available via RandomActivationByType._agents_by_type", - DeprecationWarning, - stacklevel=2, - ) - - agentsbytype = defaultdict(dict) - for k, v in self._agents_by_type.items(): - agentsbytype[k] = {agent.unique_id: agent for agent in v} - - return agentsbytype - - def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None: - """Initialize RandomActivationByType instance. - - Args: - model (Model): The model to which the schedule belongs - agents (Iterable[Agent], None, optional): An iterable of agents who are controlled by the schedule - """ - super().__init__(model, agents) - # can't be a defaultdict because we need to pass model to 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) - except KeyError: - self._agents_by_type[type(agent)] = AgentSet( - [agent], self.model.random - ) - - def add(self, agent: Agent) -> None: - """Add an Agent object to the schedule. - - Args: - agent: An Agent to be added to the schedule. - """ - super().add(agent) - - try: - self._agents_by_type[type(agent)].add(agent) - except KeyError: - self._agents_by_type[type(agent)] = AgentSet([agent], self.model.random) - - def remove(self, agent: Agent) -> None: - """Remove all instances of a given agent from the schedule. - - Args: - agent: An Agent to be removed from the schedule. - - """ - super().remove(agent) - self._agents_by_type[type(agent)].remove(agent) - - def step(self, shuffle_types: bool = True, shuffle_agents: bool = True) -> None: - """Executes the step of each agent type, one at a time, in random order. - - Args: - shuffle_types: If True, the order of execution of each types is shuffled. - shuffle_agents: If True, the order of execution of each agents in a type group is shuffled. - """ - # 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()) - if shuffle_types: - self.model.random.shuffle(type_keys) - for agent_class in type_keys: - self.step_type(agent_class, shuffle_agents=shuffle_agents) - self.steps += 1 - self.time += 1 - - def step_type(self, agenttype: type[Agent], shuffle_agents: bool = True) -> None: - """Shuffle order and run all agents of a given type. - - Args: - agenttype: Class object of the type to run. - shuffle_agents: If True, shuffle agents - """ - agents = self._agents_by_type[agenttype] - - if shuffle_agents: - agents.shuffle(inplace=True) - agents.do("step") - - 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]) - - -class DiscreteEventScheduler(BaseScheduler): - """This class has been removed and replaced by the functionality provided by experimental.devs.""" - - def __init__(self, model: Model, time_step: TimeT = 1) -> None: - """Initialize DiscreteEventScheduler. - - Args: - model (Model): The model to which the schedule belongs - time_step (TimeT): The fixed time step between steps - - """ - super().__init__(model) - raise Exception( - "DiscreteEventScheduler is removed in favor of the functionality provided by experimental.devs" - ) diff --git a/tests/test_import_namespace.py b/tests/test_import_namespace.py index 449727b95d8..ec52cb6b420 100644 --- a/tests/test_import_namespace.py +++ b/tests/test_import_namespace.py @@ -7,11 +7,6 @@ def test_import(): See https://github.com/projectmesa/mesa/pull/1294. """ import mesa - from mesa.time import RandomActivation - - _ = mesa.time.RandomActivation - _ = RandomActivation - from mesa.space import MultiGrid _ = mesa.space.MultiGrid diff --git a/tests/test_time.py b/tests/test_time.py deleted file mode 100644 index f180ac25465..00000000000 --- a/tests/test_time.py +++ /dev/null @@ -1,317 +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, seed=None - ): - """Creates a Model instance with a schedule. - - Args: - shuffle (Bool): whether or not 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: whether or not to enable kill_other_agent - seed : rng - """ - super().__init__(seed=seed) - 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) - a = mock.Mock() - model.schedule._agents.random = a - assert a.shuffle.call_count == 0 - model.step() - assert a.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 the staged activation can remove 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) - a = mock.Mock() - model.schedule._agents.random = a - model.schedule.step() - assert a.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 the random activation can remove an agent in a step 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) - a = mock.Mock() - model.random = a - for agentset in model.schedule._agents_by_type.values(): - agentset.random = a - model.schedule.step() - assert a.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()