import math
import numpy as np
import pandas
from deprecated import deprecated
from ECAgent.Core import Agent, Environment, Component, Model, ComponentNotFoundError
from typing import Optional, List
[docs]def discrete_grid_pos_to_id(x: int, y: int = 0, width: int = 0, z: int = 0, height: int = 0):
"""Returns a unique number of based on the x, y and z coordinates entered.
Uniqueness is dimension dependent. The equation for calculating uniqueness is defined as:
``(z * width * height) + (y * width) + x``
Parameters
----------
x : int
The x-coordinate.
y : int, Optional
The y-coordinate. Defaults to 0.
width : int, Optional
The width of the environment. Defaults to 0.
z : int, Optional
The z-coordinate. Defaults to 0.
height : int, Optional
The height of the environment. Defaults to 0.
Returns
-------
int
The unique ID.
"""
return (z * width * height) + (y * width) + x
[docs]@deprecated(reason='For not meeting standard python naming conventions. Use "discrete_grid_pos_to_id" instead.')
def discreteGridPosToID(x: int, y: int = 0, width: int = 0, z: int = 0, height: int = 0): # pragma: no cover
"""Deprecated. Use ``discrete_grid_pos_to_id`` instead."""
return discrete_grid_pos_to_id(x, y, width, z, height)
[docs]class PositionComponent(Component):
"""A position component. It contains three float properties: x, y, z.
This component can be used to store the position of an Agent in a 1-3D world.
It is used by ``DiscreteWorld`` classes to do exactly that.
"""
__slots__ = ['x', 'y', 'z']
def __init__(self, agent, model, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> None:
super().__init__(agent, model)
self.x = x
self.y = y
self.z = z
[docs] def get_position(self) -> (float, float, float):
"""Returns the x,y and z values of the component as a tuple"""
return self.x, self.y, self.z
[docs] @deprecated(reason='For not meeting standard python naming conventions. Use "get_position()" instead.')
def getPosition(self) -> (float, float, float): # pragma: no cover
"""Deprecated. Use ``get_position()`` instead."""
return self.get_position()
[docs] def xy(self):
"""Returns the x and y values of the component as a 2-tuple.
"""
return self.x, self.y
[docs] def xz(self):
"""Returns the x and z values of the component as a 2-tuple.
"""
return self.x, self.z
[docs] def yz(self):
"""Returns the y and z values of the component as a 2-tuple.
"""
return self.y, self.z
[docs] def xyz(self):
"""Returns the x, y and z values of the component as a 3-tuple.
Equivalent to ``PositionComponent.get_position()``
"""
return self.get_position()
[docs]def distance(a: PositionComponent, b: PositionComponent) -> float:
"""Calculates the distance from ``PositionComponent`` a to ``PositionComponent`` b.
Parameters
----------
a : PositionComponent
The first set of coordinates.
b : PositionComponent
The second set of coordinates.
Returns
-------
float
The distance from a to b.
"""
return math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2)
[docs]def distance_sqr(a: PositionComponent, b: PositionComponent) -> float:
"""Calculates the squared distance from ``PositionComponent`` a to ``PositionComponent`` b.
This function does not call ``math.sqrt()`` so it is more performant than calling ``distance(a,b)``.
This can be useful in situations where you don't need the exact distance (e.g. when comparing distances).
Parameters
----------
a : PositionComponent
The first set of coordinates.
b : PositionComponent
The second set of coordinates.
Returns
-------
float
The squared distance from a to b.
"""
return (a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2
[docs]class ConstantGenerator:
"""A functor used to create CellComponents with a constant value.
The idea behind this class is to create a ``ConstantGenerator(val)`` object and supply it as the generator when
calling ``add_cell_component`` to a DiscreteWorld (e.g. ``GridWorld``). The component will then be created with all
cells having ``value == val``
Assuming a ``3x3 DiscreteWorld``::
env.add_cell_component('constant', ConstantGenerator(1))
will create a cell component called ``'constant'`` which will have values stored in a
contiguous array: ``[1,1,1,1,1,1,1,1,1]`` which when viewed in 2D looks like:
+---------+---------+---------+
| 1 | 1 | 1 |
+---------+---------+---------+
| 1 | 1 | 1 |
+---------+---------+---------+
| 1 | 1 | 1 |
+---------+---------+---------+
Attributes
----------
value : Any
The value you want to set your cell component's values to.
"""
def __init__(self, value):
self.value = value
def __call__(self, pos: tuple, cells: pandas.DataFrame):
"""Used by the ``add_cell_component`` methods to populate a cell component with the value of ``self.value``.
"""
return self.value
[docs]class LookupGenerator:
"""A functor used to create CellComponents based on a Lookup table.
The intention behind this class is to add a convenient way for users to create Cell Components with different
values. This is done by creating a lookup table (of the same dimensions as the environment) and supplying it to the
generator ``LookupGenerator(lookup_table)``.
Note that coordinates are filled in a [x, y, z] fashion. This means you may need to transform your data
(e.g. an image) so that x-coordinates can be referenced first.
Assuming a ``3x3 DiscreteWorld``::
table = [[0, 1, 2],
[3, 4, 5],
[6, 7, 8]]
env.add_cell_component('lookup', LookupGenerator(table))
will create a cell component called ``'lookup'`` which will have values stored in a
contiguous array: ``[0,1,2,3,4,5,6,7,8]`` which when viewed in 2D looks like:
+---------+---------+-----------+
| 6 | 7 | 8 |
+---------+---------+-----------+
| 3 | 4 | 5 |
+---------+---------+-----------+
| 0 | 1 | 2 |
+---------+---------+-----------+
Attributes
----------
table : Any
The lookup table.
"""
def __init__(self, table):
self.table = table
def __call__(self, pos, cells: pandas.DataFrame):
"""Used by the ``addCellComponent`` methods to populate a cell component with the value of ``self.table``.
at coordinate ``pos``.
"""
if type(pos) == int: # LineWorld
return self.table[pos]
elif len(pos) == 2: # GridWorld
return self.table[pos[0]][pos[1]]
else: # CubeWorld
return self.table[pos[0]][pos[1]][pos[2]]
[docs]class SpaceWorld(Environment):
"""Base Class for all Spacial Environments. It inherits from the Environment base class and contains properties
related to the spacial extents of the environment. Currently, the environment can, at most, be three dimensional.
Note that the origin is (0,0,0) and is assumed to be located in the bottom left corner of the environment.
Attributes
----------
width : float
The width of the environment. This attribute can be thought of as the spacial extent of the x-axis.
height : float
The height of the environment. This attribute can be thought of as the spacial extent of the y-axis.
depth : float
The depth of the environment. This attribute can be thought of as the spacial extent of the z-axis.
wrap_env : bool
Determines if the environment is toroidal (i.e. agents wrap around the environment instead of moving out of
bounds).
"""
__slots__ = ['width', 'height', 'depth', 'wrap_env', '_index_offset']
def __init__(self, model: Model, width: float, height: Optional[float] = 0.0, depth: Optional[float] = 0.0,
id: Optional[str] = 'ENVIRONMENT', wrap_env: Optional[bool] = False):
"""Creates a SpaceWorld environment.
Parameters
----------
model : Model
The ``Model`` the environment belongs to.
width : float
The width of the environment.
height : Optional[float]
The height of the environment. Defaults to ``0.0``
depth : Optional[float]
The depth of the environment. Defaults to ``0.0``
id : Optional[str]
The id of the environment. Defaults to ``'ENVIRONMENT'``.
wrap_env : Optional[bool]
Determines if the environment is toroidal (i.e. agents wrap around the environment instead of moving out of
bounds).
"""
super().__init__(model, id=id)
self.width = width
self.height = height
self.depth = depth
self.wrap_env = wrap_env
self._index_offset = 0 # This property is used manage spatial extents in Discrete vs Continuous Environments
[docs] def add_agent(self, agent: Agent, x_pos: int = 0, y_pos: int = 0, z_pos: int = 0):
"""Adds an agent to the environment. Overrides the base ``Environment.add_agent`` class function.
This function will also add a ``PositionComponent`` to the agent object.
If the x, y or z positions are greater than or equal to the width, height and depth of the world
(or less than zero), an error will be thrown.
Parameters
----------
agent : Agent
The agent being added to the environment.
x_pos : int, Optional
The starting x-position of the agent. Defaults to 0.
y_pos : int, Optional
The starting y-position of the agent. Defaults to 0.
z_pos : int, Optional
The starting z-position of the agent. Defaults to 0.
Raises
------
DuplicateAgentError
If the agent already exists in the environment.
Exception
If the agent's initial position is outside the environment's spacial extents.
"""
# TODO create Error for being outside spacial extents.
x_bool = x_pos > self.width - self._index_offset or x_pos < 0 if self.width > 0 else False
y_bool = y_pos > self.height - self._index_offset or y_pos < 0 if self.height > 0 else False
z_bool = z_pos > self.depth - self._index_offset or z_pos < 0 if self.depth > 0 else False
if x_bool or y_bool or z_bool:
raise Exception("Cannot add the Agent to position not on the map.")
super().add_agent(agent)
agent.add_component(PositionComponent(agent, agent.model, x=x_pos, y=y_pos, z=z_pos))
[docs] def remove_agent(self, a_id: str):
"""Removes the agent from the environment. Overrides the base ``Environment.remove_agent`` method.
This method will also remove the ``PositionComponent`` from the agent.
Parameters
----------
a_id : str
The ``id`` of the agent to remove.
Raises
------
AgentNotFoundError
If no agent with an ``agent.id == a_id`` can be found.
"""
if a_id in self.agents:
self.agents[a_id].remove_component(PositionComponent)
super().remove_agent(a_id)
[docs] def get_agents_at(self, x_pos: float = 0.0, y_pos: float = 0.0, z_pos: float = 0.0, leeway: float = 0.0,
x_leeway: float = 0, y_leeway: float = 0, z_leeway: float = 0) -> List[Agent]:
"""Returns a list of agents at position (x_pos, y_pos, z_pos) +/- any leeway.
The function will return ``[]`` empty if no agents are within the specified region.
The function also uses the maximum possible leeway for a given axis. So if the following is executed::
env.get_agents_at(10.0, 10.0, 10.0, leeway = 3.0, y_leeway = 5.0)
The function will return a list of agents within ``3.0`` units of the coordinates ``(10.0, 10.0, 10.0)`` on the
x and z axes and within ``5.0`` units of the coordinates ``(10.0, 10.0, 10.0)`` on the y-axis.
Note: This function respects if the environment is toroidal (i.e. ``wrap_mode == True``).
Parameters
----------
x_pos : float, Optional
The x-coordinate of the search origin point. Defaults to ``0.0``.
y_pos : float, Optional
The y-coordinate of the search origin point. Defaults to ``0.0``.
z_pos : float, Optional
The z-coordinate of the search origin point. Defaults to ``0.0``.
leeway : float, Optional
The general leeway value (i.e. leeway applied to all axes). Defaults to ``0.0``.
x_leeway : float, Optional
The x-axis leeway (i.e. leeway applied to x-axis). Defaults to ``0.0``.
y_leeway : float, Optional
The y-axis leeway (i.e. leeway applied to y-axis). Defaults to ``0.0``.
z_leeway : float, Optional
The z-axis leeway (i.e. leeway applied to z-axis). Defaults to ``0.0``.
Returns
-------
List[Agent]
A list of agents within the specified coordinates. An empty list ``[]`` is returned if no agents are found.
"""
# TODO Account for clamp mode
xmin, xmax = min(x_pos - x_leeway, x_pos - leeway), max(x_pos + x_leeway, x_pos + leeway)
ymin, ymax = min(y_pos - y_leeway, y_pos - leeway), max(y_pos + y_leeway, y_pos + leeway)
zmin, zmax = min(z_pos - z_leeway, z_pos - leeway), max(z_pos + z_leeway, z_pos + leeway)
return [self.agents[agentKey] for agentKey in self.agents
if xmin <= self.agents[agentKey][PositionComponent].x <= xmax
and ymin <= self.agents[agentKey][PositionComponent].y <= ymax
and zmin <= self.agents[agentKey][PositionComponent].z <= zmax]
[docs] def get_dimensions(self) -> (int, int, int):
"""Returns a 3-tuple containing the extents of the environment:
``(width, height, depth)``."""
return self.width, self.height, self.depth
[docs] def move(self, agent: Agent, x: float = 0, y: float = 0, z: float = 0):
"""Moves an agent (x,y,z) units in the environment.
The function automatically clamps agent movement to the range::
(0, 0, 0) <= x < (self.width, self.height, self.depth)
If environment is non-toroidal (i.e. ``wrap_env == False``).
Parameters
----------
agent : Agent
The agent object to be moved.
x : int, Optional
The number of discrete units to move the agent. Defaults to 0.
y : int, Optional
The number of discrete units to move the agent. Defaults to 0.
z : int, Optional
The number of discrete units to move the agent. Defaults to 0.
Raises
------
ComponentNotFoundError
If ``agent`` does not have a ``PositionComponent``.
"""
if PositionComponent not in agent:
raise ComponentNotFoundError(agent, PositionComponent)
component = agent[PositionComponent]
if self.wrap_env:
if self.width != 0:
component.x = (component.x + x) % self.width
if self.height != 0:
component.y = (component.y + y) % self.height
if self.depth != 0:
component.z = (component.z + z) % self.depth
else:
component.x = max(min(component.x + x, self.width - self._index_offset), 0)
component.y = max(min(component.y + y, self.height - self._index_offset), 0)
component.z = max(min(component.z + z, self.depth - self._index_offset), 0)
[docs] def move_to(self, agent: Agent, x: float = 0, y: float = 0, z: float = 0):
"""Moves an agent to position (x,y,z) in the environment.
Parameters
----------
agent : Agent
The agent object to be moved.
x : int, Optional
The new x-coordinate of the agent. Defaults to 0.
y : int, Optional
The new y-coordinate of the agent. Defaults to 0.
z : int, Optional
The new z-coordinate of the agent. Defaults to 0.
Raises
------
ComponentNotFoundError
If ``agent`` does not have a ``PositionComponent``.
IndexError
If coordinates are out of bounds.
"""
if PositionComponent not in agent:
raise ComponentNotFoundError(agent, PositionComponent)
elif (0 <= x <= self.width - self._index_offset or self.width < 1) and (
0 <= y <= self.height - self._index_offset or self.height < 1) and (
0 <= z <= self.depth - self._index_offset or self.depth < 1):
component = agent[PositionComponent]
component.x = x
component.y = y
component.z = z
else:
raise IndexError(f'Position ({x},{y},{z}) is out of the environment\'s range')
# TODO ADD WRAP ENV TO BELOW WORLDS
[docs]class DiscreteWorld(SpaceWorld):
"""Base Class for all Discrete Spacial Environments. It inherits from ``SpaceWorld``class and contains properties
and methods related to grid-based environments.
This class also adds functionality to add cell components. This is a special type of component that stores a single
value for every cell in the DiscreteWorld.
Assuming a ``3x3 DiscreteWorld``::
env = DiscreteWorld(model, 3, 3)
You can add a cell component to the environment by calling::
env.add_cell_component('example', generator)
Where ``'example'`` will be the name of cell component. The second argument is known as a generator and is a function
object (functor) that populates the gridworld with the value of the cell component.
A custom generator can be written as follows::
def custom_generator(pos, cells):
# add logic here
return value_of_cell
When writing a generator, your function must accept two arguments: the ``cells`` which is the pandas dataframe of
of the environment and ``pos`` which is a 3-tuple which contains the coordinates ``(x,y,z)`` of the grid cell
you are generating for. So using the example::
def sum_generator(pos, cells):
return sum(*pos)
env.add_cell_component('sum', sum_generator)
Our original ``3x3 DiscreteWorld`` will get a cell component called ``'sum'`` which will have values stored in a
contiguous array: ``[0,1,2,1,2,3,2,3,4]`` which when viewed in 2D looks like:
+---------+---------+---------+
| 2 | 3 | 4 |
+---------+---------+---------+
| 1 | 2 | 3 |
+---------+---------+---------+
| 0 | 1 | 2 |
+---------+---------+---------+
**Note** that all cell components are stored as 1D arrays which you can access using ``env.cells[cell_name]''. To
translate a 3d coordinate (or PositionComponent) into a unique integer to get the value of a specific cell in a
``DiscreteWorld``, use the ``discrete_grid_pos_to_id`` method. Additionally, all ``DiscreteWorld`` environments
are initialized with a ``'pos'`` cell component which contains the 3d coordinate representation of the cell.
Attributes
----------
model : Model
The ``Model`` the environment belongs to.
width : int
The width of the environment.
height : int
The height of the environment. Defaults to ``0``
depth : int
The depth of the environment. Defaults to ``0``
cells : Pandas.DataFrame
A table containing all of the cell components in the environment.
id : str
The id of the environment. Defaults to ``'ENVIRONMENT'``.
wrap_env : bool
Determines if the environment is toroidal (i.e. agents wrap around the environment instead of moving out of
bounds).
"""
def __init__(self, model, width: int, height: Optional[int] = 0, depth: Optional[int] = 0,
id: Optional[str] = 'ENVIRONMENT', wrap_env: Optional[bool] = False):
super().__init__(model, width, height, depth, id=id, wrap_env=wrap_env)
if type(width) != int or type(height) != int or type(depth) != int:
raise AttributeError(f"DiscreteWorld environment's dimensions must of type (int, int, int) not "
f"({type(width)}, {type(height)}, {type(depth)}).")
self._index_offset = 1 # DiscreteWorlds operate at discrete coordinates starting at 0, this accounts for that.
# Create cells
self.cells = pandas.DataFrame({
'pos': [(x, y, z) for z in range(max(depth, 1)) for y in range(max(height, 1)) for x in range(max(width, 1))]
})
[docs] def add_cell_component(self, name: str, generator):
"""Adds the component supplied by the generator functor to each of the cells.
The functor is supplied with the cell's position ``(x,y,z)`` and the environment pandas dataframe as input.
A custom generator can be written as follows::
def custom_generator(pos, cells):
# add logic here
return value_of_cell
You can also use the one of the included generators: ``ConstantGenerator`` or ``LookupGenerator``. Alternatively,
you can populate the cells by directly supplying their data as a 1D contiguous array that is the same size
as the environment. Assuming a ``3x3 GridWorld``::
data = [0, 1, 2, 3, 4, 5, 6, 7, 8]
env.add_cell_component('data', data)
will create a cell component called ``'data'`` which will have values stored in a
contiguous array: ``[0,1,2,3,4,5,6,7,8]`` which when viewed in 2D looks like:
+---------+---------+---------+
| 6 | 7 | 8 |
+---------+---------+---------+
| 3 | 4 | 5 |
+---------+---------+---------+
| 0 | 1 | 2 |
+---------+---------+---------+
Parameters
----------
name : str
The name of the cell component.
generator : obj | numpy.ndarray | list
The generator used to populate the cell component. If a obj is supplied, it must have the ``__call__``
method implemented. If a ``numpy.ndarray`` or ``list`` is used, it must be 1-dimensional and of size
``width * height * depth``.
"""
if isinstance(generator, np.ndarray):
self.cells[name] = np.copy(generator)
elif isinstance(generator, list):
self.cells[name] = generator
else:
self.cells[name] = [generator(pos, self.cells) for pos in self.cells['pos']]
[docs] @deprecated(reason='For not meeting standard python naming conventions. Use "add_cell_component" instead.')
def addCellComponent(self, name: str, generator): # pragma: no cover
"""Deprecated. Use ``add_cell_component`` instead."""
self.add_cell_component(name, generator)
[docs] def remove_cell_component(self, name: str):
"""Removes a cell component from the environment.
Parameters
----------
name : str
The name of the cell component.
Raises
------
ComponentNotFoundError
If no cell component with the specified name can be found.
"""
if name not in self.cells:
raise ComponentNotFoundError(self, name)
else:
self.cells.drop(columns=[name], inplace=True)
[docs] def get_cell(self, x, y: int = 0, z: int = 0) -> pandas.Series:
"""Returns a ``Pandas.Series`` containing the values of cell components at the specified grid cell.
Assuming a ``3x1 DiscreteWorld`` with two cell components called ``'rainfall'`` and ``'slope'``::
# The cell DataFrame will look something like:
[{'pos': (0,0,0), 'rainfall': 10.0, 'slope': 4.0},
{'pos': (1,0,0), 'rainfall': 22.0, 'slope': 21.0},
{'pos': (2,0,0), 'rainfall': 15.0, 'slope': 32.0}]
cell = env.get_cell(1)
print(cell)
# Will print something like:
{'pos': (1,0,0), 'rainfall': 22.0, 'slope': 21.0}
Parameters
----------
x : int
The x-coordinate.
y : int, Optional
The y-coordinate. Defaults to 0.
z : int, Optional
The z-coordinate. Defaults to 0.
Returns
-------
Pandas.Series
Containing all of the values for the cell components at the indexed grid cell.
Raises
------
IndexError
If the specified coordinates are our outside the bound of the environment.
"""
if x < 0 or x >= self.width or y < 0 or y >= self.height or z < 0 or z >= self.depth:
raise IndexError(f'Coordinate ({x},{y},{z}) is not within the bounds of the environment.')
else:
return self.cells.iloc[discrete_grid_pos_to_id(x, y, self.width, z, self.height)]
[docs] @deprecated(reason='For not meeting standard python naming conventions. Use "get_cell" instead.')
def getCell(self, x, y: int = 0, z: int = 0) -> pandas.Series: # pragma: no cover
"""Deprecated. Use ``get_cell`` instead."""
return self.get_cell(x, y, z)
def _get_cell_pos_as_tuple(self, cell_pos) -> (int, int, int):
"""Returns the coordinates of a ``DiscreteWorld`` cell based in the type supplied by ``cell_pos``.
The function accepts three types: ``int``, ``tuple`` or ``PositionComponent``.
If ``int`` is supplied, it will be assumed to be the *unique identifier* of the cell (i.e. the value returned
by ``discrete_grid_pos_to_id()``. If a ``tuple`` is supplied, it is assumed that it will be the coordinates of
the cell (e.g. ``(2,5,3)``). If a ``PositionComponent`` is supplied, it's values will be truncated and turned
into a 3d integer coordinate (i.e. A ``PositionComponent`` with value ``x = 2.5, y = 5.9, z = 3.1``) will be
converted into coordinates ``(2,5,3)``.
Parameters
----------
cell_pos : int, tuple, PositionComponent
The cell whose neighbours you want to get.
Returns
-------
(int, int, int)
The coordinates of the cell represented as a 3-tuple.
Raises
------
TypeError
If the type of cell_pos is not ``int``, ``tuple`` or ``PositionComponent``.
"""
if isinstance(cell_pos, int):
return self.cells['pos'][cell_pos]
elif isinstance(cell_pos, tuple):
return cell_pos
elif isinstance(cell_pos, PositionComponent):
center = cell_pos.xyz()
# Convert to int
return int(center[0]), int(center[1]), int(center[2])
else:
raise TypeError(f'cell_pos of type {type(cell_pos)} is not supported. Use an int, tuple or '
f'PositionComponent instead')
[docs] def get_moore_neighbours(self, cell_pos, radius: int = 1, incl_center: bool = False, ret_type: type = int) -> list:
"""Returns a list of all cells within the specified moore neighbourhood.
If incl_center = true the supplied cell will also be included in that list.
The function accepts three types: ``int``, ``tuple`` or ``PositionComponent``.
If ``int`` is supplied, it will be assumed to be the *unique identifier* of the cell (i.e. the value returned
by ``discrete_grid_pos_to_id()``. If a ``tuple`` is supplied, it is assumed that it will be the coordinates of
the cell (e.g. ``(2,5,3)``). If a ``PositionComponent`` is supplied, it's values will be truncated and turned
into a 3d integer coordinate (i.e. A ``PositionComponent`` with value ``x = 2.5, y = 5.9, z = 3.1``) will be
converted into coordinates ``(2,5,3)``.
The same functionality applied to the ``ret_type`` parameters. By default a list of integers containing the
*unique identifiers* of the neighbouring cells are returned. If ``tuple`` is supplied, the function will return
the 3d coordinates of the neighbouring will be returned.
Parameters
----------
cell_pos : int, tuple, PositionComponent
The cell whose neighbours you want to get.
radius : int, Optional
The size of the Moore neighbourhood. Defaults to ``1``.
incl_center : bool, Optional
Flags whether you want the supplied ``cell_pos`` to be included in the returned ``list``
ret_type : type, Optional
The representation of the neighbouring cells, Defaults to ``int`` but may also be ``tuple``.
Returns
-------
list
A list of neighbouring cells in the representation specified by ``ret_type``. Defaults to a list of ``int``.
Raises
------
TypeError
If the type of cell_pos is not ``int``, ``tuple`` or ``PositionComponent`` or if the type of ``ret_type`` is
not ``int`` or ``tuple``.
"""
center = self._get_cell_pos_as_tuple(cell_pos)
# Get search bounds
if self.width > 0:
xlower_bound = max(0, center[0] - radius)
xupper_bound = min(self.width, center[0] + radius + 1)
else:
xlower_bound, xupper_bound = 0, 1
if self.height > 0:
ylower_bound = max(0, center[1] - radius)
yupper_bound = min(self.height, center[1] + radius + 1)
else:
ylower_bound, yupper_bound = 0, 1
if self.depth > 0:
zlower_bound = max(0, center[2] - radius)
zupper_bound = min(self.depth, center[2] + radius + 1)
else:
zlower_bound, zupper_bound = 0, 1
def if_int(env, x, y, z):
return discrete_grid_pos_to_id(x, y, env.width, z, env.height)
def if_tuple(env, x, y, z):
return x, y, z
if ret_type == int:
ret_func = if_int
elif ret_type == tuple:
ret_func = if_tuple
else:
raise TypeError(f'ret_type of type {ret_type} is not supported. Use an int or tuple instead.')
neighbours = []
for z in range(zlower_bound, zupper_bound):
for y in range(ylower_bound, yupper_bound):
for x in range(xlower_bound, xupper_bound):
val = ret_func(self, x, y, z)
if center[0] == x and center[1] == y and center[2] == z:
if incl_center:
neighbours.append(val)
else:
neighbours.append(val)
return neighbours
[docs] def get_neumann_neighbours(self, cell_pos, radius: int = 1, incl_center: bool = False, ret_type: type = int) -> list:
"""Returns a list of all cells within the specified von Neumann neighbourhood.
If incl_center = true the supplied cell will also be included in that list.
The function accepts three types: ``int``, ``tuple`` or ``PositionComponent``.
If ``int`` is supplied, it will be assumed to be the *unique identifier* of the cell (i.e. the value returned
by ``discrete_grid_pos_to_id()``. If a ``tuple`` is supplied, it is assumed that it will be the coordinates of
the cell (e.g. ``(2,5,3)``). If a ``PositionComponent`` is supplied, it's values will be truncated and turned
into a 3d integer coordinate (i.e. A ``PositionComponent`` with value ``x = 2.5, y = 5.9, z = 3.1``) will be
converted into coordinates ``(2,5,3)``.
The same functionality applied to the ``ret_type`` parameters. By default a list of integers containing the
*unique identifiers* of the neighbouring cells are returned. If ``tuple`` is supplied, the function will return
the 3d coordinates of the neighbouring will be returned.
Parameters
----------
cell_pos : int, tuple, PositionComponent
The cell whose neighbours you want to get.
radius : int, Optional
The size of the Moore neighbourhood. Defaults to ``1``.
incl_center : bool, Optional
Flags whether you want the supplied ``cell_pos`` to be included in the returned ``list``
ret_type : type, Optional
The representation of the neighbouring cells, Defaults to ``int`` but may also be ``tuple``.
Returns
-------
list
A list of neighbouring cells in the representation specified by ``ret_type``. Defaults to a list of ``int``.
Raises
------
TypeError
If the type of cell_pos is not ``int``, ``tuple`` or ``PositionComponent`` or if the type of ``ret_type`` is
not ``int`` or ``tuple``.
"""
center = self._get_cell_pos_as_tuple(cell_pos)
# Get search bounds
if self.width > 0:
xlower_bound = max(0, center[0] - radius)
xupper_bound = min(self.width, center[0] + radius + 1)
else:
xlower_bound, xupper_bound = 0, 1
if self.height > 0:
ylower_bound = max(0, center[1] - radius)
yupper_bound = min(self.height, center[1] + radius + 1)
else:
ylower_bound, yupper_bound = 0, 1
if self.depth > 0:
zlower_bound = max(0, center[2] - radius)
zupper_bound = min(self.depth, center[2] + radius + 1)
else:
zlower_bound, zupper_bound = 0, 1
def if_int(env, x, y, z):
return discrete_grid_pos_to_id(x, y, env.width, z, env.height)
def if_tuple(env, x, y, z):
return x, y, z
if ret_type == int:
ret_func = if_int
elif ret_type == tuple:
ret_func = if_tuple
else:
raise TypeError(f'ret_type of type {ret_type} is not supported. Use an int or tuple instead.')
neighbours = []
for z in range(zlower_bound, zupper_bound):
for y in range(ylower_bound, yupper_bound):
for x in range(xlower_bound, xupper_bound):
if abs(x - center[0]) + abs(y - center[1]) + abs(z - center[2]) < radius + 1: # Calc Manhattan
val = ret_func(self, x, y, z)
if center[0] == x and center[1] == y and center[2] == z:
if incl_center:
neighbours.append(val)
else:
neighbours.append(val)
return neighbours
[docs] def get_neighbours(self, cell_pos, radius: int = 1, incl_center: bool = False, ret_type: type = int,
mode: str = 'moore') -> list:
"""Returns a list of all cells within the specified neighbourhood.
If incl_center = true the supplied cell will also be included in that list. Both Moore and Von Neumann
neighbourhoods are supported.
The function accepts three types: ``int``, ``tuple`` or ``PositionComponent``.
If ``int`` is supplied, it will be assumed to be the *unique identifier* of the cell (i.e. the value returned
by ``discrete_grid_pos_to_id()``. If a ``tuple`` is supplied, it is assumed that it will be the coordinates of
the cell (e.g. ``(2,5,3)``). If a ``PositionComponent`` is supplied, it's values will be truncated and turned
into a 3d integer coordinate (i.e. A ``PositionComponent`` with value ``x = 2.5, y = 5.9, z = 3.1``) will be
converted into coordinates ``(2,5,3)``.
The same functionality applied to the ``ret_type`` parameters. By default a list of integers containing the
*unique identifiers* of the neighbouring cells are returned. If ``tuple`` is supplied, the function will return
the 3d coordinates of the neighbouring will be returned.
Parameters
----------
cell_pos : int, tuple, PositionComponent
The cell whose neighbours you want to get.
radius : int, Optional
The size of the Moore neighbourhood. Defaults to ``1``.
incl_center : bool, Optional
Flags whether you want the supplied ``cell_pos`` to be included in the returned ``list``
ret_type : type, Optional
The representation of the neighbouring cells, Defaults to ``int`` but may also be ``tuple``.
mode : str
The type of neighbourhood to return. Can either be ``'moore'`` or ``'neumann'``. Defaults to ``'moore'`.
Returns
-------
list
A list of neighbouring cells in the representation specified by ``ret_type``. Defaults to a list of ``int``.
Raises
------
TypeError
If the type of cell_pos is not ``int``, ``tuple`` or ``PositionComponent`` or if the type of ``ret_type`` is
not ``int`` or ``tuple``.
KeyError
If the ``mode`` supplied is not ``'moore'`` or ``'neumann'``.
"""
if mode == 'moore':
return self.get_moore_neighbours(cell_pos, radius, incl_center, ret_type)
elif mode == 'neumann':
return self.get_neumann_neighbours(cell_pos, radius, incl_center, ret_type)
else:
raise KeyError(f'Mode {mode} unrecognized. Use either "moore" or "neumann".')
[docs]class LineWorld(DiscreteWorld):
"""LineWorld is a discrete environment with only 1 axis (x-axis). It is a simplified version of its parent
``DiscreteWorld``.
A LineWorld's dimensions are defined by a ``width`` property.
Attributes
----------
model : Model
The ``Model`` the environment belongs to.
width : float
The width of the environment.
cells : Pandas.DataFrame
A table containing all of the cell components in the environment.
id : str
The id of the environment. Defaults to ``'ENVIRONMENT'``.
wrap_env : bool
Determines if the environment is toroidal (i.e. agents wrap around the environment instead of moving out of
bounds).
"""
def __init__(self, model: Model, width: int, id: str = 'ENVIRONMENT', wrap_env: Optional[bool] = False):
"""Initializes a ``LineWorld`` object.
Parameters
----------
model : Model
The model the environment belongs to.
width : int
The width of the ``LineWorld``.
id : str, Optional
id of the ``LineWorld``.
wrap_env : Optional[bool]
Determines if the environment is toroidal (i.e. agents wrap around the environment instead of moving out of
bounds). Defaults to ``False``.
Raises
------
IndexError
If the ``width`` of the environment is negative.
"""
if width < 1:
raise IndexError("Cannot create a LineWorld with a negative width.")
super().__init__(model, width, 0, 0, id=id, wrap_env=wrap_env)
[docs] def get_dimensions(self) -> int:
"""Gets the dimension of the ``LineWorld``.
Returns
-------
int
The ``width`` of the ``LineWorld``.
"""
return self.width
[docs]class GridWorld(DiscreteWorld):
"""GridWorld is a discrete environment with 2 axes (x and y). It is a simplified version of its parent
``DiscreteWorld``.
A GridWorld's dimensions are defined by a ``width`` and ``height`` properties.
Attributes
----------
model : Model
The ``Model`` the environment belongs to.
width : int
The width of the environment.
height : int
The height of the environment. Defaults to ``0``
cells : Pandas.DataFrame
A table containing all of the cell components in the environment.
id : str
The id of the environment. Defaults to ``'ENVIRONMENT'``.
wrap_env : bool
Determines if the environment is toroidal (i.e. agents wrap around the environment instead of moving out of
bounds).
"""
def __init__(self, model: Model, width: int, height: int, id: str = 'ENVIRONMENT',
wrap_env: Optional[bool] = False):
if width < 1 or height < 1:
raise IndexError("Cannot create a GridWorld with a negative width or height.")
super().__init__(model, width, height, 0, id=id, wrap_env=wrap_env)
[docs] def get_dimensions(self) -> (int, int):
"""Gets the dimension of the ``GridWorld``.
Returns
-------
(int, int)
The ``width`` and ``height`` of the ``GridWorld``.
"""
return self.width, self.height