Introductory Example: Declarations, Logger, Robot Shell

In qulog/examples/introduction there is a very simple TeleoR program tr_eg.qlg. We shall use this to introduce the basic declarations and semantics and describe how to use the logger and robot shell. The purpose of the TeleoR agent programmed in this file is to search for an object, move to it and grab it.

TeleoR procedures require type declarations similar to that of relations, functions and actions. Like functions they only take ground arguments and so mode are not included in the declaration. We have two declarations of TeleoR procedures:

tel get_object()
tel get_to()

As well as declaring the types of the TeleoR procedures, the minimal requirement is to declare the percepts and primitivie actions:

percept see(num, dir), holding()
def robotic_action ::=
move(num) | turn(dir) | grab() | release()

The percept declaration has a dual purpose: it declares the terms that the robot side will send to the agents percept handling thread; and also declares them as dyn terms. The percept handler processes percept messages and updates the BS.

In response to percept changes the agents evaluator thread will typically send a collection of robotic actions to the robot side to execute. The robotic_action declaration lists the allowed robotic actions.

These two declarations are therefore describing the interface between the TeleoR agent and the robot side.

As we said above, the purpose of the TeleoR agent programmed in this file is to search for an object, move to it and grab it. It contains the top-level TeleoR procedure get_object and the TeleoR procedure get_to that is called from the first:

tel get_object()
get_object() {
holding() & see(0, centre) ~> ()
not holding() & see(0, centre) ~> grab()
not holding() ~> get_to()
true ~> release()
}

tel get_to()
get_to() {
see(0, centre) ~> ()
see(0, Dir) ~> turn(Dir)
see(_, centre) ~> move(6)
see(_, Dir) ~> move(4) , turn(Dir)
true ~> turn(left)
}

Each TeleoR procedure definition consists of an ordered sequence of rules that are guarded actions. In simple TeleoR procedures the guard of the first rule is the goal of the procedure and the action is typically the empty action and, in particular, the guard of the first rule of the top-level procedure is the overall goal of the program. In this case that goal is to be holding something and to see an object immediately in front. In this example we take that to mean the robot is holding the object. Later we will see procedures where the goal of the procedure is not the guard of the first rule.

The guards of the procedures typically query directly, or indirectly, the BS. In this example the guards directly access the percept part of the belief store. This means that, as the percepts change, different rules will be chosen and different actions will be enacted. Later we will see examples where the belief store contains facts that come from sources other than the percept handler.

To start the program running we first have to start the TeleoR agent:

| ?~> start_agent(robo@localhost, all)

The first argument is the address of an external process which is the interface to the robotic devices and the sensors. It could be a simulation process. The second argument gives the percept update convension. When this argument is all, the agent will expect the external process to send it a list of percept facts representing the lastest readings of every sensor whenever any sensor reading changes. When this argument is updates, the agent will expect only to be told the percept facts that must be deleted, added, or changed to record just the changed sensor readings.

The start_agent call does the following:

  • start the message handling thread - it will receive agent messages
  • start the task evaluation thread that is responsible for starting and ending tasks and to (re)evaluate each task's call stack (a sequence of information about the procedure calls which is discussed below) when the BS changes
  • start the percept handling thread and carry out a handshake with the robot side (using the address in the first argument)

The percept handler sends the message initialise_ to the robot side to let the robot side know the address of the percept handler and that it should send back an initial list of percepts. If the robot side does not respond it will resend the message every five seconds until the robot side responds with an initial list of percepts. The percept handler will update the BS with these percepts and then enter a loop where it will wait for, and then process, percept messages.

The next step is to start a task that that starts with an initial TeleoR procedure call:

| ?~> start_named_task(get_object(), collector)

The first argument is the initial procedure call and the second argument is the name given to this task. You will see this task name in the logger and will become more important in later multi-task agents.

This call will cause the evaluator thread to wake up, add this task to the list of tasks the evaluator manages, and evaluate its call stack and possibly send actions to the robot side.

The typical sequence of events that now happens as the program runs is that the actions that are sent to the robot side cause the robot to either change the environment or change its view of the environment which in turn changes the robots perceptions which in turn causes the robot side to send new percepts to the percept handler thread. This causes the percept handler thread to update the BS which in turn causes the evaluator thread to wake up and re-evaluate the call stack of the task, sending new actions to the robot side. This process continues for as long as the task is running even if it has achieved its goal.

How the robot side responds to action messages and when and what percepts are sent to the agent is determined by its programming. We will discuss that later.

We will shortly explain the semantics of this TeleoR program but first we show you how to set things up so that you can experiment with the program. This will involve running the program, running a tool so you can act as the robot side and running the logger so that you can see what the program is doing.

In order to do this you will need three terminal windows.

In terminal 1, you should start the Pedro inter-process communication server (if it is not already running) and the logger process, naming it logger (or another name of your choice), with the two OS commands:

pedro
logger.py logger
The name logger will be registered with pedro as the name of the Python process allowing messages to be sent from the TeleoR agent.

In terminal 2, you should start the robot shell giving it a name robo (or some other name) using:

robot_shell.py robo
The name robo will be registered with pedro for this process. There is an optional -w flag that can be given in the command line. Its role is explained later.

In terminal 3, in the qulog/examples/introduction directory, you should execute the following five commands:

teleor -Aagent % Start the teleor interpreter and name the process
| ?~> [tr_eg]. % Consult the example program
success % System response
| ?~> logging logger. % Turn logging of task behaviour on
success
| ?~> go(). % start the agent and task
success

In the program there is the following definition for the go action.

go : act()
go() ~>
start_agent(robo@localhost, all) ;
start_named_task(get_object(), collector)

This starts the agent and the task that runs the program, giving an architecture as depicted in Figure 1. For this experiment the source of the percepts and the destination for the control actions is the robot shell process.

You can use the task name to later terminate the task using a

kill_task(collector)
command. If testing a TeleoR program with several procedures you can now start another task executing a different top level procedure call. A good strategy is to start with procedure calls that are at the bottom of the call chain and to work up testing procedures they call already tested procedures one at a time - bottom up testing of your control program. If your consulted TeleoR program allowed for multiple robotic resource use, then you can have more than one task running at a time.

Immediately after start_agent is executed, you will see an initialise_ message in the robot shell window - this is part of the handshake with the percept handling thread.

The interaction between the agent, the robot shell and the logger is depicted in the upper configuration of Figure 2.

Figure 2: Emulating a robot and remote robot control
\includegraphics[width=5.8in,height=3.8in]{config1}

We will now step through a scenario that you can reproduce by running the programs as described above - remember to start to robot shell and logger before the agent.

The first thing you will notice is that the agent shell will display initialise_ in the window and if you do nothing then it will repeat this message. If you now press the Send button without anything in the Percepts entry box it will send the empty list of pecepts to the agent.

In the logger you will now see


agent::: —— Current Percepts and Dynamic Beliefs ....
[]

agent::: collector: New call stack evaluation:
********************
get_object() - 3 fired
get_to() - 5 fired
[turn(left)]

Robotic actions for collector are: [turn(left)]
and in the robot shell you will see
actions(collector, [turn(left)])

The logger shows that the empty list of percepts arrive and then displays the result of the evaluator calculating the call stack. The third rule of get_object is chosen because the guard of this rule is the first guard that is true reading from the first rule. The action of this rule is a call to get_to and its fifth rule is chosen for the same reason. The action for this rule is the singleton list containing the robotic action turn(left). This is shown in the logger and the actions term, which includes the task name and the list of actions is sent to the robot shell.

The call stack is actually a sequence of data structures that contain the TeleoR procedure call, the index of the rule chosen and the instantiation of any variables from the guard of the chosen rules that are used in the action.

Now, playing the role of the robot, you see an object on the left at 10 units distance. You simply enter see(10, left) in the percepts entry box in the robot shell. The agent responds by re-evaluation the call stack based on this new percept. The logger diaplays

agent::: —— Current Percepts and Dynamic Beliefs ....
[see(10, left)]

agent::: collector: New call stack evaluation:
get_object() - 3 continued
********************
get_to() - 4 fired
[move(4), turn(left)]

Robotic actions for collector are: [move(4), turn(left)]

The call stack above the line of asterisks are unchanged in the re-evaluation (as the first true guard has not changed) but the chosen rule for get_to has changed because there is now a see percept. Now the chosen action is a combination of moving and turning. Although not shown in the logger, the entry in the call stack for get_to has the variable Dir instantiated to left. The robot shell displays the new robotic actions.

Now imagine something in the environment moves this object and so you might enter see(8, right). Again, the agent will re-evaluate its call stack based on the new percept and the logger will show

agent::: —— Current Percepts and Dynamic Beliefs ....
[see(8, right)]

agent::: collector: New call stack evaluation:
get_object() - 3 continued
********************
get_to() - 4 refired
[move(4), turn(right)]

Robotic actions for collector are: [move(4), turn(right)]
The same rule of get_to is chosen but the variable Dir is now instantiated to right causing the robot to now start turning right. The robot shell displays the updated actions.

Now you might enter the percept see(5, centre). The agent will re-evaluate its call stack based on the new percept and the logger will show

agent::: —— Current Percepts and Dynamic Beliefs ....
[see(5, centre)]

agent::: collector: New call stack evaluation:
get_object() - 3 continued
********************
get_to() - 3 fired
[move(6)]

Robotic actions for collector are: [move(6)]
Now you might imagine the robot arriving at the object and so you enter the percept see(0, centre). The logger will now display
agent::: —— Current Percepts and Dynamic Beliefs ....
[see(0, centre)]

agent::: collector: New call stack evaluation:
********************
get_object() - 2 fired
[grab()]

Robotic actions for collector are: [grab()]

Now rule 2 of get_object is chosen and you might now enter the percepts:

see(0,centre), holding()

Note that you need to repeat the see percept because you are using the all percept interface.

The logger now shows

agent::: —— Current Percepts and Dynamic Beliefs ....
[holding(), see(0, centre)]

agent::: collector: New call stack evaluation:
********************
get_object() - 1 fired
[]

Robotic actions for collector are: []

Now the overall goal of the program has been achieved.

If this was a traditional program you might think the program would now terminate as it has achieved its goal. However this is not the case as TeleoR prgrams are achieve-and-maintain and so the program continues to run.

If you now imagine removing the object from the robot's grasp then you would send the empty (set of) percepts. In response to this the logger will now show the same as when you began:

agent::: —— Current Percepts and Dynamic Beliefs ....
[]

agent::: collector: New call stack evaluation:
********************
get_object() - 3 fired
get_to() - 5 fired
[turn(left)]

Robotic actions for collector are: [turn(left)]
and it would start scanning for a new object.

We now consider the part of the semantics of TeleoR programs necessary to explain the behaviour of this program. We will give the extended semantics of other constructs as we introduce them via the later examples.

The robot side will send lists of percepts (starting with the initial percepts) to the percept handling thread. Each time the percept handler gets a list of percepts it will update the BS. This will continue for as long as the agent is running.

At some point start_task will be called and that will cause the evaluator thread to generate an initial call stack for the task. Each call stack is a sequence of triples consisting of a procedure call, a rule number and a list of variable bindings for variables that are instantiated in the rule guard and used in the rule action. The procedure call of the first triple in the sequence is the call in start_task. For each subsequent triple in the sequence the procedure call is the action of the chosen rule of the previous element in the sequence.

The action of the chosen rule of the last element of the sequence is a list of robotic actions (possibly the empty list).

To compute the chosen rule for each element of the call stack the evaluator begins at the first rule and evaluates the guard. If that guard is true (possibly instantiating variables) then that rule is chosen. If the guard is false then it moves on to the second rule and repeats the process. Therefore the chosen rule is the first rule whose guard is true.

If none of the guards are true the evaluator raises an exception and the agent is terminated.

When this program is run (assuming that the percept handler has not added any percepts to the BS) then, as highlighted in the logger, the initial call stack will be

<(get_object(), 3, []), (get_to(), 5, [])>

When the percept handler updates the BS with see(10, left) the evaluator will build the following call stack.

<(get_object(), 3, []), (get_to(), 4, [left])>
Recall that the variable Dir in the guard will be instantiated to this value.

Now when the percept see(8, right) arrives the percept handler will replace the fact see(10, left) with see(8, right) in the BS. The evaluator will then build the call stack

<(get_object(), 3, []), (get_to(), 4, [right])>

We say that, upon re-evaluation of the call stack, an entry that remains the same is a rule continuation, an entry with the same procedure and same rule number but with a different variable instantiation is a rule refiring and otherwise is a rule firing.

So, in the example above the first entry is a continuation in each re-evaluation, the second entry in the first re-evaluation is a firing and the second entry in the second re-evaluation is a refiring.

The astute reader will notice that, for this program, it is not necessary to construct a call stack at all. Simply following the chain of chosen rules would be enough. We will see later as we add more constructs that we will need to keep track of the previous rule choices and variable instantiations - the previous call stack.

Related to semantics, Nilsson introduced two definitions that are useful for both designing and reasoning about TeleoR procedures: regression and completeness.

A TeleoR procedure has the regression property if the action of each rule (except the first) will normally, eventually, make an earlier guard true and it has the completeness property if, for a given state of the system, at least one guard is true. If a TeleoR procedure has these properties then it will typically make progress towards its ultimate goal.

We say “normally, eventually” because the environment might continually thwart the agent. We assume the environment is not so hostile that the agent won't be able to make progress if it tries for long enough.

As you might imagine, keeping these ideas in mind when designing TeleoR agents can be of great help. They have also been used to prove properties about progress given reasonable assumptions on the environment.

Later we will see examples where we slightly relax the regression property and another example where we generalize this property making it more flexible in certain applications.