Skip to content

Comments

feat/fix: PID quaternion-based DP controller#643

Draft
ppakr wants to merge 15 commits intomainfrom
dp-quaternion-control
Draft

feat/fix: PID quaternion-based DP controller#643
ppakr wants to merge 15 commits intomainfrom
dp-quaternion-control

Conversation

@ppakr
Copy link

@ppakr ppakr commented Nov 12, 2025

Goal: correctly implement and validate the existing quaternion-based DP controller

Completed work

  • The core control mathematics have been reimplemented and validated. The implementation sucessfully passes all unit tests.

Known issue

  • Axis/Gain Mapping Swap: The control efforts/gains appear to be swapped for the planar axes (X and Y), and potentially for the rotational axes (Roll and Pitch). This needs investigation into the input vector mapping.
  • RQT Gain Bug: When updating a single gain via the rqt_reconfigure tool, the system receives the correct value, but all other gains are immediately reset to 0 in the controller's internal state

TODO

  1. Resolve the XY axis mapping issue
  2. Fix the RQT gain update to allow persistent configuration
  3. Clean code
  4. Documentation

@codecov
Copy link

codecov bot commented Nov 12, 2025

Codecov Report

❌ Patch coverage is 36.55303% with 335 lines in your changes missing coverage. Please review.
✅ Project coverage is 35.24%. Comparing base (e2dde64) to head (f1df4f7).

Files with missing lines Patch % Lines
...ntrol/pid_controller_dp/src/pid_controller_ros.cpp 0.00% 155 Missing ⚠️
...ol/pid_controller_dp/test/pid_controller_tests.cpp 60.76% 0 Missing and 113 partials ⚠️
control/pid_controller_dp/src/pid_controller.cpp 16.94% 49 Missing ⚠️
...d_controller_dp/src/pid_controller_conversions.cpp 0.00% 13 Missing ⚠️
...ntroller_dp/include/pid_controller_dp/typedefs.hpp 0.00% 2 Missing ⚠️
...rol/pid_controller_dp/src/pid_controller_utils.cpp 80.00% 2 Missing ⚠️
...trol/pid_controller_dp/src/pid_controller_node.cpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #643      +/-   ##
==========================================
+ Coverage   33.53%   35.24%   +1.70%     
==========================================
  Files          44       45       +1     
  Lines        3003     3459     +456     
  Branches      780     1078     +298     
==========================================
+ Hits         1007     1219     +212     
- Misses       1813     1944     +131     
- Partials      183      296     +113     
Flag Coverage Δ
unittests 35.24% <36.55%> (+1.70%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...trol/pid_controller_dp/src/pid_controller_node.cpp 0.00% <0.00%> (ø)
...ntroller_dp/include/pid_controller_dp/typedefs.hpp 0.00% <0.00%> (ø)
...rol/pid_controller_dp/src/pid_controller_utils.cpp 76.47% <80.00%> (+76.47%) ⬆️
...d_controller_dp/src/pid_controller_conversions.cpp 0.00% <0.00%> (ø)
control/pid_controller_dp/src/pid_controller.cpp 33.76% <16.94%> (+33.76%) ⬆️
...ol/pid_controller_dp/test/pid_controller_tests.cpp 60.76% <60.76%> (ø)
...ntrol/pid_controller_dp/src/pid_controller_ros.cpp 0.00% <0.00%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@Andeshog Andeshog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is still a draft I wont comment on the debugs and prints and so on. But I do encourage utilizing Eta and Nu from vortex-utils, along with the relevant functions 👍 Also, when time comes it would be cool to see a version of the integration test utilizing this controller for a continuous proof of the quality 💯

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything in this file exist in vortex-utils, and most is embedded in the Eta struct as well 😄

Comment on lines 59 to 63
// TODO: parameter callback for dynamic reconfigure of PID gains
//@brief Callback function for parameter updates
// @param parameters: vector of parameters to be set
rcl_interfaces::msg::SetParametersResult parametersCallback(
const std::vector<rclcpp::Parameter>& parameters);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a yaml-based approach like Anbit did. Mixing this approach with a service call (Trigger) allows for the same dynamic reconfiguration, but without relying so much on ROS.

Comment on lines 3 to 23
Kp: [70.0, 70.0, 70.0, 12.0, 12.0, 12.0]
Ki: [2.0, 2.0, 2.0, 0.12, 0.12, 0.12]
Kd: [10.0, 10.0, 10.0, 4.0, 5.0, 4.0]
# Kp: [70.0, 70.0, 70.0, 12.0, 12.0, 12.0]
# Ki: [2.0, 2.0, 2.0, 0.12, 0.12, 0.12]
# Kd: [10.0, 10.0, 10.0, 4.0, 5.0, 4.0]
Kp_x: 10.0
Kp_y: 0.0
Kp_z: 0.0
Kp_roll: 0.0
Kp_pitch: 0.0
Kp_yaw: 0.0
Ki_x: 0.0
Ki_y: 0.0
Ki_z: 0.0
Ki_roll: 0.0
Ki_pitch: 0.0
Ki_yaw: 0.0
Kd_x: 0.0
Kd_y: 0.0
Kd_z: 0.0
Kd_roll: 0.0
Kd_pitch: 0.0
Kd_yaw: 0.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not more tidy to have the parameters in vectors here?

@chrstrom
Copy link
Member

@jorgenfj @ppakr will this be taken out of draft at some point or abandoned? If you're looking to merge I'd really want to do a thorough review of it just to minimize the chances of next year's team re-implementing it for the 7th time 😭 😭

@jorgenfj
Copy link
Contributor

This is actively being worked on. I guess we just forgot to change it from draft

Copy link
Member

@chrstrom chrstrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass, I'll have another read-through when its out of draft / feature-complete 🚀

src/pid_controller_conversions.cpp
)

ament_target_dependencies(${LIB_NAME} PUBLIC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sticking to the "keep non-ROS code separate from everything else", you wouldn't want to pull in anything to your lib using ament_target_dependencies. Instead, use pure cmake for the library, and link in ros-deps + the lib with ament for the final executable

Comment on lines +351 to +356

rcl_interfaces::msg::SetParametersResult PIDControllerNode::parametersCallback(
const std::vector<rclcpp::Parameter>& parameters) {
rcl_interfaces::msg::SetParametersResult result;
result.successful = true;
result.reason = "success";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would consider vectorizing parameters here, or make an unordered map for "parameter sting rep" -> "some struct holding gain type and eigen index"

@@ -114,20 +250,68 @@ void PIDControllerNode::publish_tau() {
}

void PIDControllerNode::set_pid_params() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would argue that you should disallow default parameters here and throw / panic if unset. Easy to miss one in config and could lead to painful debugging times

Comment on lines 91 to 96
spdlog::info("Eta: ");
print_eta(eta);
spdlog::info("Eta desired: ");
print_eta(eta_d);
spdlog::info("Eta error:");
print_eta(error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would suggest making this spdlog::trace (you could pass in log level to the print methods). Lets you f.ex compile debug with trace log level and release with info/warn, and set different log levels for file/terminal (io is expensive)

#include "pid_controller_dp/pid_controller.hpp"
#include "pid_controller_dp/pid_controller_utils.hpp"

void print_eta(const types::Eta& eta) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all these printing utils, consider checking out #include <spdlog/fmt/ostr.h> and/or overloading << and/or adding custom fmt:: formatters to the vortex-utils lib, would make things much neater

bool killswitch_on_;

std::string software_mode_;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably applies to more than just this pr / package, but dealing with string-representation for stuff like this can get really annoying. if you need a string-rep at any point, i'd suggest converting to an enum (magic-enum f.ex) as early as possible in the chain

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm seems to have been changed in #656

// calculate pid control
types::Vector6d tau =
pid_controller_.calculate_tau(eta_, eta_d_, nu_, eta_dot_d_);
pid_controller_.calculate_tau(eta_body_, eta_d_body_, nu_, nu_d_body_);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

been thinking about the calculate_tau interface for a bit, don't have anything definite here, but, some food for thought:

I think it could be very easy for someone to accidentally pass state/desired in different frames, but I also don't see an easy way to enforce this, without wrapping eta/nu in some POD with a field for frame definitions. Could be mitigated by letting calculate_tau just take errors, but then you're shifting a bit of a burden onto the user-side.

Comment on lines +62 to 63
return eta - eta_d;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

been a while since i've dealt with any of this, but would you want to negate the quat part if the scalar error < 0? like to ensure shortest path

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not, then I guess you don't need to wrap this, just use the - overload directly wherever needed

Comment on lines +69 to 70
return vortex::utils::math::clamp_values(values, min_val, max_val);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could also just call this directly instead of wrapping it?

types::Matrix6d Kp_;
types::Matrix6d Ki_;
types::Matrix6d Kd_;
types::Vector7d integral_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this consistent? as in like, accumulating error in an overrepresentation (by 1 dim), instead of first transforming to 6d with J then keeping the integration 6d?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess in either case it would be nice to keep the integral 6d, so the anti-windup makes physical sense and everythings easier to reason with?

@ppakr ppakr force-pushed the dp-quaternion-control branch from b064f9f to 2d71411 Compare February 21, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants