Module pacai.bin.gridworld

Expand source code
import argparse
import logging
import os
import random
import sys
import textwrap

from pacai.agents.learning.reinforcement import ReinforcementAgent
from pacai.core.environment import Environment
from pacai.core.mdp import MarkovDecisionProcess
from pacai.student.qlearningAgents import QLearningAgent
from pacai.student.valueIterationAgent import ValueIterationAgent
from pacai.ui.gridworld.text import TextGridworldDisplay
from pacai.ui.gridworld.utils import wait_for_keys
from pacai.util.logs import initLogging
from pacai.util.logs import updateLoggingLevel

class Gridworld(MarkovDecisionProcess):
    def __init__(self, grid):
        # layout
        if (isinstance(grid, list)):
            grid = makeGrid(grid)

        self.grid = grid

        # parameters
        self.livingReward = 0.0
        self.noise = 0.2

    def setLivingReward(self, reward):
        """
        The (negative) reward for exiting "normal" states.

        Note that in the R+N text, this reward is on entering
        a state and therefore is not clearly part of the state's
        future rewards.
        """

        self.livingReward = reward

    def setNoise(self, noise):
        """
        The probability of moving in an unintended direction.
        """

        self.noise = noise

    def getPossibleActions(self, state):
        """
        Returns list of valid actions for 'state'.

        Note that you can request moves into walls and
        that "exit" states transition to the terminal
        state under the special action "done".
        """

        if state == self.grid.terminalState:
            return ()

        x, y = state
        if isinstance(self.grid[x][y], int):
            return ('exit', )

        return ('north', 'west', 'south', 'east')

    def getStates(self):
        """
        Return list of all states.
        """

        # The true terminal state.
        states = [self.grid.terminalState]
        for x in range(self.grid.width):
            for y in range(self.grid.height):
                if self.grid[x][y] != '#':
                    state = (x, y)
                    states.append(state)

        return states

    def getReward(self, state, action, nextState):
        """
        Get reward for state, action, nextState transition.

        Note that the reward depends only on the state being
        departed (as in the R+N book examples, which more or
        less use this convention).
        """

        if state == self.grid.terminalState:
            return 0.0

        x, y = state
        cell = self.grid[x][y]
        if isinstance(cell, int) or isinstance(cell, float):
            return cell

        return self.livingReward

    def getStartState(self):
        for x in range(self.grid.width):
            for y in range(self.grid.height):
                if self.grid[x][y] == 'S':
                    return (x, y)

        raise Exception('Grid has no start state')

    def isTerminal(self, state):
        """
        Only the TERMINAL_STATE state is *actually* a terminal state.
        The other "exit" states are technically non-terminals with
        a single action "exit" which leads to the true terminal state.
        This convention is to make the grids line up with the examples
        in the R+N textbook.
        """

        return state == self.grid.terminalState

    def getTransitionStatesAndProbs(self, state, action):
        """
        Returns list of (nextState, prob) pairs
        representing the states reachable
        from 'state' by taking 'action' along
        with their transition probabilities.
        """

        if action not in self.getPossibleActions(state):
            raise Exception('Illegal action!')

        if self.isTerminal(state):
            return []

        x, y = state

        if isinstance(self.grid[x][y], int) or isinstance(self.grid[x][y], float):
            termState = self.grid.terminalState
            return [(termState, 1.0)]

        successors = []

        northState = (self.__isAllowed(y + 1, x) and (x, y + 1)) or state
        westState = (self.__isAllowed(y, x - 1) and (x - 1, y)) or state
        southState = (self.__isAllowed(y - 1, x) and (x, y - 1)) or state
        eastState = (self.__isAllowed(y, x + 1) and (x + 1, y)) or state

        if action == 'north' or action == 'south':
            if action == 'north':
                successors.append((northState, 1 - self.noise))
            else:
                successors.append((southState, 1 - self.noise))

            massLeft = self.noise
            successors.append((westState, massLeft / 2.0))
            successors.append((eastState, massLeft / 2.0))

        if action == 'west' or action == 'east':
            if action == 'west':
                successors.append((westState, 1 - self.noise))
            else:
                successors.append((eastState, 1 - self.noise))

            massLeft = self.noise
            successors.append((northState, massLeft / 2.0))
            successors.append((southState, massLeft / 2.0))

        successors = self.__aggregate(successors)
        return successors

    def __aggregate(self, statesAndProbs):
        counter = {}
        for state, prob in statesAndProbs:
            if state not in counter:
                counter[state] = 0.0
            counter[state] += prob

        newStatesAndProbs = []
        for state, prob in counter.items():
            newStatesAndProbs.append((state, prob))

        return newStatesAndProbs

    def __isAllowed(self, y, x):
        if y < 0 or y >= self.grid.height:
            return False

        if x < 0 or x >= self.grid.width:
            return False

        return self.grid[x][y] != '#'

class GridworldEnvironment(Environment):
    def __init__(self, gridWorld):
        self.gridWorld = gridWorld
        self.reset()

    def getCurrentState(self):
        return self.state

    def getPossibleActions(self, state):
        return self.gridWorld.getPossibleActions(state)

    def doAction(self, action):
        successors = self.gridWorld.getTransitionStatesAndProbs(self.state, action)
        sum = 0.0
        rand = random.random()
        state = self.getCurrentState()

        for nextState, prob in successors:
            sum += prob
            if sum > 1.0:
                raise Exception('Total transition probability more than one; sample failure.')

            if rand < sum:
                reward = self.gridWorld.getReward(state, action, nextState)
                self.state = nextState
                return (nextState, reward)

        raise Exception('Total transition probability less than one; sample failure.')

    def reset(self):
        self.state = self.gridWorld.getStartState()

class Grid(object):
    """
    A 2-dimensional array of immutables backed by a list of lists.
    Data is accessed via grid[x][y] where (x, y) are cartesian coordinates with x horizontal,
    y vertical and the origin (0, 0) in the bottom left corner.

    The __str__ method constructs an output that is oriented appropriately.
    """

    def __init__(self, width, height, initialValue=' '):
        self.width = width
        self.height = height
        self.data = [[initialValue for y in range(height)] for x in range(width)]
        self.terminalState = 'TERMINAL_STATE'

    def __getitem__(self, i):
        return self.data[i]

    def __setitem__(self, key, item):
        self.data[key] = item

    def __eq__(self, other):
        if (other is None):
            return False
        return self.data == other.data

    def __hash__(self):
        return hash(self.data)

    def copy(self):
        g = Grid(self.width, self.height)
        g.data = [x[:] for x in self.data]
        return g

    def deepCopy(self):
        return self.copy()

    def shallowCopy(self):
        g = Grid(self.width, self.height)
        g.data = self.data
        return g

    def _getLegacyText(self):
        t = [[self.data[x][y] for x in range(self.width)] for y in range(self.height)]
        t.reverse()
        return t

    def __str__(self):
        return str(self._getLegacyText())

def makeGrid(gridString):
    width, height = len(gridString[0]), len(gridString)
    grid = Grid(width, height)
    for ybar, line in enumerate(gridString):
        y = height - ybar - 1
        for x, el in enumerate(line):
            grid[x][y] = el

    return grid

def getUserAction(state, actionFunction):
    """
    Get an action from the user (rather than the agent).

    Used for debugging and lecture demos.
    """

    action = None

    while True:
        keys = wait_for_keys()

        if ('Up' in keys):
            action = 'north'

        if ('Down' in keys):
            action = 'south'

        if ('Left' in keys):
            action = 'west'

        if ('Right' in keys):
            action = 'east'

        if ('q' in keys):
            sys.exit(0)

        if (action is None):
            continue

        break

    actions = actionFunction(state)
    if action not in actions:
        action = actions[0]

    return action

def runEpisode(agent, environment, discount, decision, display, message, pause, episode):
    returns = 0
    totalDiscount = 1.0
    environment.reset()

    if (isinstance(agent, ReinforcementAgent)):
        agent.startEpisode()

    logging.info('BEGINNING EPISODE: ' + str(episode) + "\n")

    while True:
        # DISPLAY CURRENT STATE
        state = environment.getCurrentState()
        display(state)
        pause()

        # END IF IN A TERMINAL STATE
        actions = environment.getPossibleActions(state)
        if (len(actions) == 0):
            logging.info('EPISODE ' + str(episode) + ' COMPLETE: RETURN WAS ' + str(returns) + '\n')
            return returns

        # GET ACTION (USUALLY FROM AGENT)
        action = decision(state)
        if (action is None):
            raise Exception('Error: Agent returned None action')

        # EXECUTE ACTION
        nextState, reward = environment.doAction(action)
        logString = ''
        logString += '\nStarted in state: ' + str(state)
        logString += '\nTook action: ' + str(action)
        logString += '\nEnded in state: ' + str(nextState)
        logString += '\nGot reward: ' + str(reward) + '\n'
        logging.debug(logString)

        # Update learner.
        if (isinstance(agent, ReinforcementAgent)):
            agent.observeTransition(state, action, nextState, reward)

        returns += reward * totalDiscount
        totalDiscount *= discount

    if (isinstance(agent, ReinforcementAgent)):
        agent.stopEpisode()

def parseOptions(argv):
    """
    Processes the command used to run gridworld from the command line.
    """

    description = """
    DESCRIPTION:
        This program will create a gridworld. Explore and find the best path to the reward!

    EXAMPLES:
        (1) python -m pacai.bin.gridworld
            - Creats a gridworld with default settings.
        (2) python -m pacai.bin.gridworld --discount 0.7
            - Creats a gridworld with a 0.7 discount factor.
    """

    parser = argparse.ArgumentParser(description = textwrap.dedent(description),
        prog = os.path.basename(__file__), formatter_class = argparse.RawTextHelpFormatter)

    parser.add_argument('-a', '--agent', dest = 'agent',
            action = 'store', type = str, default = 'random',
            help = 'agent type (options are \'random\', \'value\' and \'q\', default %(default)s)')

    parser.add_argument('-d', '--debug', dest = 'debug',
            action = 'store_true', default = False,
            help = 'set logging level to debug (default: %(default)s)')

    parser.add_argument('-e', '--epsilon', dest = 'epsilon',
            action = 'store', type = float, default = 0.3,
            help = 'chance of taking a random action in q-learning (default %(default)s)')

    parser.add_argument('-g', '--grid', dest = 'grid',
            action = 'store', type = str, default = 'BookGrid',
            help = 'grid type: BookGrid, BridgeGrid, CliffGrid, MazeGrid, %(default)s (default)')

    parser.add_argument('-i', '--iterations', dest = 'iters',
            action = 'store', type = int, default = 10,
            help = 'number of rounds of value iteration (default %(default)s)')

    parser.add_argument('-k', '--episodes', dest = 'episodes',
            action = 'store', type = int, default = 1,
            help = 'number of epsiodes of the MDP to run (default %(default)s)')

    parser.add_argument('-l', '--learning-rate', dest = 'learningRate',
            action = 'store', type = float, default = 0.5,
            help = 'set the learning rate (default %(default)s)')

    parser.add_argument('-n', '--noise', dest = 'noise',
            action = 'store', type = float, default = 0.2,
            help = 'set how often actions result in unintended directions (default %(default)s)')

    parser.add_argument('-p', '--pause', dest = 'pause',
            action = 'store_true', default = False,
            help = 'pause GUI after each time step when running the MDP (default %(default)s)')

    parser.add_argument('-q', '--quiet', dest = 'quiet',
            action = 'store_true', default = False,
            help = 'set logging level to warning (default: %(default)s)')

    parser.add_argument('-r', '--living-reward', dest = 'livingReward',
            action = 'store', type = float, default = 0.0,
            help = 'reward for living for a time step (default %(default)s)')

    parser.add_argument('-s', '--speed', dest = 'speed',
            action = 'store', type = float, default = 1.0,
            help = 'speed of animation, S>1.0 is faster, 0<S<1 is slower (default %(default)s)')

    parser.add_argument('-v', '--value-steps', dest = 'valueSteps',
            action = 'store_true', default = False,
            help = 'display each step of value iteration (default %(default)s)')

    parser.add_argument('-y', '--discount', dest = 'discount',
            action = 'store', type = float, default = 0.9,
            help = 'discount on future (default %(default)s)')

    parser.add_argument('--manual', dest = 'manual',
            action = 'store_true', default = False,
            help = 'manually control agent (default %(default)s)')

    parser.add_argument('--null-graphics', dest = 'nullGraphics',
            action = 'store_true', default = False,
            help = 'generate no graphics (default: %(default)s)')

    parser.add_argument('--text-graphics', dest = 'textGraphics',
            action = 'store_true', default = False,
            help = 'display output as text only (default: %(default)s)')

    parser.add_argument('--window-size', dest = 'gridSize',
            action = 'store', type = int, default = 150,
            help = 'request a window width of X pixels *per grid cell* (default %(default)s)')

    options, otherjunk = parser.parse_known_args(argv)

    if len(otherjunk) != 0:
        raise ValueError('Unrecognized options: \'%s\'.' % (str(otherjunk)))

    # Set the logging level
    if options.quiet and options.debug:
        raise ValueError('Logging cannont be set to both debug and quiet.')

    if options.quiet:
        updateLoggingLevel(logging.WARNING)
    elif options.debug:
        updateLoggingLevel(logging.DEBUG)

    if options.manual and options.agent != 'q':
        logging.info('Disabling Agents in Manual Mode.')
        options.agent = None

    # MANAGE CONFLICTS
    if options.textGraphics or options.nullGraphics:
        options.pause = False

    if options.manual:
        options.pause = True

    return options

def main(argv):
    """
    Entry point for the gridworld simulation
    The args are a blind pass of `sys.argv` with the executable stripped.
    """

    initLogging()

    opts = parseOptions(argv)

    ###########################
    # GET THE GRIDWORLD
    ###########################

    mdp = _getGridWorld(opts.grid)
    mdp.setLivingReward(opts.livingReward)
    mdp.setNoise(opts.noise)
    env = GridworldEnvironment(mdp)

    ###########################
    # GET THE DISPLAY ADAPTER
    ###########################

    display = TextGridworldDisplay(mdp)
    if not opts.textGraphics and not opts.nullGraphics:
        from pacai.ui.gridworld.gui import GraphicsGridworldDisplay
        display = GraphicsGridworldDisplay(mdp, opts.gridSize, opts.speed)

    display.start()

    ###########################
    # GET THE AGENT
    ###########################

    a = None
    if (opts.agent == 'value'):
        a = ValueIterationAgent(0, mdp, opts.discount, opts.iters)
    elif (opts.agent == 'q'):
        qLearnOpts = {
            'gamma': opts.discount,
            'alpha': opts.learningRate,
            'epsilon': opts.epsilon,
            'actionFn': lambda state: mdp.getPossibleActions(state),
        }
        a = QLearningAgent(0, **qLearnOpts)
    elif (opts.agent == 'random'):
        # No reason to use the random agent without episodes.
        if (opts.episodes == 0):
            opts.episodes = 10

        class RandomMDPAgent:
            def getAction(self, state):
                return random.choice(mdp.getPossibleActions(state))

            def getValue(self, state):
                return 0.0

            def getQValue(self, state, action):
                return 0.0

            def getPolicy(self, state):
                "NOTE: 'random' is a special policy value; don't use it in your code."
                return 'random'

            def update(self, state, action, nextState, reward):
                pass

        a = RandomMDPAgent()
    else:
        if (not opts.manual):
            raise 'Unknown agent type: ' + opts.agent

    ###########################
    # RUN EPISODES
    ###########################

    # Display q/v values before simulation of episodes.
    if (not opts.manual and opts.agent == 'value'):
        if (opts.valueSteps):
            for i in range(opts.iters):
                tempAgent = ValueIterationAgent(0, mdp, opts.discount, i)
                display.displayValues(tempAgent, message = 'VALUES AFTER ' + str(i) + ' ITERATIONS')
                display.pause()

        display.displayValues(a, message = 'VALUES AFTER ' + str(opts.iters) + ' ITERATIONS')
        display.pause()
        display.displayQValues(a, message = 'Q-VALUES AFTER ' + str(opts.iters) + ' ITERATIONS')
        display.pause()

    # Figure out what to display each time step (if anything).
    displayCallback = lambda x: None
    if (not opts.nullGraphics):
        if (opts.manual and opts.agent is None):
            displayCallback = lambda state: display.displayNullValues(state)
        else:
            if (opts.agent == 'random'):
                displayCallback = lambda state: display.displayValues(a, state, 'CURRENT VALUES')
            elif (opts.agent == 'value'):
                displayCallback = lambda state: display.displayValues(a, state, 'CURRENT VALUES')
            elif (opts.agent == 'q'):
                displayCallback = lambda state: display.displayQValues(a, state, 'CURRENT Q-VALUES')

    messageCallback = lambda x: print(x)
    if (opts.nullGraphics):
        messageCallback = lambda x: None

    # FIGURE OUT WHETHER TO WAIT FOR A KEY PRESS AFTER EACH TIME STEP
    pauseCallback = lambda: None
    if (opts.pause):
        pauseCallback = lambda: display.pause()

    # Figure out whether the user wants manual control (for debugging and demos).
    if (opts.manual):
        decisionCallback = lambda state: getUserAction(state, mdp.getPossibleActions)
    else:
        decisionCallback = a.getAction

    # Run episodes.
    if (opts.episodes > 0):
        logging.debug('RUNNING ' + str(opts.episodes) + ' EPISODES')

    returns = 0
    for episode in range(1, opts.episodes + 1):
        returns += runEpisode(a, env, opts.discount, decisionCallback, displayCallback,
                messageCallback, pauseCallback, episode)

    if (opts.episodes > 0):
        logging.debug('AVERAGE RETURNS FROM START STATE:' + str((returns + 0.0) / opts.episodes))

    # Display post-learning values / q-values.
    if (opts.agent == 'q' and not opts.manual):
        display.displayQValues(a, message = 'Q-VALUES AFTER ' + str(opts.episodes) + ' EPISODES')
        display.pause()
        display.displayValues(a, message = 'VALUES AFTER ' + str(opts.episodes) + ' EPISODES')
        display.pause()

def _getGridWorld(name):
    name = name.lower()

    grid = None
    if (name == 'bookgrid'):
        grid = BOOK_GRID
    elif (name == 'bridgegrid'):
        grid = BRIDGE_GRID
    elif (name == 'cliffgrid'):
        grid = CLIFF_GRID
    elif (name == 'cliff2grid'):
        grid = CLIFF2_GRID
    elif (name == 'discountgrid'):
        grid = DISCOUNT_GRID
    elif (name == 'mazegrid'):
        grid = MAZE_GRID
    else:
        raise ValueError("Unknown grid name: '%s'." % (name))

    return Gridworld(grid)

BOOK_GRID = [
    [' ', ' ', ' ', +1],
    [' ', '#', ' ', -1],
    ['S', ' ', ' ', ' '],
]

BRIDGE_GRID = [
    ['#', -100, -100, -100, -100, -100, '#'],
    [1, 'S', ' ', ' ', ' ', ' ', 10],
    ['#', -100, -100, -100, -100, -100, '#'],
]

CLIFF_GRID = [
    [' ', ' ', ' ', ' ', ' '],
    ['S', ' ', ' ', ' ', 10],
    [-100, -100, -100, -100, -100],
]

CLIFF2_GRID = [
    [' ', ' ', ' ', ' ', ' '],
    [8, 'S', ' ', ' ', 10],
    [-100, -100, -100, -100, -100],
]

DISCOUNT_GRID = [
    [' ', ' ', ' ', ' ', ' '],
    [' ', '#', ' ', ' ', ' '],
    [' ', '#', 1, '#', 10],
    ['S', ' ', ' ', ' ', ' '],
    [-10, -10, -10, -10, -10],
]

MAZE_GRID = [
    [' ', ' ', ' ', +1],
    ['#', '#', ' ', '#'],
    [' ', '#', ' ', ' '],
    [' ', '#', '#', ' '],
    ['S', ' ', ' ', ' '],
]

if __name__ == '__main__':
    main(sys.argv[1:])

Functions

def getUserAction(state, actionFunction)

Get an action from the user (rather than the agent).

Used for debugging and lecture demos.

Expand source code
def getUserAction(state, actionFunction):
    """
    Get an action from the user (rather than the agent).

    Used for debugging and lecture demos.
    """

    action = None

    while True:
        keys = wait_for_keys()

        if ('Up' in keys):
            action = 'north'

        if ('Down' in keys):
            action = 'south'

        if ('Left' in keys):
            action = 'west'

        if ('Right' in keys):
            action = 'east'

        if ('q' in keys):
            sys.exit(0)

        if (action is None):
            continue

        break

    actions = actionFunction(state)
    if action not in actions:
        action = actions[0]

    return action
def main(argv)

Entry point for the gridworld simulation The args are a blind pass of sys.argv with the executable stripped.

Expand source code
def main(argv):
    """
    Entry point for the gridworld simulation
    The args are a blind pass of `sys.argv` with the executable stripped.
    """

    initLogging()

    opts = parseOptions(argv)

    ###########################
    # GET THE GRIDWORLD
    ###########################

    mdp = _getGridWorld(opts.grid)
    mdp.setLivingReward(opts.livingReward)
    mdp.setNoise(opts.noise)
    env = GridworldEnvironment(mdp)

    ###########################
    # GET THE DISPLAY ADAPTER
    ###########################

    display = TextGridworldDisplay(mdp)
    if not opts.textGraphics and not opts.nullGraphics:
        from pacai.ui.gridworld.gui import GraphicsGridworldDisplay
        display = GraphicsGridworldDisplay(mdp, opts.gridSize, opts.speed)

    display.start()

    ###########################
    # GET THE AGENT
    ###########################

    a = None
    if (opts.agent == 'value'):
        a = ValueIterationAgent(0, mdp, opts.discount, opts.iters)
    elif (opts.agent == 'q'):
        qLearnOpts = {
            'gamma': opts.discount,
            'alpha': opts.learningRate,
            'epsilon': opts.epsilon,
            'actionFn': lambda state: mdp.getPossibleActions(state),
        }
        a = QLearningAgent(0, **qLearnOpts)
    elif (opts.agent == 'random'):
        # No reason to use the random agent without episodes.
        if (opts.episodes == 0):
            opts.episodes = 10

        class RandomMDPAgent:
            def getAction(self, state):
                return random.choice(mdp.getPossibleActions(state))

            def getValue(self, state):
                return 0.0

            def getQValue(self, state, action):
                return 0.0

            def getPolicy(self, state):
                "NOTE: 'random' is a special policy value; don't use it in your code."
                return 'random'

            def update(self, state, action, nextState, reward):
                pass

        a = RandomMDPAgent()
    else:
        if (not opts.manual):
            raise 'Unknown agent type: ' + opts.agent

    ###########################
    # RUN EPISODES
    ###########################

    # Display q/v values before simulation of episodes.
    if (not opts.manual and opts.agent == 'value'):
        if (opts.valueSteps):
            for i in range(opts.iters):
                tempAgent = ValueIterationAgent(0, mdp, opts.discount, i)
                display.displayValues(tempAgent, message = 'VALUES AFTER ' + str(i) + ' ITERATIONS')
                display.pause()

        display.displayValues(a, message = 'VALUES AFTER ' + str(opts.iters) + ' ITERATIONS')
        display.pause()
        display.displayQValues(a, message = 'Q-VALUES AFTER ' + str(opts.iters) + ' ITERATIONS')
        display.pause()

    # Figure out what to display each time step (if anything).
    displayCallback = lambda x: None
    if (not opts.nullGraphics):
        if (opts.manual and opts.agent is None):
            displayCallback = lambda state: display.displayNullValues(state)
        else:
            if (opts.agent == 'random'):
                displayCallback = lambda state: display.displayValues(a, state, 'CURRENT VALUES')
            elif (opts.agent == 'value'):
                displayCallback = lambda state: display.displayValues(a, state, 'CURRENT VALUES')
            elif (opts.agent == 'q'):
                displayCallback = lambda state: display.displayQValues(a, state, 'CURRENT Q-VALUES')

    messageCallback = lambda x: print(x)
    if (opts.nullGraphics):
        messageCallback = lambda x: None

    # FIGURE OUT WHETHER TO WAIT FOR A KEY PRESS AFTER EACH TIME STEP
    pauseCallback = lambda: None
    if (opts.pause):
        pauseCallback = lambda: display.pause()

    # Figure out whether the user wants manual control (for debugging and demos).
    if (opts.manual):
        decisionCallback = lambda state: getUserAction(state, mdp.getPossibleActions)
    else:
        decisionCallback = a.getAction

    # Run episodes.
    if (opts.episodes > 0):
        logging.debug('RUNNING ' + str(opts.episodes) + ' EPISODES')

    returns = 0
    for episode in range(1, opts.episodes + 1):
        returns += runEpisode(a, env, opts.discount, decisionCallback, displayCallback,
                messageCallback, pauseCallback, episode)

    if (opts.episodes > 0):
        logging.debug('AVERAGE RETURNS FROM START STATE:' + str((returns + 0.0) / opts.episodes))

    # Display post-learning values / q-values.
    if (opts.agent == 'q' and not opts.manual):
        display.displayQValues(a, message = 'Q-VALUES AFTER ' + str(opts.episodes) + ' EPISODES')
        display.pause()
        display.displayValues(a, message = 'VALUES AFTER ' + str(opts.episodes) + ' EPISODES')
        display.pause()
def makeGrid(gridString)
Expand source code
def makeGrid(gridString):
    width, height = len(gridString[0]), len(gridString)
    grid = Grid(width, height)
    for ybar, line in enumerate(gridString):
        y = height - ybar - 1
        for x, el in enumerate(line):
            grid[x][y] = el

    return grid
def parseOptions(argv)

Processes the command used to run gridworld from the command line.

Expand source code
def parseOptions(argv):
    """
    Processes the command used to run gridworld from the command line.
    """

    description = """
    DESCRIPTION:
        This program will create a gridworld. Explore and find the best path to the reward!

    EXAMPLES:
        (1) python -m pacai.bin.gridworld
            - Creats a gridworld with default settings.
        (2) python -m pacai.bin.gridworld --discount 0.7
            - Creats a gridworld with a 0.7 discount factor.
    """

    parser = argparse.ArgumentParser(description = textwrap.dedent(description),
        prog = os.path.basename(__file__), formatter_class = argparse.RawTextHelpFormatter)

    parser.add_argument('-a', '--agent', dest = 'agent',
            action = 'store', type = str, default = 'random',
            help = 'agent type (options are \'random\', \'value\' and \'q\', default %(default)s)')

    parser.add_argument('-d', '--debug', dest = 'debug',
            action = 'store_true', default = False,
            help = 'set logging level to debug (default: %(default)s)')

    parser.add_argument('-e', '--epsilon', dest = 'epsilon',
            action = 'store', type = float, default = 0.3,
            help = 'chance of taking a random action in q-learning (default %(default)s)')

    parser.add_argument('-g', '--grid', dest = 'grid',
            action = 'store', type = str, default = 'BookGrid',
            help = 'grid type: BookGrid, BridgeGrid, CliffGrid, MazeGrid, %(default)s (default)')

    parser.add_argument('-i', '--iterations', dest = 'iters',
            action = 'store', type = int, default = 10,
            help = 'number of rounds of value iteration (default %(default)s)')

    parser.add_argument('-k', '--episodes', dest = 'episodes',
            action = 'store', type = int, default = 1,
            help = 'number of epsiodes of the MDP to run (default %(default)s)')

    parser.add_argument('-l', '--learning-rate', dest = 'learningRate',
            action = 'store', type = float, default = 0.5,
            help = 'set the learning rate (default %(default)s)')

    parser.add_argument('-n', '--noise', dest = 'noise',
            action = 'store', type = float, default = 0.2,
            help = 'set how often actions result in unintended directions (default %(default)s)')

    parser.add_argument('-p', '--pause', dest = 'pause',
            action = 'store_true', default = False,
            help = 'pause GUI after each time step when running the MDP (default %(default)s)')

    parser.add_argument('-q', '--quiet', dest = 'quiet',
            action = 'store_true', default = False,
            help = 'set logging level to warning (default: %(default)s)')

    parser.add_argument('-r', '--living-reward', dest = 'livingReward',
            action = 'store', type = float, default = 0.0,
            help = 'reward for living for a time step (default %(default)s)')

    parser.add_argument('-s', '--speed', dest = 'speed',
            action = 'store', type = float, default = 1.0,
            help = 'speed of animation, S>1.0 is faster, 0<S<1 is slower (default %(default)s)')

    parser.add_argument('-v', '--value-steps', dest = 'valueSteps',
            action = 'store_true', default = False,
            help = 'display each step of value iteration (default %(default)s)')

    parser.add_argument('-y', '--discount', dest = 'discount',
            action = 'store', type = float, default = 0.9,
            help = 'discount on future (default %(default)s)')

    parser.add_argument('--manual', dest = 'manual',
            action = 'store_true', default = False,
            help = 'manually control agent (default %(default)s)')

    parser.add_argument('--null-graphics', dest = 'nullGraphics',
            action = 'store_true', default = False,
            help = 'generate no graphics (default: %(default)s)')

    parser.add_argument('--text-graphics', dest = 'textGraphics',
            action = 'store_true', default = False,
            help = 'display output as text only (default: %(default)s)')

    parser.add_argument('--window-size', dest = 'gridSize',
            action = 'store', type = int, default = 150,
            help = 'request a window width of X pixels *per grid cell* (default %(default)s)')

    options, otherjunk = parser.parse_known_args(argv)

    if len(otherjunk) != 0:
        raise ValueError('Unrecognized options: \'%s\'.' % (str(otherjunk)))

    # Set the logging level
    if options.quiet and options.debug:
        raise ValueError('Logging cannont be set to both debug and quiet.')

    if options.quiet:
        updateLoggingLevel(logging.WARNING)
    elif options.debug:
        updateLoggingLevel(logging.DEBUG)

    if options.manual and options.agent != 'q':
        logging.info('Disabling Agents in Manual Mode.')
        options.agent = None

    # MANAGE CONFLICTS
    if options.textGraphics or options.nullGraphics:
        options.pause = False

    if options.manual:
        options.pause = True

    return options
def runEpisode(agent, environment, discount, decision, display, message, pause, episode)
Expand source code
def runEpisode(agent, environment, discount, decision, display, message, pause, episode):
    returns = 0
    totalDiscount = 1.0
    environment.reset()

    if (isinstance(agent, ReinforcementAgent)):
        agent.startEpisode()

    logging.info('BEGINNING EPISODE: ' + str(episode) + "\n")

    while True:
        # DISPLAY CURRENT STATE
        state = environment.getCurrentState()
        display(state)
        pause()

        # END IF IN A TERMINAL STATE
        actions = environment.getPossibleActions(state)
        if (len(actions) == 0):
            logging.info('EPISODE ' + str(episode) + ' COMPLETE: RETURN WAS ' + str(returns) + '\n')
            return returns

        # GET ACTION (USUALLY FROM AGENT)
        action = decision(state)
        if (action is None):
            raise Exception('Error: Agent returned None action')

        # EXECUTE ACTION
        nextState, reward = environment.doAction(action)
        logString = ''
        logString += '\nStarted in state: ' + str(state)
        logString += '\nTook action: ' + str(action)
        logString += '\nEnded in state: ' + str(nextState)
        logString += '\nGot reward: ' + str(reward) + '\n'
        logging.debug(logString)

        # Update learner.
        if (isinstance(agent, ReinforcementAgent)):
            agent.observeTransition(state, action, nextState, reward)

        returns += reward * totalDiscount
        totalDiscount *= discount

    if (isinstance(agent, ReinforcementAgent)):
        agent.stopEpisode()

Classes

class Grid (width, height, initialValue=' ')

A 2-dimensional array of immutables backed by a list of lists. Data is accessed via grid[x][y] where (x, y) are cartesian coordinates with x horizontal, y vertical and the origin (0, 0) in the bottom left corner.

The str method constructs an output that is oriented appropriately.

Expand source code
class Grid(object):
    """
    A 2-dimensional array of immutables backed by a list of lists.
    Data is accessed via grid[x][y] where (x, y) are cartesian coordinates with x horizontal,
    y vertical and the origin (0, 0) in the bottom left corner.

    The __str__ method constructs an output that is oriented appropriately.
    """

    def __init__(self, width, height, initialValue=' '):
        self.width = width
        self.height = height
        self.data = [[initialValue for y in range(height)] for x in range(width)]
        self.terminalState = 'TERMINAL_STATE'

    def __getitem__(self, i):
        return self.data[i]

    def __setitem__(self, key, item):
        self.data[key] = item

    def __eq__(self, other):
        if (other is None):
            return False
        return self.data == other.data

    def __hash__(self):
        return hash(self.data)

    def copy(self):
        g = Grid(self.width, self.height)
        g.data = [x[:] for x in self.data]
        return g

    def deepCopy(self):
        return self.copy()

    def shallowCopy(self):
        g = Grid(self.width, self.height)
        g.data = self.data
        return g

    def _getLegacyText(self):
        t = [[self.data[x][y] for x in range(self.width)] for y in range(self.height)]
        t.reverse()
        return t

    def __str__(self):
        return str(self._getLegacyText())

Methods

def copy(self)
Expand source code
def copy(self):
    g = Grid(self.width, self.height)
    g.data = [x[:] for x in self.data]
    return g
def deepCopy(self)
Expand source code
def deepCopy(self):
    return self.copy()
def shallowCopy(self)
Expand source code
def shallowCopy(self):
    g = Grid(self.width, self.height)
    g.data = self.data
    return g
class Gridworld (grid)

Helper class that provides a standard way to create an ABC using inheritance.

Expand source code
class Gridworld(MarkovDecisionProcess):
    def __init__(self, grid):
        # layout
        if (isinstance(grid, list)):
            grid = makeGrid(grid)

        self.grid = grid

        # parameters
        self.livingReward = 0.0
        self.noise = 0.2

    def setLivingReward(self, reward):
        """
        The (negative) reward for exiting "normal" states.

        Note that in the R+N text, this reward is on entering
        a state and therefore is not clearly part of the state's
        future rewards.
        """

        self.livingReward = reward

    def setNoise(self, noise):
        """
        The probability of moving in an unintended direction.
        """

        self.noise = noise

    def getPossibleActions(self, state):
        """
        Returns list of valid actions for 'state'.

        Note that you can request moves into walls and
        that "exit" states transition to the terminal
        state under the special action "done".
        """

        if state == self.grid.terminalState:
            return ()

        x, y = state
        if isinstance(self.grid[x][y], int):
            return ('exit', )

        return ('north', 'west', 'south', 'east')

    def getStates(self):
        """
        Return list of all states.
        """

        # The true terminal state.
        states = [self.grid.terminalState]
        for x in range(self.grid.width):
            for y in range(self.grid.height):
                if self.grid[x][y] != '#':
                    state = (x, y)
                    states.append(state)

        return states

    def getReward(self, state, action, nextState):
        """
        Get reward for state, action, nextState transition.

        Note that the reward depends only on the state being
        departed (as in the R+N book examples, which more or
        less use this convention).
        """

        if state == self.grid.terminalState:
            return 0.0

        x, y = state
        cell = self.grid[x][y]
        if isinstance(cell, int) or isinstance(cell, float):
            return cell

        return self.livingReward

    def getStartState(self):
        for x in range(self.grid.width):
            for y in range(self.grid.height):
                if self.grid[x][y] == 'S':
                    return (x, y)

        raise Exception('Grid has no start state')

    def isTerminal(self, state):
        """
        Only the TERMINAL_STATE state is *actually* a terminal state.
        The other "exit" states are technically non-terminals with
        a single action "exit" which leads to the true terminal state.
        This convention is to make the grids line up with the examples
        in the R+N textbook.
        """

        return state == self.grid.terminalState

    def getTransitionStatesAndProbs(self, state, action):
        """
        Returns list of (nextState, prob) pairs
        representing the states reachable
        from 'state' by taking 'action' along
        with their transition probabilities.
        """

        if action not in self.getPossibleActions(state):
            raise Exception('Illegal action!')

        if self.isTerminal(state):
            return []

        x, y = state

        if isinstance(self.grid[x][y], int) or isinstance(self.grid[x][y], float):
            termState = self.grid.terminalState
            return [(termState, 1.0)]

        successors = []

        northState = (self.__isAllowed(y + 1, x) and (x, y + 1)) or state
        westState = (self.__isAllowed(y, x - 1) and (x - 1, y)) or state
        southState = (self.__isAllowed(y - 1, x) and (x, y - 1)) or state
        eastState = (self.__isAllowed(y, x + 1) and (x + 1, y)) or state

        if action == 'north' or action == 'south':
            if action == 'north':
                successors.append((northState, 1 - self.noise))
            else:
                successors.append((southState, 1 - self.noise))

            massLeft = self.noise
            successors.append((westState, massLeft / 2.0))
            successors.append((eastState, massLeft / 2.0))

        if action == 'west' or action == 'east':
            if action == 'west':
                successors.append((westState, 1 - self.noise))
            else:
                successors.append((eastState, 1 - self.noise))

            massLeft = self.noise
            successors.append((northState, massLeft / 2.0))
            successors.append((southState, massLeft / 2.0))

        successors = self.__aggregate(successors)
        return successors

    def __aggregate(self, statesAndProbs):
        counter = {}
        for state, prob in statesAndProbs:
            if state not in counter:
                counter[state] = 0.0
            counter[state] += prob

        newStatesAndProbs = []
        for state, prob in counter.items():
            newStatesAndProbs.append((state, prob))

        return newStatesAndProbs

    def __isAllowed(self, y, x):
        if y < 0 or y >= self.grid.height:
            return False

        if x < 0 or x >= self.grid.width:
            return False

        return self.grid[x][y] != '#'

Ancestors

Methods

def getPossibleActions(self, state)

Returns list of valid actions for 'state'.

Note that you can request moves into walls and that "exit" states transition to the terminal state under the special action "done".

Expand source code
def getPossibleActions(self, state):
    """
    Returns list of valid actions for 'state'.

    Note that you can request moves into walls and
    that "exit" states transition to the terminal
    state under the special action "done".
    """

    if state == self.grid.terminalState:
        return ()

    x, y = state
    if isinstance(self.grid[x][y], int):
        return ('exit', )

    return ('north', 'west', 'south', 'east')
def getReward(self, state, action, nextState)

Get reward for state, action, nextState transition.

Note that the reward depends only on the state being departed (as in the R+N book examples, which more or less use this convention).

Expand source code
def getReward(self, state, action, nextState):
    """
    Get reward for state, action, nextState transition.

    Note that the reward depends only on the state being
    departed (as in the R+N book examples, which more or
    less use this convention).
    """

    if state == self.grid.terminalState:
        return 0.0

    x, y = state
    cell = self.grid[x][y]
    if isinstance(cell, int) or isinstance(cell, float):
        return cell

    return self.livingReward
def getStartState(self)

Inherited from: MarkovDecisionProcess.getStartState

Return the start state of the MDP.

Expand source code
def getStartState(self):
    for x in range(self.grid.width):
        for y in range(self.grid.height):
            if self.grid[x][y] == 'S':
                return (x, y)

    raise Exception('Grid has no start state')
def getStates(self)

Return list of all states.

Expand source code
def getStates(self):
    """
    Return list of all states.
    """

    # The true terminal state.
    states = [self.grid.terminalState]
    for x in range(self.grid.width):
        for y in range(self.grid.height):
            if self.grid[x][y] != '#':
                state = (x, y)
                states.append(state)

    return states
def getTransitionStatesAndProbs(self, state, action)

Returns list of (nextState, prob) pairs representing the states reachable from 'state' by taking 'action' along with their transition probabilities.

Expand source code
def getTransitionStatesAndProbs(self, state, action):
    """
    Returns list of (nextState, prob) pairs
    representing the states reachable
    from 'state' by taking 'action' along
    with their transition probabilities.
    """

    if action not in self.getPossibleActions(state):
        raise Exception('Illegal action!')

    if self.isTerminal(state):
        return []

    x, y = state

    if isinstance(self.grid[x][y], int) or isinstance(self.grid[x][y], float):
        termState = self.grid.terminalState
        return [(termState, 1.0)]

    successors = []

    northState = (self.__isAllowed(y + 1, x) and (x, y + 1)) or state
    westState = (self.__isAllowed(y, x - 1) and (x - 1, y)) or state
    southState = (self.__isAllowed(y - 1, x) and (x, y - 1)) or state
    eastState = (self.__isAllowed(y, x + 1) and (x + 1, y)) or state

    if action == 'north' or action == 'south':
        if action == 'north':
            successors.append((northState, 1 - self.noise))
        else:
            successors.append((southState, 1 - self.noise))

        massLeft = self.noise
        successors.append((westState, massLeft / 2.0))
        successors.append((eastState, massLeft / 2.0))

    if action == 'west' or action == 'east':
        if action == 'west':
            successors.append((westState, 1 - self.noise))
        else:
            successors.append((eastState, 1 - self.noise))

        massLeft = self.noise
        successors.append((northState, massLeft / 2.0))
        successors.append((southState, massLeft / 2.0))

    successors = self.__aggregate(successors)
    return successors
def isTerminal(self, state)

Only the TERMINAL_STATE state is actually a terminal state. The other "exit" states are technically non-terminals with a single action "exit" which leads to the true terminal state. This convention is to make the grids line up with the examples in the R+N textbook.

Expand source code
def isTerminal(self, state):
    """
    Only the TERMINAL_STATE state is *actually* a terminal state.
    The other "exit" states are technically non-terminals with
    a single action "exit" which leads to the true terminal state.
    This convention is to make the grids line up with the examples
    in the R+N textbook.
    """

    return state == self.grid.terminalState
def setLivingReward(self, reward)

The (negative) reward for exiting "normal" states.

Note that in the R+N text, this reward is on entering a state and therefore is not clearly part of the state's future rewards.

Expand source code
def setLivingReward(self, reward):
    """
    The (negative) reward for exiting "normal" states.

    Note that in the R+N text, this reward is on entering
    a state and therefore is not clearly part of the state's
    future rewards.
    """

    self.livingReward = reward
def setNoise(self, noise)

The probability of moving in an unintended direction.

Expand source code
def setNoise(self, noise):
    """
    The probability of moving in an unintended direction.
    """

    self.noise = noise
class GridworldEnvironment (gridWorld)

Helper class that provides a standard way to create an ABC using inheritance.

Expand source code
class GridworldEnvironment(Environment):
    def __init__(self, gridWorld):
        self.gridWorld = gridWorld
        self.reset()

    def getCurrentState(self):
        return self.state

    def getPossibleActions(self, state):
        return self.gridWorld.getPossibleActions(state)

    def doAction(self, action):
        successors = self.gridWorld.getTransitionStatesAndProbs(self.state, action)
        sum = 0.0
        rand = random.random()
        state = self.getCurrentState()

        for nextState, prob in successors:
            sum += prob
            if sum > 1.0:
                raise Exception('Total transition probability more than one; sample failure.')

            if rand < sum:
                reward = self.gridWorld.getReward(state, action, nextState)
                self.state = nextState
                return (nextState, reward)

        raise Exception('Total transition probability less than one; sample failure.')

    def reset(self):
        self.state = self.gridWorld.getStartState()

Ancestors

Methods

def doAction(self, action)

Inherited from: Environment.doAction

Performs the given action in the current environment state and updates the enviornment …

Expand source code
def doAction(self, action):
    successors = self.gridWorld.getTransitionStatesAndProbs(self.state, action)
    sum = 0.0
    rand = random.random()
    state = self.getCurrentState()

    for nextState, prob in successors:
        sum += prob
        if sum > 1.0:
            raise Exception('Total transition probability more than one; sample failure.')

        if rand < sum:
            reward = self.gridWorld.getReward(state, action, nextState)
            self.state = nextState
            return (nextState, reward)

    raise Exception('Total transition probability less than one; sample failure.')
def getCurrentState(self)

Inherited from: Environment.getCurrentState

Returns the current state of enviornment.

Expand source code
def getCurrentState(self):
    return self.state
def getPossibleActions(self, state)

Inherited from: Environment.getPossibleActions

Returns possible actions the agent can take in the given state. Can return the empty list if we are in a terminal state.

Expand source code
def getPossibleActions(self, state):
    return self.gridWorld.getPossibleActions(state)
def isTerminal(self)

Inherited from: Environment.isTerminal

Has the enviornment entered a terminal state? This means there are no successors.

def reset(self)

Inherited from: Environment.reset

Resets the current state to the start state.

Expand source code
def reset(self):
    self.state = self.gridWorld.getStartState()