Skip to content

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

Open
ppakr wants to merge 23 commits into
mainfrom
dp-quaternion-control
Open

feat/fix: PID quaternion-based DP controller#643
ppakr wants to merge 23 commits into
mainfrom
dp-quaternion-control

Conversation

@ppakr
Copy link
Copy Markdown

@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
Copy Markdown

codecov Bot commented Nov 12, 2025

Codecov Report

❌ Patch coverage is 36.07477% with 342 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.78%. Comparing base (4fba8ee) to head (ae83763).

Files with missing lines Patch % Lines
...ntrol/pid_controller_dp/src/pid_controller_ros.cpp 0.00% 162 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   36.89%   37.78%   +0.88%     
==========================================
  Files          74       75       +1     
  Lines        5288     5738     +450     
  Branches     1601     1896     +295     
==========================================
+ Hits         1951     2168     +217     
- Misses       2916     3036     +120     
- Partials      421      534     +113     
Flag Coverage Δ
unittests 37.78% <36.07%> (+0.88%) ⬆️

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 1 file 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor

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

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
@ppakr ppakr marked this pull request as ready for review March 11, 2026 18:52
const types::Nu& nu,
const types::Eta& eta_dot_d) {
types::Eta error = error_eta(eta, eta_d);
types::Eta error = error_eta(eta, eta_d); // calculate eta error
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it better to make the error 6d and change the corresponding matrices?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With 6d error you could also replace pseudo_inv with standard inverse

Comment on lines 77 to 108
types::Vector6d PIDController::calculate_tau(const types::Eta& eta,
const types::Eta& eta_d,
const types::Nu& nu,
const types::Eta& eta_dot_d) {
types::Eta error = error_eta(eta, eta_d);
types::Eta error = error_eta(eta, eta_d); // calculate eta error

// set quaternion scalar part w = 0 (only use vector part of quaternion for
// error)
error.qw = 0.0;

types::Matrix6x7d J_inv = calculate_J_sudo_inv(error);
auto eta_dot_d_copy = eta_dot_d;
eta_dot_d_copy.qw = 0.0; // set w = 0 for desired eta_dot

types::Vector6d nu_d = J_inv * eta_dot_d.as_vector();
types::Matrix6x7d J_inv =
calculate_J_sudo_inv(eta); // calculate J pseudo inverse

types::Vector6d error_nu = nu.as_vector() - nu_d;
types::Vector6d nu_d =
J_inv * eta_dot_d_copy.to_vector(); // calculate velocity

types::Vector6d P = Kp_ * J_inv * error.as_vector();
types::Vector6d error_nu = nu.to_vector() - nu_d; // calculate vel error

types::Vector6d I = Ki_ * J_inv * integral_;
types::Vector6d P = Kp_ * J_inv * error.to_vector(); // P term

types::Vector6d D = Kd_ * error_nu;
types::Vector6d I = Ki_ * J_inv * integral_; // I term

types::Vector6d D = Kd_ * error_nu; // D term

types::Vector6d tau = -clamp_values((P + I + D), -80.0, 80.0);

integral_ = anti_windup(dt_, error, integral_);

return tau;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with @jorgenfj. I think a cleaner way would be to simply extract the vector part of the quaternion, something like defining quaternion class or type as q_r and q_v then using q_v and a proper 6x6 jacobian (not the pseudoinv).

This is based on Fossens feedback approach where they prove that only the vector part of the quaternion is needed to be driven to 0 when proving lyapunov stability. I dont remember the exact page off the top of my head

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.

5 participants