Adding a C++ Control Algorithm¶
This guide covers how to add a new path-following algorithm in C++ to kompass_cpp and expose it to Python via nanobind bindings. Once the C++ side is in place, wrap it in Python following the Python Algorithm Guide – Option B.
C++ Class Hierarchy¶
All control algorithms in kompass_cpp live under the Kompass::Control namespace:
Controller (controller.h)
├─ setCurrentState() Update robot pose
├─ setLinearControlLimits() Set velocity bounds
├─ setAngularControlLimits() Set angular bounds
├─ restrictVelocityTolimits() Clamp velocity to limits
└─ Result { Status, Velocity2D }
│
└─ Follower (follower.h)
├─ setCurrentPath() Receive and interpolate a global path
├─ isGoalReached() Check if end of path is reached
├─ determineTarget() Find the tracked point on the path
├─ getTrackedTarget() Get crosstrack/heading errors
├─ getLinearVelocityCmdX/Y() Clamped velocity getters
├─ getAngularVelocityCmd()
└─ FollowerParameters Configuration (inherits Parameters)
│
└─ YourAlgorithm (your_algorithm.h)
├─ computeVelocityCommand(timeStep)
├─ execute(currentPosition, deltaTime)
└─ YourAlgorithmParameters
The key base classes:
Controller– manages robot state, control type (Ackermann / Differential / Omni), and velocity limits. ProvidesrestrictVelocityTolimits()for clamping computed velocities.Follower(extendsController) – adds path management: interpolation, segmentation, closest-point tracking, and goal checking. Your algorithm inherits from this.
Step-by-Step¶
1. Create the Header¶
Create kompass_cpp/include/controllers/your_algorithm.h:
#pragma once
#include "controllers/follower.h"
#include "datatypes/control.h"
#include "datatypes/parameter.h"
#include "datatypes/path.h"
namespace Kompass {
namespace Control {
class YourAlgorithm : public Follower {
public:
// --- Parameters ---
class YourAlgorithmParameters : public Follower::FollowerParameters {
public:
YourAlgorithmParameters() : Follower::FollowerParameters() {
// addParameter(name, Parameter(default, min, max))
addParameter("my_gain", Parameter(1.0, 0.0, 100.0));
addParameter("my_threshold", Parameter(0.05, 0.001, 10.0));
}
};
// --- Constructors ---
YourAlgorithm();
YourAlgorithm(YourAlgorithmParameters config);
~YourAlgorithm() = default;
/// Main entry point: compute a velocity command for the given time step.
Controller::Result computeVelocityCommand(double timeStep);
/// Convenience: set state + compute in one call.
Controller::Result execute(Path::State currentPosition, double deltaTime);
protected:
YourAlgorithmParameters config;
// Algorithm-specific members
double my_gain{1.0};
double my_threshold{0.05};
};
} // namespace Control
} // namespace Kompass
2. Implement the Algorithm¶
Create kompass_cpp/src/controllers/your_algorithm.cpp:
#include "controllers/your_algorithm.h"
#include "utils/angles.h"
#include "utils/logger.h"
#include <algorithm>
#include <cmath>
namespace Kompass {
namespace Control {
YourAlgorithm::YourAlgorithm() : Follower() {
// Read parameters from the config map
my_gain = config.getParameter<double>("my_gain");
my_threshold = config.getParameter<double>("my_threshold");
}
YourAlgorithm::YourAlgorithm(YourAlgorithmParameters config)
: YourAlgorithm() {
setParams(config);
}
Controller::Result YourAlgorithm::execute(Path::State currentPosition,
double deltaTime) {
setCurrentState(currentPosition);
return computeVelocityCommand(deltaTime);
}
Controller::Result YourAlgorithm::computeVelocityCommand(double timeStep) {
// No path loaded yet
if (!path_processing_) {
return {(reached_goal_ ? Result::Status::GOAL_REACHED
: Result::Status::NO_COMMAND_POSSIBLE),
{0.0, 0.0, 0.0}};
}
// Find the tracked target on the path (crosstrack error, heading error, etc.)
determineTarget();
const Target target = *currentTrackedTarget_;
// --- Your control law ---
double target_speed = target.reverse ? -ctrlimitsParams.velXParams.maxVel
: ctrlimitsParams.velXParams.maxVel;
double steering = my_gain * Angle::normalizeToMinusPiPlusPi(target.heading_error);
// Clamp to velocity limits using the inherited helper
double vx_cmd = restrictVelocityTolimits(
latest_velocity_command_.vx(), target_speed,
ctrlimitsParams.velXParams.maxAcceleration,
ctrlimitsParams.velXParams.maxDeceleration,
ctrlimitsParams.velXParams.maxVel, timeStep);
double omega_cmd = restrictVelocityTolimits(
latest_velocity_command_.omega(), steering,
ctrlimitsParams.omegaParams.maxAcceleration,
ctrlimitsParams.omegaParams.maxDeceleration,
ctrlimitsParams.omegaParams.maxOmega, timeStep);
latest_velocity_command_ = Control::Velocity2D{vx_cmd, 0.0, omega_cmd, 0.0};
// Update segment tracking
current_segment_index_ = target.segment_index;
current_position_in_segment_ = target.position_in_segment;
return {Result::Status::COMMAND_FOUND, latest_velocity_command_};
}
} // namespace Control
} // namespace Kompass
Key inherited members you will use:
Member |
Type |
Description |
|---|---|---|
|
|
Current robot pose (x, y, yaw) |
|
|
Current robot velocity |
|
|
Velocity/acceleration limits |
|
|
Tracked point on the path |
|
|
Last issued command (for smoothing) |
|
|
Whether a valid path is loaded |
|
|
Whether the goal was reached |
|
method |
Compute the tracked target from current state |
|
method |
Clamp a velocity respecting acc/decel limits |
The Target struct (computed by determineTarget()):
Field |
Type |
Description |
|---|---|---|
|
|
Lateral distance to the path (m) |
|
|
Orientation error to the path tangent (rad) |
|
|
The tracked point’s pose on the path |
|
|
Whether driving in reverse |
|
|
Lookahead distance used |
|
|
Current path segment index |
The Result struct:
Field |
Values |
|---|---|
|
|
|
|
3. Add nanobind Bindings¶
Add your algorithm’s bindings to bindings/bindings_control.cpp. There are two parts: the parameters class and the algorithm class.
// At the top, add the include
#include "controllers/your_algorithm.h"
// Inside bindings_control(), add:
// Parameters binding -- inherit from FollowerParameters
py::class_<Control::YourAlgorithm::YourAlgorithmParameters,
Control::Follower::FollowerParameters>(
m_control, "YourAlgorithmParameters")
.def(py::init<>());
// Algorithm binding -- inherit from Follower
py::class_<Control::YourAlgorithm, Control::Follower>(
m_control, "YourAlgorithm")
.def(py::init<>(),
"Init with default parameters")
.def(py::init<Control::YourAlgorithm::YourAlgorithmParameters>(),
"Init with custom config")
.def("compute_velocity_commands",
&Control::YourAlgorithm::computeVelocityCommand,
py::rv_policy::reference_internal)
.def("execute", &Control::YourAlgorithm::execute);
Key binding patterns:
Parameter classes always inherit from their parent’s parameter class in the binding (
FollowerParametersfor followers).The
Parametersbase class already exposesfrom_dict()to Python, which is how the Python config’sto_kompass_cpp()method passes values to C++.Use
py::rv_policy::reference_internalfor methods that return references to internal state (likecomputeVelocityCommand).The inherited
Followerbindings (set_current_path,is_goal_reached,get_vx_cmd, etc.) are automatically available – you only need to bind your algorithm-specific methods.
4. Build¶
The build system uses CMake with scikit-build-core. Source files are auto-discovered via file(GLOB_RECURSE), so you only need to place your .h and .cpp files in the right directories:
kompass_cpp/
├── include/controllers/your_algorithm.h # <-- header here
├── src/controllers/your_algorithm.cpp # <-- implementation here
bindings/
└── bindings_control.cpp # <-- add bindings here
Build and install:
cd kompass-navigation
pip install -e .
Verify the binding is accessible:
import kompass_cpp
algo = kompass_cpp.control.YourAlgorithm()
print(algo) # Should print the object
Parameter System¶
The Parameters / Parameter classes form the C++ configuration system. Each parameter has a default value, min/max bounds, and an optional description.
// In your Parameters constructor:
addParameter("name", Parameter(default, min, max)); // double or int
addParameter("flag", Parameter(true)); // bool (no min/max)
addParameter("label", Parameter(std::string("value"))); // string
// Read in your algorithm:
double val = config.getParameter<double>("name");
// Set from Python via from_dict() -- automatic via nanobind
The Python side uses to_kompass_cpp() on the attrs config to convert to the C++ parameters:
def to_kompass_cpp(self) -> kompass_cpp.control.YourAlgorithmParameters:
params = kompass_cpp.control.YourAlgorithmParameters()
params.from_dict(self.asdict()) # Maps attrs fields -> C++ parameters by name
return params
Important: The keys in the Python attrs config must match the C++ parameter names exactly (e.g., my_gain in both places).
Complete Example: Stanley¶
The Stanley controller is a good reference for the full pattern:
Layer |
File |
|---|---|
C++ header |
|
C++ implementation |
|
nanobind bindings |
|
Python wrapper |
|
Python config |
|
Next Steps¶
Once the C++ algorithm and bindings are in place:
Write the Python wrapper – see Python Algorithm Guide – Option B.
Register in kompass-core – see Common Registration Steps.
Test – verify the algorithm works end-to-end through the Controller component.