Skip to content

Add three new anti-windup techniques and a Saturation feature #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: ros2-master
Choose a base branch
from

Conversation

ViktorCVS
Copy link

@ViktorCVS ViktorCVS commented Mar 11, 2025

Overview

This PR adds three new anti-windup techniques: back‑calculation, the conditioning technique, and conditional integration. It also adds a saturation feature for the PID output. New parameters have been introduced, and additional overloads have been implemented to ensure compatibility.

What was added/changed in this PR

  • Added three new anti-windup techniques
    • back-calculation
    • conditioning technique
    • conditional integration
  • Added saturation feature to PID output

About compatibility

The packages compile correctly and have passed the pre‑commit and colcon tests (packages with dependencies continue to show the same number of failures before and after my modifications). If the new parameters are not used, the package retains its old behavior.

About the older anti-windup technique

My plan, either by the end of this PR or in a subsequent one, is to completely remove the older anti‑windup technique that has been used so far. This method, which is a form of conditional integration, has several disadvantages:

  • If set incorrectly, it may cause a steady‑state error.
  • If set incorrectly, it may not affect the system at all.
  • Even if it is set between the steady‑state error limit and the value beyond which it has no effect, it is still difficult to find a configuration that improves the system as effectively as the other techniques.

Additionally, regardless of whether the 'antiwindup' parameter is set to true or false, the anti-windup technique is applied (using the same method with a different approach), so the user does not have the option to disable it.

About unit tests

I've added 10 new unit tests for the new features and updated the existing ones to accommodate the new parameters.

Related PR's

Important notes

These three techniques are common anti‑windup strategies used to mitigate the windup effect and are widely employed in control applications: back‑calculation [1], the conditioning technique [1,2], and conditional integration [1,3].

The default values for the tracking time constant are defined in [3,4] for back‑calculation and in [1] for the conditioning technique.

Both back‑calculation and the conditioning technique use forward Euler discretization; this may change before merging this PR.

Graphs

I tested it on ros2_control_demos to better illustrate this feature and test it on simulation to valide the equations. The tests were conducted using a modified version of Example 1: RRBot, which uses a PID controller instead of the default forward position controller. It was tested on Docker, Ubuntu Noble, and Jazzy.

PID values: p = 4.0, i = 25.0, d = 0.5; u_max = 13, u_min = -13; and the tracking time constant was left at its default value.

unsat+sat+back

The standard response with a settling time (ts) of 5.2 seconds, the response affected by saturation, resulting in a settling time (ts_sat) of 8.6 seconds (+65.4% increase) and the response using the back-calculation technique, which improves performance with a settling time (ts_back) of 4.1 seconds (–21.2% decrease), even lower than the standard response.

back+ct+cin back+ct+cin2

Those figures compares three anti-windup methods applied to the step response, a zoomed-in view of the step response is provided here to clearly distinguish between the three anti-windup strategies. They are all very similar due to the system and PID values, but they may vary significantly between applications.

cs_unsat+sat+back

The standard control output, the control output affected by saturation, with a recovery time from saturation of 6.8s and the control output using the back-calculation technique, with a recovery time from saturation of 2s (-70.6%).

cs_back+ct+cin cs_back+ct+cin2

Those figures compares three control outputs using anti-windup methods, a zoomed-in view of the control output is provided here to clearly distinguish between the three anti-windup strategies. They are all very similar due to the system and PID values, but they may vary significantly between applications.

All the equations have been validated with these simulations, providing a feature with three techniques to address windup.

Final notes

I'm very open to any recommendations to improve this code.

References

[1] VISIOLI, A. Pratical PID Control. London: Springer-Verlag London Limited, 2006. 476 p.
[2] VRANCIC, D. Some Aspects and Design of Anti-Windup and Conditioned Transfer.
Thesis (Master in Electrical Engineering) — University of Ljubljana, Faculty of
Electrical Engeneering, 1995.
[3] BOHN, C.; ATHERTON, D. An analysis package comparing pid anti-windup strategies.
IEEE Control Systems Magazine, p. 34–40, 1995.
[4] ASTRöM, K.; HäGGLUND, T. PID Controllers: Theory, Design and Tuning. ISA Press.
Research Triangle Park, USA: Springer-Verlag London Limited, 1995. 343 p.

@ViktorCVS ViktorCVS changed the title Add three new anti-windup techniques Add three new anti-windup techniques and a Saturation feature Mar 12, 2025
Copy link

mergify bot commented Mar 12, 2025

This pull request is in conflict. Could you fix it @ViktorCVS?

@ViktorCVS ViktorCVS force-pushed the ros2-master branch 2 times, most recently from d0feb10 to bd7c8f0 Compare March 13, 2025 16:56
@ViktorCVS ViktorCVS marked this pull request as ready for review March 13, 2025 18:06
@ViktorCVS
Copy link
Author

@christophfroehlich, it appears that no reviewers have been assigned to this PR. Could you please help with that? If you have time, I'd appreciate it if you could also take a look at the changes.

Copy link
Contributor

@christophfroehlich christophfroehlich left a comment

Choose a reason for hiding this comment

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

Thx for this thorough PR, but it will need some time to properly review it

@codecov-commenter
Copy link

codecov-commenter commented Mar 14, 2025

Codecov Report

Attention: Patch coverage is 91.97708% with 28 lines in your changes missing coverage. Please review.

Project coverage is 79.92%. Comparing base (0936394) to head (e1b7413).

Files with missing lines Patch % Lines
control_toolbox/src/pid.cpp 70.83% 9 Missing and 5 partials ⚠️
control_toolbox/src/pid_ros.cpp 77.41% 12 Missing and 2 partials ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           ros2-master     #298      +/-   ##
===============================================
+ Coverage        77.24%   79.92%   +2.68%     
===============================================
  Files               29       29              
  Lines             1336     1659     +323     
  Branches            93      100       +7     
===============================================
+ Hits              1032     1326     +294     
- Misses             252      275      +23     
- Partials            52       58       +6     
Flag Coverage Δ
unittests 79.92% <91.97%> (+2.68%) ⬆️

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

Files with missing lines Coverage Δ
control_toolbox/include/control_toolbox/pid.hpp 95.65% <100.00%> (+17.87%) ⬆️
...ontrol_toolbox/include/control_toolbox/pid_ros.hpp 100.00% <ø> (ø)
control_toolbox/test/pid_ros_parameters_tests.cpp 100.00% <100.00%> (ø)
control_toolbox/test/pid_ros_publisher_tests.cpp 95.00% <100.00%> (ø)
control_toolbox/test/pid_tests.cpp 100.00% <100.00%> (ø)
control_toolbox/src/pid.cpp 82.81% <70.83%> (-8.20%) ⬇️
control_toolbox/src/pid_ros.cpp 73.61% <77.41%> (-0.28%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bmagyar
Copy link
Member

bmagyar commented Apr 24, 2025

@ViktorCVS could you please resolve the current conflicts?

@ViktorCVS
Copy link
Author

@ViktorCVS could you please resolve the current conflicts?

done

Copy link
Contributor

@christophfroehlich christophfroehlich left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the nice work including tests etc. Please fix the pre-commit errors, and only some minor comments in the code

}

if (gains.antiwindup_strat_ == "back_calculation" && gains.i_gain_ != 0) {
if (gains.trk_tc_ == 0.0 && gains.d_gain_ != 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.

better to use something like
std::abs(val) < std::numeric_limits<double>::epsilon() instead of comparing a double to 0.0

applies to every occurrence here, you can create a helper method for that.

template<typename T>
bool is_zero(T value, T tolerance = std::numeric_limits<T>::epsilon()) {
    return std::abs(value) <= tolerance;
}

Copy link
Author

Choose a reason for hiding this comment

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

Nice! I added "inline" tag and putted it in pid.hpp. I also created a "is_not_zero(x)" to adress !=0.0 instead of use !is_zero(x). If it isn't ok, I can keep only is_zero. Adressed in commit 4c1fc9b.

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I prefer !is_zero, but as you like

Copy link
Author

Choose a reason for hiding this comment

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

I agree. Change it to use only is_zero() in commit 5513ff0.

if (gains.antiwindup_strat_ == "back_calculation" && gains.i_gain_ != 0) {
if (gains.trk_tc_ == 0.0 && gains.d_gain_ != 0.0) {
// Default value for tracking time constant for back calculation technique
gains.trk_tc_ = std::sqrt(gains.d_gain_/gains.i_gain_);
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't this go in the set_gains method, instead of calculating this at every update step?

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @christophfroehlich

@christophfroehlich
Copy link
Contributor

christophfroehlich commented Apr 26, 2025

My plan, either by the end of this PR or in a subsequent one, is to completely remove the older anti‑windup technique that has been used so far. This method, which is a form of conditional integration, has several disadvantages:

Please add a deprecation notice to the code, as well as a warning on std::cout if "none" is configured, for example "xxx is deprecated. This option will be removed by the ROS 2 Kilted Kaiju release."

*/
Pid(double p, double i, double d, double i_max, double i_min,
double u_max, double u_min, double trk_tc, bool saturation,
bool antiwindup, std::string antiwindup_strat);
Copy link
Contributor

Choose a reason for hiding this comment

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

thinking once again, maybe we shouldn't use std::string here in the base class, but an enum instead?
the pid_ros can have the string parameter, and maybe do the conversion manually there, or use a pattern like here.

Copy link
Author

Choose a reason for hiding this comment

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

I had some problems on this, but change it in commit b7c8866, 9826771

if (gains.antiwindup_strat_ == "back_calculation" && gains.i_gain_ != 0) {
if (gains.trk_tc_ == 0.0 && gains.d_gain_ != 0.0) {
// Default value for tracking time constant for back calculation technique
gains.trk_tc_ = std::sqrt(gains.d_gain_/gains.i_gain_);
Copy link
Member

Choose a reason for hiding this comment

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

I agree with @christophfroehlich

Add 3 unit tests for saturation feature.
Add 7 unit tests for saturation and anti-windup feature.
Add new parameters to the existing unit tests in the package.
The i_bounds and u_bounds conditions were combined using an 'or'
operator. I have updated them to use separate if else statements.
Delete comments related to dynamic reconfigure and ROS, and update several
other comments.
The i_bounds and u_bounds conditions were combined using an 'or'
operator. I have updated them to use separate if else statements.
This commit adds a helper function for comparing values to zero, improving
the readability of the code.
@christophfroehlich christophfroehlich linked an issue Apr 28, 2025 that may be closed by this pull request
Copy link
Contributor

@christophfroehlich christophfroehlich left a comment

Choose a reason for hiding this comment

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

For the future: Please don't force push to PRs because it makes it harder for reviewers to check the changes since the last review ;) The history does not have to be linear, because we squash that anyways.

We get closer to the finish line, but some of our comments haven't been addressed yet.

}

if (gains.antiwindup_strat_ == "back_calculation" && gains.i_gain_ != 0) {
if (gains.trk_tc_ == 0.0 && gains.d_gain_ != 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.

Personally I prefer !is_zero, but as you like

Copy link
Contributor

@christophfroehlich christophfroehlich left a comment

Choose a reason for hiding this comment

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

The tests are failing now, can you have a look please?

}
else
{
// Qualquer valor desconhecido agora cai em NONE
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Qualquer valor desconhecido agora cai em NONE

not everyone will understand Portuguese ;)

};

constexpr AntiwindupStrategy() : value_(NONE) {}
explicit constexpr AntiwindupStrategy(Value v) : value_(v) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
explicit constexpr AntiwindupStrategy(Value v) : value_(v) {}
constexpr AntiwindupStrategy(Value v) : value_(v) {} // NOLINT(runtime/explicit)

And then the constructor can be omitted and this just works

  Pid pid(
    0.0, 1.0, 0.0, 0.0, 0.0, 5.0, -5.0, 1.0, true, false,
    AntiwindupStrategy::BACK_CALCULATION);

What do you think? This can be simplified in the tests then

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.

Understanding the Anti-Windup Action
5 participants