Nilsson's Simple Block Tower Builder: Cognitive control and Recursion

The file qulog/examples/towers/towerBuilder.qlg contains a very simple tower builder program - a direct translation of the example given by Nilsson in [3].

We give this simple example for three reasons: firstly to pay homage to Nilsson and his elegant invention of Teleor-Reactive Programming; secondly to give an example of a recursive TeleoR procedure; and thirdly as a precursor to the introduction of multiple tasks, sharing and alternating the use of one or more physical resources.

The tower builder problem introduced by Nilsson consists of a table containing a collection of numbered blocks and a robot arm that can pick up blocks and build a tower of numbered blocks in a given order. The environment might cover needed blocks, shuffle (partly built) towers or even help build the tower or uncover needed blocks. There is a simplifying assumption that there is enough room on the table for every block to be placed directly on the table in the area that can be reached by the robot arm.

The relatively high level robotic actions and percepts that we can assume are:

def robotic_action ::= pickup(block) | put_on_block(block) |
put_on_table()

percept holding(block), on_table(block), on(block,block)
In this case the emphasis of the TeleoR agent control program is cognitive control - knowing what block move action should be attempted next to efficiently achieve a built tower goal.

The top-level TeleoR procedure does not directly invoke any robotic actions. It calls subsidiary TeleoR procedures, and itself. It is recursive.

tel makeTower(list(block))
makeTower(Blocks){
tower(Blocks) ~> ()

stack(Blocks) & Blocks=[Block,..] ~> unpile(Block)

Blocks=[Block1,Block2,..Rest] & tower([Block2,..Rest]) ~>
move_to_block(Block1,Block2)

Blocks=[Block] ~> move_to_table(Block)

Blocks = [_,..Rest] ~> makeTower(Rest)
}

The unpile procedure clears blocks above a block of interest.

tel unpile(block)
unpile(Block){
clear(Block) ~> () % The goal of the procedure is achieved

on(OnBlock, Block) ~> move_to_table(OnBlock)
}
tower, stack and clear are inferred from the on_table and on percepts using the following rules.
rel stack(?list(block))
% The ? means list of labels may be given or generated
stack([Block]) <= on_table(Block)
stack([Block1,Block2,..Blocks]) <=
on(Block1,Block2) & stack([Block2,..Blocks])

rel tower(list(block))
tower([Block,..Blocks]) <=
not on(_,Block) & stack([Block,..Blocks])

rel clear(block)
clear(B) <= not on(_,B)

The basic move procedures are given below.

tel move_to_block(Block1:block, Block2:block)
% Moves Block1 from wherever to be on Block2
move_to_block(Block1,Block2){ % Only called if Block2 is clear

on(Block1,Block2) ~> () % The goal is achieved

holding(Block1) & clear(Block2) ~> put_on_block(Block2)

holding(_) ~> put_on_table()

clear(Block1) & clear(Block2) ~> pickup(Block1)

clear(Block2) ~> unpile(Block1)
% Need to clear Block1 to pick it up

true ~> unpile(Block2)
}

tel move_to_table(block)
move_to_table(Block){
on_table(Block) ~> () % Goal is achieved

holding(_) ~> put_on_table()

clear(Block) ~> pickup(Block)

true ~> unpile(Block)

}

Note that makeTower is a recursive procedure and that unpile and move_to_table are mutually recursive.

To reinforce the differences between TeleoR procedures and procedures of traditional procedural code we focus on two aspects of this program.

First we consider recursion. In traditional programming a recursive call is made and eventually the child call will return and the parent will continue and eventually return itself. For TeleoR procedures the parent will pick a rule that calls the child TeleoR procedure. This is similar to traditional procedure calls. The important difference is that the child procedure never returns. Each procedure is continually monitoring the state and eventually the parent will pick a different rule and the child procedure will no longer be active.

The second procedure we consider is unpile. On superficial reading you might think that the second rule will move blocks to the table. This is only partially true. Consider the case where Block is covered by exactly one block OnBlock. In this case the third rule of the procedure call move_to_table(OnBlock) will fire causing the arm to pick up the block. As soon as the block is picked up the first rule of unpile fires and so it does not complete the move to the table. For this reason it might have been better to rename unpile to something like remove_blocks_above.

You will notice that both move_to_table and move_to_block have a rule to deal will holding an unwanted block. This is partially to deal with this problem but also because the environment might rearrange the blocks in a partial tower and the block being held intended for that partial tower will now need to be put on the table.