Tutorial for the C++ Beliefstate Client Library

Please keep in mind that you need the Beliefstate system running in order to let your program interact with it.

What it can do

The Beliefstate Client library allows easy, lightweight access to the Beliefstate infrastructure for semantic event logging. Its main tasks consist of opening a new semantic context in which actions take place, and ending that context again, annotating it with success or failure. Such contexts can be nested at will.

Besides recording such events from a live running system (such as a robotic agent performing tasks in the real world, or a simulated machine working out given tasks), the recorded events can be exported as well-structured OWL files, allowing symbolic reasoning about the available information.

What is needed

In order to equip a program with logging functionality, it needs to be linked against the client library. The library is currently only available in C++, but the functionality is available to all programming languages that have access to the ROS messaging (and service) bus.

To get the necessary libraries, you must check out the following repositories into your catkin ROS workspace:

wstool set designator_integration --git https://github.com/code-iai/designator_integration
wstool set iai_common_msgs --git https://github.com/code-iai/iai_common_msgs
wstool set beliefstate_client --git https://github.com/fairlight1337/beliefstate_client

After doing a wstool update to make sure all library checkouts are up to date, make sure that everything compiles just fine:

catkin_make --pkg beliefstate_client

If no problems come up, you are set to connect your own applications to the logging infrastructure.

Using the client library

To clarify how to implement a logging infrastructure interface into your own ROS application, a basic ROS node template will be used. We first create this node's package by running:

catkin_create_pkg logging_node_client roscpp beliefstate_client

The package's node will be linked against the ROS C++ framework (roscpp) and the Beliefstate Client library (beliefstate_client).

This package's main code consists of a simple skeleton .cpp file including:

#include <cstdlib>
#include <ros/ros.h>
#include <beliefstate_client/BeliefstateClient.h>
#include <beliefstate_client/Context.h>
using namespace beliefstate_client;
int main(int argc, char** argv) {
    ros::init(argc, argv, "logging_client");
    // Begin main code here.
    // ...
    // Main code ends here.
    return EXIT_SUCCESS;

We now add an instance of the Beliefstate Client class that takes advantage of the already present ROS connection:

    // Begin main code here.
    BeliefstateClient* bscl = new BeliefstateClient("logging_client");
    // Begin actual logging here.
    // ...
    // Actual logging ends here.
    delete bscl;
    // Main code ends here.

To log semantic events, we create some sample nested contexts:

    // Begin actual logging here.
    // Open a top-level context (level 0)
    Context* ctxMain = new Context(bscl, "MainTimeline");
    // Open a sub-context (level 1)
    Context* ctxCupOnTable = ctxMain->startContext("ObjectsInContact");
    // Open another sub-context (level 1)
    Context* ctxCupInRoom = ctxMain->startContext("ObjectsInRoom");
    // Close the first sub-context
    // Close the second sub-context
    // End the top-level context
    // Actual logging ends here.

The logging result of this program is a nested tree with two branches from the top-level node. The logged circumstances here could be that a cup that formerly sat on a table in a room was first removed from the table (ending the ObjectsInContact context) and then transporting it out of the room (ending the ObjectsInRoom context).

Contexts can be hierarchically nested, or can be concatenated concurrently. A good practice is to always have one main context from which all other contexts derive. Don't create multiple contexts using new Context(bscl, …);, as it will definitely mess up your results. Always use ctxMain→startContext(…); to create subsequent events and contexts.

To export the logged tree now, we add another call from inside the library:

    // ...
    // Export files
    // Actual logging ends here.

The Beliefstate system (if properly configured with exporter plugins) now exports a .owl-file, including well-structured OWL data describing the tree semantics, and a .dot-file, in graphviz format for PDF generation.

A resulting PDF could then look like this:

Non-Default Contexts

To override default-parameters, the function calls startContext and end allow optional parameters for changing

  • the start and end timestamps for the context timepoints, and
  • the result success flag for ending contexts

The signature of the startContext call looks like this:

Context* Context::startContext(context-name, optional start-timestamp)
So the time point noted in the resulting log-tree can be annotated with a custom timepoint (for non-realtime logging uses). If this parameter is omitted, the current Unix time on the system running the Beliefstate logging system is used.

The same accounts for the end call:

void Context::end(optional success-flag, optional end-timestamp)
The success flag is by default set to true. For the endTime timestamp, the same rules apply as for the startTime timestamp.

Changing the OWL Class and Individual Name in the Resulting OWL File

To change the rdf:type value and the related node individual name into a class that fits your purpose more, you can use the extended signature of startContext:

Context* Context::startContext(context-name, optional class-namespace, optional class-name, optional start-timestamp)

So in order to produce OWL output like this:

<owl:namedIndividual rdf:about="&log;CustomClass_RhBAibh4">
    <rdf:type rdf:resource="&knowrob;CustomClass"/>
    <knowrob:taskContext rdf:datatype="&xsd;string">Task-Ctx-1</knowrob:taskContext>
    <knowrob:startTime rdf:resource="&log;timepoint_0"/>
    <knowrob:endTime rdf:resource="&log;timepoint_10"/>

In you C++-program, you would just call:

Context* ctxContext = ctxMain->startContext("Task-Ctx-1", "&knowrob;", "CustomClass", 0);
ctxContext->end(true, 10)

Of course, for real-time purposes (or if you don't care about the timestamps), you can leave out the numerical last parameters of each call.

Issueing Discrete Events

When your application requires the issuance of discrete, momentarily events you can use the convenience function discreteEvent:

void Context::discreteEvent(event-name, optional class-namespace, optional class-name, optional success-flag, optional timestamp)

This implicitly starts a context with the given information, and directly closes it again. If you decide to specify a timestamp, the start and end time of the event will be the same as this value. Other than that, this call produces the same output as manually starting and ending a context.

Annotating Custom Parameters

To add custom information to individual task contexts, parameters can be manually annotated to each context. Let's assume, the information

X = 5
Room = Kitchen
should be added to the current context. To achieve this, in your program, call these lines while you are in the appropriate context:
ctxContext->annotateParameter("X", 5);
ctxContext->annotateParameter("Room", "Kitchen");
The resulting .owl-file will now include information about the manually annotated parameters:
<owl:namedIndividual rdf:about="&log;SomeClass_1zpsieIi">
    <rdf:type rdf:resource="&knowrob;SomeClass"/>
    <knowrob:taskContext rdf:datatype="&xsd;string">some-task-context</knowrob:taskContext>
    <knowrob:taskSuccess rdf:datatype="&xsd;boolean">true</knowrob:taskSuccess>
    <knowrob:startTime rdf:resource="&log;timepoint_5120"/>
    <knowrob:endTime rdf:resource="&log;timepoint_5200"/>
    <knowrob:annotatedParameterType rdf:datatype="&xsd;string">X</knowrob:annotatedParameterType>
    <knowrob:parameterAnnotation rdf:resource="&log;object_4RfZlRiev7LPJW"/>
    <knowrob:annotatedParameterType rdf:datatype="&xsd;string">Room</knowrob:annotatedParameterType>
    <knowrob:parameterAnnotation rdf:resource="&log;object_4RfZlRiev7LPJW"/>
The lines
now explicitly denote the given parameter values, while
<knowrob:annotatedParameterType rdf:datatype="&xsd;string">X</knowrob:annotatedParameterType>
<knowrob:annotatedParameterType rdf:datatype="&xsd;string">Room</knowrob:annotatedParameterType>
Describe the parameter names that were manually annotated. So looking for all properties of type knowrob:annotatedParameterType gives a list of all (for this task) relevant parameter annotations. Finally,
<knowrob:parameterAnnotation rdf:resource="&log;object_4RfZlRiev7LPJW"/>
is a reference to the designator published via the ROS topic /logged_designators in case this mechanism is used.

Adding Object References

In some situations, it is useful to manually annotate objects that are part of the current scene, and maybe play a significant role for the current task. Object references to the currently active task can be annotated in a convenient way, producing output like this:

<owl:namedIndividual rdf:about="&log;ObjectsInContact_unmkCtDP">
    <rdf:type rdf:resource="&sim;ObjectsInContact"/>
    <knowrob:taskContext rdf:datatype="&xsd;string">ObjectsInContact</knowrob:taskContext>
    <knowrob:taskSuccess rdf:datatype="&xsd;boolean">true</knowrob:taskSuccess>
    <knowrob:startTime rdf:resource="&log;timepoint_1410254697"/>
    <knowrob:endTime rdf:resource="&log;timepoint_1410254697"/>
    <knowrob:objectInContact rdf:resource="&sim;Cup_object_s8hJIQ3L"/>
    <knowrob:objectInContact rdf:resource="&sim;Table_object_jfFU038A"/>
This is achieved by using the following, simple code:
Context* ctxInCtct = ctxMain->startContext("ObjectsInContact", "&sim;", "ObjectsInContact");
Object* objCup = new Object("&sim;", "Cup");
ctxInCtct->addObject(objCup, "knowrob:objectInContact");
Object* objTable = new Object("&sim;", "Table");
ctxInCtct->addObject(objTable, "knowrob:objectInContact");
delete objCup;
delete objTable;
Additionally, two object individuals will be created, reflecting the object's respective type:
<owl:namedIndividual rdf:about="&sim;Cup_object_s8hJIQ3L">
    <knowrob:designator rdf:resource="&log;object_syiJzh35HBm4Tf"/>
    <rdf:type rdf:resource="&sim;Cup"/>
<owl:namedIndividual rdf:about="&sim;Table_object_jfFU038A">
    <knowrob:designator rdf:resource="&log;object_6ti909YZVsLifo"/>
    <rdf:type rdf:resource="&sim;Table"/>
The Object class offers two constructor parameters:
new Object(string object-class-namespace, string object-class)
If they are left out, the object will be of type &knowrob;HumanScaleObject. When adding the object to the current context, the call
void Context::addObject(object, optional string property)
produces the actual addition of the object reference to the current context (and creation of the object individual). The optional property parameter specifies the property tag in the OWL context individual. If left out, it defaults to knowrob:objectActedOn. You have to specify it along its namespace.

Registering custom OWL namespaces

When using custom namespaces in the entity class definitions, these namespaces need to be registered properly in the OWL file when exporting it. In order to do that, add this line to your program before exporting your logged data:

bscl->registerOWLNamespace("sim", "http://some-namespace.org/#");
with both parameters representing your use-case, of course.

Custom namespaces can therefore be registered by calling void BeliefstateClient::registerOWLNamespace(std::string strShortcut, std::string strIRI); where the shortcut is the short version of the namespace, i.e. sim, and the IRI is the actual URL you want to have associated.

Sample Program

A complete sample program depicting the beliefstate_client usage can be found here.