Configuration Conventions
The pntOS-Python API does not specify a configuration convention, and so choosing a config convention is left to the implementation. The convention described in this document is merely a convenience feature for storing and grabbing config from the registry in the Cobra environment - plugins are free to interact with the registry directly and handle configuration apart from this convention.
In summary, Cobra’s config comes in the form of python
dataclasses
which inherit from the BaseConfig type.
An App instantiates these config dataclasses and passes them to the Registry
Plugin’s constructor which then loads these configs into
each new Registry via
config_to_registry(). When a plugin
wishes to retrieve it’s config, it simply calls
config_from_registry().
Let’s dive into each of these concepts in greater detail.
Dataclasses
A dataclass fundamentally is a data container of fields. For example, a simple dataclass can be defined and used in the following manner:
from dataclasses import dataclass
@dataclass
class MyDataclass:
value1: int
value2: str
value3: float = 4.7 # Default value
data = MyDataclass(
value1=42,
value2="hello world"
value3=3.14 # This line is optional since there is a default value.
)
print(data.value1) # 42
print(data.value2) # "hello world"
print(data.value3) # 3.14
Dataclasses allow for sub-typing. A sub-type dataclass contains all fields on the super-type plus any additional fields defined in the sub-type. For example:
@dataclass
class Foo:
value1: int
value2: str
@dataclass
class Bar(Foo):
value3: float
data = Bar(
value1=42, # From super-type Foo
value2="hello world", # From super-type Foo
value3=3.14
)
print(data.value1) # 42
Note
For clarity in Cobra, inherited fields are redeclared on the subclass:
@dataclass
class Bar(Foo):
value1: int
value2: str
value3: float
You can specify any Python type on a generic dataclass, including other dataclasses:
@dataclass
class Baz:
value1: int
value2: str
@dataclass
class Qaz:
nested: Baz
label: str
data = Qaz(
nested=Baz(
value1=42
value2="hello world"
)
label="important"
)
print(data.nested.value2) # "hello world"
For more information on dataclasses, please see the Python dataclass documentation.
Cobra Config Dataclasses
Cobra’s config dataclasses must adhere to the following rules:
1. Inherit from BaseConfig or sub-type
As described in Group-Key-Value Implementation, the
registry contains of a set of groups, with each group containing a set of key-value
pairs. The fields of a dataclass are essentially a set of key-value pairs to store into
a particular group. Thus, in order to pack dataclasses into the registry,
config_to_registry() needs to know the
group in which to store these values. To accomplish this, all config dataclasses must
inherit from BaseConfig, which contains a
single group field.
@dataclass
class BaseConfig(ABC):
"""
A basic config that all other configs should inherit from.
"""
group: str
"""
A user-defined config group name, corresponding to a group in the registry.
When a config object is stored in the registry, this field determines
which group in the registry the object's fields will be stored in.
"""
To implement a custom Cobra config type, simply sub-type
BaseConfig or another sub-type, then add
additional fields:
from pntos.cobra.config import BaseConfig
@dataclass
class SensorConfig(BaseConfig):
group: str # Inherited from BaseConfig
label: str
frequency: float
@dataclass
class AltitudeSensorConfig(SensorConfig):
group: str # Inherited from BaseConfig
label: str # Inherited from SensorConfig
frequency: float # Inherited from SensorConfig
initial_height: float
Note
When nesting config, it is not necessary that the nested config group be equal to the
outer config’s group.
@dataclass
class FooConfig(BaseConfig):
group: str # Inherited from BaseConfig
val: int
@dataclass
class BarConfig(BaseConfig):
group: str # Inherited from BaseConfig
foo: FooConfig
config = BarConfig(
group="config/bar",
foo=FooConfig(
group="config/foo", # Nested config stored in a different group
val=42,
)
)
2. Only use supported types on the config dataclass
Since all Cobra config dataclasses need to be stored in the registry, all fields must
be convertible to supported registry types.
config_to_registry() and
config_from_registry() currently
support the following types:
Type Category |
Type Hint |
Constraints |
Example |
|---|---|---|---|
Primitives |
|
N/A |
|
|
Accepts |
|
|
|
N/A |
|
|
|
N/A |
|
|
Enums |
|
Any subclass of |
|
EstimateWithCovariance |
N/A |
|
|
Nested Configs |
|
Any subclass of |
|
1-D List |
|
|
|
1-D Tuple |
|
|
|
2-D List |
|
|
|
2-D Tuple |
|
|
|
NumPy Array |
|
Only |
|
Config Series |
|
Any subclass of |
|
|
Any subclass of |
|
|
Optional |
|
Any supported type |
|
Note
Storage conversions: Numerical series (lists/tuples of numbers) are stored as numpy
arrays in the registry. String series are stored as lists. When extracting via
config_from_registry, they are converted back to the type specified on the dataclass.
Example: All supported types used in a nested “VehicleConfig”
Below is an example showing all supported types in a well-structured config:
from dataclasses import dataclass
from enum import Enum
from numpy.typing import NDArray
import numpy as np
from pntos.api import EstimateWithCovariance, EstimateWithCovarianceType
from pntos.cobra.config import BaseConfig
# Enum type
class SensorMode(Enum):
ACTIVE = 1
PASSIVE = 2
CALIBRATION = 3
# Nested config demonstrating primitives
@dataclass
class CameraConfig(BaseConfig):
group: str
resolution_width: int # Primitive: int
resolution_height: int
frame_rate: float # Primitive: float (accepts int)
label: str # Primitive: str
auto_exposure: bool # Primitive: bool
mode: SensorMode # Enum
# Nested config with series types
@dataclass
class ImuConfig(BaseConfig):
group: str
bias: tuple[float, ...] # 1-D Tuple
scale_factors: list[float] # 1-D List
rotation_matrix: tuple[tuple[float, ...], ...] # 2-D Tuple
calibration_data: NDArray[np.float64] # NumPy Array
# Top-level config with all type categories
@dataclass
class VehicleConfig(BaseConfig):
group: str
# Primitives
vehicle_id: int
name: str
max_speed: float
is_operational: bool
# Enum
primary_mode: SensorMode
# EstimateWithCovariance
initial_state: EstimateWithCovariance
# Nested config (single)
primary_camera: CameraConfig
# Nested config (optional)
backup_camera: CameraConfig | None
# Config series
imus: list[ImuConfig]
additional_sensors: tuple[CameraConfig, ...]
# 1-D series
waypoint_ids: list[int]
position: tuple[float, ...]
labels: list[str]
# 2-D series (matrix)
transformation_matrix: list[list[float]]
covariance: tuple[tuple[float, ...], ...]
# NumPy arrays
trajectory: NDArray[np.float64]
timestamps: NDArray[np.int64]
# Optional types
description: str | None
backup_frequency: float | None
# Instantiation example
vehicle_config = VehicleConfig(
group="vehicle_params",
vehicle_id=42,
name="Rover-1",
max_speed=15.5,
is_operational=True,
primary_mode=SensorMode.ACTIVE,
initial_state=EstimateWithCovariance(
type=EstimateWithCovarianceType.POSE,
estimate=np.array([0.0, 0.0, 0.0]),
covariance=np.eye(3)
),
primary_camera=CameraConfig(
group="camera_primary",
resolution_width=1920,
resolution_height=1080,
frame_rate=30.0,
label="front_camera",
auto_exposure=True,
mode=SensorMode.ACTIVE
),
backup_camera=None,
imus=[
ImuConfig(
group="imu_1",
bias=(0.01, 0.02, 0.01),
scale_factors=[1.0, 1.0, 1.0],
rotation_matrix=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
calibration_data=np.array([[1.0, 2.0], [3.0, 4.0]])
)
],
additional_sensors=(),
waypoint_ids=[1, 2, 3, 4, 5],
position=(10.5, 20.3, 5.0),
labels=["primary", "autonomous"],
transformation_matrix=[[1.0, 0.0], [0.0, 1.0]],
covariance=((0.1, 0.0), (0.0, 0.1)),
trajectory=np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]),
timestamps=np.array([0, 100, 200], dtype=np.int64),
description=None,
backup_frequency=None
)
3. Contain no field names that start with an underscore
For example:
@dataclass
class BarConfig(BaseConfig):
value1: int # Valid
_value2: int # Invalid!
config_to_registry() stores auxiliary
key-value pairs prefixed with an underscore to enable proper extraction of
dataclasses, particularly nested config. To avoid conflict with these keys,
dataclass fields should not be prefixed with an underscore.
Accessing Config
To see examples of config definitions, see the documentation for the
pntos.cobra.config module. This module contains all Cobra config
object definitions in addition to all config utility functions such as
config_from_registry().
To access config from within a plugin, first ensure that the config object is imported,
instantiated, and passed to the registry in the current App (For more
information on Apps, see the first App walkthrough).
Then, once your plugin has access to a
Mediator (after
init_plugin()), you can retrieve config
using config_from_registry():
from pntos.cobra.config import BaseConfig, config_from_registry
# In the App:
config: list[BaseConfig] = [ # list of configs that will be passed into the registry
...
AltitudeSensorConfig(
group="config/sensor",
label="barometer",
frequency=1.0,
initial_height=738.2,
)
]
# In the plugin after init_plugin()
my_config = config_from_registry(AltitudeSensorConfig, mediator, "config/sensor")
print(type(my_config)) # AltitudeSensorConfig
print(my_config.initial_height) # 738.2