TeamBotsTM Software Package: Clay

Welcome
Welcome to the Clay information page. This documentation is also included with the TeamBots distribution in EDU/gatech/cc/is/clay/index.html .

Jump to the Clay Package Index

Introduction
Clay is a group of Java classes that can be easily combined to create behavior-based robot control systems. Clay takes advantage of Java syntax to facilitate combining, blending and abstraction of behavior. Clay can be used to create simple reactive systems or complex hierarchical configurations with learning and memory.

Configuring Behavior with Clay
The basic building block in Clay is a node. There are two important phases in a node's life: intialization and runtime. Most nodes have only two methods, corresponding to these phases: the constructor, used for initialization; and Value(), called repeatedly at runtime.

Nodes often have other nodes embedded within them (e.g. an avoid_obstacle node typically has a detect_obstacle node embedded within it). The embedding is specified at initialization time using the node's constructor. Here is an example of how we'd embed one node in another:

detect_obstacles = new va_Obstacles_r(abstract_robot);
avoid_obstacles  = new v_Avoid_va(2.0, 1.0, detect_obstacles);
In this example, a detect_obstacles node is created using the va_Obstacles_r class (the va_Obstacle_r class knows how to query the robot hardware for information about obstacles). Next an avoid_obstacles node is generated by embedding the detect_obstacles node in a v_Avoid_va object. The lower-case letters before and after the node names refer to the input and output types of the node; these hints are helpful when configuring nodes. More on that later.

Note that the embedding provides for code re-use. We could, for instance, avoid robots instead by embedding detect_robots versus detect_obstacles in the v_Avoid_va node. It is also possible to re-use instantiated nodes by embedding them in several other nodes. In this next example, detect_obstacle is imbedded in an avoid_obstacle node and a swirl_obstacle node:

detect_obstacles = new va_Obstacles_r(abstract_robot);
avoid_obstacles  = new v_Avoid_va(2.0, 1.0, detect_obstacles);
swirl_obstacles  = new v_Swirl_va(2.0, 1.0, detect_obstacles, heading);

Nodes are combined or blended by embedding them in a blending node. v_StaticWeightedSum_va is an example blending node class. If you are familiar with motor schema theory, this type node is used for the "sum and normalize" step of schema and assemblage combining. It takes an array of Nodes and an array of weights as input at configuration time. At runtime, it multiplies the output of each embedded node by the associated weight or gain, then sums them. The following statements generate a new node, avoid_n_swirl, that is the average of its two embedded nodes:

avoid_n_swirl = new v_StaticWeightedSum_va();
avoid_n_swirl.embedded[0] = avoid_obstacles;
avoid_n_swirl.weights[0] = 0.5; avoid_n_swirl.embedded[1] = swirl_obstacles; avoid_n_swirl.weights[1] = 0.5;

Runtime
Once a Clay-based behavioral system has been specifed, it is repeatedly called at runtime for it's present value, based on the current situation of the robot. The configuration is the entire behavioral system encapsulated in a single object. For convention, we usually call this object configuration. It is often useful to create separate configurations for each actuator, as might be the case with a Nomad 150 that has steering and turret actuators. In this case we would call the configurations steering_configuration and turret_configuration.

At each timestep the configuration is activated through a call to configuration.Value(). configuration.Value() implicitly activates any embedded nodes through calls to their Value() methods, and so a heirarchical top-down chain of calls is initiated. How do we avoid a computational explosion? Several mechanisms are included in Clay to address this potential problem.

First all the Value() methods take a timestamp as a parameter. They remember the last time they were called, and if the timestamp has not increased, they return the last computed value. Thus if a node is reused by embedding in several other nodes, it will only go to the trouble of computing its value once per timestep. To ensure this, all nodes call embedded nodes with the timestamp they were passed. The timestamp is never incremented by a node. It is set at the highest level by the object calling the configuration (either TBHard or TBSim).

Second, Clay includes some node types that select rather than blend. In a selection node, only one embedded sub-node is activated at a time, thus fan-out calls are eliminated.

Designing Your Own Nodes
Developing a new node is fairly easy. There are two things to consider first: what types of input will it require? and what will its output type be? Input is provided by the Value() method of embedded nodes or by fixed parameters passed to the constructor at initialization time. Java type-checking ensures that we can only embed nodes whose output types match the input type specified by the parent node's constructor.

As mentioned above, input types for a node are specified by the constructor declaration. The output type is specified by its Value() definition. You should implement your node by extending one of the following classes, based on the desired output type for your node:

Although Clay can certainly be utilized for other types of behavioral paradigms, it specifically targets motor schema-based control. Several motor and perceptual schemas have already been designed, and are included in the Clay distribution. Motor schemas are usually NodeVec2s. Perceptual schemas may be NodeVec2s or NodeVec2Arrays depending on what sorts of things they sense. Perceptual features are typically NodeBooleans or NodeInts. Once you have selected the input and output types of your node, you must implement the constructor and Value() methods. Probably the best way to get started is by looking at some of the exisiting nodes for inspiration. v_LinearAttraction_v and v_AvoidArray_va are good examples.

Naming Convention
To help make it easier for designers to determine which nodes are which, we have developed a naming convention. Each node class is named as follows:

{output type}_{name}_{embedded node 1 type}{embedded node 2 type}{...}
The name is the name you give your node. Variations that provide slightly different outputs or use many instead of one input are distinguished by the output and embedded type prefix and suffixes. Note that a node can only have one output type. The types are given by one or two letter abbreviations as follows:
iint
iaint[]
ddouble
dadouble[]
vVec2
vaVec2[]
bboolean
baboolean[]
sScalar
saScalar[]
rrobot - only valid as an input.
So a node named Avoid that takes an array of Vec2s as input and outputs a single Vec2 would be called v_Avoid_va. The name of the node should clearly and succintly indicate its purpose (e.g. Obstacle, LinearAttraction). Nodes taking a robot object as input usually provide some sort of sensor output. va_Obstacles_r for example, outputs a list of detected obstacles.