projectmesa/mesa

Proposal: Flexible move_agent method that allows multiple movement strategies

EwoutH opened this issue · 5 comments

The Mesa Space module allows a few specific agent movements and interactions within a grid environment. However, the current methods lack a consistent way to apply different movement strategies. This proposal suggests integrating a single, versatile move_agent method, accepting a parameter that can either be a tuple for specific coordinates, a string indicating a movement strategy (like "random" or "empty"), or an object defining a neighborhood.

In the future, it can be extended with movement strategies based on properties (see #1898).

Motivation

I was building a toy model, and

x = self.random.randrange(self.grid.width)
y = self.random.randrange(self.grid.height)
self.grid.place_agent(a, (x, y))

just looked weird and limited.

Proposed changes

  1. Unified move method:

    • move_agent(agent: Agent, destination) → None
    • The destination parameter is versatile:
      • It can be a tuple (x, y) for moving the agent to specific coordinates.
      • It can be a string, such as "random" for a random cell, "empty" for a random empty cell.
      • It can be an object or a dictionary defining a neighborhood, allowing custom definitions of neighboring cells. For this we need a formal neighborhood definition, see #1900.
  2. Retiring redundant methods:

    • Methods like move_to_empty and place_agent would be redundant and can be removed, as their functionalities are integrated into the new move_agent.
  3. Enhanced out-of-bounds and validity checking:

    • Maintain out_of_bounds method, but possibly enhance it to include checks for cell occupancy, ensuring valid movement destinations.
  4. Agent removal and position swapping:

    • remove_agent and swap_pos methods remain useful and unchanged.

Example Implementations

  • Moving to a Specific Cell:
    space.move_agent(agent, (x, y))
  • Moving to a Random Cell:
    space.move_agent(agent, "random")
  • Moving to a Random Empty Cell:
    space.move_agent(agent, "empty")
  • Moving to a Custom-Defined Neighborhood:
    (see #1900)
    neighborhood_def = {"type": "Moore", "radius": 2}
    space.move_agent(agent, neighborhood_def)

Conclusion

This proposal aims to simplify and unify the movement methods in the Mesa Space module. By consolidating various movement strategies into a single method, we enhance the API's usability and flexibility, allowing users to execute complex movements with minimal and more intuitive code.

Notes

Structural Pattern Matching in Python 3.10 might help a lot with the implementation.

I was thinking about how this would integrate with the _PropertyGrid introduced in #1898. That currently offers separate functions for the selection of target cells, and then moving to the target cells.

def select_cells_by_properties():
def move_agent_to_cell_by_properties():
def select_extreme_value_cells():
def move_agent_to_extreme_value_cell():

If move_agent would also take a list or mask of target cells, that could allow removing the two movement methods from #1898, and make the API more consistent. All movement goes through move_agents, and any amount of custom methods can be written to select some target cells.

Currently, #1898 also include two mask functions which limit the number of candidate cells to cells empty or in a neighborhood.

def get_empty_mask():
def get_neighborhood_mask():

We might also want to integrate that into the move_agent method. It could take a mask or a list of masks.

A utility method to combine a list of masks into a single mask could also be provided.

Maybe we can combine things:

def move_agent(agent, pos=None, empty=False, neighborhood=None, mask=None, selection="random"):
  • pos: A single position or list of possible positions
  • empty: False allows all cells, True only empty ones
  • neighborhood: Optional neighborhood (#1900)
  • mask: a mask or list of masks of allowed input cells
  • selection: Can be "random" or "closest". Maybe something else in the future (in the _PropertyGrid it could be highest or lowest maybe).

@jackiekazil @tpike3 Same story as with #1905, I would like to discuss and potentially implement this before moving on the PropertyLayer (#1898), since this is a more generalized solution for a problem that I specifically solve in that PR. So if we can implement this, it would make especially the _PropertyGrid in #1898 a lot simpler.

So my specific questions are:

  1. Do you agree we should expand the built-in move_agent() movement method to allow moving to an empty, random or neighbouring cell?
  2. What do you think of the current proposed API? Do you like the initial one better or the last one?

I went for a relatively simple implementation: The move_agent method now can take a list of positions and choose either one randomly or the closest:

Selecting neighborhoods and empty cells can be done with other functions, and support for masks will be added in #1898.

There is still possibility for improvement here. The current most elegant way to place an agent on an empty cell I discovered is:

self.grid.place_agent(agent, pos=mesa.model.random.choice(list(self.grid.empties)))

This can't be the best way. The current problems are:

  • place_agent doesn't directly allow selecting an empty cell, or a random cell for that matter. It needs a single position, and doesn't allow a list of positions like move_agent
  • All move functions (move_to_empty, move_agent_to_one_of and move_agent) require a current position to be had, since they call remove_agent internally.

I would like to discuss how we can make placing an agent on a random empty cell more elegant.