Creating action designators for the TurtleSim

Description: In this tutorial you will learn what designators are, and in particular, work with action designators: you will learn how to define one and how to use it.

Previous Tutorial: Implementing simple plans to move a turtle
Next Tutorial: Creating process modules

Designators: an overview

From a user's point of view, a designator is a Common Lisp object that contains a sequence of key-value pairs representing a high-level, symbolic description of some aspect of robot's activity. The power and usefulness of the designator concept comes from the ability to infer concrete parameters when needed, based on user-specified rules, from the context in which the robot operates and the symbolic description in the designator. This is known as 'resolving' or '[de]referencing' a designator, and it returns an object containing the newly resolved values that can then be used by the robot to specify some task.

Currently there are four types of designators defined in CRAM:

  • location designators: describe locations taking various constraints into account (for example, reachability, visibility etc)
  • object designators: describe objects on a symbolic level (what they are, what they may be used for etc)
  • action designators: describe an action that a robot should take and serve as input to process modules
  • human designators: describe a human actor in the scene

These types should cover most use cases in robotics, however, if needed, new designator types can be defined as subclasses of the designator class.

As a simple example of a designator creation, suppose we want to create a designator for a location from which some object (for which we already have a designator) can be seen. We would then write

(defparameter spy-location (make-designator :location `((:to :see) (:object ,prime-minister))))

(Do not add this code to your tutorial files, it is meant simply for illustration here.)

This line of code creates a location designator (spy-location) which “knows” of the given object (prime-minister, which we assume is an already defined object designator), and knows that its purpose is to see the object. We do not actually specify a location in terms of coordinates at this moment. When we do want to find a suitable set of coordinates for this location, we would call

(reference spy-location)

and, assuming we have set up rules to take into account knowledge of the associated object, the environment, and the vision capabilities of the robot, we would have a process which can enumerate possible coordinates for the location that would satisfy its purpose and return these as coordinate vectors.

For the rest of this tutorial, we will work with action designators and show a few basics of their creation and use. For a more detailed look at designators, it is recommended that you also read the cram_designators page.

Using action designators

First, you will need to add a few more dependencies to the tutorial package files you created previously. To package.xml, add build and run dependencies on cram_prolog and cram_designators:

  <build_depend>cram_prolog</build_depend>
  <build_depend>cram_designators</build_depend>

  <run_depend>cram_prolog</run_depend>
  <run_depend>cram_designators</run_depend>

In your cram-beginner-tutorial.asd file, on the :depends-on line, add dependencies to cram-designators and cram-prolog. Let's also create a new source file for the code in this tutorial (under src directory), call it action-designators.lisp. We will need to add the file to the :components of the src module in your cram-beginner-tutorial.asd, which should now look something like this:

(defsystem cram-beginner-tutorial
  :depends-on (cram-language roslisp turtlesim-msg geometry_msgs-msg cl-transforms
                             cram-designators cram-prolog)
  :components
  ((:module "src"
            :components
            ((:file "package")
             (:file "control-turtlesim" :depends-on ("package"))
             (:file "simple-plans" :depends-on ("package" "control-turtlesim"))
             (:file "action-designators" :depends-on ("package"))))))

Finally, we add cram-designators namespace to the use list of our package, for convenience, and import some of the symbols of the cram-prolog package. We don't use cram-prolog, as it causes conflicts with some of the similarly named symbols of the cram-language package:

(defpackage :cram-beginner-tutorial
  (:nicknames :tut)
  (:use :cpl :roslisp :cl-transforms :cram-designators)
  (:import-from :cram-prolog :def-fact-group :<- :lisp-fun))

Now, reload the tutorial in roslisp_repl (which also loads the newly added dependencies).

Creating an action designator

Let's try to create an action designator in the REPL command line:

TUT> (defparameter my-desig (make-designator :action '((:type :shape) (:shape :triangle) (:radius 3))))
MY-DESIG
TUT> (desig-prop-value my-desig :radius)
3

As we can see, the make-designator function simply returns a designator object with the properties we specify. We can also read those already specified property values with the desig-prop-value function. So far, so good, but the interesting thing about designators is their resolution. Let's try that in the REPL command line:

TUT> (reference my-desig)
WARNING:
   Trying to prove goal (ACTION-DESIG
                         #<ACTION-DESIGNATOR
                           ((TYPE SHAPE) (SHAPE TRIANGLE) (RADIUS 3))
                           {100A0907B3}>
                         ?ACT) with undefined functor ACTION-DESIG.
; Evaluation aborted on #<DESIGNATOR-ERROR "Cannot resolve action designator ~a." {1007447C53}>.

This rather obscure error message is CRAM's way of telling you there are no rules in place to resolve this designator, so let's try to provide a few such rules.

In case you were wondering why we use keywords in designator properties, that is because we would like to be able to specify rules to resolve designators in one package and create designators that are resolved through these rules in a different package. To avoid naming clashes between packages and huge import lists, we simply define all designator properties in the keyword package.

Defining inference rules for designators

Append the following to your action-designators.lisp file:

(in-package :tut)
 
(defstruct turtle-shape
  "represents an object in continuous space matching a symbolic description"
  radius
  edges)
 
(def-fact-group shape-actions (action-desig)
  ;; for each kind of shape, call MAKE-TURTLE-SHAPE with the right number of edges
 
  ;; triangle
  (<- (action-desig ?desig (draw-shape ?action))
    (desig-prop ?desig (:type :shape))
    (desig-prop ?desig (:shape :triangle))
    (desig-prop ?desig (:radius ?radius))
    (lisp-fun make-turtle-shape :radius ?radius :edges 3 ?action))
 
  ;; square
  (<- (action-desig ?desig (draw-shape ?action))
    (desig-prop ?desig (:type :shape))
    (desig-prop ?desig (:shape :square))
    (desig-prop ?desig (:radius ?radius))
    (lisp-fun make-turtle-shape :radius ?radius :edges 4 ?action))
 
  ;; pentagon
  (<- (action-desig ?desig (draw-shape ?action))
    (desig-prop ?desig (:type :shape))
    (desig-prop ?desig (:shape :pentagon))
    (desig-prop ?desig (:radius ?radius))
    (lisp-fun make-turtle-shape :radius ?radius :edges 5 ?action))
 
  ;; hexagon
  (<- (action-desig ?desig (draw-shape ?action))
    (desig-prop ?desig (:type :shape))
    (desig-prop ?desig (:shape :hexagon))
    (desig-prop ?desig (:radius ?radius))
    (lisp-fun make-turtle-shape :radius ?radius :edges 6 ?action)))

Let's see what this code does. The defstruct declares a structure type to hold values resulting from the inference. It's an instantiation of our action in a space of (possibly) continuous parameters, which we deduce from a symbolic description of the designator via rules given in the def-fact-group.

As for the inference rules themselves, these are Prolog code embedded in Lisp, for it is Prolog that powers the inference behind designator resolution. To learn more about CRAM Prolog look at the Using Prolog for reasoning tutorial. The def-fact-group is a collection of several rules, each of similar structure, so it helps to look at one of them in more detail:

  (<- (action-desig ?desig (draw-shape ?action))
    (desig-prop ?desig (:type :shape))
    (desig-prop ?desig (:shape :pentagon))
    (desig-prop ?desig (:radius ?radius))
    (lisp-fun make-turtle-shape :radius ?radius :edges 5  ?action))

The first parameter to is the “rule head”

(action-desig ?desig (draw-shape ?action))

while all other parameters comprise the “rule body”

    (desig-prop ?desig (:type :shape))
    (desig-prop ?desig (:shape :pentagon))
    (desig-prop ?desig (:radius ?radius))
    (lisp-fun make-turtle-shape :radius ?radius :edges 5  ?action)

Prolog's inference semantics is, in a nutshell, 'IF there is some assignment to variables such that all elements of the body are true, THEN use that assignment of variables to evaluate the head'. Note that in CRAM Prolog for a symbol to be considered a variable, its name must begin with a ?.

For the example rule we've selected, Prolog looks at the designator and asks “does it contain a key-value pair that is (:type :shape)?” (so no variables here except the designator itself). Assuming this is true, it then asks “does it contain a key-value pair that is (:shape :pentagon)?” If still true, it asks “does it contain a key-value pair that is (radius <some variable value>)?” This last question has the effect of extracting the value of the radius parameter that we created the designator with and making it available to Prolog. Finally, it “asks” Lisp to create a turtle-shape structure with the ?radius value just discovered and the number of edges selected via the shape parameter, and store this in ?action.

If all of the above body rules are true (or can be executed sucessfully) then the head is itself evaluated, and what it does is glue the newly created ?action, the object containing the concrete values, to the designator ?desig. Or in more concrete terms, it defines what the value of the action-desig predicate is if applied to ?desig, and that value is a list containing the instruction draw-shape and the value of ?action.

If in any step of the inference Prolog stumbles upon false (or NIL) it exists the current search branch and continues searching somewhere else.

For this tutorial, the inference is very simple: in fact, it is just a conditional switch based on the shape parameter which translates a name like triangle or pentagon into the corresponding number of edges.

Reload the tutorial package in roslisp_repl. This will also load the newly defined inference rules.

Let's try to create, then reference a designator in the REPL command line again:

TUT> (defparameter my-desig-2 (make-designator :action '((:type :shape) (:shape :triangle) (:radius 5))))
MY-DESIG-2
TUT> (reference my-desig-2)
(DRAW-SHAPE #S(TURTLE-SHAPE :RADIUS 5 :EDGES 3))

No more resolution errors this time, as we have specified inference rules which allow us to have a shape object returned, which we could then pass as a parameter to a process module that will actually move the turtle along this shape.

Next

Now that we know how to handle designators, we need to have the robot act on them in some way. CRAM process modules allow us to do just that …

Creating process modules