Skip to content

Python Custom KnowledgeRecord Types

James Edmondson edited this page Apr 3, 2020 · 5 revisions

FEATURE DEPRECATED

As for 3.3.0, this feature is deprecated. We specifically removed this due to complications with Boost long term support and the requirement of boost filesystem, which was causing issues with moving to UE4 as a simulation environment for GAMS. If you are interested in this feature, let us know, and we will try to prioritize its reinclusion. However, there is no planned resurrection for this tool at the moment.

This feature lives in v3.2.3, at the latest.


Madara's KnowledgeBase and KnowledgeRecord support several useful types natively: integers, doubles, vectors of both, strings, and blobs. Where appropriate, it is best to use those types. They will be accessible by any KnowledgeBase, and a variety of useful functions will work with them, including overloaded math operators and indexing operations. For types which cannot easily be represented by those native types, KnowledgeRecord, and thus KnowledgeBase, can store an Any type, provided by Madara, which can in turn store custom types directly.

Table of Contents

Supported Types

There are two main kinds of objects supported by Any: native C++ types, and Cap'n Proto messages. Supported native C++ types must be default constructible, copy constructible, and support serialization via either the Cereal Library, or via Madara's translation system to a compatible Cap'n Proto message struct. Note that this translation system is an alternative to using Cap'n Proto generated types and schemas directly.

Types must be registered with Madara using a string "tag" which identifies them. This tag should be used on all systems and platforms to refer to the same kind of data, but each might register different types, as long as they have the same serialized format.

To be used from Python, a native C++ custom type must be registered within C++ code, or with the predefined Any.register* functions provided in the Python port for various commonly used types.

Cap'n Proto message schemas can be registered within Python code, and thus do not rely on C++ registration code. Madara uses the pycapnp Python port of Cap'n Proto. Be sure to familiarize yourself with its documentatation, and follow its installation instructions.

Loading Native C++ External Types

Native C++ custom types may only be defined in C++, but can be made available to Python via a shared library. To load a shared library, use the ctypes.cdll.LoadLibrary() or ctypes.windll.LoadLibrary calls. To ensure successful linking, ensure madara.so is loaded first. For example:

from madara.knowledge import *
import ctypes;
ctypes.cdll.LoadLibrary("libdatatypes.so"); # replace with your library's name

Note that if Python is used in a process that uses native C++ types backed by Cap'n Proto serialization, Python code should use the native C++ type support, not attempt to use Cap'n Proto directly.

Registering Cap'n Proto Schemas

To store and load Cap'n Proto messages to and from Any objects in Python, they must be registered with the Any.register_class() static method. It takes two arguments. The first is is a string "tag" which will be used to portably identify the type across all systems using it. The second is the Cap'n Proto generated type, which can be obtained by calling capnp.load() on a schema file. For example:

using os
using capnp

geo = capnp.load(os.environ["MADARA_ROOT"] + "/tests/capnfiles/Point.capn")
Any.register_class("Point", geo.Point)
Any.register_class("Pose", geo.Pose)

Note that you must register the python class even if the tag is registered in C++ or Java already. You will not be able to use any Cap'n Proto message in Python with Madara that hasn't been registered in Python.

The Any Class

The Python port provides a version of the C++ implementation of Any.

To create an Any holding a Cap'n Proto message, construct it with a pycapnp builder, which you can obtain using init_message(), and modify with accessors:

msg = geo.Point.new_message();
msg.x = 2
msg.y = 4
msg.z = 6

any = Any(msg)

To create an Any holding a native C++ type, construct it with the name of a registered class:

/**** C++ code ****/
struct Point {
  double x, y, z;
};
// Some details omitted, see C++ version documentation for more.

// Register the above
Any::register_type<Point>("point");
/**** Python code ****/
// Register C++ type std::map<std::string, std::string> with tag "smap"
Any.register_string_string_map("smap")

point = new Any("point")
map = new Any("smap")

These Any objects now hold ownership of a default constructed C++ object.

All Any objects will be garbage collected automatically by Python. No explicit memory management is required (in contrast to the Java port).

Accessing Cap'n Proto message Any Objects

The Any object provides a reader() method which will return a pycapnp reader object for the held object. If the held object is not a registered Cap'n Proto message, an exception will be thrown. For example:

msg = geo.Point.new_message();
msg.x = 2
msg.y = 4
msg.z = 6

any = Any(msg)

reader = any.reader()

assert (reader.y == 4)

Manipulating Native C++ Any Objects

To access fields and contained elements of the held object, Any implements the __getattr__ special function, allowing access like any other attribute:

xref = point.x;

In the above example, xref is an instance of the AnyRef class. The AnyRef acts much like Any itself, and has most of the same methods, but doesn't own the object it points to. It also does not extend the lifetime of that object. It acts like a C pointer. If the Any it was constructed from is destroyed, it will be left dangling, so be careful keeping these objects long-term.

You can modify the data held by the C++ object pointed to by an AnyRef using the assign() method. You can also set fields like attributes:

xref.assign(2);
point.y = 3.5;

An AnyRef (or Any itself) can be converted to various types, the ones supported natively by KnowledgeRecord. Typically, you'll use to_integer(), to_double(), or to_string():

double x = xref.to_double(); // == 2
double y = point.y.to_double(); // == 3.5

These are also accessible with the int(), float(), and str() standard python functions.

You can use normal indexing to access elements of the held type, if it supports indexing by integer or string:

fooRef = map["foo"]; // Creates a new entry in the map, fooRef holds an AnyRef
map["bar"] = "world";
fooRef.assign("hello");
foo = str(fooRef); // == "Hello"
bar = map["bar"].to_string(); // == "World"

You can also call size() to call that method on the held object, if it supports it, and list_fields() to get a list of field names supported by ref().

Any with KnowledgeRecord and KnowledgeBase

You can store Any within KnowledgeRecord, and in turn into KnowledgeBase. To store an Any in a KnowledgeRecord, simply construct the record with the Any object:

pointRecord = KnowledgeRecord(point);

If you have a KnowledgeRecord, you can get its contents as an Any using the to_any() method:

p = pointRecord.to_any();

This will work on any KnowledgeRecord. If it contains an Any, you will get a copy of it. If it doesn't, you will get a copy of the stored data held within a new Any. To access that data, be sure to register the appropriate types using the Any.register* static methods.

You can store an Any into a KnowledgeBase using the set() method:

kb = new KnowledgeBase;
kb.set("pointKey", point);

To get an Any from the KnowledgeBase, use the get() method to retrieve a KnowledgeRecord as usual, then call to_any() on it.