Skip to content

Commit 2a39569

Browse files
authored
Implement PID/Position/Orientation controllers (#3730)
* Add PID controller class and basic test cases * Rename directory to position_controller * Create position controller class specs * Add missing BUILD file * Fix BUILD file * Add position controller basic test cases * Implement position controller * Re-write position controller for feedforward * Rename directory to motion_control * Add specification for OrientationController * Implement OrientationController * Remove orientation and position controller tests * Add motion_control BUILD file * Add reset() method * Update parameter names and doc comments * Remove old tests * Tag issue in TODO * Formatting * Remove default argument for delta_time * Remove integrator reset on error sign swap * Add doc comment * Use length instead of lengthSquared checking for pure pid * Add generic motion controller interface * Fix include * Add test placeholders for position and orientation controller * Use assertion instead of throwing exception * Remove exception tests * Make pid controller gain variables private
1 parent d6347c4 commit 2a39569

11 files changed

Lines changed: 458 additions & 0 deletions
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
cc_library(
4+
name = "controller",
5+
hdrs = [
6+
"controller.h",
7+
],
8+
deps = [
9+
"//software/time:duration",
10+
],
11+
)
12+
13+
cc_library(
14+
name = "position_controller",
15+
srcs = [
16+
"position_controller.cpp",
17+
],
18+
hdrs = [
19+
"position_controller.h",
20+
],
21+
deps = [
22+
":controller",
23+
":pid_controller",
24+
"//software/ai/navigator/trajectory:trajectory_path",
25+
],
26+
)
27+
28+
cc_library(
29+
name = "orientation_controller",
30+
srcs = [
31+
"orientation_controller.cpp",
32+
],
33+
hdrs = [
34+
"orientation_controller.h",
35+
],
36+
deps = [
37+
":controller",
38+
":pid_controller",
39+
"//software/ai/navigator/trajectory:bang_bang_trajectory_1d_angular",
40+
"//software/geom:angle",
41+
"//software/geom:angular_velocity",
42+
],
43+
)
44+
45+
cc_library(
46+
name = "pid_controller",
47+
srcs = [
48+
"pid_controller.cpp",
49+
],
50+
hdrs = [
51+
"pid_controller.h",
52+
],
53+
)
54+
55+
cc_test(
56+
name = "pid_controller_test",
57+
srcs = [
58+
"pid_controller_test.cpp",
59+
],
60+
deps = [
61+
":pid_controller",
62+
"//shared/test_util:tbots_gtest_main",
63+
],
64+
)
65+
66+
cc_test(
67+
name = "position_controller_test",
68+
srcs = [
69+
"position_controller_test.cpp",
70+
],
71+
deps = [
72+
":position_controller",
73+
"//shared/test_util:tbots_gtest_main",
74+
],
75+
)
76+
77+
cc_test(
78+
name = "orientation_controller_test",
79+
srcs = [
80+
"orientation_controller_test.cpp",
81+
],
82+
deps = [
83+
":orientation_controller",
84+
"//shared/test_util:tbots_gtest_main",
85+
],
86+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#include "software/time/duration.h"
2+
3+
/**
4+
* A generic interface for a motion controller.
5+
*
6+
* @tparam StateType The type of the system's current feedback state
7+
* @tparam TrajectoryType The type of the state's trajectory
8+
* @tparam OutputType The type of the calculated control effort
9+
*/
10+
template <typename StateType, typename TrajectoryType, typename OutputType>
11+
class MotionController
12+
{
13+
public:
14+
/**
15+
* Evaluates a single time step of the control loop and returns an output to minimize
16+
* error between the current state and target trajectory.
17+
*
18+
* @param current_state The actual measured current state.
19+
* @param target_trajectory The target trajectory of motion.
20+
* @param elapsed_time The total elapsed time since the trajectory was created.
21+
* @param delta_time The elapsed time since the last time step.
22+
*/
23+
virtual OutputType step(const StateType& current_state,
24+
const TrajectoryType& target_trajectory,
25+
Duration elapsed_time, Duration delta_time) = 0;
26+
27+
/**
28+
* Resets the internal state of the motion controller. For example, any
29+
* accumulated error terms used for calculating control effort.
30+
*/
31+
virtual void reset() = 0;
32+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include "software/embedded/motion_control/orientation_controller.h"
2+
3+
AngularVelocity OrientationController::step(
4+
const Angle& orientation, const BangBangTrajectory1DAngular& target_trajectory,
5+
Duration elapsed_time, Duration delta_time)
6+
{
7+
const Angle difference_from_target =
8+
(target_trajectory.getDestination() - orientation).clamp();
9+
10+
if (difference_from_target.abs().toDegrees() < ANGULAR_PURE_PID_THRESHOLD_DEGREES)
11+
{
12+
// if target orientation is close enough, use pure PID for angular velocity
13+
return AngularVelocity::fromRadians(w_pid_close_.step(
14+
difference_from_target.toRadians(), delta_time.toSeconds()));
15+
}
16+
else
17+
{
18+
// feedforward trajectory angular velocity with small pid control effort
19+
const Angle error_angular =
20+
(target_trajectory.getPosition(elapsed_time.toSeconds()) - orientation)
21+
.clamp();
22+
const AngularVelocity pid_effort_angular = AngularVelocity::fromRadians(
23+
w_pid_.step(error_angular.toRadians(), delta_time.toSeconds()));
24+
return target_trajectory.getVelocity(elapsed_time.toSeconds()) +
25+
pid_effort_angular;
26+
}
27+
}
28+
29+
void OrientationController::reset()
30+
{
31+
w_pid_.reset();
32+
w_pid_close_.reset();
33+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#pragma once
2+
3+
#include "software/ai/navigator/trajectory/bang_bang_trajectory_1d_angular.h"
4+
#include "software/embedded/motion_control/controller.h"
5+
#include "software/embedded/motion_control/pid_controller.h"
6+
#include "software/geom/angle.h"
7+
#include "software/geom/angular_velocity.h"
8+
#include "software/time/duration.h"
9+
10+
class OrientationController
11+
: public MotionController<Angle, BangBangTrajectory1DAngular, AngularVelocity>
12+
{
13+
public:
14+
/**
15+
* Constructs an orientation controller that uses measurements over multiple
16+
* time intervals to calculate the target angular velocity to minimize error.
17+
*/
18+
OrientationController() = default;
19+
20+
/**
21+
* Given an orientation and target orientation, returns a target angular
22+
* velocity to minimize the error between the two.
23+
*
24+
* @param orientation The actual orientation.
25+
* @param target_trajectory The target angular trajectory.
26+
* @param elapsed_time The elapsed time since the trajectory was created.
27+
* @param delta_time The time passed since last time step.
28+
*/
29+
AngularVelocity step(const Angle& orientation,
30+
const BangBangTrajectory1DAngular& target_trajectory,
31+
Duration elapsed_time, Duration delta_time) override;
32+
33+
/**
34+
* Resets the state of this orientation controller.
35+
*/
36+
void reset() override;
37+
38+
private:
39+
// TODO(#3737): tune constants
40+
PidController w_pid_{0.7, 0.0, 2.0, 0.0};
41+
PidController w_pid_close_{2.0, 0.0, 4.0, 0.0};
42+
43+
static constexpr double ANGULAR_PURE_PID_THRESHOLD_DEGREES = 25.0;
44+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include "software/embedded/motion_control/orientation_controller.h"
2+
3+
#include <gtest/gtest.h>
4+
5+
// TODO(#3743): write proper tests after pid constants have been tuned
6+
7+
TEST(OrientationControllerTest, BasicTest)
8+
{
9+
OrientationController controller;
10+
BangBangTrajectory1DAngular trajectory;
11+
controller.step(Angle::zero(), trajectory, Duration::fromSeconds(1.0),
12+
Duration::fromSeconds(0.01));
13+
controller.reset();
14+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#include "software/embedded/motion_control/pid_controller.h"
2+
3+
#include <algorithm>
4+
#include <cassert>
5+
6+
PidController::PidController(double k_p, double k_i, double k_d, double max_integral)
7+
: k_p_(k_p), k_i_(k_i), k_d_(k_d), max_integral_(max_integral)
8+
{
9+
assert(max_integral >= 0.0);
10+
};
11+
12+
double PidController::step(double error, double delta_time)
13+
{
14+
integral_ = std::clamp(integral_ + error * delta_time, -max_integral_, max_integral_);
15+
16+
const double derivative = (error - last_error_.value_or(error)) / delta_time;
17+
18+
last_error_ = error;
19+
20+
return error * k_p_ + integral_ * k_i_ + derivative * k_d_;
21+
}
22+
23+
void PidController::reset()
24+
{
25+
integral_ = 0.0;
26+
last_error_ = std::nullopt;
27+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#pragma once
2+
3+
#include <optional>
4+
5+
class PidController
6+
{
7+
public:
8+
/**
9+
* Constructs a new PID controller.
10+
*
11+
* A PID controller is used to calculate corrections based on error values over
12+
* time as the difference between a desired value and the actual measured value.
13+
* This PID controller also limits integral windup using a max_integral value.
14+
*
15+
* Resources:
16+
* - https://raw.org/book/control-theory/introduction-to-pid-controllers/
17+
* - http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-reset-windup/
18+
*
19+
* @pre max_integral must be >= 0.0
20+
*
21+
* @param k_p The proportional gain.
22+
* @param k_i The integral gain.
23+
* @param k_d The derivative gain.
24+
* @param max_integral The maximum absolute value that the integrator term can
25+
* accumulate to.
26+
**/
27+
PidController(double k_p, double k_i, double k_d, double max_integral);
28+
29+
/**
30+
* Given an error, returns the control effort to minimize it.
31+
*
32+
* @param error The amount of error between desired and actual output
33+
* @param delta_time The time passed since last step, for calculating the integrator
34+
* and derivative.
35+
**/
36+
double step(double error, double delta_time);
37+
38+
/**
39+
* Resets the integrator and clears the last error used for derivative calculation.
40+
**/
41+
void reset();
42+
43+
private:
44+
double k_p_;
45+
double k_i_;
46+
double k_d_;
47+
double max_integral_;
48+
49+
double integral_ = 0.0;
50+
std::optional<double> last_error_ = std::nullopt;
51+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#include "software/embedded/motion_control/pid_controller.h"
2+
3+
#include <gtest/gtest.h>
4+
5+
TEST(PidControllerTest, OnlyProportionTermNonZero)
6+
{
7+
PidController pid{1.0, 0.0, 0.0, 0.0};
8+
9+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), 0.0);
10+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), 0.0);
11+
EXPECT_DOUBLE_EQ(pid.step(1.0, 1.0), 1.0);
12+
EXPECT_DOUBLE_EQ(pid.step(8.0, 1.0), 8.0);
13+
EXPECT_DOUBLE_EQ(pid.step(-5.0, 1.0), -5.0);
14+
EXPECT_DOUBLE_EQ(pid.step(-1.0, 1.0), -1.0);
15+
16+
pid = PidController{5.0, 0.0, 0.0, 0.0};
17+
18+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), 0.0);
19+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), 0.0);
20+
EXPECT_DOUBLE_EQ(pid.step(1.0, 1.0), 5.0);
21+
EXPECT_DOUBLE_EQ(pid.step(8.0, 1.0), 40.0);
22+
EXPECT_DOUBLE_EQ(pid.step(-5.0, 1.0), -25.0);
23+
EXPECT_DOUBLE_EQ(pid.step(-1.0, 1.0), -5.0);
24+
}
25+
26+
TEST(PidControllerTest, OnlyIntegralTermNonZero)
27+
{
28+
constexpr double k_i = 2.0;
29+
PidController pid{0.0, k_i, 0.0, 10.0};
30+
31+
EXPECT_DOUBLE_EQ(pid.step(1.0, 1.0), k_i * 1.0);
32+
EXPECT_DOUBLE_EQ(pid.step(1.0, 1.0), k_i * 2.0);
33+
EXPECT_DOUBLE_EQ(pid.step(1.0, 1.0), k_i * 3.0);
34+
EXPECT_DOUBLE_EQ(pid.step(0.5, 1.0), k_i * 3.5);
35+
36+
EXPECT_DOUBLE_EQ(pid.step(-0.2, 1.0), k_i * 3.3);
37+
EXPECT_DOUBLE_EQ(pid.step(-1.0, 1.0), k_i * 2.3);
38+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), k_i * 2.3);
39+
40+
// should not accumulate integral term above max_integral
41+
EXPECT_DOUBLE_EQ(pid.step(6.7, 1.0), k_i * 9.0);
42+
EXPECT_DOUBLE_EQ(pid.step(5.0, 1.0), k_i * 10.0);
43+
EXPECT_DOUBLE_EQ(pid.step(1.0, 1.0), k_i * 10.0);
44+
}
45+
46+
TEST(PidControllerTest, OnlyDerivativeTermNonZero)
47+
{
48+
constexpr double k_d = 3.0;
49+
PidController pid{0.0, 0.0, k_d, 0.0};
50+
51+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), k_d * 0.0);
52+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), k_d * 0.0);
53+
EXPECT_DOUBLE_EQ(pid.step(1.0, 1.0), k_d * 1.0);
54+
EXPECT_DOUBLE_EQ(pid.step(8.0, 1.0), k_d * 7.0);
55+
EXPECT_DOUBLE_EQ(pid.step(-5.0, 1.0), k_d * -13.0);
56+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), k_d * 5.0);
57+
EXPECT_DOUBLE_EQ(pid.step(5.0, 0.5), k_d * 5.0 / 0.5);
58+
EXPECT_DOUBLE_EQ(pid.step(8.0, 2.0), k_d * 3.0 / 2.0);
59+
}
60+
61+
TEST(PidControllerTest, GeneralApplication)
62+
{
63+
constexpr double k_p = 10.0;
64+
constexpr double k_i = 3.0;
65+
constexpr double k_d = -1.0;
66+
constexpr double max_integral = 10.0;
67+
PidController pid{k_p, k_i, k_d, max_integral};
68+
69+
EXPECT_DOUBLE_EQ(pid.step(12.0, 0.75), k_p * 12.0 + k_i * 9.0 + k_d * 0.0);
70+
EXPECT_DOUBLE_EQ(pid.step(24.0, 0.75), k_p * 24.0 + k_i * 10.0 + k_d * 12.0 / 0.75);
71+
EXPECT_DOUBLE_EQ(pid.step(4.0, 1.0), k_p * 4.0 + k_i * 10.0 + k_d * -20.0);
72+
EXPECT_DOUBLE_EQ(pid.step(0.0, 1.0), k_p * 0.0 + k_i * 10.0 + k_d * -4.0);
73+
EXPECT_DOUBLE_EQ(pid.step(2.0, 1.0), k_p * 2.0 + k_i * 10.0 + k_d * 2.0);
74+
EXPECT_DOUBLE_EQ(pid.step(-2.0, 1.0), k_p * -2.0 + k_i * 8.0 + k_d * -4.0);
75+
}

0 commit comments

Comments
 (0)