Source code for agentpy.space

"""
Agentpy Space Module
Content: Class for continuous spatial environments
"""

# TODO Add option of space without shape (infinite)
# TODO Custom iterator for neighbors() & select() for performance

import itertools
import numpy as np
import random as rd
import collections.abc as abc
from scipy import spatial
from .objects import SpatialEnvironment
from .tools import make_list, make_matrix
from .sequences import AgentList, AgentIter


[docs]class Space(SpatialEnvironment): """ Environment that contains agents with a continuous spatial topology. To add new space environments to a model, use :func:`Model.add_space`. For a discrete spatial topology, see :class:`Grid`. This class can be used as a parent class for custom space types. All agentpy model objects call the method :func:`setup` after creation, and can access class attributes like dictionary items. Arguments: model (Model): The model instance. shape (tuple of float): Size of the space. The length of the tuple defines the number of dimensions, and the values in the tuple define the length of each dimension. torus (bool, optional): Whether to connect borders (default False). If True, the space will be toroidal, meaning that agents who move over a border will re-appear on the opposite side. If False, they will remain at the edge of the border. **kwargs: Will be forwarded to :func:`Space.setup`. Attributes: agents (AgentIter): Iterator over all agents in the space. positions (dict of Agent): Dictionary linking each agent instance to its position. shape (tuple of float): Length of each spatial dimension. ndim (int): Number of dimensions. kdtree (scipy.spatial.cKDTree or None): KDTree of agent positions for neighbor lookup. Will be recalculated if agents have moved. If there are no agents, tree is None. """ def __init__(self, model, shape, torus=False, **kwargs): super().__init__(model) self._torus = torus self._cKDTree = None self._sorted_agents = None self._sorted_agent_points = None self.positions = {} self.shape = tuple(shape) self.ndim = len(self.shape) self._set_var_ignore() self.setup(**kwargs) @property def agents(self): return AgentIter(self.model, self.positions.keys()) @property def kdtree(self): # Create new KDTree if necessary if self._cKDTree is None and len(self.agents) > 0: self._sorted_agents = [] self._sorted_agent_points = [] for a in self.agents: self._sorted_agents.append(a) self._sorted_agent_points.append(self.positions[a]) if self._torus: self._cKDTree = spatial.cKDTree(self._sorted_agent_points, boxsize=self.shape) else: self._cKDTree = spatial.cKDTree(self._sorted_agent_points) return self._cKDTree # Return existing or new KDTree # Add and remove agents ------------------------------------------------- #
[docs] def add_agents(self, agents, positions=None, random=False): """ Adds agents to the space environment. Arguments: agents (Sequence of Agent): Instance or iterable of agents to be added. positions (Sequence of positions, optional): The positions of the agents. Must have the same length as 'agents', with each entry being a position (array of float). If none is passed, all positions will be either be zero or random based on the argument 'random'. random (bool, optional): Whether to choose random positions (default False). """ self._cKDTree = None # Reset KDTree if not positions: n_agents = len(agents) if random: positions = [[self.model.random.random() * d_max for d_max in self.shape] for _ in range(n_agents)] else: positions = [np.zeros(self.ndim) for _ in range(n_agents)] for agent, pos in zip(agents, positions): pos = pos if isinstance(pos, np.ndarray) else np.array(pos) self.positions[agent] = pos # Add pos to agent_dict
[docs] def remove_agents(self, agents): """ Removes agents from the space. """ self._cKDTree = None # Reset KDTree for agent in make_list(agents): del self.positions[agent] # Remove agent from env
# Move and select agents ------------------------------------------------ # @staticmethod def _border_behavior(position, shape, torus): # Border behavior # Connected - Jump to other side if torus: for i in range(len(position)): while position[i] > shape[i]: position[i] -= shape[i] while position[i] < 0: position[i] += shape[i] # Not connected - Stop at border else: for i in range(len(position)): if position[i] > shape[i]: position[i] = shape[i] elif position[i] < 0: position[i] = 0
[docs] def move_to(self, agent, pos): """ Moves agent to new position. Arguments: agent (Agent): Instance of the agent. pos (array_like): New position of the agent. """ self._cKDTree = None # Reset KDTree self._border_behavior(pos, self.shape, self._torus) self.positions[agent][...] = pos # In-place
[docs] def move_by(self, agent, path): """ Moves agent to new position, relative to current position. Arguments: agent (Agent): Instance of the agent. path (array_like): Relative change of position. """ pos = [p + c for p, c in zip(self.positions[agent], path)] self.move_to(agent, pos)
[docs] def neighbors(self, agent, distance): """ Select agent neighbors within a given distance. Takes into account wether space is toroidal. Arguments: agent (Agent): Instance of the agent. distance (float): Radius around the agent in which to search for neighbors. Returns: AgentIter: Iterator over the selected neighbors. """ list_ids = self.kdtree.query_ball_point( self.positions[agent], distance) agents = [self._sorted_agents[list_id] for list_id in list_ids] agents.remove(agent) # Remove original agent return AgentIter(self.model, agents)
[docs] def select(self, center, radius): """ Select agents within a given area. Arguments: center (array_like): Coordinates of the center of the search area. radius (float): Radius around the center in which to search. Returns: AgentIter: Iterator over the selected agents. """ if self.kdtree: list_ids = self.kdtree.query_ball_point(center, radius) agents = [self._sorted_agents[list_id] for list_id in list_ids] return AgentIter(self.model, agents) else: return AgentIter(self.model)