How to integrate a C++ component#

You have developed a new navground component in C++ (i.e., one of navground::core::Behavior, navground::core::BehaviorModulation, navground::core::Kinematics, navground::sim::Scenario, navground::sim::StateEstimation, or navground::sim::Task).

#include "navground/core/<component>.h"

namespace core = navground::core;

struct MyComponent : public Component {
  // ...
};

You can already use as it is but if you register it, you will integrate it better in the navground API and CLI, and you will simplify its usage by others.

For example, you have developed a new navigation behavior called “MyBehavior” (no trivial task, congrats!) and you want to run experiments to compare its performance with other navigation algorithms (which is actually one of the main design goals of navground).

After

  • registering the type

  • exposing as a property any parameter to be configure from YAML

  • specialize the YAML encoding/decoding if the basics functionality is not sufficient

  • installing it as a plugin

you can use the behavior in an experiment, e.g., configured for like this

scenario:
   type: Cross
   groups:
     - number: 20
       behavior:
         type: MyBehavior
         my_param: 1
       kinematics: ...
       state_estimation: ...

Each step adds functionality, summarized in the table below, that is not needed to compile or use the component but that provides a more complete integration with navground.

to

example

Register the type

instantiate by name

auto comp = Component::make_type("MyName");

Define Properties

access parameters by name

comp.set("value", 1);

YAML

customize the YAML conversion

auto comp = YAML::load<Component>(node);

Install as a plugin

share the extension

load_plugins();

Register the type#

Register your component to the base class register using navground::core::HasRegister::register_type() to be able to instantiate it by name.

// declaration

struct MyComponent : public Component {
  // ...
  static const std::string type;
};

/// definition

const std::string type = register_type<MyComponent>("MyName");

The name of the static member (type, in this case) used to hold register_type can be anything.

Note

You can also register the class inside the class declaration:

struct MyComponent : public Component {
  // ...
  inline static const std::string type = register_type<MyComponent>("MyName");
};

Once a class has been registered, it can be instantiated using a generic factory method navground::core::HasRegister::make_type() by providing its name

std::shared_ptr<Component> c = Component::make_type("MyName");
const auto type = c->get_type();
// type is equal to "MyName"
// equivalently using types
// const auto type = Component::get_type<MyComponent>();

and loaded from a YAML such

type: MyName

using

YAML::Node node(...);
std::shared_ptr<Component> c = YAML::load<Component>(node);
const auto type = c->get_type();
// type is equal to "MyName"

Define Properties#

When registering the class, expose any configuration parameter as a property, providing a getter, an optional setter, a default value, and an optional description.

Getters can be

  • functions/lambdas of type std::function<T(const MyComponent *)>

  • methods of type T MyComponent::*()

  • members of type T MyComponent::*

while setters can be

  • nullptr to define readonly properties

  • functions/lambdas of type std::function<void (const MyComponent *, const & T)> or std::function<void (const MyComponent *, T)>

  • methods of type void MyComponent::*(const & T) or void MyComponent::*(T)

As (optional) second argument of navground::core::HasRegister::register_type(), pass a map of type navground::core::Properties associating names to properties, instantiated using a combination of navground::core::Property::make(), navground::core::Property::make_readwrite(), and navground::core::Property::make_readonly():

/// definition

const std::string type = register_type<MyComponent>(
    "MyName", {{"my_param", core::Property::make(...)}});

In practice, if the class defines accessors like

struct MyComponent : public Component {
  // ...
  Component() : _value(true) {}
  bool get_value() const { return _value; }
  void set_value(bool value) { _value = value; }
private:
  bool _value;
};

you can define properties like

/// definition

const std::string type = register_type<MyComponent>(
    "MyName", {{"my_param", core::Property::make(&MyComponent::get_value,
                                                 &MyComponent::set_value, true,
                                                 "my description")}});

Warning

Not all compilers support defining the properties inline such as in

struct MyComponent : public Component {
  // ...
  bool get_value() const { return _value; }
  void set_value(bool value) { _value = value; }
  inline static const std::string type = register_type<MyComponent>(
      "MyName", {{"my_param", core::Property::make(&MyComponent::get_value,
                                                   &MyComponent::set_value,
                                                   true, "my description")}});
private:
  bool _value;
};

because the initialization order in not guaranteed. To be safe, instantiate your properties outside of the class declaration.

Once the class has been registered, it gains generic accessors navground::core::HasProperties::get(), navground::core::HasProperties::set(), and navground::core::HasProperties::set_value(), that uses names to identify properties.

MyComponent c;
bool value = c.get("value").get<bool>();
c.set("value", !value);

Note

When we have access to the class declaration, being able to get/set properties by name is not very useful, as we could directly use the class own accessors.

Instead, this become very useful when we don’t have access to the class definition, like when loading from a shared library, from YAML or when using it in Python. In this case, properties allows a generic way to access to instance attributes (from C++, Python, and YAML).

We can also query for all properties

MyComponent c;
core::Properties & properties = c.get_properties();
// equivalently using types
// core::Properties & properties = Component::get_properties<MyComponent>();

Moreover, properties will also appear in the YAML representation

YAML::dump<Component>(&c);

as additional fields (one for each property)

type: MyName
my_param: false

and, similarly, will be loaded from YAML.

Property Schema#

Pass an optional argument of type void(YAML::Node &) to methods like navground::core::Property::make() to add validation constrains to the property. For example, to mark an integer property as strictly positive, add

core::Property::make(&MyComponent::get_value, &MyComponent::set_value,
                     10, "my description", &YAML::schema::strict_positive)}});

YAML#

In case the conversion from/to YAML provided by navground is not sufficient, specialize the methods navground::core::HasRegister::encode() and navground::core::HasRegister::decode(). There is no need to call the base implementation as it is empty.

struct MyComponent : public Component {
  // ...

  void encode(YAML::Node &node) const override {
    // put additional information int the YAML node;
  }

  void decode(const YAML::Node &node) override {
    // extract additional information from the YAML node;
  }
};

Through these methods you can read more complex parameters from the YAML than navground::core::Property::Field. For example, you can configure a value of type std::map<std::string, int> from a YAML such as

my_complex_param:
   a: 1
   b: false

if you implement the custom logic in the decoder and the encoder, like

void encode(YAML::Node &node) const override {
  node["my_complex_param"]["a"] = my_int_a;
  node["my_complex_param"]["b"] = my_bool_b;
}

void decode(const YAML::Node &node) override {
  if (node["my_complex_param"]) {
    auto param = node["my_complex_param"];
    if (param["a"]) {
      my_int_a = param["a"]..as<int>();
    }
    if (param["a"]) {
      my_bool_b = param["b"]..as<bool>();
    }
  }
}

Warning

Properties are treated as random variables in a navground scenario. For example:

scenario:
  groups:
    - number: 10
      behavior:
        type: MyBehavior
        my_param:
          sampler: uniform
          from: 1
          to: 2

defines a group of agents whose behavior “my_param” parameter has a random value. This does not extend to parameters read using custom YAML decoders. In case this is required, users will need to implement this logic in a scenario.

Therefore, we suggest to restrict parameters exposed to YAML to properties, so to get the treatment as random variable for free.

Class Schema#

If your class defines a custom YAML representation, it should also register the related JSON-schema, by passing a function of type void(YAML::Node&) as last argument to navground::core::HasRegister::register_type().

In the example above, we add the appropriate schema

static void schema(YAML::Node &node) {
  Node my_complex_param;
  my_complex_param["type"] = "object";
  my_complex_param["properties"]["a"]["type"] = "integer";
  my_complex_param["properties"]["b"]["type"] = "boolean";
  my_complex_param["additionalProperties"] = false;
  node["properties"]["my_complex_param"] = my_complex_param;
}

const std::string type = register_type<MyComponent>(
    "MyName", {{"my_param", core::Property::make(...)}}, &schema);

Class skelethon#

Using the appropriate macro, the class skeleton simplifies to

// declaration

struct MyComponent : public Component {
  // ...
  static const std::string type;
  // void encode(YAML::Node &node) const override;
  // void decode(const YAML::Node &node) override;
  // static void schema(YAML::Node &node);
};

// definition

const std::string type = register_type<MyComponent>(
    "MyName",
    {
        {name, core::Property::make(&MyComponent::getter, &MyComponent::setter,
                                    default_value, "description")},
    }
    // , &MyComponent::schema
);

Install as a plugin#

This is a build-time step. Wraps one or more components in a shared library and install it as a plugin to integrate it in the navground CLI and API.

In the project CMakeLists.txt, pass the list of shared libraries you want to install as plugins to the cmake function register_navground_plugins.

For example, to build and install component MyComponent implemented in file my_component.cpp, add

add_library(my_component SHARED my_component.cpp)
target_link_libraries(my_component navground_core::navground_core ...)
register_navground_plugins(
   TARGETS my_component
   DESTINATION $<IF:$<BOOL:${WIN32}>,bin,lib>
)

install(
  TARGETS my_component
  EXPORT my_componentTargets
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  INCLUDES DESTINATION include
)

Note

We use DESTINATION $<IF:$<BOOL:${WIN32}>,bin,lib> instead of DESTINATION lib because on Windows shared libraries are installed to bin.

Once installed, the components will be automatically discovered when calling navground::core::load_plugins().

#include "navground/core/plugins.h"
#include "navground/core/<component>.h"

namespace core = navground::core;

int main() {
   // will load the shared library with MyComponent
   // which in turn will call register_type<MyComponent>("MyName");
   // and make the component discoverable in this process.
   core::load_plugins();
   auto c = Component::make_type("MyName");
   // do something with this component.
}

Note

This will also make the component discoverable and available in the navground CLI, provided that the option --no-plugins is not set.

If you share it with them, the library can be installed by other users too, which just need to copy add edit add the library location to the navground plugin index.

Complete example#

See C++ example for an example where we implement and register a new (dummy) navigation behavior in C++.