diff --git a/examples/Schelling/model.py b/examples/Schelling/model.py index 18b157a1894..f5853d0392a 100644 --- a/examples/Schelling/model.py +++ b/examples/Schelling/model.py @@ -8,6 +8,7 @@ class SchellingAgent(Agent): ''' Schelling segregation agent ''' + def __init__(self, pos, model, agent_type): ''' Create a new Schelling agent. @@ -72,7 +73,7 @@ def __init__(self, height=20, width=20, density=0.8, minority_pc=0.2, homophily= agent_type = 0 agent = SchellingAgent((x, y), self, agent_type) - self.grid.position_agent(agent, (x, y)) + self.grid.position_agent(agent, x, y) self.schedule.add(agent) self.running = True diff --git a/mesa/agent.py b/mesa/agent.py index 445dd1b2867..ed014f6a8e6 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -8,11 +8,24 @@ class Agent: - """ Base class for a model agent. """ + """ Base class for a model agent. + + Properties: + unique_id: Unique identifer for the agent + model: Model that the agent is situated in + pos: Position of the agent in the parameter space (if exists) + """ + def __init__(self, unique_id, model): - """ Create a new agent. """ + """ Create a new agent. + + Args: + unique_id: Unique identifer for the agent + model: Model that the agent is situated in + """ self.unique_id = unique_id self.model = model + self.pos = None def step(self): """ A single step of the agent. """ diff --git a/mesa/space.py b/mesa/space.py index aefaf2ee0aa..e485e904f15 100644 --- a/mesa/space.py +++ b/mesa/space.py @@ -15,37 +15,22 @@ # pylint: disable=invalid-name import itertools - import numpy as np -def accept_tuple_argument(wrapped_function): - """ Decorator to allow grid methods that take a list of (x, y) coord tuples - to also handle a single position, by automatically wrapping tuple in - single-item list rather than forcing user to do it. - - """ - - def wrapper(*args): - if isinstance(args[1], tuple) and len(args[1]) == 2: - return wrapped_function(args[0], [args[1]]) - else: - return wrapped_function(*args) - - return wrapper - - class Grid: - """ Base class for a square grid. + """ Class for a 2D regular lattice. Grid cells are indexed by [x][y], where [0][0] is assumed to be the bottom-left and [width-1][height-1] is the top-right. If a grid is toroidal, the top and bottom, and left and right, edges wrap to each other Properties: + grid: Internal representation of the grid width, height: The grid's width and height. torus: Boolean which determines whether to treat the grid as a torus. - grid: Internal list-of-lists which holds the grid cells themselves. + multigrid: If True, a cell may hold more than one agent + empties: A list of empty cells Methods: get_neighbors: Returns the objects surrounding a given cell. @@ -58,47 +43,36 @@ class Grid: move_agent: Moves an agent from its current position to a new position. iter_neighborhood: Returns an iterator over cell coordinates that are in the neighborhood of a certain point. - torus_adj: Converts coordinate, handles torus looping. - out_of_bounds: Determines whether position is off the grid, returns - the out of bounds coordinate. - iter_cell_list_contents: Returns an iterator of the contents of the - cells identified in cell_list. - get_cell_list_contents: Returns a list of the contents of the cells - identified in cell_list. remove_agent: Removes an agent from the grid. is_cell_empty: Returns a bool of the contents of a cell. """ - def __init__(self, width, height, torus): + def __init__(self, width, height, torus, multigrid=False): """ Create a new grid. Args: + model: Subtype of Model that the grid is situated in width, height: The width and height of the grid torus: Boolean whether the grid wraps or not. - + multigrid: If True, a cell may hold more than one agent """ + self.height = height self.width = width self.torus = torus - + self.multigrid = multigrid self.grid = [] - for x in range(self.width): + for _ in range(self.width): col = [] - for y in range(self.height): - col.append(self.default_val()) + for _ in range(self.height): + col.append(self.empty_value) self.grid.append(col) - # Add all cells to the empties list. - self.empties = list(itertools.product( + self.empties = set(itertools.product( *(range(self.width), range(self.height)))) - @staticmethod - def default_val(): - """ Default value for new cell elements. """ - return None - def __getitem__(self, index): return self.grid[index] @@ -107,238 +81,260 @@ def __iter__(self): # rows of grid together as if one list: return itertools.chain(*self.grid) - def coord_iter(self): - """ An iterator that returns coordinates as well as cell contents. """ - for row in range(self.width): - for col in range(self.height): - yield self.grid[row][col], row, col # agent, x, y + @property + def empty_value(self): + if self.multigrid: + return set() + else: + return None - def neighbor_iter(self, pos, moore=True): - """ Iterate over position neighbors. + def is_cell_empty(self, pos): + """ Returns a bool of the contents of a cell. """ + x, y = pos + return self.grid[x][y] == self.empty_value - Args: - pos: (x,y) coords tuple for the position to get the neighbors of. - moore: Boolean for whether to use Moore neighborhood (including - diagonals) or Von Neumann (only up/down/left/right). + def exists_empty_cells(self): + """ Return True if any cells empty else False. """ + return len(self.empties) > 0 - """ - neighborhood = self.iter_neighborhood(pos, moore=moore) - return self.iter_cell_list_contents(neighborhood) + def grid_iter(self): + """ Returns an iterator of tuples (agent, x, y) over the whole grid. """ + for x in range(self.width): + for y in range(self.height): + yield self.grid[x][y], x, y - def iter_neighborhood(self, pos, moore, - include_center=False, radius=1): - """ Return an iterator over cell coordinates that are in the - neighborhood of a certain point. + def agents_iter(self): + """ Returns an iterator of all the agents over the whole grid. """ + for x in range(self.width): + for y in range(self.height): + if not self.is_cell_empty((x, y)): + if self.multigrid: + for agent in self.grid[x][y]: + yield agent + else: + yield self.grid[x][y] + + def coords_iter(self): + """ Returns an iterator of coordinates (x, y) over the whole grid. """ + for x in range(self.width): + for y in range(self.height): + yield (x, y) + + def cells_at_row(self, row, include_agents=False): + """ Return an iterator over a specific row Args: - pos: Coordinate tuple for the neighborhood to get. - moore: If True, return Moore neighborhood + include_agents: return ((x, y), agent) if True, otherwise (x, y) + """ + _, y = self.torus_adj((0, row)) + for x in range(self.width): + if include_agents: + yield ((x, y), self.grid[x][y]) + else: + yield (x, y) + + def cells_at_col(self, col, include_agents=False): + """ Return an iterator over a specific column """ + x, _ = self.torus_adj((col, 0)) + for y in range(self.height): + if include_agents: + yield ((x, y), self.grid[x][y]) + else: + yield (x, y) + + def neighbors(self, pos, moore=True, radius=1, get_agents=False, include_empty=False): + """ + Args: + pos: coordinates (x, y) for the neighborhood to get + moore: if True, return Moore neighborhood (including diagonals) - If False, return Von Neumann neighborhood + if False, return Von Neumann neighborhood (exclude diagonals) - include_center: If True, return the (x, y) cell as well. - Otherwise, return surrounding cells only. - radius: radius, in cells, of neighborhood to get. + radius: range of the Moore/von Neumann neighborhood + get_agents: + if True, return (agent, (x, y)) as a set element + if False, return (x, y) as a set element + include_empty: + if True, treat empty cells as valid adjacent cells + if False, skip empty cells Returns: - A list of coordinate tuples representing the neighborhood. For - example with radius 1, it will return list with number of elements - equals at most 9 (8) if Moore, 5 (4) if Von Neumann (if not - including the center). + A list of adjacent cells of a single cell at `pos`. + The number of cells + in the Moore neighborhood with radius n is (2n+1)^2 -1. + The number of cells in the Moore neighborhood with radius n is [(2n+1)^2 -1] + (http://www.conwaylife.com/wiki/Moore_neighbourhood). + + The number of cells in the von Neumann neighbourhood of + radius n of a single cell is + [2n(n+1)](http://www.conwaylife.com/wiki/Von_Neumann_neighborhood). """ - x, y = pos - coordinates = set() - for dy in range(-radius, radius + 1): - for dx in range(-radius, radius + 1): - if dx == 0 and dy == 0 and not include_center: - continue - # Skip coordinates that are outside manhattan distance - if not moore and abs(dx) + abs(dy) > radius: - continue - # Skip if not a torus and new coords out of bounds. - if not self.torus and (not (0 <= dx + x < self.width) or not (0 <= dy + y < self.height)): - continue + if moore: + neighbors = self._moore(pos, radius, get_agents, include_empty) + else: + neighbors = self._von_neumann( + pos, radius, get_agents, include_empty) + + return neighbors - px, py = self.torus_adj((x + dx, y + dy)) + def _moore(self, pos, radius, get_agents, include_empty): + neighbors = [] - # Skip if new coords out of bounds. - if self.out_of_bounds((px, py)): + for dx in range(-radius, radius + 1): + for dy in range(-radius, radius + 1): + x, y = self.torus_adj((pos[0] + dx, pos[1] + dy)) + # If a cell is empty and empty cell should not be returned, skip + if (self.grid[x][y] == self.empty_value) and (not include_empty): continue + if (get_agents): + neighbors.append((self.grid[x][y], (x, y))) + else: + neighbors.append((x, y)) + return neighbors - coords = (px, py) - if coords not in coordinates: - coordinates.add(coords) - yield coords + def _von_neumann(self, pos, radius, get_agents, include_empty): + neighbors = [] - def get_neighborhood(self, pos, moore, - include_center=False, radius=1): - """ Return a list of cells that are in the neighborhood of a - certain point. + for dx in range(-radius, radius + 1): + for dy in range(-radius, radius + 1): + x, y = self.torus_adj((pos[0] + dx, pos[1] + dy)) + # If a cell is empty and empty cell should not be returned, skip + if (self.grid[x][y] == self.empty_value) and (not include_empty): + continue + # Skip coordinates that are outside manhattan distance + if abs(dx) + abs(dy) > radius: + break + if (get_agents): + neighbors.append((self.grid[x][y], (x, y))) + else: + neighbors.append((x, y)) - Args: - pos: Coordinate tuple for the neighborhood to get. - moore: If True, return Moore neighborhood - (including diagonals) - If False, return Von Neumann neighborhood - (exclude diagonals) - include_center: If True, return the (x, y) cell as well. - Otherwise, return surrounding cells only. - radius: radius, in cells, of neighborhood to get. + return neighbors - Returns: - A list of coordinate tuples representing the neighborhood; - With radius 1, at most 9 if Moore, 5 if Von Neumann (8 and 4 - if not including the center). + def place_agent(self, agent, pos): + """ Position an agent on the grid, and set its pos variable. - """ - return list(self.iter_neighborhood(pos, moore, include_center, radius)) - - def iter_neighbors(self, pos, moore, - include_center=False, radius=1): - """ Return an iterator over neighbors to a certain point. + If the grid is not a multigrid and the new position is already occupied, + an exception will be raised. Args: - pos: Coordinates for the neighborhood to get. - moore: If True, return Moore neighborhood - (including diagonals) - If False, return Von Neumann neighborhood - (exclude diagonals) - include_center: If True, return the (x, y) cell as well. - Otherwise, - return surrounding cells only. - radius: radius, in cells, of neighborhood to get. - - Returns: - An iterator of non-None objects in the given neighborhood; - at most 9 if Moore, 5 if Von-Neumann - (8 and 4 if not including the center). - + agent: agent to place at `pos` + pos: coordinates of the grid (x, y) + replace: if True, replace the possibly existed agent at `pos` with `agent` """ - neighborhood = self.iter_neighborhood( - pos, moore, include_center, radius) - return self.iter_cell_list_contents(neighborhood) - - def get_neighbors(self, pos, moore, - include_center=False, radius=1): - """ Return a list of neighbors to a certain point. - - Args: - pos: Coordinate tuple for the neighborhood to get. - moore: If True, return Moore neighborhood - (including diagonals) - If False, return Von Neumann neighborhood - (exclude diagonals) - include_center: If True, return the (x, y) cell as well. - Otherwise, - return surrounding cells only. - radius: radius, in cells, of neighborhood to get. + if (pos[0] == "random") or (pos[1] == "random"): + x, y = self.pick_random_empty_cell(agent, pos) + else: + x, y = self.torus_adj(pos) - Returns: - A list of non-None objects in the given neighborhood; - at most 9 if Moore, 5 if Von-Neumann - (8 and 4 if not including the center). + if self.grid[x][y] != self.empty_value and not self.multigrid: + raise Exception("Cell has been ocupied by agent {}.".format( + self.grid[x][y].unique_id)) + else: + # Update agent and grid attributes + if agent.pos != None: + self.remove_agent(agent) + else: + if self.multigrid: + self.grid[x][y].add(agent) + if (x, y) in self.empties: + self.empties.remove((x, y)) + else: + self.grid[x][y] = agent + self.empties.remove((x, y)) - """ - return list(self.iter_neighbors( - pos, moore, include_center, radius)) + agent.pos = (x, y) - def torus_adj(self, pos): - """ Convert coordinate, handling torus looping. """ - if not self.out_of_bounds(pos): - return pos - elif not self.torus: - raise Exception("Point out of bounds, and space non-toroidal.") + def remove_agent(self, agent): + """ Remove the agent from the grid and set its pos variable to None. """ + x, y = agent.pos + if self.multigrid: + self.grid[x][y].remove(agent) + if self.is_cell_empty((x, y)): + self.empties.add((x, y)) else: - x, y = pos[0] % self.width, pos[1] % self.height - return x, y + self.grid[x][y] = None + self.empties.add((x, y)) - def out_of_bounds(self, pos): - """ - Determines whether position is off the grid, returns the out of - bounds coordinate. - """ - x, y = pos - return x < 0 or x >= self.width or y < 0 or y >= self.height + agent.pos = None - @accept_tuple_argument - def iter_cell_list_contents(self, cell_list): - """ - Args: - cell_list: Array-like of (x, y) tuples, or single tuple. + def move_to_empty(self, agent): + """ Moves agent to a random empty cell. """ - Returns: - An iterator of the contents of the cells identified in cell_list + if not self.exists_empty_cells(): + raise Exception("ERROR: No empty cells") + else: + new_pos = self.pick_random_empty_cell(agent) + self.place_agent(agent, new_pos) - """ - return ( - self[x][y] for x, y in cell_list if not self.is_cell_empty((x, y))) + def pick_random_empty_cell(self, agent, pos=("random", "random")): + """ Return a random empty cell in the grid. - @accept_tuple_argument - def get_cell_list_contents(self, cell_list): - """ Args: - cell_list: Array-like of (x, y) tuples, or single tuple. - - Returns: - A list of the contents of the cells identified in cell_list - - """ - return list(self.iter_cell_list_contents(cell_list)) - - def move_agent(self, agent, pos): + pos: Default value is ("random", "random"). + Column and/or row index can be given to narrow the search area. """ - Move an agent from its current position to a new position. + if not self.exists_empty_cells(): + raise Exception("No empty cells") + else: + if (pos[0], pos[1]) == ("random", "random"): + return agent.random.choice(list(self.empties)) + else: + empties = [] + if pos[0] == "random": + # We pick a random cell in a specified row + for coords, cell in self.cells_at_row(pos[1], include_agents=True): + if cell == self.empty_value: + empties.append(coords) + else: + # We pick a random cell in a specified column + for coords, cell in self.cells_at_col(pos[0], include_agents=True): + if cell == self.empty_value: + empties.append(coords) + + if len(empties) > 0: + return agent.random.choice(empties) + else: + raise Exception("No empty cells at the given row/column.") + + def agents_on_coords(self, cells): + """ Given a list of cell coordinates (x, y), return a list of respective agents Args: - agent: Agent object to move. Assumed to have its current location - stored in a 'pos' tuple. - pos: Tuple of new position to move the agent to. + cells: Array-like of (x, y) tuples, or single tuple. + Returns: + A list of agents corresponding to the given positions. """ - pos = self.torus_adj(pos) - self._remove_agent(agent.pos, agent) - self._place_agent(pos, agent) - agent.pos = pos + if not isinstance(cells, list): + cells = [cells] + return [self.grid[x][y] for x, y in cells] - def place_agent(self, agent, pos): - """ Position an agent on the grid, and set its pos variable. """ - self._place_agent(pos, agent) - agent.pos = pos - - def _place_agent(self, pos, agent): - """ Place the agent at the correct location. """ - x, y = pos - self.grid[x][y] = agent - if pos in self.empties: - self.empties.remove(pos) + def torus_adj(self, pos): + """ Convert coordinate, handling torus looping. """ + if self.torus: + x, y = pos[0] % self.width, pos[1] % self.height + return x, y + else: + if (0 <= pos[0] < self.width) and (0 <= pos[1] < self.height): + return pos + else: + raise IndexError( + "Coordinates out of bounds. Grid is non-toroidal.") - def remove_agent(self, agent): - """ Remove the agent from the grid and set its pos variable to None. """ - pos = agent.pos - self._remove_agent(pos, agent) - agent.pos = None + # Deprecated methods below + def move_agent(self, agent, pos): + """ Deprecated method. Call place_agent(agent, pos) instead""" + self.place_agent(agent, pos) - def _remove_agent(self, pos, agent): - """ Remove the agent from the given location. """ - x, y = pos - self.grid[x][y] = None - self.empties.append(pos) + def coord_iter(self): + from warnings import warn - def is_cell_empty(self, pos): - """ Returns a bool of the contents of a cell. """ - x, y = pos - return True if self.grid[x][y] == self.default_val() else False + warn(("Deprecated method. Call grid_iter() instead."), + DeprecationWarning) - def move_to_empty(self, agent): - """ Moves agent to a random empty cell, vacating agent's old cell. """ - pos = agent.pos - if len(self.empties) == 0: - raise Exception("ERROR: No empty cells") - new_pos = agent.random.choice(self.empties) - self._place_agent(new_pos, agent) - agent.pos = new_pos - self._remove_agent(pos, agent) + return self.grid_iter() def find_empty(self): """ Pick a random empty cell. """ @@ -349,7 +345,7 @@ def find_empty(self): "`random` instead of the model-level random-number generator. " "Consider replacing it with having a model or agent object " "explicitly pick one of the grid's list of empty cells."), - DeprecationWarning) + DeprecationWarning) if self.exists_empty_cells(): pos = random.choice(self.empties) @@ -357,102 +353,50 @@ def find_empty(self): else: return None - def exists_empty_cells(self): - """ Return True if any cells empty else False. """ - return len(self.empties) > 0 - - -class SingleGrid(Grid): - """ Grid where each cell contains exactly at most one object. """ - empties = [] - - def __init__(self, width, height, torus): - """ Create a new single-item grid. - - Args: - width, height: The width and width of the grid - torus: Boolean whether the grid wraps or not. + def get_neighbors(self, pos, moore, + include_center=False, radius=1): + import warnings + warnings.warn( + "Deprecated method. Call neighbors(pos, moore, radius, True) instead. \ + In addition, parameter `include_center` is removeed.Use `self` in the code. ", + DeprecationWarning) - """ - super().__init__(width, height, torus) + return self.neighbors(pos, moore, radius, True) def position_agent(self, agent, x="random", y="random"): - """ Position an agent on the grid. - This is used when first placing agents! Use 'move_to_empty()' - when you want agents to jump to an empty cell. - Use 'swap_pos()' to swap agents positions. - If x or y are positive, they are used, but if "random", - we get a random position. - Ensure this random position is not occupied (in Grid). + import warnings + warnings.warn( + "Deprecated method. Call place_agent(pos) instead.", DeprecationWarning) - """ - if x == "random" or y == "random": - if len(self.empties) == 0: - raise Exception("ERROR: Grid full") - coords = agent.random.choice(self.empties) - else: - coords = (x, y) - agent.pos = coords - self._place_agent(coords, agent) + self.place_agent(agent, (x, y)) - def _place_agent(self, pos, agent): - if self.is_cell_empty(pos): - super()._place_agent(pos, agent) - else: - raise Exception("Cell not empty") + def get_cell_list_contents(self, cells): + import warnings + warnings.warn( + "Deprecated method. Call agents_on_coords(cells) instead.", DeprecationWarning) + return self.agents_on_coords(cells) -class MultiGrid(Grid): - """ Grid where each cell can contain more than one object. + def iter_cell_list_contents(self, cells): + import warnings + warnings.warn( + "Deprecated method. Call agents_on_coords(cells) instead.", DeprecationWarning) - Grid cells are indexed by [x][y], where [0][0] is assumed to be at - bottom-left and [width-1][height-1] is the top-right. If a grid is - toroidal, the top and bottom, and left and right, edges wrap to each other. - - Each grid cell holds a set object. - - Properties: - width, height: The grid's width and height. - - torus: Boolean which determines whether to treat the grid as a torus. + return self.agents_on_coords(cells) - grid: Internal list-of-lists which holds the grid cells themselves. - Methods: - get_neighbors: Returns the objects surrounding a given cell. - """ - - @staticmethod - def default_val(): - """ Default value for new cell elements. """ - return set() - - def _place_agent(self, pos, agent): - """ Place the agent at the correct location. """ - x, y = pos - self.grid[x][y].add(agent) - if pos in self.empties: - self.empties.remove(pos) +class SingleGrid(Grid): + """ Depreciated class. Use Grid with multigrid set to False instead. """ - def _remove_agent(self, pos, agent): - """ Remove the agent from the given location. """ - x, y = pos - self.grid[x][y].remove(agent) - if self.is_cell_empty(pos): - self.empties.append(pos) + def __init__(self, width, height, torus): + super().__init__(width, height, torus, multigrid=False) - @accept_tuple_argument - def iter_cell_list_contents(self, cell_list): - """ - Args: - cell_list: Array-like of (x, y) tuples, or single tuple. - Returns: - A iterator of the contents of the cells identified in cell_list +class MultiGrid(Grid): + """ Depreciated class. Use Grid with multigrid set to True instead. """ - """ - return itertools.chain.from_iterable( - self[x][y] for x, y in cell_list if not self.is_cell_empty((x, y))) + def __init__(self, width, height, torus): + super().__init__(width, height, torus, multigrid=True) class HexGrid(Grid): @@ -474,6 +418,9 @@ class HexGrid(Grid): """ + def __getitem__(self, index): + return self.grid[index] + def iter_neighborhood(self, pos, include_center=False, radius=1): """ Return an iterator over cell coordinates that are in the @@ -613,7 +560,7 @@ class ContinuousSpace: to store agent objects, to speed up neighborhood lookups. """ - _grid = None + grid = None def __init__(self, x_max, y_max, torus, x_min=0, y_min=0): """ Create a new continuous space. @@ -652,7 +599,8 @@ def place_agent(self, agent, pos): if self._agent_points is None: self._agent_points = np.array([pos]) else: - self._agent_points = np.append(self._agent_points, np.array([pos]), axis=0) + self._agent_points = np.append( + self._agent_points, np.array([pos]), axis=0) self._index_to_agent[self._agent_points.shape[0] - 1] = agent self._agent_to_index[agent] = self._agent_points.shape[0] - 1 agent.pos = pos @@ -710,7 +658,8 @@ def get_neighbors(self, pos, radius, include_center=True): dists = deltas[:, 0] ** 2 + deltas[:, 1] ** 2 idxs, = np.where(dists <= radius ** 2) - neighbors = [self._index_to_agent[x] for x in idxs if include_center or dists[x] > 0] + neighbors = [self._index_to_agent[x] + for x in idxs if include_center or dists[x] > 0] return neighbors def get_heading(self, pos_1, pos_2): @@ -827,5 +776,6 @@ def get_all_cell_contents(self): return list(self.iter_cell_list_contents(self.G)) def iter_cell_list_contents(self, cell_list): - list_of_lists = [self.G.node[node_id]['agent'] for node_id in cell_list if not self.is_cell_empty(node_id)] + list_of_lists = [self.G.node[node_id]['agent'] + for node_id in cell_list if not self.is_cell_empty(node_id)] return [item for sublist in list_of_lists for item in sublist]