Gridsim: Grid-world robot simulator¶
Index¶
Getting Started¶
Installation¶
Note
This assumes that you’re already familiar with virtual environments and pip.
Virtual Environment Setup¶
Create a Python 3 virtual environment in the current location in subfolder called venv
, then set it as the Python source.
$ python3 -m venv venv
$ source venv/bin/activate
You can deactivate the virtual environment with deactivate
.
Quick Install¶
This package is available through pip, so it’s easy to install. With your virual environment active, run:
$ pip install gridsim
Within your own code, you can now import the Gridsim library components, such as:
import gridsim as gs
# Create an empty World of 100 x 100 grid cells
my_world = gs.World(100, 100)
Potential Issues¶
If you get an error when trying to install PyGame (possibly due to Python 3.8) that says sdl-config: not found
, you might need to install system dependencies because PyGame uses an older version (1.2) of SDL. For Ubuntu-like systems, you can use the following:
$ sudo apt install libsdl-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libportmidi-dev
Basic Usage¶
This will walk you through setting up your first robot and complete simulation.
On this page
Test using built in examples¶
The examples are in the examples directory of the source code. In the near future, I’ll set up a way to run the examples directly when you install the package.
Creating a simple robot¶
For more detailed information about developing custom robots, see Make your own Robot.
To start, we will only need to make a simple robot based on the GridRobot
. This needs to implement three methods:
receive_msg()
: Code that is run when a robot receives a messageinit()
: Code that is run once when the robot is createdloop()
: Code that is run in every step of the simulation
Create a file for your robot class. Let’s call it random_robot.py
. Below is a simple Robot that moves randomly and changes direction every 10 seconds. You can copy this or directly download random_robot.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import random
from gridsim.grid_robot import GridRobot
import gridsim as gs
class RandomRobot(GridRobot):
# Change direction every 10 ticks
DIR_DURATION = 10
def init(self):
self.set_color(255, 0, 0)
self._msg_sent = False
# Next tick when Robot will change direction
self._next_dir_change = self.get_tick()
def receive_msg(self, msg: gs.Message, dist_sqr: float):
# This robot got a message from another robot
self._msg_sent = True
def loop(self):
# Change direction every DIR_DURATION ticks
tick = self.get_tick()
if tick >= self._next_dir_change:
new_dir = random.choice(GridRobot.DIRS)
self.set_direction(new_dir)
self._next_dir_change = tick + RandomRobot.DIR_DURATION
# Broadcast a test message to any robots nearby
msg = gs.Message(self.id, {'test': 'hello'})
self.set_tx_message(msg)
# Sample the environment at the current location
c = self.sample()
# Change color depending on whether messages have been sent or received
# Robot will be white when it has successfully sent & received a message
blue = 255 * self._msg_sent
# self.set_color(255, green, 0)
self.set_color(255-c[0], 255-c[1], blue)
|
A minimal simulation example¶
To run a simulation, you need to create a couple of robots, place them in a World
. Then you call the step()
method to execute you simulation step-by-step. step()
will handle running all of the robots’ code, as well as communication and movement.
We also want give our Robots something to sense by adding en environment to the World. An environment here is represented with an image. (You’ll see what this looks like in the next step.) In each cell, the Robots can sense the color of the cell (i.e., the RGB pixel value) at that location with the sample()
method. If you set up the environment with an image whose resolution doesn’t match the grid dimensions, it will be rescaled, possibly stretching the image. To avoid any surprises, you should use an image whose resolution matches your grid dimensions (e.g., for a 50 × 50 grid, use a 50px × 50px image).
Use the code below or download minimal_simulation.py
and the example environment ex_env.png
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import gridsim as gs
from random_robot import RandomRobot
def main():
grid_width = 50 # Number of cells for the width & height of the world
num_robots = 5
num_steps = 100 # simulation steps to run
# Create a few robots to place in your world
robots = []
for n in range(num_robots):
robots.append(RandomRobot(grid_width/2 - n*2,
grid_width/2 - n*2))
# Create a 50 x 50 World with the Robots
world = gs.World(grid_width, grid_width,
robots=robots,
environment="ex_env.png")
# Run the simulation
for n in range(num_steps):
# Execute a simulation step
world.step()
# To make sure it works, print the tick (world time)
print('Time:', world.get_time())
print('SIMULATION FINISHED')
if __name__ == '__main__':
# Run the simulation if this program is called directly
main()
|
With these files and random_robot.py
in the same directory, and gridsim
installed, you should be able to run the code with:
$ python3 minimal_simulation.py
Adding the Viewer¶
With that simple example, you have no way to see what the robots are doing. For that, we add a Viewer
. This requires adding only two lines of code to our minimal simulation above.
Use the code below or download viewer_simulation.py
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import gridsim as gs
from random_robot import RandomRobot
def main():
grid_width = 50 # Number of cells for the width & height of the world
num_robots = 5
num_steps = 100 # simulation steps to run
# Create a few robots to place in your world
robots = []
for n in range(num_robots):
robots.append(RandomRobot(grid_width/2 - n*2,
grid_width/2 - n*2))
# Create a 50 x 50 World with the Robots
world = gs.World(grid_width, grid_width,
robots=robots,
environment="ex_env.png")
# Create a Viewer to display the World
viewer = gs.Viewer(world)
# Run the simulation
for n in range(num_steps):
# Execute a simulation step
world.step()
# Draw the world
viewer.draw()
# To make sure it works, print the tick (world time)
print('Time:', world.get_time())
print('SIMULATION FINISHED')
if __name__ == '__main__':
# Run the simulation if this program is called directly
main()
|
Notice that adding the Viewer slows down the time to complete the simulation, because the display rate of the Viewer limits the simulation rate. If you want to run lots of simulations, turn off your Viewer.
Using configuration files¶
Gridsim also provides the ConfigParser
for using YAML configuration files. This simplifies loading parameters and (as described in the next section) saving parameters with simulation results data.
The ConfigParser
is un-opinionated; it doesn’t place any restrictions on what your configuration files look like, as long as they’re valid YAML files.
Compared to our minimal_simulation.py
, we only need one line to create our ConfigParser
, from which we can retrieve any parameter values.
Use the code below or download config_simulation.py
and YAML configuration file simple_config.yml
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import gridsim as gs
from random_robot import RandomRobot
def main():
config = gs.ConfigParser('simple_config.yml')
print(config.get('name'))
grid_width = config.get('grid_width')
num_robots = config.get('num_robots')
# You can specify a default value in case a parameter isn't in the
# configuration file
num_steps = config.get('num_steps', default=100)
# Create a few robots to place in your world
robots = []
# Configuration values can also be lists, not just single values.
x_pos = config.get('robot_x_pos')
for n in range(num_robots):
robots.append(RandomRobot(x_pos[n],
grid_width/2 - n*2))
# Create a 50 x 50 World with the Robots
world = gs.World(grid_width, grid_width, robots=robots)
# Run the simulation
for n in range(num_steps):
# Execute a simulation step
world.step()
# To make sure it works, print the tick (world time)
print('Time:', world.get_time())
print('SIMULATION FINISHED')
if __name__ == '__main__':
# Run the simulation if this program is called directly
main()
|
Logging data¶
Gridsim has a built-in Logger
, designed to easily save data from your simulations to HDF5 files. This allows you to store complex data and simulation configurations together in one place. HDF5 files are also easy to read and write in many different programming languages.
There are three main ways to save data to your log files:
Save the parameters in your configuration with
log_config()
. (Note that not all data types can be saved withlog_config
. See its documentation for more details.)Save a single parameter (that’s not in your configuration file) with
log_param()
Save the state of your simulation/robots with
log_state()
. (This requires some setup.)
In order to log the state of the World, you first need to tell the Logger
what you want to save about the log_state
, this function is called and the result is added to your dataset. You can add as many aggregators as you want, each with their own name.
We can extend our config_simulation.py
to show the three types of logging described above. Use the code below or download logger_simulation.py
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | import gridsim as gs
from typing import List
import numpy as np
from datetime import datetime
from random_robot import RandomRobot
def green_agg(robots: List[gs.Robot]) -> np.ndarray:
"""
This is a dummy aggregator function (for demonstration) that just saves
the value of each robot's green color channel
"""
out_arr = np.zeros([len(robots)])
for i, r in enumerate(robots):
out_arr[i] = r._color[1]
return out_arr
def main():
config = gs.ConfigParser('simple_config.yml')
print(config.get('name'))
grid_width = config.get('grid_width')
num_robots = config.get('num_robots')
# You can specify a default value in case a parameter isn't in the
# configuration file
num_steps = config.get('num_steps', default=100)
# Create a few robots to place in your world
robots = []
# Configuration values can also be lists, not just single values.
x_pos = config.get('robot_x_pos')
for n in range(num_robots):
robots.append(RandomRobot(x_pos[n],
grid_width/2 - n*2))
# Create a 50 x 50 World with the Robots
world = gs.World(grid_width, grid_width, robots=robots)
# Logger
trial_num = config.get('trial_num', default=1)
# Create a logger for this world that saves to the `test.h5` file
logger = gs.Logger(world, 'test.h5', trial_num=trial_num,
overwrite_trials=True)
# Tell the logger to run the `green_agg` function every time that
# `log_state` is called
logger.add_aggregator('green', green_agg)
# Save the contents of the configuration, but leave out the 'name' parameter
logger.log_config(config, exclude='name')
# Save the date/time that the simulation was run
logger.log_param('date', str(datetime.now()))
# Run the simulation
for n in range(num_steps):
# Execute a simulation step
world.step()
# Log the state every step
logger.log_state()
# To make sure it works, print the tick (world time)
print('Time:', world.get_time())
print('SIMULATION FINISHED')
if __name__ == '__main__':
# Run the simulation if this program is called directly
main()
|
Complete example¶
Most simulations will involve all of these components, and multiple trials. You can download a complete, detailed example here: complete_simulation.py
, as well as a corresponding YAML configuration file: ex_config.yml
Here, the configuration file is used as a command line argument, so it’s easy to switch what configuration file you use. Run it like this:
$ python3 complete_simulation.py ex_config.yml
Make your own Robot¶
Note
This assumes familiarity with object-oriented programming (particularly inheritance and abstract classes).
The Gridsim library provides a Robot
class that manages underlying behavior and drawing of robots, making it easy for you to quickly implement your own functionality and algorithms.
In fact, the default Robot
class is an abstract class; you must implement your own Robot
subclass. There are five abstract Robot
methods that you must implement in your own class. (Inputs and outputs are not shown.)
move()
: Step-wise movement of the robot on the gridcomm_criteria()
: Distance-based criteria for whether or not another robot is within communication range of this robot.receive_msg()
: Code that is run when a robot receives a messageinit()
: Code that is run once when the robot is createdloop()
: Code that is run in every step of the simulation
It also includes an optional method you may want to implement in your subclass:
msg_received()
: Code that is run when a robot’s successfully sends a message to another robot.
In general, you will likely want to implement your own robots with an additional two layers of subclasses, as seen in the graph below. This allows you to separate the physical robot platform you are representing from the algorithms/code you are running on that platform.
First, you create a subclass that represents the physical robot system you are representing (such as a Turtlebot or Kilobot). This is still an abstract class. It implements abstract methods that are properties of the physical system, such as the communication range (comm_criteria()
) and movement restrictions (move()
). Gridsim include the GridRobot
as a simple robot platform. You can also create your down, as in the YourRobot
above.
Second, you create a subclass of your new class for implementing specific algorithms or code on your new robot platform. Here you will implement message handling (receive_msg()
and optionally msg_received()
) and onboard code (init()
and loop()
). You can have multiple subclasses of your platform to run different code on the same platform, such as RandomRobot
(created below as an example) and AnotherAlgorithm
.
Custom robot example¶
Below is an example of the structure described above to create a simple robot that bounces around the arena.
First, we create , a robot with a circular communication radius of 5 grid cells that can move in the cardinal directions to any of four cells surrounding it. This robot is already provided in the library as GridRobot
; you need not re-implement this robot platform if it meets your needs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | from typing import Tuple
from .robot import Robot
# If you are building your own Robot class, you would instead use:
# from gridsim import Robot
class GridRobot(Robot):
"""
A robot that moves along the cardinal directions, with customizable communication range.
It provides constants for moving up, down, left, and right.
Parameters
----------
x : int
Starting x position (grid cell) of the robot
y : int
Starting y position (grid cell) of the robot
comm_range : float, optional
Communication radius (in grid cells) of the robot, by default 5
"""
#: Robot stays where it is
STAY = 'stay'
#: Robot moves up 1 cell (decrease y position by 1)
UP = 'up'
#: Robot moves down 1 cell (increase y position by 1)
DOWN = 'down'
#: Robot moves left 1 cell (decrease x position by 1)
LEFT = 'left'
#: Robot moves right 1 cell (increase x position by 1)
RIGHT = 'right'
DIRS = [STAY, UP, DOWN, LEFT, RIGHT]
def __init__(self, x: int, y: int, comm_range: float = 5):
# Run all of the initialization for the default Robot class, including
# setting the starting position
super().__init__(x, y)
self._comm_range = comm_range
# Start with the robot stationary
self._move_cmd = GridRobot.STAY
def set_direction(self, dir: str):
"""
Helper function to set the direction the robot will move. Note that this will persist (the
robot will keep moving) until the direction is changed.
Parameters
----------
dir : int
Direction to move, one of ``GridRobot.UP``, ``GridRobot.DOWN``, ``GridRobot.LEFT``,
``GridRobot.RIGHT``, or ``GridRobot.STAY``
Raises
------
ValueError
If given direction is not one of `GridRobot.UP``, ``GridRobot.DOWN``,
``GridRobot.LEFT``, ``GridRobot.RIGHT``, or ``GridRobot.STAY``
"""
if dir in GridRobot.DIRS:
self._move_cmd = dir
else:
raise ValueError('Invalid movement direction "{dir}"')
def move(self) -> Tuple[int, int]:
"""
Determine the cell the Robot will move to, based on the direction set in by
:meth:`~gridsim.grid_robot.GridRobot.set_motors`.
Returns
-------
Tuple[int, int]
(x,y) grid cell the robot will move to, if possible/allowed
"""
x, y = self.get_pos()
if self._move_cmd == GridRobot.UP:
y -= 1
elif self._move_cmd == GridRobot.DOWN:
y += 1
elif self._move_cmd == GridRobot.RIGHT:
x += 1
elif self._move_cmd == GridRobot.LEFT:
x -= 1
# else STAY, which keeps current position
return x, y
def comm_criteria(self, dist_sqr: int) -> bool:
"""
Robots can communicate if their Euclidean distance is <= the radius specified at
initialization (by default, 5 cells)
Parameters
----------
dist_sqr : int
Squared Euclidean distance of the other robot with which to communicate
Returns
-------
bool
Whether distance is <= the communication radius
"""
return dist_sqr <= self._comm_range**2
|
With our robot platform in place, we can now implement a Robot that implements whatever code we want the robot to run. In this case, it’s a simple robot that chooses a random movement every 10 ticks. Its color is based on the color it samples at its current location, and whether it has communicated with another robot.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import random
from gridsim.grid_robot import GridRobot
import gridsim as gs
class RandomRobot(GridRobot):
# Change direction every 10 ticks
DIR_DURATION = 10
def init(self):
self.set_color(255, 0, 0)
self._msg_sent = False
# Next tick when Robot will change direction
self._next_dir_change = self.get_tick()
def receive_msg(self, msg: gs.Message, dist_sqr: float):
# This robot got a message from another robot
self._msg_sent = True
def loop(self):
# Change direction every DIR_DURATION ticks
tick = self.get_tick()
if tick >= self._next_dir_change:
new_dir = random.choice(GridRobot.DIRS)
self.set_direction(new_dir)
self._next_dir_change = tick + RandomRobot.DIR_DURATION
# Broadcast a test message to any robots nearby
msg = gs.Message(self.id, {'test': 'hello'})
self.set_tx_message(msg)
# Sample the environment at the current location
c = self.sample()
# Change color depending on whether messages have been sent or received
# Robot will be white when it has successfully sent & received a message
blue = 255 * self._msg_sent
# self.set_color(255, green, 0)
self.set_color(255-c[0], 255-c[1], blue)
|
Notice that the abstraction layers mean that you have to write very little additional code to implement a new algorithm for your robot.
Class Reference¶
Each page contains details and full API reference for all the classes in the Gridsim library.
For an explanation of how to use all of it together, see Basic Usage.
World¶
Simulate the grid-based world, full of robots
The World is where all of the simulation happens. Robots are added to the World, and the Viewer and Logger refer to a World to draw the simulation and save data.
Once the World is created and you have added your robots, you will likely only need to call the
step()
method.
-
class
gridsim.world.
World
(width: int, height: int, robots: List[gridsim.robot.Robot] = [], environment: str = '', allow_collisions: bool = True, observation_stdev: float = 0.0)¶ A simulated 2D grid world for simulated Robots.
- Parameters
width (int) – Width of the world (number of cells)
height (int) – Height of the world (number of cells)
robots (List[Robot], optional) – List of Robots to place in the World to start, by default []. Additional robots can be added after initialization with the
add_robot()
method.environment (str, optional) – Filename of an image to use for a background in the World. Robots will be able to sense the color of this image. If the environment dimensions do not match the World dimensions, the image will be re-scaled (and possibly stretched). I recommend using an image with the same resolution as your grid size. This supports using
~
to indicate the user home directory.allow_collisions (bool, optional) – Whether or not to allow Robots to exist in the same grid cell, by default True.
observation_stdev (float, optional) – If 0 (this is the default), observations will return the exact RGB value of environment image in each cell. If non-zero (should be >= 0), each component of the observations will be drawn from a normal distribution with mean at the image value, and using this standard deviation. If no image is provided as the
environment
, observations will be returned as 0s, regardless of theobservation_std
.
-
add_environment
(img_filename: str)¶ Add an image to the environment for the Robots to sense. This will also be shown by the Viewer.
Because sensing is cell-based, images will be scaled to the size of the World’s grid. If the aspect ratio does not match, images will be stretched. To avoid any surprises from rescaling, I recommend using an image with the same resolution as your grid size. (e.g., if you have a 50x50 grid, use a 50px x 50px image.)
- Parameters
img_filename (str) – Filename of the RGB image to use as a background environment. Any transparency (alpha) is ignored by the robot sensing.
-
add_robot
(robot: gridsim.robot.Robot)¶ Add a single robot to the World. Robots can also be added in bulk (as a list) when the
World
is created, using therobots
keyword.- Parameters
robot (Robot) – Robot to add to the World
Count the total number of tagged cells in the World. This is useful for seeing (for example) how many cells in the World have been observed.
- Returns
Number of cells that have been tagged in the World (using the
tag()
method).- Return type
int
-
get_dimensions
() → Tuple[int, int]¶ Get the dimensions (in grid cells) of the World
- Returns
(width, height) of the World, in grid cells
- Return type
Tuple[int, int]
-
get_robots
() → pygame.sprite.Group¶ Get a list of all the robots in the World
- Returns
All Robots currently in the World
- Return type
pygame.sprite.Group
-
get_time
() → float¶ Get the current time of the World. At the moment, that’s just the number of ticks (time steps) since the simulation started, since this is a discrete-time world.
- Returns
Number of ticks (steps) since simulation started
- Return type
float
-
step
()¶ Run a single step of the simulation. This moves the robots, manages the clock, and runs the robot controllers.
-
tag
(pos: Tuple[int, int], color: Optional[Tuple[int, int, int]] = None)¶ Tag a cell position in the World with an RGB color to display in the viewer. There will be a semi-transparent overlay with the given color in that cell in the World. This is primarily for use with the Viewer, to visualize what has been sampled in the World.
- Parameters
pos (Tuple[int, int]) – (x, y) grid cell position to mark.
color (Tuple[int, int, int] or None, optional) – (R, G, B) color to set as the cell’s overlay color (each in the range [0, 255]). If you use
None
instead of a color, this will clear the tag. (If no tag is set at that position, nothing will happen.)
- Raises
ValueError – If you give an invalid color, or if you try to tag a position outside the World
Robots¶
Gridsim provides two levels of abstract robot classes. The first, Robot
, is designed to allow a user full control over their robot platform, specifying to communication criteria and allowed movements.
To get started faster, GridRobot
implements a simple movement protocol and communication criterion, allowing the user to quickly start implementing their own code on the GridRobot platform.
For details on extending the Robot classes to create your own, see Make your own Robot.
-
class
gridsim.robot.
Robot
(x: int, y: int)¶ Abstract base class for all robot classes
- Parameters
x (int) – Starting x position (grid cell) of the robot
y (int) – Starting y position (grid cell) of the robot
-
abstract
comm_criteria
(dist_sqr: int) → bool¶ Criterion for whether messages can be communicated (based on distance).
Note
This takes the squared distance instead of the distance, because it is much less computationally expensive to compute. If you need to take the square root, you can do so here, but because this function is called so much, it will be slow. Instead, consider comparing to the squared communication range.
Note
For simplicity and speed, communication criterion is assumed to be symmetric; therefore, this should return the same value regardless of whether this is called by the sending or receiving robot.
- Parameters
dist_sqr (int) – SQUARED distance between this robot and the other robot
- Returns
Whether or not the other robot is within communication range
- Return type
bool
-
distance
(pos: Tuple[int, int]) → float¶ Get the Euclidean distance (in grid cells) between this robot and the specified (x, y) grid cell position.
If you want to change the distance metric (e.g., use Manhattan distance instead), you can override this method when you extend the Robot class.
- Parameters
pos (Tuple[int, int]) – (x, y) grid cell coordinate to get the distance to
- Returns
Euclidean distance of this robot from the given coordinate
- Return type
float
-
get_pos
() → Tuple[int, int]¶ Get the position of the robot in the grid
- Returns
(x, y) grid position of the robot, from the top left
- Return type
Tuple[int, int]
-
get_tick
() → int¶ Get the current tick of the robot (how many steps since the simulation started).
- Returns
Number of ticks since start of simulation
- Return type
int
-
get_world_dim
() → Tuple[int, int]¶ Get the dimensions of the World that this Robot is in, so it can plan to avoid hitting the boundaries.
- Returns
(width, height) dimensions of the world, in grid cells
- Return type
Tuple[int, int]
- Raises
ValueError – Cannot get dimensions if Robot is not in a World. Add it during creation of a World or with
add_robot()
.
-
id
: int = None¶ Unique ID of the Robot
-
abstract
init
()¶ Robot-specific initialization that will be run when the robot is set up.
This is called when a Robot is added to a :class:gridsim.world.World`.
-
abstract
loop
()¶ User-implemented loop operation (code the robot runs every loop)
-
abstract
move
() → Tuple[int, int]¶ User-facing move command, essentially sending a request to move to a particular cell.
The robot will only make this move if it doesn’t violate any movement conditions (such as edge of arena or, if enabled, collisions with other robots). Therefore, you do NOT need to implement any collision or edge-of-arena detection in this function.
- Returns
(x, y) grid cell position the robot intends to move to
- Return type
Tuple[int, int]
-
msg_received
()¶ This is called when a robot successfully sent its message (i.e., when another robot received its message.)
By default, this does nothing. You can override it in your robot class to execute some operation or set a flag when a message is sent.
-
abstract
receive_msg
(msg: Message, dist_sqr: float)¶ Function called when the robot receives a message. This allows the specific robot implementation to choose how to process the messages that it receives, asynchronously.
Note
This used the squared distance instead of distance. To understand why, see
comm_criteria()
.- Parameters
msg (Message) – Received message from another robot
dist_sqr (float) – Squared distance of the sending robot from this robot
-
sample
(pos: Optional[Tuple[int, int]] = None, tag: Optional[Tuple[int, int, int]] = None) → Optional[Tuple[float, float, float]]¶ Sample the RGB environment at the given cell location, or (if no
pos
given) and the robot’s current position.This allows you to sample any location in the World, but this is probably cheating. The robot platform you’re modeling likely doesn’t have such extensive sensing capabilities. This function is provided so that you can define any custom sensing capabilities (such as within a radius around your robot, or a line of sight sensor).
- Parameters
pos (Tuple[int, int], optional) – (x, y) grid cell position of the World to sample. If not specified, the current robot position is sampled.
tag (Tuple[int, int, int], optional) – RGB color to tag this position in the World, by default None. If not provided, the cell in the World won’t be tagged with any color. Otherwise, there will be a semi-transparent overlay with the given color in that cell in the World. This is primarily for use with the Viewer, to visualize what has been sampled in the World.
- Returns
(red, green, blue) color at the given coordinate in the range [0, 255]. If the world doer not have an environment set, this will return (0, 0, 0). If the given position is outside the boundaries of the World, it will return
None
.- Return type
Tuple[float, float, float] or None
-
set_color
(r: int, g: int, b: int)¶ Set the color of the robot (as shown in Viewer) with 8-bit RGB values
- Parameters
r (int) – Red channel [0, 255]
g (int) – Green channel [0, 255]
b (int) – Blue channel [0, 255]
- Raises
ValueError – If all values are not in the range [0, 255]
-
class
gridsim.grid_robot.
GridRobot
(x: int, y: int, comm_range: float = 5)¶ A robot that moves along the cardinal directions, with customizable communication range.
It provides constants for moving up, down, left, and right.
- Parameters
x (int) – Starting x position (grid cell) of the robot
y (int) – Starting y position (grid cell) of the robot
comm_range (float, optional) – Communication radius (in grid cells) of the robot, by default 5
-
DOWN
= 'down'¶ Robot moves down 1 cell (increase y position by 1)
-
LEFT
= 'left'¶ Robot moves left 1 cell (decrease x position by 1)
-
RIGHT
= 'right'¶ Robot moves right 1 cell (increase x position by 1)
-
STAY
= 'stay'¶ Robot stays where it is
-
UP
= 'up'¶ Robot moves up 1 cell (decrease y position by 1)
-
comm_criteria
(dist_sqr: int) → bool¶ Robots can communicate if their Euclidean distance is <= the radius specified at initialization (by default, 5 cells)
- Parameters
dist_sqr (int) – Squared Euclidean distance of the other robot with which to communicate
- Returns
Whether distance is <= the communication radius
- Return type
bool
-
move
() → Tuple[int, int]¶ Determine the cell the Robot will move to, based on the direction set in by
set_motors()
.- Returns
(x,y) grid cell the robot will move to, if possible/allowed
- Return type
Tuple[int, int]
-
set_direction
(dir: str)¶ Helper function to set the direction the robot will move. Note that this will persist (the robot will keep moving) until the direction is changed.
- Parameters
dir (int) – Direction to move, one of
GridRobot.UP
,GridRobot.DOWN
,GridRobot.LEFT
,GridRobot.RIGHT
, orGridRobot.STAY
- Raises
ValueError – If given direction is not one of GridRobot.UP`,
GridRobot.DOWN
,GridRobot.LEFT
,GridRobot.RIGHT
, orGridRobot.STAY
Viewer¶
The Viewer is a simple way to visualize your simulations. After creating the Viewer, just call
draw()
each step (or less frequently) to see the current state of the
World.
Note
The maximum Viewer refresh rate (set at creation with the display_rate
argument) also limits
the simulation rate. If you want to run faster/higher-throughput simulations, don’t use the
Viewer, or make it draw less frequently than every tick.
-
class
gridsim.viewer.
Viewer
(world: gridsim.world.World, window_width: int = 1080, display_rate: int = 10, show_grid: bool = False, show_network: bool = False, show_time: bool = False)¶ Viewer to display the simulation of a World.
This is optional (for debugging and visualization); simulations can be run much faster if the Viewer is not used.
- Parameters
world (World) – World to display
window_width (int, optional) – Width (in pixels) of the window to display the World, by default 1080
display_rate (int, optional) – How fast to update the view (ticks/s), by default 10. In each tick, robots will move by one cell, so keep this low to be able to interpret what’s going on.
show_grid (bool, optional) – Whether to show the underlying grid in the World, by default False.
show_network (bool, optional) – Whether to visualize the current communication network of the robots, by default False. Communication connections between robots are shown as lines between robots.
show_time (bool, optional) – Whether to draw the time (ticks) as text within the viewer window, by default False. (It is placed in the upper right corner.)
-
draw
()¶ Draw all of the robots in the World into the World and its environment.
This will also draw the World’s environment (if one is set) and any tagged cells in the World.
Configuration Parser¶
The ConfigParser
is an optional class to help separate your code for experimental configurations
by using YAML files for configuration. This imposes very few restrictions on
the way you set up your configuration files; it mostly makes it easier to access their contents and
save the configuration parameters with your data using the Logger
.
This is useful for managing both values that are fixed through all experiments (e.g., dimensions of the arena) and experimental values that vary between conditions (e.g., number of robots). The latter may be saved as an array and a single value used for different conditions.
While the ConfigParser
can load any valid YAML files, the largest restriction is what
configuration parameter types can be saved to log files. For details, see the
log_config()
documentation.
-
class
gridsim.config_parser.
ConfigParser
(config_filename: str, show_warnings: bool = False)¶ Class to handle YAML configuration files.
This can be directly passed to the
log_config()
to save all configuration values with the trial data.- Parameters
config_filename (str) – Location and filename of the YAML config file
show_warnings (bool) – Whether to print a warning if trying to
get
a value that returns None (useful for debugging), by defaultFalse
.
-
get
(key: Optional[str] = None, default: Any = None) → Any¶ Get a parameter value from the configuration, or get a dictionary of the parameters if no parameter name (key) is specified.
Note
If no default is specified and the key is not found in the configuration file, this will return
None
instead of rasing an exception.- Parameters
key (str, optional) – Name of the parameter to retrieve, by default None. If not specified, a dictionary of all parameters will be returned.
default (Any, optional) – Default value to return if the key is not found in the configuration, by default
None
.
- Returns
Parameter value for the given key, or the default value is the key is not found. If no key is given, a dictionary of all parameters is returned.
- Return type
Any
Logger¶
For logging data out of the simulator using HDF5 files.
The logger provides an interface for easily saving time series data from many simulation trials, along with the parameters used for the simulation. Data is logged in HDF5 (Hierarchical Data Format) files.
Data is stored by trial, in a hierarchy like a file structure, as shown below. Values in <
>
are determined by what you actually log, but the params
group and time
dataset are always
created.
log_file.h5
├── trial_<1>
│ ├── params
│ │ ├── <param_1>
│ │ ├── <param_2>
│ │ ├── ⋮
│ │ └── <param_n>
│ ├── system_info
│ │ ├── datetime_local
│ │ ├── gridsim_version
│ │ ├── ⋮
│ │ └── version
│ ├── time
│ ├── <aggregator_1>
│ ├── <aggregator_2>
│ ├── ⋮
│ └── <aggregator_n>
├── trial_<2>
│ └── ⋮
├── ⋮
└── trial_<n>
All values logged with log_param()
and
log_config()
are saved in params
.
Time series data is stored in datasets directly under the trial_<n>
group. They are created by
add_aggregator()
, and new values are added by
log_state()
. Calling this method also adds a value to the time
dataset, which corresponds to the World
time at which the state was saved.
-
class
gridsim.logger.
Logger
(world: gridsim.world.World, filename: str, trial_num: int, overwrite_trials: bool = False)¶ Logger to save data to an HDF5 file, from a single simulation trial.
Note that creating this only creates the Logger with which you can save data. You must use the methods below to actually save anything to the file with the Logger.
- Parameters
world (World) – World whose simulation data you want to save.
filename (str) – Name of the HDF5 file to save data to (
.hdf
extension). If the file does not exist, it will be created. If it does exist, it will be appended to (with the overwriting caveat specified below). Using~
to indicate the home directory in the path is supported. If the directory does not exist, it will be created (if possible).trial_num (int) – Trial number under which to save the data.
overwrite_trials (bool, optional) – Whether to overwrite a trial’s data if it already exists, by default False
-
add_aggregator
(name: str, func: Callable[[List[gridsim.robot.Robot]], numpy.ndarray])¶ Add an aggregator function that will map from the list of all Robots in the world to a 1D array of floats. This will be used for logging the state of the World; the output of the aggregator is one row in the HDF5 Dataset named with the
name
.The function reduces the state of the Robots to a single or multiple values. It could map to one float per robot (such as a state variable of each Robot) or a single value (length 1 array, such as an average value over all Robots).
Because of Python’s dynamic typing, this does not validate whether the subclass of Robot has any parameters or functions that are called by the aggregator. The user is responsible for adding any necessary checks in the aggregator function.
Notes
The width of the aggregator table is set when this function is called, which is determined by the length of the output of
func
. If the length depends on the number of Robots, all Robots should be added to theWorld
before adding any aggregators to theLogger
.The aggregator
func
will be applied to all robots in the world, regardless of type. However, if you have multiple types of Robots in yourWorld
, you can make an aggregator that applies to one type by filtering the robots by type within thefunc
.- Parameters
name (str) – Key that will be used to identify the aggregator results in the HDF5 log file.
func (Callable[[List[Robot]], np.ndarray]) – Function that maps from a list of Robots to a 1D array to log some state of the Robots at the current time.
- Raises
ValueError – If aggregator
name
uses a reserved name (see above) or if aggregator does not return a 1D numpy array.
-
get_trial
() → int¶ Get the trial number that this Logger is logging
- Returns
Number of the current trial being logged
- Return type
int
-
log_config
(config: gridsim.config_parser.ConfigParser, exclude: List[str] = [])¶ Save all of the parameters in the configuration.
Notes
Due to HDF5 limitations (and my own laziness), only the following datatypes can be saved in the HDF5 parameters:
string
integer
float
boolean
list of integers and/or floats
- Parameters
config (ConfigParser) – Configuration loaded from a YAML file.
exclude (List[str], optional) – Names (keys) of any configuration parameters to exclude from the saved parameters. This can be useful for excluding an array of values that vary by condition, and you want to only include the single value used in this instance.
-
log_param
(param_name: str, val: Union[str, int, float, bool, list, dict], sub_group: Optional[str] = None)¶ Save a single parameter value. This is useful for saving fixed parameters that are not part of your configuration file, and therefore not saved with
log_config()
.This has the same type restrictions for values as
log_config()
.- Parameters
param_name (str) – Name/key of the parameter value to save
val (Union[str, int, float, bool, list, dict]) – Value of the parameter to save
sub_group (str) – Name of a sub-group of the “params” group in which to log the parameter. If not given, the parameter will be placed directly in the params group. You can specify multiple levels of sub-groups by concatenating names with
/
. e.g.,sub/subsub
- Warns
UserWarning – If user attempts to save invalid parameter (e.g., invalid data type)
-
log_state
()¶ Save the output of all of the aggregator functions. If you have not added any aggregators with
log_state()
, nothing will be saved by this function.The runs each previously-added aggregator function and appends the result to the respective HDF5 Dataset. It also saves the current time of the World to the
time
Dataset.
-
log_system_info
()¶ Log system information for future validation and comparison. This saves the following information as individual datasets within the
trial_#/system_info
group:system
: System name (platform.system()
) (e.g., ‘Linux’, ‘Windows’)node
: System node/host/network name (platform.node()
)release
: System release (platform.release
) (e.g., kernel version)version
: System version (platform.version
)python_version
: Python version (platform.python_version
) (e.g., ‘3.8.2’)gridsim_version
: Currently installed Gridsim versiondatetime_local
: Local date and time when trial was run
Messages¶
This provides a basic Message protocol for robot communication. Each message contains the ID of the sender and a dictionary of message contents. The values of the message contents may be any type, so the receiver must know how to process the data.
Additionally, Messages can optionally include a receiver type (rx_type
). This is only needed if
there are multiple types of robots in the World, and you only want certain types of robots to
receive the message.
If no arguments are provided when a Message is created, it creates a null message, which signals that the robot is not broadcasting anything.
While it is possible to extend this class, the default Message class should meet most needs.
-
class
gridsim.message.
Message
(tx_id: Optional[int] = None, content: Dict[str, Any] = {}, rx_type: Type[gridsim.robot.Robot] = <class 'gridsim.robot.Robot'>)¶ A message sent by robots
Messages can be either a null (empty) message if no arguments are provided to the constructor. Or it contains the sender’s ID, a dictionary of content, and (optionally) the type of robot that receives the message.
- Parameters
tx_id (int, optional) – ID of the sending (transmitting) robot, by default None
content (Dict[str, Any]], optional) – Dictionary of message keys and values, by default an empty dictionary. Keys must be strings, but values can be of any type (incumbent on receiver to correctly interpret incoming data).
rx_type (Type[Robot], optional) – Type of the receiving robot, by default Robot (i.e., message will be processed by any Robot.)
- Raises
TypeError – If
rx_type
is not a subclass ofRobot
, orcontent
is not a dictionary with strings for keys
-
get
(key: Optional[str] = None) → Optional[Dict[str, Any]]¶ Get the contents of the message
- Parameters
key (str, optional) – Name of the parameter to retrieve, by default None. If not specified, a dictionary of all parameters will be returned.
- Returns
Dictionary of the message contents
- Return type
Dict[str, Any] or None
- Raises
KeyError – If a key is provided but is not in the message contents
-
sender
() → Optional[int]¶ Get the ID (32-bit integer) of the robot that sent the message
- Returns
ID of the sending (transmitting) robot
- Return type
int or None
-
set
(key: Optional[str], value: Any)¶ Set the message contents
In the message contents, set the given key to have the given value. If this is an empty (null) message, this will raise an error. If the key already exists, the existing value will be overwritten.
- Parameters
key (str) – Key in the message contents for which to set the value
value (Any) – Value to set for the given key. This will overwrite any existing value, if the key already exists.
- Raises
ValueError – If the message is null/empty, the message contents cannot be set
-
set_all
(content: Dict[str, Any])¶ Replace the whole message content of the dictionary.
This will replace all of the existing message content.
If you want to clear the message contents, you can pass any empty dictionary as the
content
({}
).- Parameters
content (Dict[str, Any]) – Complete dictionary to set as the message contents
- Raises
ValueError – If the message is null/empty, the message contents cannot be set
Development¶
This is reference material for local development and publishing new versions.
If you just want to use the library, you don’t need any of this.
Release checklist¶
Verify tests and examples work. (It must be passing on Travis CI.)
Check that all documentation is updated
Update version number (
__version__
) ingridsim/__init__.py
Update changelog: move “Unreleased” to new version
Push to master
Create release on Github. (This will automatically create a new Stable and version-numbered documentation version on Read The Docs and deploy an updated release to PyPi.)
Build Documentation¶
from the docs
directory, run:
make html
Then open the documentation:
open _build/html/index.html
Build the distributable for PyPi¶
(From the PyPi tutorial)
You shouldn’t need to do this manually anymore; this will be handled by Travis CI
Make sure the necessary dependencies are installed.
pip3 install --upgrade setuptools wheel twine
Build the project. From the project root folder, run:
python3 setup.py sdist bdist_wheel
Upload it to the testing index:
python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
Upload it to the actual index:
python3 -m twine upload dist/*
Profiling code¶
Use the packages cProfile and cprofilev to profile Python code. This is useful for finding slow spots in the simulator itself, as well as your own user code.
Note that these are not included as dependencies for Gridsim, so you will have to install them yourself:
pip install cprofilev
Option 1: Using just cprofilev
¶
Run your code and profile it at the same time.
python -m cprofilev main.py test_config.yml
Option 1: Using cProfile
and cprofilev
¶
This saves the output as a file, so you can go back and view it later without having to re-run the code.
Run your script and save the output to a log file. For example:
python -m cProfile -o out_log main.py test_config.yml
View the output:
cprofilev -f out_log
And go to localhost:4000 to view the output
Changelog¶
This documents changes for each Gridsim release. These can also be found with each Github release.
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased (probably v0.5)¶
Added¶
World
now has the capability for probabilistic observations. For more information, see the World constructor documentation.New method
count_tags()
returns the number of tagged locations in the worldLogger
now supports logging dictionary parameters (including those saved from config files).When logging parameters with
log_param()
, you can now specify a subgroup ofparams
in which to save the data.Message
now has a methodset_all()
to replace the whole contents of the Message without creating a new message. (And as opposed to setting the contents key-by-key withset_all()
.)Documentation for profiling code (under Development)
Option in Viewer initialization to draw time as text within window
Option in Viewer initialization to draw the communication network between robots
Changed¶
comm_criteria()
andreceive_msg()
now have an argument of dist_sqr instead of dist. This is because the square root call in computing the distance is computationally expensive and is called for communication (an N^2) operation.For
tag()
, you can now passNone
instead of a color to remove an existing tag.[Under the hood] Convert World tagging to use 3D numpy array instead of list of Tuples. This also improves drawing speed ( it doesn’t slow down so much when more tags are added), and you don’t get gaps between tagged cells for certain window sizes.
Improve interpolation for resized environment images
Improved code documentation.
Add documentation of errors and warnings
Move constructor documentation from
__init__
to class documentation. (I think it looks nicer.)Move documentation from *.rst to *.py files, for the sake of having all the documentation in the same place.
Fixed¶
You can now no longer
tag()
cells that are outside of the World dimensions.An error is raised when trying to create a
Logger
aggregator (withadd_aggregator()
) using a reserved name (e.g.,params
ortime
).Fix Viewer bug that didn’t correctly display background color underneath Robots
0.4 (2020-08-20)¶
Added¶
You can now set the contents of a
Message
by key, without needing to create a new message.When creating a
ConfigParser
, you can now choose to show warnings when getting a value that isn’t in the config fileIf a data directory (in the path for a
Logger
filename) does not exist, it will be created.New method
log_system_info()
allows you to easily save information about the system on which the experiments are being run.Paths for both
Logger
andImageEnvironment
(used viaWorld
support using~
to indicate home directory
Changed¶
Trying to have a
Robot
sample outside of the arena now returnsNone
. Previously, this threw a lower-level error about an image index being out of range.Decrease
World
tag opacityFormatting: Changed to 100-character line limit (from 80).
[Under the hood] Renamed
WorldEnvironment
toImageEnvironment
Fixed¶
Previously, if you tried to
sample()
a negative position in the World, it would loop the index around and give you the value of a position on the other side of the environment. Now, this is considered out of bounds and returnsNone
.Improve performance for drawing large number of tags in the
Viewer
(by converting coordinates to integers).Trying to use the
Viewer
without an environment image in the World would cause a crash. Now it doesn’t.Return type and documentation for
sample()
now matches that of the environment (returns None if sampling outside boundaries).Fix broken
get_version()
function.Time in
Logger
is now stored as an integer (since it’s ticks). Previously, it was a float.
0.3 (2020-06-29)¶
Added¶
Grid cells in the World can now be tagged with a color by the
tag()
method. (The color tag is only used by theViewer
when it draws the World.)The Robot’s
sample()
method now includes an option to tag the sampled location in theWorld
with a color.Message
now has “truthiness”: null messages areFalse
and non-null messages areTrue
.Messages contents can be accessed by key with the
get()
method, as well as still being able to retrieve the entire message dictionary contents.Created this changelog
Changed¶
Message.tx_id()
has been renamed to the (more informative)sender()
.Robot’s
init()
isn’t run until the robot is placed in the World. This allows robots to have access toWorld
information (like the arena size) in theinit()
method.[Under the hood] World’s environments are abstracted to have empty and non-empty types, which cleans up code to get rid of reliance on checking for environments being
None
.[Under the hood] Reduce reliance on cheating and accessing private variables and methods (underscore-prefixed methods/variables)
Removed¶
Message.is_null
has been removed. Instead, directly use the boolean conversion described above.
Fixed¶
Order of commands run on the robot resulted in incorrect movements (robot-specific
move()
, then Robot controller/loop function, then collision/environment-aware_move
operation to move the robots which was using a different move command)Remove mypy/flake8 from requirements, since they’re for local development/linting.
About¶
Gridsim is a Python 3 library for simulating robots in a grid-based world. It has a simple, well-documented API, making it easy to implement your own algorithms with minimal overhead.
Key features include:
Viewer for debugging and visualizing your simulations
Built-in data logging to HDF5 files
Support for YAML configuration files
Extendable robot classes to avoid repeating your code
Comprehensive documentation and examples
Quick Install¶
$ pip install gridsim
For more information and instructions, check out the documentation.
Contact¶
If you have questions, or if you’ve done something interesting with this package, send me an email: julia@juliaebert.com.
If you find a problem or want something added to the library, open an issue on Github.