No version for distro humble showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro kilted showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro lyrical showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro rolling showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro ardent showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro bouncy showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro crystal showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro eloquent showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro dashing showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro galactic showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro foxy showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro iron showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro lunar showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro jade showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro indigo showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro hydro showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro kinetic showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro melodic showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)
No version for distro noetic showing jazzy. Known supported distros are highlighted in the buttons above.

Repository Summary

Checkout URI https://github.com/simone-contorno/prox_mpc.git
VCS Type git
VCS Version main
Last Updated 2026-07-28
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

README

ProxMPC

ROS 2 CI ROS 2 Jazzy License: Apache 2.0

Nonlinear Model Predictive Control for ROS 2, packaged as a reusable core and a Nav2 controller plugin.

The controller solves the nonlinear optimal-control problem with a Sequential Quadratic Programming (SQP) scheme that repeatedly builds and solves a Quadratic Program with the ProxQP solver, using Eigen for linear algebra. The same engine handles linear models for free: with linear dynamics the SQP converges in a single QP solve.

Table of Contents

Demonstration

ProxMPC demo - no-obstacle, static, dynamic-line, and dynamic-circle scenarios

The predictive ProxMPC controller reaching the goal in the four benchmark scenarios (no obstacle, static box, dynamic line, dynamic circle) on the kinematic plant, shown in RViz. Each obstacle is drawn as a ground-truth body (the orange cylinder) next to its costmap footprint. The GIF loops inline and links to the full-resolution mp4.

Regenerate it - the per-scenario clips land in prox_mpc_benchmark/results/ (gitignored), and the combiner writes the committed grid mp4 + inline GIF to doc/media/ (see prox_mpc_benchmark/doc/videos.md for the Xvfb/display note on Wayland and every parameter):

ros2 run prox_mpc_benchmark record_scenarios.py
ros2 run prox_mpc_benchmark combine_grid.sh --output doc/media/prox_mpc_demo_grid.mp4

Where it stands

ProxMPC is benchmarked head-to-head against the four stock Nav2 Jazzy local controllers - DWB, MPPI, Regulated Pure Pursuit, and Graceful - plus Vector Pursuit, the one external community controller included as a fair peer (Apache-2.0). Every controller drives the same plant from the same start to the same goal, at a matched 0.5 m/s speed cap and a shared 2.0 s prediction horizon, and perceives obstacles through the same costmaps. The full method and every number are in doc/controller-comparison-results.md; the summary is below.

These are simulation results on a kinematic plant, measured on an x86-64 dev host (Intel Core i7-10750H, 6 cores / 12 threads, 31 GiB RAM, Ubuntu 24.04.4) - not on physical robot hardware and not contact-dynamics. A collision is a would-be overlap of the robot and obstacle discs, scored identically for every controller. Gazebo validation is a single open-cell run; full Gazebo and hardware validation remain open.

Controller Tracking RMS (open) Compute p50 / p95 (open) Static clearance Multi-obstacle margin
ProxMPC 0.0004 m 0.75 / 1.15 ms +0.352 m -0.118 m, +0.190 m predictive
DWB 0.0001 m 2.46 / 2.70 ms +0.093 m +0.080 m
MPPI 0.0029 m 2.61 / 2.91 ms +0.207 m +0.048 m
Regulated Pure Pursuit 0.0000 m 0.21 / 0.25 ms +0.213 m +0.125 m
Vector Pursuit 0.0000 m 0.21 / 0.25 ms +0.175 m (stops short) +0.024 m
Graceful 0.0000 m 0.15 / 0.20 ms +0.207 m -0.013 m

Multi-obstacle margin is the median closest approach over six two-mover cells (30 runs per controller, 60 for MPPI’s 10 repeats); positive clears the obstacle. The margin is reported instead of a collision count on purpose. Those cells are deliberately marginal, so 40-80 % of runs finish within 0.15 m of the threshold and the collision count is dominated by scheduling jitter: the same cell, with the same binary, returned 1/5, 5/5, and 2/5 collisions on three separate runs. The median margin is stable across the same runs and is the honest discriminator. Counts are still reported per cell in doc/controller-comparison-results.md, which is the source of truth.

Strengths

  • Tracking on par with the best. Sub-millimetre cross-track on an empty straight traverse (0.0004 m RMS, 5/5 success).
  • Lightest of the optimising controllers. ~0.75 ms median per cycle on the open cell, ~3.3x lighter than DWB and ~3.5x than MPPI at equal tracking accuracy, and 1.1-2.7x lighter across the obstacle cells (the margin narrows as the obstacle field tightens and the QP gets harder), at 5.0-9.1 %

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ProxMPC

Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern.

Table of Contents

Code style

  • Language defaults. C++17 is the primary language across every package (prox_mpc_core, prox_mpc_controller, prox_mpc_obstacle_tracker, prox_mpc_msgs, prox_mpc_test_models, prox_mpc_demo, prox_mpc_benchmark). Python is used only where the repo already uses it: the orchestration/analysis scripts under prox_mpc_benchmark/scripts/, targeting Python 3.12. Those scripts live in an ament_cmake package (prox_mpc_benchmark/package.xml declares <build_type>ament_cmake</build_type>) and are installed, not built as an ament_python package - follow that pattern rather than converting a package to ament_python.
  • CMake. cmake_minimum_required(VERSION 3.28) is the floor in every package’s CMakeLists.txt; do not lower it.
  • Formatting is ament_uncrustify-only. As stated in the package READMEs (e.g. prox_mpc_core/README.md, prox_mpc_controller/README.md, prox_mpc_obstacle_tracker/README.md): cpplint and ament_copyright are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in LICENSE, for example:
  // Copyright 2026 Simone Contorno
  // SPDX-License-Identifier: Apache-2.0
  

Match this two-line header (adapted for # comments in Python) at the top of every new source file; do not add a full license block per file.

  • Lint runs through colcon test, not standalone. Every package’s CMakeLists.txt calls find_package(ament_lint_auto REQUIRED) and ament_lint_auto_find_test_dependencies() under BUILD_TESTING, and .github/workflows/ci.yaml invokes colcon test --return-code-on-test-failure after the build. That is the whole lint path in this repo - there is no separate ament_uncrustify --reformat or standalone lint invocation in CI, so verify locally the same way: build, then colcon test in your overlay.
  • RAII and ownership. Use std::unique_ptr by default for exclusive ownership (e.g. prox_mpc_obstacle_tracker’s std::unique_ptr<Tracker> tracker_); reserve std::shared_ptr for genuine shared ownership, such as the tracker node’s shared ROS infrastructure objects (std::shared_ptr<tf2_ros::Buffer>, the LifecyclePublisher). Never store a std::shared_ptr by reference or use one solely to extend an object’s lifetime; pass const std::shared_ptr& when only observing it.
  • Named constants over magic numbers, and explicit narrowing conversions - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent).

Commit conventions

  • Branching follows GitHub Flow: main is the stable branch, dev is the integration branch, and topic work happens on feat/*, fix/*, chore/*, or test/* branches merged in via pull request.
  • Commit messages follow Conventional Commits, scoped to the package or area they touch, matching real history in this repo, for example:
    • feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding
    • test(core): cover model input velocity-bound (v_min/v_max) override
    • docs(benchmark): matched-cap results table and fair-comparison prose
    • chore(release): bump packages to 1.0.0
  • One logical change per commit. Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable.
  • No DCO / sign-off is currently required. This repository has no Signed-off-by trailer convention in its commit history and no existing CONTRIBUTING-adjacent policy or .github/ template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review.

Testing standards

  • Frameworks. Every test in this repo is a GoogleTest (with GMock available) suite registered via ament_add_gtest - there is no ament_add_pytest_test, no launch_testing, and no .py test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up.
  • Where tests live. Tests live in <package>/test/, one .cpp file per suite, registered in that package’s CMakeLists.txt under if(BUILD_TESTING). Current suites, for reference:
    • prox_mpc_core/test/: test_model_interface, test_mpc_regression, test_custom_model, test_obstacle_k, test_utils.
    • prox_mpc_controller/test/test_prox_mpc_controller.cpp.
    • prox_mpc_obstacle_tracker/test/: test_clustering, test_imm_filter, test_tracker, test_obstacle_tracker_node.
    • prox_mpc_demo/test/test_simulation_node.cpp.
    • prox_mpc_benchmark/test/: test_metrics, test_obstacle_field.

File truncated at 100 lines see the full file

# Contributing to ProxMPC Thanks for your interest in contributing to ProxMPC. This is a ROS 2 Jazzy package set, and contributions are expected to match the conventions already established in this codebase rather than introduce new ones. When in doubt, grep for how an existing package already solved the same problem and follow that pattern. ## Table of Contents - [Code style](#code-style) - [Commit conventions](#commit-conventions) - [Testing standards](#testing-standards) - [Security](#security) - [Documentation standards](#documentation-standards) - [License](#license) ## Code style - **Language defaults.** C++17 is the primary language across every package (`prox_mpc_core`, `prox_mpc_controller`, `prox_mpc_obstacle_tracker`, `prox_mpc_msgs`, `prox_mpc_test_models`, `prox_mpc_demo`, `prox_mpc_benchmark`). Python is used only where the repo already uses it: the orchestration/analysis scripts under `prox_mpc_benchmark/scripts/`, targeting Python 3.12. Those scripts live in an `ament_cmake` package (`prox_mpc_benchmark/package.xml` declares `ament_cmake`) and are installed, not built as an `ament_python` package - follow that pattern rather than converting a package to `ament_python`. - **CMake.** `cmake_minimum_required(VERSION 3.28)` is the floor in every package's `CMakeLists.txt`; do not lower it. - **Formatting is `ament_uncrustify`-only.** As stated in the package READMEs (e.g. `prox_mpc_core/README.md`, `prox_mpc_controller/README.md`, `prox_mpc_obstacle_tracker/README.md`): `cpplint` and `ament_copyright` are disabled - uncrustify is the single enforced C++ formatter, and files carry a short SPDX header with the full text in [LICENSE](LICENSE), for example: ```cpp // Copyright 2026 Simone Contorno // SPDX-License-Identifier: Apache-2.0 ``` Match this two-line header (adapted for `#` comments in Python) at the top of every new source file; do not add a full license block per file. - **Lint runs through `colcon test`, not standalone.** Every package's `CMakeLists.txt` calls `find_package(ament_lint_auto REQUIRED)` and `ament_lint_auto_find_test_dependencies()` under `BUILD_TESTING`, and `.github/workflows/ci.yaml` invokes `colcon test --return-code-on-test-failure` after the build. That is the whole lint path in this repo - there is no separate `ament_uncrustify --reformat` or standalone lint invocation in CI, so verify locally the same way: build, then `colcon test` in your overlay. - **RAII and ownership.** Use `std::unique_ptr` by default for exclusive ownership (e.g. `prox_mpc_obstacle_tracker`'s `std::unique_ptr tracker_`); reserve `std::shared_ptr` for genuine shared ownership, such as the tracker node's shared ROS infrastructure objects (`std::shared_ptr`, the `LifecyclePublisher`). Never store a `std::shared_ptr` by reference or use one solely to extend an object's lifetime; pass `const std::shared_ptr&` when only observing it. - **Named constants over magic numbers**, and **explicit narrowing conversions** - narrow to a lower-precision type only at a tightly scoped boundary, with a comment explaining why (see the existing exceptions called out in ROS parameter and Eigen/solver code for precedent). ## Commit conventions - **Branching follows GitHub Flow**: `main` is the stable branch, `dev` is the integration branch, and topic work happens on `feat/*`, `fix/*`, `chore/*`, or `test/*` branches merged in via pull request. - **Commit messages follow Conventional Commits**, scoped to the package or area they touch, matching real history in this repo, for example: - `feat(controller): predictive dynamic-obstacle avoidance and model speed-cap forwarding` - `test(core): cover model input velocity-bound (v_min/v_max) override` - `docs(benchmark): matched-cap results table and fair-comparison prose` - `chore(release): bump packages to 1.0.0` - **One logical change per commit.** Keep unrelated refactors, formatting-only changes, and behavior changes in separate commits so the history stays reviewable and bisectable. - **No DCO / sign-off is currently required.** This repository has no `Signed-off-by` trailer convention in its commit history and no existing `CONTRIBUTING`-adjacent policy or `.github/` template requiring one - do not add a sign-off trailer unless a maintainer asks for it in review. ## Testing standards - **Frameworks.** Every test in this repo is a GoogleTest (with GMock available) suite registered via `ament_add_gtest` - there is no `ament_add_pytest_test`, no `launch_testing`, and no `.py` test file anywhere in the tree. If you add Python-facing behavior that needs its own test (as opposed to being exercised through a C++ node under test), discuss the framework choice in the PR first rather than assuming pytest is already wired up. - **Where tests live.** Tests live in `/test/`, one `.cpp` file per suite, registered in that package's `CMakeLists.txt` under `if(BUILD_TESTING)`. Current suites, for reference: - `prox_mpc_core/test/`: `test_model_interface`, `test_mpc_regression`, `test_custom_model`, `test_obstacle_k`, `test_utils`. - `prox_mpc_controller/test/test_prox_mpc_controller.cpp`. - `prox_mpc_obstacle_tracker/test/`: `test_clustering`, `test_imm_filter`, `test_tracker`, `test_obstacle_tracker_node`. - `prox_mpc_demo/test/test_simulation_node.cpp`. - `prox_mpc_benchmark/test/`: `test_metrics`, `test_obstacle_field`. File truncated at 100 lines [see the full file](https://github.com/simone-contorno/prox_mpc/tree/main/CONTRIBUTING.md)