7. Gridworlds

The gridworld module provides routines for working with 2-dimensional, 4-connected rectangular grids. Examples are given in the directory examples/gridworlds. The primary source of detailed documentation is in docstrings found in the source code itself. There are several ways to get at this; e.g., you could use pydoc by

$ pydoc tulip.gridworld

(See virtualenv and pydoc for troubleshooting.) The basic representation is provided by the class GridWorld. In all example code below, we assume the gridworld module has been imported as gw

import tulip.gridworld as gw

7.1. Description format

A gridworld problem can be defined by a “gridworld description string.” The core parsing routine is the GridWorld.loads; it is the current reference implementation.

classmethod GridWorld.loads(gw_desc: str) GridWorld[source]

Reincarnate using given gridworld description string.

@param gw_desc:

String containing a gridworld description.

In a gridworld description, any line beginning with # is ignored (regarded as a comment). The first non-blank and non-comment line must give the grid size as two positive integers separated by whitespace, with the first being the number of rows and the second the number of columns.

Each line after the size line is used to construct a row of the gridworld. These are read in order with maximum number of lines being the number of rows in the gridworld. A row definition is whitespace-sensitive up to the number of columns (any characters beyond the column count are ignored, so in particular trailing whitespace is allowed) and can include the following symbols:

  • ‘ ‘ : an empty cell,

  • ‘*’ : a statically occupied cell,

  • ‘I’ : possible initial cell,

  • ‘G’ : goal cell (must be visited infinitely often).

If the end of file is reached before all rows have been constructed, then the remaining rows are assumed to be empty. After all rows have been constructed, the remainder of the file is ignored.

7.1.1. Examples

Consider a 2 x 3 gridworld where you wish to declare the cell at (1,2) (the bottom-right) as the initial position and (1,0) as a goal cell. This is achieved with the description string

#    0 1 2
#   -------
#  0| |*| |
#   -------
#  1|G| |I|
#   -------

2 3
 *
G I

Any line beginning with # is treated as a comment and ignored. The purpose of the comment in this example is to provide an annotated view of the grid; for large problems, comments can be very helpful for humans, and anyway provide a means to make notes such as who created the file, a timestamp, and other experimental parameters. In this example, only the last three lines are critical. 2 3 declares that there are 2 rows and 3 columns. The other two lines define grid contents. Explicitly, there is a static obstacle at the first row and second column (i.e., at (0,1)). The last line indicates where the goal G and initial position I should be.

If the above description string were in a file called trivial.txt, then you could load it using

with open("trivial.txt", "r") as f:
    triv = gw.GridWorld(f.read(), prefix="Y")

To prettily print the result, and then to print the variable name of the cell located at (0,0), you would then

print(triv)
print(triv[0,0])

See the method pretty for more formatting options (the first line above internally invokes pretty with sane defaults). Notice that the variable name has prefix “Y”. This could be changed in the prefix argument used above when instantiating GridWorld. The string returned by triv[0,0] can be written in specifications. Indexing follows that of Python; in particular, negative indices are supported.

7.2. Generating continuous-space partitions

Given a GridWorld object Y, you can create a PropPreservingPartition object describing the grid in a continuous state space with the method dump_ppartition. An example is to generate a random gridworld, generate an initial proposition-preserving partition, and then refine it based on continuous state space dynamics, as shown in the code below. Note that we use mostly default argument values to minimize clutter.

import numpy as np
from tulip.abstract import discretize
from tulip import gridworld as gw
from tulip.hybrid import LtiSysDyn
from polytope import Polytope
from polytope.plot import plot_partition

# Trivial dynamics
A = np.eye(2)
B = np.eye(2)
E = np.eye(2)
U = Polytope(np.array([[1., 0.],[-1., 0.], [0., 1.], [0., -1.]]),
             np.array([[1.],[1.],[1.],[1.]]))
W = Polytope(np.array([[1.,0.],[-1.,0.],[0.,1.],[0.,-1.]]),
             0.01*np.array([1., 1., 1., 1.]))
sys_dyn = LtiSysDyn(A,B,E, Uset=U, Wset=W)

# Generate random gridworld, dump it and discretize based on dynamics
Y = gw.random_world((5, 10), num_init=0, num_goals=0)
disc_dynamics = discretize(Y.dump_ppartition(), sys_dyn)

# Pretty print abstraction to terminal, and depict partition reachability
print(Y)
plot_partition(disc_dynamics.ppp, trans=True)