How to integrate a Python component#
You have developed a new navground component in Python (i.e., one of navground.core.Behavior
, navground.core.BehaviorModulation
, navground.core.Kinematics
, navground.sim.Scenario
, navground.sim.StateEstimation
, or navground.sim.Task
).
from navground import core
class MyComponent(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 |
|
---|---|---|
instantiate by name |
|
|
access parameters by name |
|
|
customize the YAML conversion |
|
|
share the extension |
|
Register the type#
Register your component to the base class register by adding name="MyName"
to the class definition, to be able to instantiate it by name
class MyComponent(Component, name="MyName"):
...
Once a class has been registered, it can be instantiated using a generic factory method Component.make_type
by providing its name:
c = Component.make_type("MyName")
# c.type returns "MyName"
Moreover, the type will also appear in the YAML representation and
core.dump(c)
as field “type”
type: MyName
...
and it will be possible to load the component from yaml
c = core.load_<component>(c)
Define Properties#
Add a Python property for any parameter you want to expose.
Below property
, add the navground.core.register()
decorator
with the default value and an optional description
class MyComponent(Component, name="MyName"):
@property
@core.register(True, "my description")
def my_param(self) -> bool:
return True
In the trivial example above, the property returns a constant value and has no setter. In general, properties will be get/set attributes of the class, like
class MyComponent(Component):
def __init__(self):
self._value = True
@property
@core.register(True, "my description")
def value(self) -> bool:
return self._value
@value.setter
def value(self, value: bool) -> None:
self._value = value
Once properties are registered, the class gains generic accessors get
and set
that uses names to identify properties.
c = MyComponent()
value = c.get("value")
c.set("value", not value)
Moreover, properties will also appear in the YAML representation
core.dump_<component>(c)
as additional fields
...
value: false
...
Note
When working with components defined in Python, navground properties are not very useful, as you could directly inspect the Python class and use its accessors, or even generic accessors like getattr()
and setattr()
. Instead, when the component is loaded from YAML or C++, properties offer a generic way to access to instance attributes.
Property Schema#
Pass an optional argument of type typing.Callable[[dict[str, typing.Any]], None]
when registering a property to add validation constrains. For example, to mark an integer property as strictly positive, add
@core.register(10, "my description", core.schema.strict_positive)
def value(self) -> int: ...
YAML#
In case the conversion from/to YAML provided by navground is not sufficient, specialize the methods encode
and decode
. There is no need to call the base implementation as it is empty.
class MyComponent(Component, name="MyName"):
# ... properties
def encode(self) -> str:
...
def decode(self, yaml: str) -> None:
...
Through these methods you can read more complex parameters from the YAML than navground.core.PropertyField
. For example, you can configure a value of type dict[str, 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, for example, like
class MyComponent(Component, name="MyName"):
def encode(node: dict[str, typing.Any]) -> None:
node['my_complex_param'] = {
'a': self.my_int_a,
'b': self.my_bool_b
}
def decode(node: dict[str, typing.Any]) -> None:
if 'my_complex_param' in node:
param = node['my_complex_param']
if 'a' in param:
self.my_int_a = int(param['a'])
if 'b' in param:
self.my_bool_a = bool(param['b'])
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, as a function of type typing.Callable[[dict[str, typing.Any]], None]
that modify the default schema of the class.
In the example above, we add the appropriate schema
class MyComponent(Component, name="MyName"):
@core.schema.register
def schema(node: dict[str, typing.Any]) -> None:
my_complex_param = {
'type': 'object',
'properties': {
'a': {
'type': 'integer'
},
'b': {
'type': 'boolean'
}
},
'additionalProperties': False
}
if not node["properties"]:
node["properties"] = {}
node["properties"]["my_complex_param"] = my_complex_param
Class skelethon#
Using the appropriate macro, the class skeleton simplifies to
class MyComponent(Component, name="MyName"):
@property
@core.register(default_value, "description") # add optional property schema
def name(self) -> Type:
return ...
@name.setter
def name(self, value: Type) -> None:
...
# def encode(self) -> str: ...
# def decode(self, yaml: str) -> None: ...
# @core.schemaregister
# def schema(node: dict[str, typing.Any]) -> None: ...
Install as a plugin#
This is a install-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.
Define an entry point for each component you want to export in the setup.cfg
or setup.py
file of the package.
For example, to install behavior MyBehavior
, add
[options.entry_points]
navground_behaviors =
my_behavior = <my_packages>.<my_module>:MyBehavior
to your setup.cfg
. The actual key “my_behavior” is currently ignored.
Note
Following end-points are available to install components of the respective type: navground_behaviors
, navground_kinematics
, navground_modulations
, navground_tasks
,
navground_state_estimations
, and navground_scenarios
.
Once installed, the behavior will be automatically discovered when calling navground.core.load_plugins()
.
>>> from navground import core
>>> core.load_plugins()
>>> print(core.Behavior.types)
[..., 'MyBehavior']
>>> behavior = core.Behavior.make_type("MyBehavior")
<my_packages>.<my_module>.MyBehavior object ...>
Note
Calling navground.core.load_plugins()
, C++ plugins are imported too and can then be instantiated from Python.
Complete example#
See Python example for an example where we implement and register a new (dummy) navigation behavior in Python.