feat/fix: PID quaternion-based DP controller#643
Conversation
…ith new test setup
for more information, see https://pre-commit.ci
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Andeshog
left a comment
There was a problem hiding this comment.
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 💯
There was a problem hiding this comment.
Everything in this file exist in vortex-utils, and most is embedded in the Eta struct as well 😄
| // 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); |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
Is it not more tidy to have the parameters in vectors here?
|
This is actively being worked on. I guess we just forgot to change it from draft |
chrstrom
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
|
|
||
| rcl_interfaces::msg::SetParametersResult PIDControllerNode::parametersCallback( | ||
| const std::vector<rclcpp::Parameter>& parameters) { | ||
| rcl_interfaces::msg::SetParametersResult result; | ||
| result.successful = true; | ||
| result.reason = "success"; |
There was a problem hiding this comment.
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() { | |||
There was a problem hiding this comment.
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
| spdlog::info("Eta: "); | ||
| print_eta(eta); | ||
| spdlog::info("Eta desired: "); | ||
| print_eta(eta_d); | ||
| spdlog::info("Eta error:"); | ||
| print_eta(error); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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_; | ||
|
|
There was a problem hiding this comment.
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
| // 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_); |
There was a problem hiding this comment.
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.
| return eta - eta_d; | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
If not, then I guess you don't need to wrap this, just use the - overload directly wherever needed
| return vortex::utils::math::clamp_values(values, min_val, max_val); | ||
| } |
There was a problem hiding this comment.
could also just call this directly instead of wrapping it?
| types::Matrix6d Kp_; | ||
| types::Matrix6d Ki_; | ||
| types::Matrix6d Kd_; | ||
| types::Vector7d integral_; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
b064f9f to
2d71411
Compare
…ement calculations
| 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 |
There was a problem hiding this comment.
Is it better to make the error 6d and change the corresponding matrices?
There was a problem hiding this comment.
With 6d error you could also replace pseudo_inv with standard inverse
… and change type in utils from stamped -> no stamped
| 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; |
There was a problem hiding this comment.
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
Goal: correctly implement and validate the existing quaternion-based DP controller
Completed work
Known issue
rqt_reconfiguretool, the system receives the correct value, but all other gains are immediately reset to 0 in the controller's internal stateTODO