Last updated 31 March 2008

Introduction to the Strict Slip-Stack POSH Implementation

NB:  Strict and Scheduled POSH are now very similar to implement, so you can use this guide for Scheduled POSH as well.  Differences that still exist are documented below.

Quick Start

How to switch from Scheduled POSH to Strict Slip-Stack POSH

To make distinction between the different implementations easier, I will call the scheduled POSH simply POSH and the strict slip-stack POSH SPOSH.

The API of SPOSH are kept as simple as possible to make switching between the two implementations of POSH as easy as possible. Nevertheless, some changes were unavoidable, mainly due to the use of a different logging architecture.

Plans need to be changed to have drive collections of type SDC or SRDC, rather than DC or RDC.

Most of the behaviours should be very easy to port, given that they do not call too many of the internal function of the agent (but they shouldn't do that anyway). An important change is that there can now be only one behaviour class per file, and both the class and the file have to have the name of the behaviour. Hence, if you want to port the behaviour Exploring, then its associated class has to be called Exploring and has to be defined in the file Exploring.py in the behaviour library. This is necessary for the agent to find the behaviours that is uses, and has the advantage that you don't need to write a separate behaviour loader any more.

The Behaviour class for both POSH and SPOSH is now the same, so for either framework, you start a behaviour library file with:

# behaviour base for POSH or SPOSH behaviours
from POSH import Behaviour

The Exploring behaviour in the file Exploring.py would then be defined with

class Exploring(Behaviour):
...

again, this works for either POSH or SPOSH.  Only the script file indicates which you are using, not the behavior library.

The class methods check_error, exit_prepare, init_acts, init_senses, add_act, add_sense and get_sense are no longer necessary, if you have them in old code you can remove them.  Still, you need to tell SPOSH (or POSH) which actions and senses the behaviour provides. This is now done in the constructor of the behaviour. Let us assume that the behaviour Exploring offers the actions move_around and move_to_other_agent, and the senses wants_to_explore and is_far_from_agent. Then, the beginning of the Exploring class would look as follows:

class Exploring(SPOSH.Behaviour):
"""Exploring Behaviour."""
def __init__(self, agent):
"""Initialises the Exploring behaviour.
"""
SPOSH.Behaviour.__init__(self, agent,
('move_around', 'move_to_other_agent'),
('wants_to_explore', 'is_far_from_agent'))
# further code here to set the initial state of the behaviour

Naturally, all the given actions and senses have to correspond to a method in the behaviour class.

Quick start on writing behaviour libraries for POSH & SPOSH agents

We will give a short step-by-step introduction how one would create the Behaviour Exploring from scratch. Firstly, we need to either create a new behaviour library (in the directory library) or add the behaviour to an existing one. Let us assume that we will add the behaviour to the macaques behaviour library. Then we need to create a new file /library/macaques/Exploring.py (it is important that the class name of the behaviour and the file name are the same!). In that file we will need to import the Behaviour class (sorry, American spelling is not an option in jyPOSH!).  A self-explanatory example of the implementation of Exploring is:

from POSH import Behaviour

class Exploring(Behaviour):
"""Exploring Behaviour."""
def __init__(self, agent):
"""Initialises the Exploring behaviour.
"""
# we need to tell the constructor of SPOSH.Behaviour which
# actions and senses our behaviour provides. The first list
# gives the actions and the second the senses.
Behaviour.__init__(self, agent,
('move_around', 'holler'),
('wants_to_explore', 'is_far_from_agent'))

# here the behaviour states get initialised
self.someStateVariable = 100
self.anotherStateVariable = -100

def move_around(self):
"""The action 'move_around'."""
# depending on what world your agent lives in, moving will almost
# certainly require talking to the machine or simulator that provides
# that world. Like other modules (see below) you should be able to access
# this through the agent, via self.agent. For example, if we use the MASON library
# for our environment, motion comes from that.

self.agent.MASON.move(self.random.int(0, self.someVariableState))

# we need to notify the caller that the action was successful
# returning 0 means that the action wasn't successful
return 1

def holler(self):
"""The action 'move_to_other_agent'."""
# you can also just access regular python commands
print "Yo! Over here!"

return 1

def wants_to_explore(self):
"""The sense 'wants_to_explore'."""
if self.anotherStateVariable > 50:
return 1
else:
return 0

def is_far_from_agent(self):
"""The sense 'is_far_from_agent'."""
closest = self.agent.MASON.get_closest_other_agent()
# we can access other behaviours using the agent's
# getBehaviour method
if not closest.Sleeping.is_sleeping() and \
self.agent.distance(closest) < 50:
return 1
else:
return 0

Note that every method returns a value.  Actions can only fail or succeed, senses can actually return more complicated values which can be tested with predicates, but if no predicate is specified in the POSH plan, they also use succeed/fail by default.

Having written all required behaviours, we now need to tell the agent to actually use them. How to initialise the agent depends on the environment you are using it in.  We have built functions for reading agent initialisation files, and we have created an idiom or pattern of also having an init_world file in the library which starts up the main environment and then reads the agent initialisation script and initialises the agent(s).  However, for some environments (like Unreal Tournament or robot systems) you will need to start up the environment independently.    Running jython jyposh.py launches a GUI that will also help you select, load and execute these files, or enter execution instructions by hand.  If you are running the same files over and over and would prefer a command line, or if you are using an environment that runs better with python than jython (python is faster so we recommend it for robots), then use launch.py instead.  For more information, see the README file in the top jyposh directory, or look at the launch.py python script to see the comments and function calls.

Directory Structure

To ease initialisation of POSH & SPOSH agents, the agent expects to find plans and behaviours at certain locations. If the files are not at the expected location, then initialising the agent fails. All path information is stored in the file config.py in the root directory of jyPOSH.

All behaviour module files need to be located in their associated behaviour library. The behaviour libraries are identified by subdirectories in the library directory, under the jyPOSH root directory. The files of the behaviour library sheepDogDemo for example, are located in the directory /library/sheepDogDemo Each behaviour has its own file that has the same name as the behaviour. Given, for example, that the latchTest behaviour library provides the behaviour Exploring, then this behaviour has to be defined in the file /library/latchTest/Exploring.py.

Plans use actions and senses of certain behaviours, and are therefore also located in the behaviour libraries, in the subdirectory plans. Plans for the latchTest behaviour library, for example, would need to be placed into the /library/latchTest/plans/ directory.

The POSH implementations are located themselves (oddly enough) in the POSH directory.  This includes the jar-file for the log4j Java logging framework. This framework will be used while SPOSH is run with Jython. Hence, the log4j jar needs to be in your Java class path!  (The jyPOSH install instructions explain how to do this.)

Behaviours

Behaviours are the elements of jyPOSH that sense the agent's environment and perform actions in it. They also hold any memory the agent has, since memory is for helping the agents sense and act.  Each behaviour class needs to inherit the POSH.Behaviour class and tell the constructor of POSH.Behaviour which actions and senses it provides. Examples of how to do this are already given in the Quick Start section.

Behaviour modules in BOD agents are not strictly encapsulated.  From any behaviour class, the module's agent can be accessed by self.agent. The other behaviour module objects of the same agent can be accessed by self.agent.BehaviourName.  See examples in the writing behaviours section.

Each agent also keeps a reference to a random-number generator library, which can be accessed by self.random. By default, this object links to the Python random module. Whenever the generation of random number is necessary, this library should be used. The reason to give the behaviours links to a random-number generator is that it allows to have better control over how the random numbers are generated and make experiments exactly reproducible. This is for example used when MASON is used to simulate the environment. In that case, the random number generator is replaced by MASON's random number generator, which allows for reproducible simulation runs.

Log messages can be generated using the behaviour's log object that can be accessed through self.log. A log message of level WARNING, for example, can be created by calling self.log.warning("My warning message"). More information about the types of log messages that can be generated can be found in the documentation of the Python logging module (if you are using Python) or the documentation for the log4j framework (if you are using Jython)

Plans

SPOSH uses a recursive-descent parser to read the plan files. Reading a plan happens in two steps: Firstly, the plan file is parsed and checked for errors. If no errors were found, SPOSH looks for name clashes and links the plan element to the actions and senses of the Behaviours. This is done for all aggregates (drive collections, action patterns and competences) that are defined in the plan. Hence, even if a competence or action-pattern is not used (not linked into the drive-collection hierarchy), if some of the actions and senses that it refers to do not exist, it needs to be commented out.

As the grammar that is accepted is fairly complex, it is described in a separate file.

When compared to the scheduled POSH implementation, there are several changes that might be worth pointing out:

The Agent

The SPOSH agent (implemented in POSH.{strict,scheduled}.agent.py) is the central element of POSH that combines the plans with the behaviours. What you need to do to instantiate an agent is already described in above Quick Start section.

When an agent is created, it firstly loads all of the senses and actions provided by the behaviours into its internal behaviour dictionary (class POSH.BehaviourDict). It then reads in the plan and parses it, using POSH.{strict,scheduled}.LAPParser. After than, an instance of class POSH.{strict,scheduled}.PlanBuilder uses the behaviour dictionary to create the different objects that correspond to the plan elements, and returns the drive collection object. This drive collection object is fired at every time step.

When following the drive, the agent can run in two modes: When the drive collection is of type SDC, then the agent is initialised with a stepped timer (of class POSH.{strict,scheduled}.SteppedTimer). In that case the agent is assumed to be driven by an outside controller, by calling the agent's method followDrive at every time step. An example of such a setup is when POSH agents are used in MASON: Then MASON controls the timing and performs stepping of the agent(s).

In the case of having a real-time drive collection of type SRDC, the agent is initialised with a real-time timer (of class SPOSH.RealTimeTimer). The agent can then be started by calling startLoop, which starts a loop in a new thread that repeatedly calls followDrive at a certain frequency (the frequency is initialised to 20Hz, but can be changed by calling setLoopFreq). The thread  can be controlled by loopWait and loopEnd.

The Logging Infrastructure

Depending on if you are running jyPOSH in Python or Jython, it will be using the Python logging module or the Java log4j framework. They were chosen due to their almost equal API and the similarity of their underlying concepts.

Logging is performed by calling methods of instances of the Logger class. Each of the plan elements, the agent itself, and all behaviours keep such an instance, that is accessible trough self.log. Each such an instance is assigned a particular name that I will refer to as log domain, and that is of the form [AgentName].[ElementType].[ElementName]. The action move_around of agent ExampleAgent, for example, creates log messages under the log domain ExampleAgent.Action.move_around. Assigning different domains to different elements (or behaviours) is useful when we want to perform filtering of the log messages.

All the messages that are generated by the SPOSH implementation are sent at the DEBUG level. As described in the Behaviours section, each behaviour has a log object assigned that allows the behaviours to generate log messages at any logging level (with the log domain being automatically set to [AgentName].[BehaviourName]).  This might be the preferred way of monitoring your program, as it is very easy to filter the messages, write them to files, and use GUI's to display them.

The following two sections give a quick start on how to display the log messages. The general idea is that all the logging messages are collected in a central pool and are then filtered and distributed to different handlers (in Python) or appenders (in log4j) that display the log messages in a configurable format. For a more detailed description please have a look at the documentation of the according logging framework.

Using the Python logging module

The Python logging framework provides a multitude of logging handlers, that allow writing to the console, writing to files, to TCP/IP sockets, to Syslog daemons, ... The easiest way to create a handler that writes log messages to the console is to call basicConfig of the logging module. To display all log messages up to and including the DEBUG log level, call

import logging;
basicConfig(level = logging.DEBUG);

at the beginning of the main script. This creates a StreamHandler with a default log message Formatter and adds to the root logger (i.e. displaying messages of all log domains). Other log levels that are available by default are INFO, WARNING, ERROR and CRITICAL.

Using the Java log4j framework

There are several easy and complicated ways of how to configure the log4j framework. The quickest way to get console output of the logging message at and above the DEBUG level is to include

from org.apache.log4j import BasicConfigurator;
BasicConfigurator.configure();

at the beginning of your main script.

A more configurable, but also more complex approach is to use the PropertyConfigurator class to parse a configuration file. It is used by adding

from org.apache.log4j import PropertyConfigurator;
PropertyConfigurator.configure("mason_simulations/mySimulation.logsetup");

at the beginning of your main script. In above example, the configuration file is located at mason_simulations/mySimulation.logsetup. A simple configuration file could look like this:

# add appender 'console' to root log handler, showing messages at DEBUG level and above
log4j.rootLogger = DEBUG, console

# configure 'console' appender to be a ConsoleAppender
log4j.appender.console = org.apache.log4j.ConsoleAppender

# 'console' uses PatternLayout to configure the logging format
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %-4r [%t] %-5p %c %x - %m%n

With this configuration, all logging messages at level DEBUG and above are written to the console.

Let us consider another example that uses the Chainsaw GUI of the log4j framework to display the log messages. The messages are forwarded to the GUI using TCP/IP sockets, and therefore we will be using a SocketAppender to route the messages. Note that the GUI needs to be running before jyPOSH is started! We can start the GUI by adding the log4j jar (situated in the SPOSH directory) to the Java class path and calling

java org.apache.log4j.chainsaw.Main

That starts the Chainsaw GUI, listening at port 4445. Now we need to create a configuration file that forwards the logging messages to that port:

# appender 'console' gets logging messages at WARN level
# appender 'chainsaw' get logging messages at DEBUG level
log4j.rootLogger = WARN, console
log4j.rootLogger = DEBUG, chainsaw

# configure 'console' appender to be a ConsoleAppender, and using PatternLayout
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %-4r [%t] %-5p %c %x - %m%n

# set up 'chainsaw' appender to send logging messages to localhost:4445
log4j.appender.chainsaw = org.apache.log4j.net.SocketAppender
log4j.appender.chainsaw.remoteHost = localhost
log4j.appender.chainsaw.port = 4445
log4j.appender.chainsaw.locationInfo = true

When now starting jyPOSH it we will get warning messages written to the console, and debug messages written to the Chainsaw GUI.


page authors: Jan Drugowitsch and Joanna Bryson