diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index b5b077e4ac0..f787d0af835 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -32,9 +32,9 @@ def run_model(model_class, seed, parameters): start_init = timeit.default_timer() if model_class.__name__ in uses_simulator: simulator = ABMSimulator() - model = model_class(simulator=simulator, seed=seed, **parameters) + model = model_class(simulator=simulator, rng=seed, **parameters) else: - model = model_class(seed=seed, **parameters) + model = model_class(rng=seed, **parameters) end_init_start_run = timeit.default_timer() diff --git a/docs/tutorials/9_batch_run.ipynb b/docs/tutorials/9_batch_run.ipynb index 98ee83e09bd..5b585f59ba6 100644 --- a/docs/tutorials/9_batch_run.ipynb +++ b/docs/tutorials/9_batch_run.ipynb @@ -410,7 +410,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.5" + "version": "3.12.2" }, "widgets": { "state": {}, diff --git a/mesa/agent.py b/mesa/agent.py index 93cd6287528..36e5679082d 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Any, Literal, overload import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence if TYPE_CHECKING: # We ensure that these are not imported during runtime to prevent cyclic @@ -29,6 +30,10 @@ from mesa.model import Model from mesa.space import Position +from mesa.util import deprecate_kwarg + +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState + class Agent: """Base class for a model agent in Mesa. @@ -133,11 +138,16 @@ def __getitem__(self, i): instance_kwargs = {k: v[i] for k, v in listlike_kwargs.items()} agent = cls(model, *instance_args, **instance_kwargs) agents.append(agent) - return AgentSet(agents, random=model.random) + return AgentSet(agents, rng=model.rng) @property def random(self) -> Random: """Return a seeded stdlib rng.""" + warnings.warn( + "the use of random is deprecated, please use rng instead", + FutureWarning, + stacklevel=2, + ) return self.model.random @property @@ -169,32 +179,39 @@ class AgentSet(MutableSet, Sequence): """ + @deprecate_kwarg("random") def __init__( self, agents: Iterable[Agent], random: Random | None = None, + rng: SeedLike | None = None, ): """Initializes the AgentSet with a collection of agents and a reference to the model. Args: agents (Iterable[Agent]): An iterable of Agent objects to be included in the set. random (Random | np.random.Generator | None): the random number generator + rng (SeedLike | None): the random number generator """ self._agents = weakref.WeakKeyDictionary(dict.fromkeys(agents)) - if (len(self._agents) == 0) and random is None: + if (len(self._agents) == 0) and (random is None and rng is None): warnings.warn( "No Agents specified in creation of AgentSet and no random number generator specified. " "This can make models non-reproducible. Please pass a random number generator explicitly", UserWarning, stacklevel=2, ) - random = Random() + rng = np.random.default_rng() if random is not None: - self.random = random - else: - # all agents in an AgentSet should share the same model, just take it from first - self.random = self._agents.keys().__next__().model.random + rng = np.random.default_rng(random.getstate()[1]) + + if rng is None: + rng = self._agents.keys().__next__().model.rng + + # if rng is a np.random.Generator, it will just return it, + # if rng can be used to seed a generator, a new generator is created with this seed + self.rng = np.random.default_rng(rng) def __len__(self) -> int: """Return the number of agents in the AgentSet.""" @@ -254,7 +271,7 @@ def agent_generator(filter_func, agent_type, at_most): agents = agent_generator(filter_func, agent_type, at_most) - return AgentSet(agents, self.random) if not inplace else self._update(agents) + return AgentSet(agents, rng=self.rng) if not inplace else self._update(agents) def shuffle(self, inplace: bool = False) -> AgentSet: """Randomly shuffle the order of agents in the AgentSet. @@ -270,14 +287,16 @@ def shuffle(self, inplace: bool = False) -> AgentSet: """ weakrefs = list(self._agents.keyrefs()) - self.random.shuffle(weakrefs) + + self.rng.shuffle(weakrefs) if inplace: self._agents.data = dict.fromkeys(weakrefs) return self else: return AgentSet( - (agent for ref in weakrefs if (agent := ref()) is not None), self.random + (agent for ref in weakrefs if (agent := ref()) is not None), + rng=self.rng, ) def sort( @@ -302,7 +321,7 @@ def sort( sorted_agents = sorted(self._agents.keys(), key=key, reverse=not ascending) return ( - AgentSet(sorted_agents, self.random) + AgentSet(sorted_agents, rng=self.rng) if not inplace else self._update(sorted_agents) ) @@ -348,7 +367,7 @@ def shuffle_do(self, method: str | Callable, *args, **kwargs) -> AgentSet: It's a fast, optimized version of calling shuffle() followed by do(). """ weakrefs = list(self._agents.keyrefs()) - self.random.shuffle(weakrefs) + self.rng.shuffle(weakrefs) if isinstance(method, str): for ref in weakrefs: @@ -557,7 +576,7 @@ def __getstate__(self): Returns: dict: A dictionary representing the state of the AgentSet. """ - return {"agents": list(self._agents.keys()), "random": self.random} + return {"agents": list(self._agents.keys()), "rng": self.rng} def __setstate__(self, state): """Set the state of the AgentSet during deserialization. @@ -565,7 +584,7 @@ def __setstate__(self, state): Args: state (dict): A dictionary representing the state to restore. """ - self.random = state["random"] + self.rng = state["rng"] self._update(state["agents"]) def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy: @@ -599,9 +618,7 @@ def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy: groups[getattr(agent, by)].append(agent) if result_type == "agentset": - return GroupBy( - {k: AgentSet(v, random=self.random) for k, v in groups.items()} - ) + return GroupBy({k: AgentSet(v, rng=self.rng) for k, v in groups.items()}) else: return GroupBy(groups) diff --git a/mesa/discrete_space/cell.py b/mesa/discrete_space/cell.py index f8621ead808..65b85dea808 100644 --- a/mesa/discrete_space/cell.py +++ b/mesa/discrete_space/cell.py @@ -14,16 +14,22 @@ from __future__ import annotations +import warnings from functools import cache, cached_property from random import Random from typing import TYPE_CHECKING +import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence + from mesa.discrete_space.cell_agent import CellAgent from mesa.discrete_space.cell_collection import CellCollection +from mesa.util import deprecate_kwarg if TYPE_CHECKING: from mesa.agent import Agent +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState Coordinate = tuple[int, ...] @@ -45,14 +51,16 @@ class Cell: "connections", "coordinate", "properties", - "random", + "rng", ] + @deprecate_kwarg("random") def __init__( self, coordinate: Coordinate, capacity: int | None = None, random: Random | None = None, + rng: SeedLike | None = None, ) -> None: """Initialise the cell. @@ -60,6 +68,7 @@ def __init__( coordinate: coordinates of the cell capacity (int) : the capacity of the cell. If None, the capacity is infinite random (Random) : the random number generator to use + rng (SeedLike | None): the random number generator """ super().__init__() @@ -72,7 +81,18 @@ def __init__( self.properties: dict[ Coordinate, object ] = {} # fixme still used by voronoi mesh - self.random = random + + if random is None and rng is None: + warnings.warn( + "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly", + UserWarning, + stacklevel=2, + ) + rng = np.random.default_rng() + if random is not None: + rng = np.random.default_rng(random.getstate()[1]) + + self.rng = np.random.default_rng(rng) def connect(self, other: Cell, key: Coordinate | None = None) -> None: """Connects this cell to another cell. @@ -173,7 +193,7 @@ def get_neighborhood( """ return CellCollection[Cell]( self._neighborhood(radius=radius, include_center=include_center), - random=self.random, + rng=self.rng, ) # FIXME: Revisit caching strategy on methods diff --git a/mesa/discrete_space/cell_collection.py b/mesa/discrete_space/cell_collection.py index 049fb0eed4d..14845bfc071 100644 --- a/mesa/discrete_space/cell_collection.py +++ b/mesa/discrete_space/cell_collection.py @@ -22,10 +22,17 @@ from random import Random from typing import TYPE_CHECKING, TypeVar +import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence + +from mesa.util import deprecate_kwarg + if TYPE_CHECKING: from mesa.discrete_space.cell import Cell from mesa.discrete_space.cell_agent import CellAgent +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState + T = TypeVar("T", bound="Cell") @@ -35,7 +42,8 @@ class CellCollection[T: Cell]: Attributes: cells (List[Cell]): The list of cells this collection represents agents (List[CellAgent]) : List of agents occupying the cells in this collection - random (Random) : The random number generator + random (Random) : The random number generator, deprecated + rng (Random) : The random number generator Notes: A `UserWarning` is issued if `random=None`. You can resolve this warning by explicitly @@ -45,16 +53,19 @@ class CellCollection[T: Cell]: """ + @deprecate_kwarg("random") def __init__( self, cells: Mapping[T, list[CellAgent]] | Iterable[T], random: Random | None = None, + rng: SeedLike | None = None, ) -> None: """Initialize a CellCollection. Args: cells: cells to add to the collection random: a seeded random number generator. + rng (SeedLike | None): the random number generator """ if isinstance(cells, dict): self._cells = cells @@ -66,14 +77,17 @@ def __init__( next(iter(self._cells.keys())).capacity if self._cells else None ) - if random is None: + if random is None and rng is None: warnings.warn( "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly", UserWarning, stacklevel=2, ) - random = Random() - self.random = random + rng = np.random.default_rng() + if random is not None: + rng = np.random.default_rng(random.getstate()[1]) + + self.rng = np.random.default_rng(rng) def __iter__(self): # noqa return iter(self._cells) @@ -98,7 +112,7 @@ def agents(self) -> Iterable[CellAgent]: # noqa def select_random_cell(self) -> T: """Select a random cell.""" - return self.random.choice(self.cells) + return self.cells[self.rng.integers(0, len(self.cells))] def select_random_agent(self) -> CellAgent: """Select a random agent. @@ -108,7 +122,8 @@ def select_random_agent(self) -> CellAgent: """ - return self.random.choice(list(self.agents)) + agents = list(self.agents) + return agents[self.rng.integers(0, len(agents))] def select( self, @@ -142,4 +157,4 @@ def cell_generator(filter_func, at_most): yield cell count += 1 - return CellCollection(cell_generator(filter_func, at_most), random=self.random) + return CellCollection(cell_generator(filter_func, at_most), rng=self.rng) diff --git a/mesa/discrete_space/discrete_space.py b/mesa/discrete_space/discrete_space.py index 770a82938ac..959d98c58fd 100644 --- a/mesa/discrete_space/discrete_space.py +++ b/mesa/discrete_space/discrete_space.py @@ -20,10 +20,15 @@ from random import Random from typing import TypeVar +import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence + from mesa.agent import AgentSet from mesa.discrete_space.cell import Cell from mesa.discrete_space.cell_collection import CellCollection +from mesa.util import deprecate_kwarg +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState T = TypeVar("T", bound=Cell) @@ -45,11 +50,13 @@ class DiscreteSpace[T: Cell]: """ + @deprecate_kwarg("random") def __init__( self, capacity: int | None = None, cell_klass: type[T] = Cell, random: Random | None = None, + rng: SeedLike | None = None, ): """Instantiate a DiscreteSpace. @@ -57,18 +64,22 @@ def __init__( capacity: capacity of cells cell_klass: base class for all cells random: random number generator + rng (SeedLike | None): the random number generator """ super().__init__() self.capacity = capacity self._cells: dict[tuple[int, ...], T] = {} - if random is None: + if random is None and rng is None: warnings.warn( "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly", UserWarning, stacklevel=2, ) - random = Random() - self.random = random + rng = np.random.default_rng() + if random is not None: + rng = np.random.default_rng(random.getstate()[1]) + + self.rng = np.random.default_rng(rng) self.cell_klass = cell_klass self._empties: dict[tuple[int, ...], None] = {} @@ -80,7 +91,7 @@ def cutoff_empties(self): # noqa @property def agents(self) -> AgentSet: """Return an AgentSet with the agents in the space.""" - return AgentSet(self.all_cells.agents, random=self.random) + return AgentSet(self.all_cells.agents, rng=self.rng) def _connect_cells(self): ... def _connect_single_cell(self, cell: T): ... @@ -151,7 +162,7 @@ def remove_connection(self, cell1: T, cell2: T): def all_cells(self): """Return all cells in space.""" return CellCollection( - {cell: cell._agents for cell in self._cells.values()}, random=self.random + {cell: cell._agents for cell in self._cells.values()}, rng=self.rng ) def __iter__(self): # noqa @@ -167,7 +178,8 @@ def empties(self) -> CellCollection[T]: def select_random_empty_cell(self) -> T: """Select random empty cell.""" - return self.random.choice(list(self.empties)) + empties = list(self.empties) + return empties[self.rng.integers(0, len(empties))] def __setstate__(self, state): """Set the state of the discrete space and rebuild the connections.""" diff --git a/mesa/discrete_space/grid.py b/mesa/discrete_space/grid.py index 7b0c5f5a586..4e7f0efc527 100644 --- a/mesa/discrete_space/grid.py +++ b/mesa/discrete_space/grid.py @@ -19,12 +19,17 @@ from random import Random from typing import Any, TypeVar +import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence + from mesa.discrete_space import Cell, DiscreteSpace from mesa.discrete_space.property_layer import ( HasPropertyLayers, PropertyDescriptor, ) +from mesa.util import deprecate_kwarg +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState T = TypeVar("T", bound=Cell) @@ -81,12 +86,14 @@ def height(self) -> int: """Convenience access to the height of the grid.""" return self.dimensions[1] + @deprecate_kwarg("random") def __init__( self, dimensions: Sequence[int], torus: bool = False, capacity: float | None = None, random: Random | None = None, + rng: SeedLike | None = None, cell_klass: type[T] = Cell, ) -> None: """Initialise the grid class. @@ -96,9 +103,12 @@ def __init__( torus: whether the space wraps capacity: capacity of the grid cell random: a random number generator + rng (SeedLike | None): the random number generator cell_klass: the base class to use for the cells """ - super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) + super().__init__( + capacity=capacity, rng=rng, random=random, cell_klass=cell_klass + ) self.torus = torus self.dimensions = dimensions self._try_random = True @@ -116,7 +126,7 @@ def __init__( coordinates = product(*(range(dim) for dim in self.dimensions)) self._cells = { - coord: self.cell_klass(coord, capacity, random=self.random) + coord: self.cell_klass(coord, capacity, rng=self.rng) for coord in coordinates } self._connect_cells() diff --git a/mesa/discrete_space/network.py b/mesa/discrete_space/network.py index 3fc0bc2ff14..d42f5f4e20b 100644 --- a/mesa/discrete_space/network.py +++ b/mesa/discrete_space/network.py @@ -15,18 +15,26 @@ from random import Random from typing import Any +import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence + from mesa.discrete_space.cell import Cell from mesa.discrete_space.discrete_space import DiscreteSpace +from mesa.util import deprecate_kwarg + +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState class Network(DiscreteSpace[Cell]): """A networked discrete space.""" + @deprecate_kwarg("random") def __init__( self, G: Any, # noqa: N803 capacity: int | None = None, random: Random | None = None, + rng: SeedLike | None = None, cell_klass: type[Cell] = Cell, ) -> None: """A Networked grid. @@ -35,16 +43,17 @@ def __init__( G: a NetworkX Graph instance. capacity (int) : the capacity of the cell random (Random): a random number generator + rng (SeedLike | None): the random number generator cell_klass (type[Cell]): The base Cell class to use in the Network """ - super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) + super().__init__( + capacity=capacity, random=random, rng=rng, cell_klass=cell_klass + ) self.G = G for node_id in self.G.nodes: - self._cells[node_id] = self.cell_klass( - node_id, capacity, random=self.random - ) + self._cells[node_id] = self.cell_klass(node_id, capacity, rng=self.rng) self._connect_cells() diff --git a/mesa/discrete_space/voronoi.py b/mesa/discrete_space/voronoi.py index 8ff195bac31..8ea400fee8c 100644 --- a/mesa/discrete_space/voronoi.py +++ b/mesa/discrete_space/voronoi.py @@ -17,9 +17,13 @@ from random import Random import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence from mesa.discrete_space.cell import Cell from mesa.discrete_space.discrete_space import DiscreteSpace +from mesa.util import deprecate_kwarg + +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState class Delaunay: @@ -179,11 +183,13 @@ class VoronoiGrid(DiscreteSpace): voronoi_coordinates: list regions: list + @deprecate_kwarg("seed") def __init__( self, centroids_coordinates: Sequence[Sequence[float]], capacity: float | None = None, random: Random | None = None, + rng: SeedLike | None = None, cell_klass: type[Cell] = Cell, capacity_function: callable = round_float, ) -> None: @@ -197,16 +203,19 @@ def __init__( centroids_coordinates: coordinates of centroids to build the tessellation space capacity (int) : capacity of the cells in the discrete space random (Random): random number generator + rng (SeedLike | None): the random number generator cell_klass (type[Cell]): type of cell class capacity_function (Callable): function to compute (int) capacity according to (float) area """ - super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) + super().__init__( + capacity=capacity, random=random, rng=rng, cell_klass=cell_klass + ) self.centroids_coordinates = centroids_coordinates self._validate_parameters() self._cells = { - i: cell_klass(self.centroids_coordinates[i], capacity, random=self.random) + i: cell_klass(self.centroids_coordinates[i], capacity, rng=self.rng) for i in range(len(self.centroids_coordinates)) } diff --git a/mesa/examples/advanced/alliance_formation/model.py b/mesa/examples/advanced/alliance_formation/model.py index 6eaa21ab414..6072c760eae 100644 --- a/mesa/examples/advanced/alliance_formation/model.py +++ b/mesa/examples/advanced/alliance_formation/model.py @@ -15,7 +15,7 @@ class MultiLevelAllianceModel(mesa.Model): Model for simulating multi-level alliances among agents. """ - def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42): + def __init__(self, n=50, mean=0.5, std_dev=0.1, rng=42): """ Initialize the model. @@ -25,7 +25,7 @@ def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42): std_dev (float): Standard deviation for normal distribution. seed (int): Random seed. """ - super().__init__(seed=seed) + super().__init__(rng=rng) self.population = n self.network = nx.Graph() # Initialize the network self.datacollector = mesa.DataCollector(model_reporters={"Network": "network"}) diff --git a/mesa/examples/advanced/epstein_civil_violence/model.py b/mesa/examples/advanced/epstein_civil_violence/model.py index 171c0cffc75..1a0fae77ccf 100644 --- a/mesa/examples/advanced/epstein_civil_violence/model.py +++ b/mesa/examples/advanced/epstein_civil_violence/model.py @@ -47,14 +47,14 @@ def __init__( arrest_prob_constant=2.3, movement=True, max_iters=1000, - seed=None, + rng=None, ): - super().__init__(seed=seed) + super().__init__(rng=rng) self.movement = movement self.max_iters = max_iters self.grid = mesa.discrete_space.OrthogonalVonNeumannGrid( - (width, height), capacity=1, torus=True, random=self.random + (width, height), capacity=1, torus=True, rng=self.rng ) model_reporters = { diff --git a/mesa/examples/advanced/pd_grid/model.py b/mesa/examples/advanced/pd_grid/model.py index 1970f662f26..149fc8d03d0 100644 --- a/mesa/examples/advanced/pd_grid/model.py +++ b/mesa/examples/advanced/pd_grid/model.py @@ -14,7 +14,7 @@ class PdGrid(mesa.Model): payoff = {("C", "C"): 1, ("C", "D"): 0, ("D", "C"): 1.6, ("D", "D"): 0} def __init__( - self, width=50, height=50, activation_order="Random", payoffs=None, seed=None + self, width=50, height=50, activation_order="Random", payoffs=None, rng=None ): """ Create a new Spatial Prisoners' Dilemma Model. @@ -25,9 +25,9 @@ def __init__( Determines the agent activation regime. payoffs: (optional) Dictionary of (move, neighbor_move) payoffs. """ - super().__init__(seed=seed) + super().__init__(rng=rng) self.activation_order = activation_order - self.grid = OrthogonalMooreGrid((width, height), torus=True, random=self.random) + self.grid = OrthogonalMooreGrid((width, height), torus=True, rng=self.rng) if payoffs is not None: self.payoff = payoffs diff --git a/mesa/examples/advanced/sugarscape_g1mt/model.py b/mesa/examples/advanced/sugarscape_g1mt/model.py index 465e7be240a..3be02e08d86 100644 --- a/mesa/examples/advanced/sugarscape_g1mt/model.py +++ b/mesa/examples/advanced/sugarscape_g1mt/model.py @@ -53,9 +53,9 @@ def __init__( vision_min=1, vision_max=5, enable_trade=True, - seed=None, + rng=None, ): - super().__init__(seed=seed) + super().__init__(rng=rng) # Initiate width and height of sugarscape self.width = width self.height = height @@ -66,7 +66,7 @@ def __init__( # initiate mesa grid class self.grid = OrthogonalVonNeumannGrid( - (self.width, self.height), torus=False, random=self.random + (self.width, self.height), torus=False, rng=self.rng ) # initiate datacollector self.datacollector = mesa.DataCollector( diff --git a/mesa/examples/advanced/wolf_sheep/agents.py b/mesa/examples/advanced/wolf_sheep/agents.py index b14963442c2..5bd5e0f7eff 100644 --- a/mesa/examples/advanced/wolf_sheep/agents.py +++ b/mesa/examples/advanced/wolf_sheep/agents.py @@ -49,9 +49,12 @@ def step(self): # Handle death and reproduction if self.energy < 0: self.remove() - elif self.random.random() < self.p_reproduce: + elif self.rng.random() < self.p_reproduce: self.spawn_offspring() + def move(self): + """abstract method to be implemented by subclasses.""" + class Sheep(Animal): """A sheep that walks around, reproduces (asexually) and gets eaten.""" @@ -94,7 +97,7 @@ def feed(self): """If possible, eat a sheep at current location.""" sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] if sheep: # If there are any sheep present - sheep_to_eat = self.random.choice(sheep) + sheep_to_eat = self.rng.choice(sheep) if len(sheep) > 1 else sheep[0] self.energy += self.energy_from_food sheep_to_eat.remove() diff --git a/mesa/examples/advanced/wolf_sheep/model.py b/mesa/examples/advanced/wolf_sheep/model.py index e93a0bffa4a..eb30b490546 100644 --- a/mesa/examples/advanced/wolf_sheep/model.py +++ b/mesa/examples/advanced/wolf_sheep/model.py @@ -40,7 +40,7 @@ def __init__( grass=True, grass_regrowth_time=30, sheep_gain_from_food=4, - seed=None, + rng=None, simulator: ABMSimulator = None, ): """Create a new Wolf-Sheep model with the given parameters. @@ -57,10 +57,10 @@ def __init__( grass_regrowth_time: How long it takes for a grass patch to regrow once it is eaten sheep_gain_from_food: Energy sheep gain from grass, if enabled - seed: Random seed + rng: Random seed simulator: ABMSimulator instance for event scheduling """ - super().__init__(seed=seed) + super().__init__(rng=rng) self.simulator = simulator self.simulator.setup(self) @@ -74,7 +74,7 @@ def __init__( [self.height, self.width], torus=True, capacity=math.inf, - random=self.random, + rng=self.rng, ) # Set up data collection @@ -96,7 +96,7 @@ def __init__( energy=self.rng.random((initial_sheep,)) * 2 * sheep_gain_from_food, p_reproduce=sheep_reproduce, energy_from_food=sheep_gain_from_food, - cell=self.random.choices(self.grid.all_cells.cells, k=initial_sheep), + cell=self.rng.choice(self.grid.all_cells.cells, size=initial_sheep), ) # Create Wolves: Wolf.create_agents( @@ -105,17 +105,21 @@ def __init__( energy=self.rng.random((initial_wolves,)) * 2 * wolf_gain_from_food, p_reproduce=wolf_reproduce, energy_from_food=wolf_gain_from_food, - cell=self.random.choices(self.grid.all_cells.cells, k=initial_wolves), + cell=self.rng.choice(self.grid.all_cells.cells, size=initial_wolves), ) # Create grass patches if enabled if grass: - possibly_fully_grown = [True, False] + possibly_fully_grown = self.rng.integers( + 0, 2, size=(height, width), dtype=bool + ) + regrowth_time = self.rng.integers( + low=0, high=grass_regrowth_time, size=(height, width) + ) + regrowth_time[possibly_fully_grown] = 0 + for cell in self.grid: - fully_grown = self.random.choice(possibly_fully_grown) - countdown = ( - 0 if fully_grown else self.random.randrange(0, grass_regrowth_time) - ) + countdown = regrowth_time[cell.coordinate] GrassPatch(self, countdown, grass_regrowth_time, cell) # Collect initial data diff --git a/mesa/examples/basic/boid_flockers/model.py b/mesa/examples/basic/boid_flockers/model.py index 58203b889ca..0637c8aee7e 100644 --- a/mesa/examples/basic/boid_flockers/model.py +++ b/mesa/examples/basic/boid_flockers/model.py @@ -5,12 +5,6 @@ Uses numpy arrays to represent vectors. """ -import os -import sys - -sys.path.insert(0, os.path.abspath("../../../..")) - - import numpy as np from mesa import Model @@ -32,7 +26,7 @@ def __init__( cohere=0.03, separate=0.015, match=0.05, - seed=None, + rng=None, ): """Create a new Boids Flocking model. @@ -46,9 +40,9 @@ def __init__( cohere: Weight of cohesion behavior (default: 0.03) separate: Weight of separation behavior (default: 0.015) match: Weight of alignment behavior (default: 0.05) - seed: Random seed for reproducibility (default: None) + rng: Random seed for reproducibility (default: None) """ - super().__init__(seed=seed) + super().__init__(rng=rng) self.agent_angles = np.zeros( population_size ) # holds the angle representing the direction of all agents at a given step @@ -57,7 +51,7 @@ def __init__( self.space = ContinuousSpace( [[0, width], [0, height]], torus=True, - random=self.random, + rng=self.rng, n_agents=population_size, ) diff --git a/mesa/examples/basic/boltzmann_wealth_model/agents.py b/mesa/examples/basic/boltzmann_wealth_model/agents.py index f293b9068a5..98442a2b8a5 100644 --- a/mesa/examples/basic/boltzmann_wealth_model/agents.py +++ b/mesa/examples/basic/boltzmann_wealth_model/agents.py @@ -30,7 +30,11 @@ def give_money(self): cellmates = [a for a in self.cell.agents if a is not self] if cellmates: # Only give money if there are other agents present - other = self.random.choice(cellmates) + other = ( + cellmates[0] + if len(cellmates) == 1 + else cellmates[self.rng.integers(0, len(cellmates))] + ) other.wealth += 1 self.wealth -= 1 diff --git a/mesa/examples/basic/boltzmann_wealth_model/model.py b/mesa/examples/basic/boltzmann_wealth_model/model.py index 53076cf0e24..cbbc18de8d1 100644 --- a/mesa/examples/basic/boltzmann_wealth_model/model.py +++ b/mesa/examples/basic/boltzmann_wealth_model/model.py @@ -27,19 +27,19 @@ class BoltzmannWealth(Model): datacollector (DataCollector): Collects and stores model data """ - def __init__(self, n=100, width=10, height=10, seed=None): + def __init__(self, n=100, width=10, height=10, rng=None): """Initialize the model. Args: n (int, optional): Number of agents. Defaults to 100. width (int, optional): Grid width. Defaults to 10. height (int, optional): Grid height. Defaults to 10. - seed (int, optional): Random seed. Defaults to None. + rng (int, optional): Random seed. Defaults to None. """ - super().__init__(seed=seed) + super().__init__(rng=rng) self.num_agents = n - self.grid = OrthogonalMooreGrid((width, height), random=self.random) + self.grid = OrthogonalMooreGrid((width, height), rng=self.rng) # Set up data collection self.datacollector = DataCollector( @@ -49,7 +49,7 @@ def __init__(self, n=100, width=10, height=10, seed=None): MoneyAgent.create_agents( self, self.num_agents, - self.random.choices(self.grid.all_cells.cells, k=self.num_agents), + self.rng.choice(self.grid.all_cells.cells, size=self.num_agents), ) self.running = True diff --git a/mesa/examples/basic/conways_game_of_life/model.py b/mesa/examples/basic/conways_game_of_life/model.py index 32b304b8f54..31da0f153fd 100644 --- a/mesa/examples/basic/conways_game_of_life/model.py +++ b/mesa/examples/basic/conways_game_of_life/model.py @@ -6,20 +6,24 @@ class ConwaysGameOfLife(Model): """Represents the 2-dimensional array of cells in Conway's Game of Life.""" - def __init__(self, width=50, height=50, initial_fraction_alive=0.2, seed=None): + def __init__(self, width=50, height=50, initial_fraction_alive=0.2, rng=None): """Create a new playing area of (width, height) cells.""" - super().__init__(seed=seed) + super().__init__(rng=rng) # Use a simple grid, where edges wrap around. - self.grid = OrthogonalMooreGrid((width, height), capacity=1, torus=True) + self.grid = OrthogonalMooreGrid( + (width, height), capacity=1, torus=True, rng=rng + ) # Place a cell at each location, with some initialized to # ALIVE and some to DEAD. + random_floats = self.rng.uniform(0, 1, size=(width, height)) + for cell in self.grid.all_cells: Cell( self, cell, init_state=Cell.ALIVE - if self.random.random() < initial_fraction_alive + if random_floats[cell.coordinate] < initial_fraction_alive else Cell.DEAD, ) diff --git a/mesa/examples/basic/schelling/model.py b/mesa/examples/basic/schelling/model.py index 181ee1cc2a2..2f1e4266633 100644 --- a/mesa/examples/basic/schelling/model.py +++ b/mesa/examples/basic/schelling/model.py @@ -1,3 +1,5 @@ +import numpy as np + from mesa import Model from mesa.datacollection import DataCollector from mesa.discrete_space import OrthogonalMooreGrid @@ -15,7 +17,7 @@ def __init__( minority_pc: float = 0.5, homophily: float = 0.4, radius: int = 1, - seed=None, + rng=None, ): """Create a new Schelling model. @@ -26,16 +28,16 @@ def __init__( minority_pc: Chance for an agent to be in minority class (0-1) homophily: Minimum number of similar neighbors needed for happiness radius: Search radius for checking neighbor similarity - seed: Seed for reproducibility + rng: Seed for reproducibility """ - super().__init__(seed=seed) + super().__init__(rng=rng) # Model parameters self.density = density self.minority_pc = minority_pc # Initialize grid - self.grid = OrthogonalMooreGrid((width, height), random=self.random, capacity=1) + self.grid = OrthogonalMooreGrid((width, height), rng=self.rng, capacity=1) # Track happiness self.happy = 0 @@ -60,11 +62,18 @@ def __init__( ) # Create agents and place them on the grid + random_numbers = self.rng.random(size=(height, width)) + occupied = random_numbers < density + agent_type = iter(self.rng.random(size=np.sum(occupied)) < minority_pc) + for cell in self.grid.all_cells: - if self.random.random() < self.density: - agent_type = 1 if self.random.random() < minority_pc else 0 + if occupied[cell.coordinate]: SchellingAgent( - self, cell, agent_type, homophily=homophily, radius=radius + self, + cell, + next(agent_type).astype(int), + homophily=homophily, + radius=radius, ) # Collect initial state @@ -78,3 +87,11 @@ def step(self): self.agents.do("assign_state") self.datacollector.collect(self) # Collect data self.running = self.happy < len(self.agents) # Continue until everyone is happy + + +if __name__ == "__main__": + agent = Schelling( + height=20, + width=20, + density=0.8, + ) diff --git a/mesa/examples/basic/virus_on_network/agents.py b/mesa/examples/basic/virus_on_network/agents.py index 73a92b4fd99..d9c0a1a4af4 100644 --- a/mesa/examples/basic/virus_on_network/agents.py +++ b/mesa/examples/basic/virus_on_network/agents.py @@ -35,7 +35,7 @@ def __init__( def try_to_infect_neighbors(self): for agent in self.cell.neighborhood.agents: if (agent.state is State.SUSCEPTIBLE) and ( - self.random.random() < self.virus_spread_chance + self.rng.random() < self.virus_spread_chance ): agent.state = State.INFECTED diff --git a/mesa/examples/basic/virus_on_network/model.py b/mesa/examples/basic/virus_on_network/model.py index 81c1b9e4ade..839a837bcf9 100644 --- a/mesa/examples/basic/virus_on_network/model.py +++ b/mesa/examples/basic/virus_on_network/model.py @@ -36,12 +36,12 @@ def __init__( virus_check_frequency=0.4, recovery_chance=0.3, gain_resistance_chance=0.5, - seed=None, + rng=None, ): - super().__init__(seed=seed) + super().__init__(rng=rng) prob = avg_node_degree / num_nodes graph = nx.erdos_renyi_graph(n=num_nodes, p=prob) - self.grid = Network(graph, capacity=1, random=self.random) + self.grid = Network(graph, capacity=1, rng=self.rng) self.initial_outbreak_size = ( initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes @@ -69,8 +69,8 @@ def __init__( # Infect some nodes infected_nodes = CellCollection( - self.random.sample(list(self.grid.all_cells), self.initial_outbreak_size), - random=self.random, + self.rng.choice(list(self.grid.all_cells), size=self.initial_outbreak_size), + rng=self.rng, ) for a in infected_nodes.agents: a.state = State.INFECTED diff --git a/mesa/experimental/continuous_space/continuous_space.py b/mesa/experimental/continuous_space/continuous_space.py index 3c99e0af464..b3b176eca14 100644 --- a/mesa/experimental/continuous_space/continuous_space.py +++ b/mesa/experimental/continuous_space/continuous_space.py @@ -6,10 +6,14 @@ from random import Random import numpy as np +from numpy.random import BitGenerator, Generator, RandomState, SeedSequence from numpy.typing import ArrayLike from scipy.spatial.distance import cdist from mesa.agent import Agent, AgentSet +from mesa.util import deprecate_kwarg + +SeedLike = int | np.ndarray[int] | SeedSequence | BitGenerator | Generator | RandomState class ContinuousSpace: @@ -45,11 +49,13 @@ def height(self): # noqa: D102 # compatibility with solara_viz return self.size[1] + @deprecate_kwarg("random") def __init__( self, dimensions: ArrayLike, torus: bool = False, random: Random | None = None, + rng: SeedLike | None = None, n_agents: int = 100, ) -> None: """Create a new continuous space. @@ -58,6 +64,7 @@ def __init__( dimensions: a numpy array like object where each row specifies the minimum and maximum value of that dimension. torus: boolean for whether the space wraps around or not random: a seeded stdlib random.Random instance + rng (SeedLike | None): the random number generator n_agents: the expected number of agents in the space Internally, a numpy array is used to store the positions of all agents. This is resized if needed, @@ -65,14 +72,17 @@ def __init__( """ - if random is None: + if random is None and rng is None: warnings.warn( "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly", UserWarning, stacklevel=2, ) - random = Random() - self.random = random + rng = np.random.default_rng() + if random is not None: + rng = np.random.default_rng(random.getstate()[1]) + + self.rng = np.random.default_rng(rng) self.dimensions: np.array = np.asanyarray(dimensions) self.ndims: int = self.dimensions.shape[0] @@ -102,7 +112,7 @@ def __init__( @property def agents(self) -> AgentSet: """Return an AgentSet with the agents in the space.""" - return AgentSet(self.active_agents, random=self.random) + return AgentSet(self.active_agents, rng=self.rng) def _add_agent(self, agent: Agent) -> int: """Helper method for adding an agent to the space. diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 0b9a45eeb2b..caa960521ff 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -26,6 +26,8 @@ from collections.abc import Callable from typing import TYPE_CHECKING, Any +import numpy as np + from .eventlist import EventList, Priority, SimulationEvent if TYPE_CHECKING: @@ -302,9 +304,9 @@ def check_time_unit(self, time) -> bool: bool: whether the time is of the correct unit """ - if isinstance(time, int): + if isinstance(time, (int, np.integer)): return True - if isinstance(time, float): + if isinstance(time, (float, np.float)): return time.is_integer() else: return False diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py index 7a5ffe049cf..7ff37d1f02d 100644 --- a/mesa/experimental/meta_agents/meta_agent.py +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -63,7 +63,7 @@ def evaluate_combination( Optional[Tuple[AgentSet, float]]: The evaluated group and its value, or None. """ - group_set = AgentSet(candidate_group, random=model.random) + group_set = AgentSet(candidate_group, rng=model.rng) if evaluation_func: value = evaluation_func(group_set) return group_set, value @@ -287,7 +287,7 @@ def __init__( name (str, optional): The name of the MetaAgent. Defaults to "MetaAgent". """ super().__init__(model) - self._constituting_set = AgentSet(agents or [], random=model.random) + self._constituting_set = AgentSet(agents or [], rng=model.rng) self.name = name # Add ref to meta_agent in constituting_agents diff --git a/mesa/model.py b/mesa/model.py index f53d92b1633..72df7e74747 100644 --- a/mesa/model.py +++ b/mesa/model.py @@ -18,6 +18,7 @@ from mesa.agent import Agent, AgentSet from mesa.mesa_logging import create_module_logger, method_logger +from mesa.util import deprecate_kwarg SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence RNGLike = np.random.Generator | np.random.BitGenerator @@ -46,6 +47,7 @@ class Model: """ + @deprecate_kwarg("seed") @method_logger(__name__) def __init__( self, @@ -109,9 +111,7 @@ def __init__( self._agents_by_type: dict[ type[Agent], AgentSet ] = {} # a dict with an agentset for each class of agents - self._all_agents = AgentSet( - [], random=self.random - ) # an agenset with all agents + self._all_agents = AgentSet([], rng=self.rng) # an agenset with all agents def _wrapped_step(self, *args: Any, **kwargs: Any) -> None: """Automatically increments time and steps after calling the user's step method.""" @@ -166,7 +166,7 @@ def register_agent(self, agent): [ agent, ], - random=self.random, + rng=self.rng, ) self._all_agents.add(agent) @@ -200,12 +200,17 @@ def run_model(self) -> None: def step(self) -> None: """A single step. Fill in here.""" - def reset_randomizer(self, seed: int | None = None) -> None: + @deprecate_kwarg("seed") + def reset_randomizer( + self, seed: int | None = None, rng: SeedLike | None = None + ) -> None: """Reset the model random number generator. Args: seed: A new seed for the RNG; if None, reset using the current seed + rng: A new seed for the RNG; if None, reset using the current seed """ + # fixme if seed is None: seed = self._seed self.random.seed(seed) diff --git a/mesa/util.py b/mesa/util.py new file mode 100644 index 00000000000..e259beaff1f --- /dev/null +++ b/mesa/util.py @@ -0,0 +1,30 @@ +"""Utilities used across mesa.""" + +import warnings +from functools import wraps + + +def deprecate_kwarg(name: str): + """Deprecation warning wrapper for specified kwarg.""" + + def inner_wrapper(method): + """Deprecation warning wrapper for seed kwarg.""" + + @wraps(method) + def wrapper(self, *args, **kwargs): + """Inner function.""" + if kwargs.get(name) is not None: + warnings.warn( + f"The use of {name} is deprecated, please use rng instead", + FutureWarning, + stacklevel=2, + ) + + if kwargs.get("rng") is not None: + raise ValueError(f"you have to pass either rng or {name}, not both") + + return method(self, *args, **kwargs) + + return wrapper + + return inner_wrapper diff --git a/tests/test_agent.py b/tests/test_agent.py index 900533cf2d1..4f22b1f3bcc 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -143,14 +143,14 @@ def test_agentset_initialization(): def test_agentset_initialization_w_random(): """Test agentset initialization.""" model = Model() - empty_agentset = AgentSet([], random=model.random) + empty_agentset = AgentSet([], rng=model.rng) assert len(empty_agentset) == 0 - assert empty_agentset.random == model.random + assert empty_agentset.rng == model.rng agents = [AgentTest(model) for _ in range(10)] agentset = AgentSet(agents) assert len(agentset) == 10 - assert agentset.random == model.random + assert agentset.rng == model.rng def test_agentset_serialization(): diff --git a/tests/test_examples.py b/tests/test_examples.py index 0acb3c301dc..b578da55a9e 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -18,7 +18,7 @@ def test_boltzmann_model(): # noqa: D103 app.page # noqa: B018 - model = BoltzmannWealth(seed=42) + model = BoltzmannWealth(rng=42) for _i in range(10): model.step() @@ -29,7 +29,7 @@ def test_conways_game_model(): # noqa: D103 app.page # noqa: B018 - model = ConwaysGameOfLife(seed=42) + model = ConwaysGameOfLife(rng=42) for _i in range(10): model.step() @@ -39,7 +39,7 @@ def test_schelling_model(): # noqa: D103 app.page # noqa: B018 - model = Schelling(seed=42) + model = Schelling(rng=42) for _i in range(10): model.step() @@ -49,7 +49,7 @@ def test_virus_on_network(): # noqa: D103 app.page # noqa: B018 - model = VirusOnNetwork(seed=42) + model = VirusOnNetwork(rng=42) for _i in range(10): model.step() @@ -59,7 +59,7 @@ def test_boid_flockers(): # noqa: D103 app.page # noqa: B018 - model = BoidFlockers(seed=42) + model = BoidFlockers(rng=42) for _i in range(10): model.step() @@ -70,7 +70,7 @@ def test_epstein(): # noqa: D103 app.page # noqa: B018 - model = EpsteinCivilViolence(seed=42) + model = EpsteinCivilViolence(rng=42) for _i in range(10): model.step() @@ -81,7 +81,7 @@ def test_pd_grid(): # noqa: D103 app.page # noqa: B018 - model = PdGrid(seed=42) + model = PdGrid(rng=42) for _i in range(10): model.step() @@ -92,7 +92,7 @@ def test_sugarscape_g1mt(): # noqa: D103 app.page # noqa: B018 - model = SugarscapeG1mt(seed=42) + model = SugarscapeG1mt(rng=42) for _i in range(10): model.step() @@ -105,7 +105,7 @@ def test_wolf_sheep(): # noqa: D103 app.page # noqa: B018 simulator = ABMSimulator() - WolfSheep(seed=42, simulator=simulator) + WolfSheep(rng=42, simulator=simulator) simulator.run_for(10) @@ -114,7 +114,7 @@ def test_alliance_formation_model(): # noqa: D103 app.page # noqa: B018 - model = MultiLevelAllianceModel(50, seed=42) + model = MultiLevelAllianceModel(50, rng=42) for _i in range(10): model.step() diff --git a/tests/test_examples_viz.py b/tests/test_examples_viz.py index 758f44a13fe..95dc5e2f2b8 100644 --- a/tests/test_examples_viz.py +++ b/tests/test_examples_viz.py @@ -104,7 +104,7 @@ def run_model_test( @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_schelling_model(solara_test, page_session: playwright.sync_api.Page): """Test schelling model behavior and visualization.""" - model = Schelling(seed=42) + model = Schelling(rng=42) def agent_portrayal(agent): return {"color": "tab:orange" if agent.type == 0 else "tab:blue"} @@ -130,7 +130,7 @@ def test_wolf_sheep_model(solara_test, page_session: playwright.sync_api.Page): ) from mesa.experimental.devs import ABMSimulator # noqa: PLC0415 - model = WolfSheep(simulator=ABMSimulator(), seed=42) + model = WolfSheep(simulator=ABMSimulator(), rng=42) def agent_portrayal(agent): if agent is None: @@ -172,7 +172,7 @@ def agent_portrayal(agent): @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_boid_flockers_model(solara_test, page_session: playwright.sync_api.Page): """Test boid flockers model behavior and visualization.""" - model = BoidFlockers(seed=42) + model = BoidFlockers(rng=42) def agent_portrayal(agent): return {"color": "tab:blue"} @@ -189,7 +189,7 @@ def agent_portrayal(agent): @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_boltzmann_wealth_model(solara_test, page_session: playwright.sync_api.Page): """Test Boltzmann wealth model behavior and visualization.""" - model = BoltzmannWealth(seed=42) + model = BoltzmannWealth(rng=42) def agent_portrayal(agent): color = agent.wealth # we are using a colormap to translate wealth to color @@ -211,7 +211,7 @@ def test_virus_on_network_model(solara_test, page_session: playwright.sync_api.P """Test virus on network model behavior and visualization.""" from mesa.examples.basic.virus_on_network.model import State # noqa: PLC0415 - model = VirusOnNetwork(seed=42) + model = VirusOnNetwork(rng=42) def agent_portrayal(agent): node_color_dict = { @@ -242,7 +242,7 @@ def test_conways_game_of_life_model( solara_test, page_session: playwright.sync_api.Page ): """Test Conway's Game of Life model behavior and visualization.""" - model = ConwaysGameOfLife(seed=42) + model = ConwaysGameOfLife(rng=42) def agent_portrayal(agent): return { @@ -277,7 +277,7 @@ def test_epstein_civil_violence_model( agent_colors, ) - model = EpsteinCivilViolence(seed=42) + model = EpsteinCivilViolence(rng=42) def agent_portrayal(agent): if agent is None: @@ -308,7 +308,7 @@ def agent_portrayal(agent): @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_sugarscape_g1mt_model(solara_test, page_session: playwright.sync_api.Page): """Test Sugarscape G1mt model behavior and visualization.""" - model = SugarscapeG1mt(seed=42) + model = SugarscapeG1mt(rng=42) def agent_portrayal(agent): return {"marker": "o", "color": "red", "size": 10} @@ -327,7 +327,7 @@ def agent_portrayal(agent): @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_pd_grid_model(solara_test, page_session: playwright.sync_api.Page): """Test Prisoner's Dilemma model behavior and visualization.""" - model = PdGrid(seed=42) + model = PdGrid(rng=42) def agent_portrayal(agent): return { diff --git a/tests/test_model.py b/tests/test_model.py index a7e054d12b3..8e5960c216c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -102,8 +102,8 @@ class Sheep(Agent): wolf = Wolf(model) sheep = Sheep(model) - assert model.agents_by_type[Wolf] == AgentSet([wolf], model) - assert model.agents_by_type[Sheep] == AgentSet([sheep], model) + assert model.agents_by_type[Wolf] == AgentSet([wolf], rng=model.rng) + assert model.agents_by_type[Sheep] == AgentSet([sheep], rng=model.rng) assert len(model.agents_by_type) == 2