RBS Module Design
This section explains how an RBS module can be initialized from a data file, and how the main AI program should interface with it.
The setup of FEAR modules is done with XML (as discussed in Chapter 4, "FEAR: A Platform for Experimentation"). In the case of RBSs, this makes it easy to describe the system in a flexible and extendable fashion. Going through the document specification will give us a good understanding of how to implement the system.
As discussed in Chapter 11, the working memory can be described by the symbols it contains. Its initial state consists of each symbol set to its default values, unless initial values are actually provided. Listing 12.1 is an example with two symbols indicating the presence of a wall on the side, and another to keep track of the context.
Listing 12.1 XML Code Describing a Simple Working Memory with Two Example Symbols in It (The symbols have default values and initial values.)
<memory> <Symbol name="sideWall" /> <Symbol name="following" initial="true" default="false" /> </memory>
Although symbols are all declared in the same way, there are two different types. Internal symbols are used by the RBS only (for instance, the "following" symbol). Native symbols correspond to sensors or effectors outside of the RBS (for instance, "leftWall"), while the others are purely internally. This enables us to handle functional extensions indirectly; the native symbols will be set automatically before the interpreter cycle starts.
The rulebase just contains a set of production rules, specified as condition/action pairs (see Listing 12.2). Conditions need to correspond with the symbols in working memory. Actions can also set symbols, although the system supports procedural sensors that don't rely on being declared explicitly.
Listing 12.2 A Simple Rulebase Expressed in XML, Containing Only One Rule (The rule is split into conditions and the action.)
<rulebase> <Rule> <conditions> <Symbol name="sideWall" value="true" /> <Symbol name="frontWall" value="false" /> </conditions> <action> <Symbol name="moveForwards" value="true" /> </action> </Rule> </rulebase>
The condition for a rule is a conjunction between the atomic statements: AND operations combine the individual symbol matches together implicitly. This is the most common type of condition. In our case, there is no need to worry about OR operations. All our conditions are conjunctions (AND), and sentences with disjunctions (OR) can just be split into two. Either the knowledge engineer can do this, or a simple tool can make sure of it.
The interface must allow data to be passed to the module at runtime. This mainly involves telling the system the location of native symbols and how to execute native actions. Runtime data is thereby synchronized with static declarations. Dynamic extensions can also be provided for advanced uses, such as setting/getting the value of symbols or adding new rules.
Variables can be registered with the module by passing pointers to their locations (first parameter) along with their name (second parameter). The name is needed to achieve correspondence with the XML declarations:
SetSensor( &SensorSideWall, "sideWall" );
The procedural actions will be implemented as functors (that is, classes that store functions [Haendel02]), which are a good way of implementing callbacks in C++. We could implement each action in its own class, overriding a method called Step(), for example. The RBS would just make this virtual call when the action needs to be applied. Instead, with functors each action can be a method in the same class. We also can place the native variables in there for a convenient implementation. See the source code with the demo for practical insights.
The accessor methods for each of the internal symbols are fairly trivial to define for Boolean values. The first function Set() can be overridden to handle different types (that is, duplicating it and changing the type of the last parameter); the second must be renamed to prevent clashes (for instance, GetBool). Indeed, type safety would not otherwise be guaranteed because the functions differ only by their return type. (The compiler will complain.)
void Set( const string& symbol, const bool value ); bool Get( const string& symbol ) const;
Adding rules dynamically to the system is not something we'll typically have to do, but it can be done as follows:
void AddCondition( const string& symbol, const bool value ); void SetAction( const string& action );
The first method can be called repeatedly to set up the conjunction, and the second method validates the condition sentence and assigns it a specific action.