""" grid_env.py This is an adaptation of the: Mesa Space Module ================================= From the GMU Mesa project. Objects used to add a spatial component to a model. GridEnv: base grid, a simple list-of-lists. """ # Instruction for PyLint to suppress variable name errors, # since we have a good reason to use one-character variable names for x and y. # pylint: disable=invalid-name import math import random import itertools import logging import indra.node as node import indra.spatial_env as se import models.grid as ta import indra.grid_agent as ga RANDOM = -1 X = 0 Y = 1 def out_of_bounds(x, y, x1, y1, x2, y2): """ Is point x, y off the grid defined by x1, y1, x2, y2? """ return(x < x1 or x >= x2 or y < y1 or y >= y2) class Cell(node.Node): """ Cells hold the grid contents. They also have a record of where they are in the grid. """ def __init__(self, coords, contents=None): super().__init__(None) self.__contents = None self.coords = coords @property def contents(self): return self.__contents @contents.setter def contents(self, item): old_item = self.__contents self.__contents = item if item is not None: item.cell = self if old_item is not None: old_item.cell = None def is_empty(self): """ Return True if cell empty, else False. """ return not self.contents def add_item(self, new_item): """ Add new_item to cell contents. Every cell item must have a cell field to store its location. """ self.contents = new_item def remove_item(self, item): """ If item is our object, set contents to None. If that is not our object, do nothing. """ if item == self.contents: self.contents = None def to_json(self): """ We're going to make a dictionary of the 'safe' parts of the object to output to a json file. (We can't output the env, for instance, since IT contains a reference to each agent!) """ safe_fields = {} safe_fields["coordx"] = self.coords[0] safe_fields["coordy"] = self.coords[1] return safe_fields @classmethod def from_json(cls, json_input): coords = (json_input["coordx"], json_input["coordy"]) return cls(coords=coords) class OutOfBounds(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class CompositeView(ga.GridView): """ A composite view combines several other views. """ def __init__(self, grid, views): self.grid = grid self.views = list(views) # the parameter will be a tuple def __iter__(self): """ Iterate over all our cells: note, right now, this return the center cell twice. """ return itertools.chain(self.views) def out_of_bounds(self, x, y): """ Is x, y not in this view? It is not only if it is not in ANY of the sub-views. """ for view in self.views: if not view.out_of_bounds(x, y): return False return True def get_neighbors(self): """ Return all of the occupied cells in this view. """ neighbors = [] for view in self.views: neighbors += view.get_neighbors() return neighbors class GridEnv(se.SpatialEnv): """ Base class for a rectangular grid. If a grid is toroidal, the top and bottom, and left and right, edges wrap to each other Properties: 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. Methods: get_neighbors: Returns the objects surrounding a given cell. """ def __init__(self, name, width, height, torus=False, preact=False, postact=False, model_nm=None, props=None): """ Create a new grid. Args: height, width: The height and width of the grid torus: Boolean whether the grid wraps or not. """ super().__init__(name, width, height, preact=preact, postact=postact, model_nm=model_nm, props=props) self.torus = torus self.num_cells = width * height self.__init_unrestorables() def __init_unrestorables(self): self.grid = [] self.empties = [] for y in range(self.height): row = [] for x in range(self.width): cell = self.__new_cell__((x, y)) row.append(cell) self.empties.append(cell) self.grid.append(row) self.set_agent_color() def __iter__(self): # create an iterator that chains the # rows of grid together as if one list: return itertools.chain(*self.grid) def __getitem__(self, index): return self.grid[index] def __new_cell__(self, coords): return Cell(coords) def add_agent(self, agent, x=RANDOM, y=RANDOM, position=True): """ Add an agent and link to cell if present in agent. """ super().add_agent(agent, x, y, position) if agent.cell is not None: if agent.cell.contents is not agent: agent.cell.add_item(agent) def remove_agent(self, agent): """ Remove agent from pop and from grid. """ super().remove_agent(agent) if agent.cell is not None: agent.cell.remove_item(agent) def torus_adj(self, coord, dim_len): """ Convert coordinate, handling torus looping. """ if self.torus: coord %= dim_len return coord def out_of_bounds(self, x, y): """ Is point x, y off the grid? """ return out_of_bounds(x, y, 0, 0, self.width, self.height) def get_max_dist(self): """ Args: none Returns: The furthest move possible in this env. """ return math.sqrt(self.width**2 + self.height**2) def get_col_view(self, col, low=None, high=None): """ Return a view of a single column. It will be the whole column from the grid, unless low or high are passed. """ if low is None: low = 0 if high is None: high = self.height return ga.GridView(self, col, low, col + 1, high) def get_row_view(self, row, left=None, right=None): """ Return a view of a single row It will be the whole row from the grid, unless left or right are passed. """ if left is None: left = 0 if right is None: right = self.width return ga.GridView(self, left, row, right, row + 1) def _adjust_coords(self, center, distance, max_val): coord1 = max(0, center - distance) coord2 = min(max_val, center + distance + 1) return (coord1, coord2) def get_vonneumann_view(self, center, distance): """ Return a Von Neumann view (row and col) centered on center. """ (x, y) = center (x1, x2) = self._adjust_coords(x, distance, self.width) (y1, y2) = self._adjust_coords(y, distance, self.height) col_view = self.get_col_view(x, y1, y2) row_view = self.get_row_view(y, x1, x2) return CompositeView(self, (col_view, row_view)) def get_square_view(self, center, distance): """ Attempt to return a view of a square centered on center. This might return a non-square rectangle if the center is near an edge. """ (center_x, center_y) = center (x1, x2) = self._adjust_coords(center_x, distance, self.width) (y1, y2) = self._adjust_coords(center_y, distance, self.height) return ga.GridView(self, x1, y1, x2, y2) def neighbor_iter(self, x, y, distance=1, moore=True, view=None, shuffle=False): """ Iterate over our neighbors. """ neighbors = self.get_neighbors(x, y, distance, moore, view) if shuffle: random.shuffle(neighbors) return map(lambda x: x.contents, iter(neighbors)) def get_neighbors(self, x, y, distance=1, moore=True, view=None): """ Return a list of neighbors within a certain purview. Args: x: X coordinates for the neighborhood to get. y: Y coordinates for the neighborhood to get. distance: distance, in cells, of neighborhood to get. view: we may already have a view we are working with. 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 view is None: if moore: view = self.get_square_view((x, y), distance) else: view = self.get_vonneumann_view((x, y), distance) return view.get_neighbors() def exists_empty_cells(self, grid_view=None): """ Return True if any cells empty else False. """ if len(self.empties) <= 0: # if no empties anywhere then none in any view! return False elif grid_view is not None: if len(grid_view.empties) <= 0: return False return True def move_to_empty(self, agent, grid_view=None): """ Moves agent to an empty cell, vacating agent's old cell. """ empty_cell = self.find_empty(grid_view) if empty_cell is None: logging.error("Agent could not move because no cells are empty") else: self._move_item(agent, empty_cell) #Functions by JacEkko (John Knox)==================================================== def get_angle(self, agent1, agent2, grid_view=None): """ Use two agents to find the angle they make, using their coordinates """ dy = abs(agent1.pos[Y] - agent2.pos[Y]) dx = abs(agent1.pos[X] - agent2.pos[X]) if(dy == 0): return 180 if(dx == 0): return 90 else: rad = math.atan(dy / dx) angle = rad * (180 / math.pi) return angle def move_to_agent(self, tar_agent, dest_agent, steps, grid_view=None): print("MTA Tar: ", tar_agent.pos[X], ",", tar_agent.pos[Y]) print("MTA Dest: ", dest_agent.pos[X], ",", dest_agent.pos[Y]) if(tar_agent.pos[X] > dest_agent.pos[X]): if self.is_cell_empty(tar_agent.pos[X] - steps, tar_agent.pos[Y]): self.move(tar_agent, tar_agent.pos[X] - steps, tar_agent.pos[Y])#tar_agent.pos[X] -= steps else: if self.is_cell_empty(tar_agent.pos[X] + steps, tar_agent.pos[Y]): self.move(tar_agent, tar_agent.pos[X] + steps, tar_agent.pos[Y])#tar_agent.pos[X] += steps if(tar_agent.pos[Y] < dest_agent.pos[Y]): if self.is_cell_empty(tar_agent.pos[X], tar_agent.pos[Y] - steps): self.move(tar_agent, tar_agent.pos[X], tar_agent.pos[Y] - steps)#tar_agent.pos[Y] -= steps else: if self.is_cell_empty(tar_agent.pos[X], tar_agent.pos[Y] + steps): self.move(tar_agent, tar_agent.pos[X], tar_agent.pos[Y] + steps)#tar_agent.pos[Y] += steps if(tar_agent.pos[Y] == dest_agent.pos[Y]): if(tar_agent.pos[X] == dest_agent.pos[X]): print("target at the destination") def move_from_agent(self, tar_agent, dest_agent, steps, grid_view=None): print("MFA Tar: ",tar_agent.pos[X], ",", tar_agent.pos[Y]) print("MFA Dest: ",tar_agent.pos[X], ",", tar_agent.pos[Y]) if(tar_agent.pos[X] > dest_agent.pos[X]): if self.is_cell_empty(tar_agent.pos[X] + steps, tar_agent.pos[Y]): tar_agent.pos[X] += steps else: if self.is_cell_empty(tar_agent.pos[X] - steps, tar_agent.pos[Y]): tar_agent.pos[X] -= steps if(tar_agent.pos[Y] < dest_agent.pos[Y]): if self.is_cell_empty(tar_agent.pos[X], tar_agent.pos[Y] - steps): tar_agent.pos[Y] += steps else: if self.is_cell_empty(tar_agent.pos[X], tar_agent.pos[Y] - steps): tar_agent.pos[Y] -= steps #===================================================================================== def find_empty(self, grid_view=None): """ Return a random, empty cell. """ if self.exists_empty_cells(): if grid_view is None: return random.choice(self.empties) else: view_empties = grid_view.get_empties() if view_empties: return random.choice(view_empties) return None def position_item(self, item, x=RANDOM, y=RANDOM, grid_view=None): """ 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. 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). """ if x == RANDOM or y == RANDOM: cell = self.find_empty(grid_view) if cell is None: return None else: cell = self._get_cell(x, y) self._place_item(cell, item) return cell def _place_item(self, cell, item): """ Place an agent in the grid. """ cell.add_item(item) if cell in self.empties: self.empties.remove(cell) def _get_contents(self, x, y): """ Extract contents from cell at x, y """ return self._get_cell(x, y).contents def move(self, item, x, y): """ Move item from its old cell to cell at x, y. """ dest = self._get_cell(x, y) self._move_item(item, dest) def _move_item(self, item, dest): old_cell = item.cell if old_cell is not None: old_cell.remove_item(item) self._check_empty(old_cell) self._place_item(dest, item) def _check_empty(self, cell): if cell.is_empty(): self.empties.append(cell) def _get_cell(self, x, y): return self.grid[y][x] def is_cell_empty(self, x, y): """ Returns True if cell is empty, else False. A non-existent cell is NOT empty, i.e., not free to move to! """ if self.out_of_bounds(x, y): return False else: return self._get_contents(x, y) is None def dist(self, agent1, agent2): """ Arguments: Two grid agents. Returns: The Euclidian distance between the two agents. There isn't numerical ill-conditioning with this because the positions are integers. If there are any applications where positions are given by nonintegral values, use caution. """ return math.sqrt((agent1.pos[X]-agent2.pos[X])**2 + (agent1.pos[Y]-agent2.pos[Y])**2) def free_spot_near(self, agent): """ Looks for an empty cell near agent. If the grid is full, returns None. Argument: The agent whose nearest empty cell we look for. Returns: Either (a) the position of this empty cell or (b) null in which case the entire grid is full of agents. """ max_poss_dist = max(self.width, self.height) for i in range(max_poss_dist): view = self.get_square_view(center=agent.pos, distance=i) for cell in view: if cell.is_empty(): return cell.coords return None def to_json(self): """ We're going to make a dictionary of the 'safe' parts of the object to output to a json file. (We can't output the env, for instance, since IT contains a reference to each agent!) """ safe_fields = super().to_json() safe_fields["torus"] = self.torus safe_fields["num_cells"] = self.num_cells return safe_fields def from_json(self, json_input): super().from_json(json_input) self.__init_unrestorables() self.torus = json_input["torus"] self.num_cells = json_input["num_cells"] def restore_agent(self, agent_json): new_agent = ta.TestGridAgent(agent_json["name"], agent_json["goal"], agent_json["max_move"], agent_json["max_detect"]) self.add_agent_to_grid(new_agent, agent_json) def add_agent_to_grid(self, agent, agent_json): x, y = agent_json["cell"]["coordx"], agent_json["cell"]["coordy"] agent.from_json_preadd(agent_json) self.add_agent(agent, x, y, True) agent.from_json_postadd(agent_json) def print_env(self): msg = "" for row in self.grid: for cell in row: msg += (str(cell.contents) + ", ") msg += "\n" logging.info(msg) def set_agent_color(self): logging.info("set_agent_color is not implemented")