Simple Bottle Collector: commit while and timed sequences

The file qulog/examples/bottle_collector/bottleCollector.qlg contains a slight extension of the example program above. In this case, the agent repeatedly finds and goes to a bottle, picks it up and delivers it to a drop. This example is supported by a Python simulation of the environment that you can interact with: creating and removing bottles and dragging them around. Instructions on running the program are in the comments near the beginning of the file.

We now update the type declarations to add dead_centre as a direction and a type declaration for things the agent is interested in.

def dir::= left | centre | dead_centre | right
def thing::= bottle | drop

The percepts and the robotic actions are extended.

percept holding(), gripper_open(), see_at(thing, int, dir),
over_drop()

def robotic_action ::=
open_gripper() | close_gripper() |
move(num) | turn(dir,num)

We have added a speed for moving and turning that we vary as we get close to the object of interest.

The top-level TeleoR procedure is more complex as it needs to collect a bottle and deliver it.

tel collect_bottle()
collect_bottle(){
delivered() ~> ()

next_to_centre(drop) & holding() ~> open_gripper()

holding() ~> get_next_to(drop)

next_to_centre(bottle) & gripper_open() ~> close_gripper()

next_to(bottle,Dir) & gripper_open() ~> turn(Dir,0.2)

gripper_open() ~> get_next_to(bottle)

true ~> open_gripper()
}

The guards are now typically relation calls that are defined in terms of the facts in the BS. For example

rel next_to(?thing,?dir)
next_to(Thing, Dir) <= see_at(Thing, Dist, Dir) & Dist < 1

rel delivered()
delivered() <= next_to(drop,_) & next_to(bottle,_) & gripper_open()

You will notice that the action of two rules are both calls on the TeleoR procedure get_next_to but with different arguments. This procedure is defined as follows.

tel get_next_to(thing)
get_next_to(Th){
next_to_centre(Th) ~> ()

next_to(Th,Dir) ~> turn(Dir,0.3)

close_to(Th,_) ~> approach(Th,0.5,0.5)
% Now very near to Th, slow right down

near(Th,_) ~> approach(Th,1.5,0.5)
% more slowly to achieve very_near(Th,Dir)

see(Th,_) ~> approach(Th,2.5,0.3)
% approach Th quickly to achieve near(Th,_)

true ~> [turn(left,0.5):7,move(2):2]
% Th not in sight, wander hoping to see it
}

Again notice that the TeleoR procedure approach takes different arguments (speeds) depending on the situation.

The new construct in this procedure is the action of the last rule: a timed sequence. This is a list of action, time pairs and the semantics for this case is that as long as this rule continues then the turn action will be active for 7 seconds and then the move action will be active for 2 seconds and this will be repeated indefinitely (for as long as the rule is active). This gives the robot a simple wandering (searching) behaviour. If the last entry has no time component then the semantics is that the last action will be active indefinitely.

This example shows where we really need the call stack as it keeps track of any timers (delays) and the evaluator needs to be able to access all the current delays recorded in the call stack.

The objective of the procedure approach is to get close to a given thing.

tel approach(thing,num,num)
approach(Th,Fs,Ts){

see(Th, Dir) & centre_dir(Dir) ~> move(Fs)

see(Th,Dir) commit_while dir_or_centre(Th, Dir) ~>
move(Fs),turn(Dir,Ts)
}

The second rule uses another new construct: commit_while. In the example the semantics is, when this rule fires, the rule will stay active within this procedure while dir_or_centre(Th, Dir) is true for the fired instantiation of Dir even if see(Th,centre) becomes true. In other words when this rule fires it “takes over” the procedure. The purpose of this construct is to “over achieve”.

Superficially, it seems that the commit_while is not needed - if the second rule is chosen it will turn in the appropriate direction until the thing is seen at centre. The problem is that we will flip rules as soon as the boundary between left, say, and centre. After moving a small distance we will flip back to the second rule. This causes the robot to “flutter” along the boundry between left and centre. If you remove the commit_while and run the program and simulation you will see this happening. By commiting until dead_centre is reached it will be a while before it moves off track.

Because the commit_while takes over you need to be very careful getting the test right. If it's not quite right it might not give up control when it should. The definition of dir_or_centre covers the cases in which we want the action to continue:

rel dir_or_centre(thing, dir)
dir_or_centre(Th, Dir) <=
see(Th, Dir)
dir_or_centre(Th, _) <=
see(Th, centre)
If the environment moved the thing from, say, left to right then
dir_or_centre(Th, Dir) will fail and the second rule would refire and the action would now be to turn right. On the other hand if the thing stays where it is then dir_or_centre(Th, Dir) will remain true until the direction becomes dead_centre.