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

int

N/A

count: int

float

Accepts int, automatically converted

frequency: float

str

N/A

label: str

bool

N/A

enabled: bool

Enums

Enum or IntEnum

Any subclass of Enum or IntEnum

mode: SensorMode

EstimateWithCovariance

EstimateWithCovariance

N/A

state: EstimateWithCovariance

Nested Configs

BaseConfig

Any subclass of BaseConfig

fogm_model: FogmConfig

1-D List

list[T]

T must be int, float, str, or BaseConfig

values: list[float]

1-D Tuple

tuple[T, ...]

T must be int, float, str, or BaseConfig; all elements same type

coords: tuple[float, ...]

2-D List

list[list[T]]

T must be int or float; all rows same length

matrix: list[list[float]]

2-D Tuple

tuple[tuple[T, ...], ...]

T must be int or float; all rows same length

matrix: tuple[tuple[float, ...], ...]

NumPy Array

NDArray[np.float64]

Only np.float64 or np.int64 dtype; any dimensions

data: NDArray[np.float64]

Config Series

list[BaseConfig]

Any subclass of BaseConfig

sensors: list[SensorConfig]

tuple[BaseConfig, ...]

Any subclass of BaseConfig

sensors: tuple[SensorConfig, ...]

Optional

T | None

Any supported type T

label: str | None

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