Re-enable polytopes for closed loop driving
I reproduced the original bug which resulted in this being disabled in a
test and fixed it already (I99a1299e538a395685c2e7e555dfccab48055349).
This commit fixes another issue observed in testing and re-enables the
polytopes.
Change-Id: Ic27e0ab8b2ea6dd480d7998c44530918443874db
diff --git a/frc971/control_loops/coerce_goal.cc b/frc971/control_loops/coerce_goal.cc
index 6482cc7..0868c2c 100644
--- a/frc971/control_loops/coerce_goal.cc
+++ b/frc971/control_loops/coerce_goal.cc
@@ -7,11 +7,10 @@
namespace frc971 {
namespace control_loops {
-Eigen::Matrix<double, 2, 1> DoCoerceGoal(const aos::controls::HPolytope<2> ®ion,
- const Eigen::Matrix<double, 1, 2> &K,
- double w,
- const Eigen::Matrix<double, 2, 1> &R,
- bool *is_inside) {
+Eigen::Matrix<double, 2, 1> DoCoerceGoal(
+ const aos::controls::HPolytope<2> ®ion,
+ const Eigen::Matrix<double, 1, 2> &K, double w,
+ const Eigen::Matrix<double, 2, 1> &R, bool *is_inside) {
if (region.IsInside(R)) {
if (is_inside) *is_inside = true;
return R;
@@ -43,6 +42,7 @@
} else {
Eigen::Matrix<double, 2, Eigen::Dynamic> region_vertices =
region.Vertices();
+ CHECK_GT(region_vertices.outerSize(), 0);
double min_distance = INFINITY;
int closest_i = 0;
for (int i = 0; i < region_vertices.outerSize(); i++) {
diff --git a/frc971/control_loops/drivetrain/drivetrain_lib_test.cc b/frc971/control_loops/drivetrain/drivetrain_lib_test.cc
index 9db00b5..9d7e223 100644
--- a/frc971/control_loops/drivetrain/drivetrain_lib_test.cc
+++ b/frc971/control_loops/drivetrain/drivetrain_lib_test.cc
@@ -224,6 +224,48 @@
}
}
+// Tests that the robot successfully drives straight forward.
+// This used to not work due to a U-capping bug.
+TEST_F(DrivetrainTest, DriveStraightForward) {
+ my_drivetrain_queue_.goal.MakeWithBuilder()
+ .control_loop_driving(true)
+ .left_goal(4.0)
+ .right_goal(4.0)
+ .Send();
+ for (int i = 0; i < 500; ++i) {
+ drivetrain_motor_plant_.SendPositionMessage();
+ drivetrain_motor_.Iterate();
+ drivetrain_motor_plant_.Simulate();
+ SimulateTimestep(true);
+ ASSERT_TRUE(my_drivetrain_queue_.output.FetchLatest());
+ EXPECT_FLOAT_EQ(my_drivetrain_queue_.output->left_voltage,
+ my_drivetrain_queue_.output->right_voltage);
+ EXPECT_GT(my_drivetrain_queue_.output->left_voltage, -3);
+ EXPECT_GT(my_drivetrain_queue_.output->right_voltage, -3);
+ }
+ VerifyNearGoal();
+}
+
+// Tests that the robot successfully drives close to straight.
+// This used to fail in simulation due to libcdd issues with U-capping.
+TEST_F(DrivetrainTest, DriveAlmostStraightForward) {
+ my_drivetrain_queue_.goal.MakeWithBuilder()
+ .control_loop_driving(true)
+ .left_goal(4.0)
+ .right_goal(3.9)
+ .Send();
+ for (int i = 0; i < 500; ++i) {
+ drivetrain_motor_plant_.SendPositionMessage();
+ drivetrain_motor_.Iterate();
+ drivetrain_motor_plant_.Simulate();
+ SimulateTimestep(true);
+ ASSERT_TRUE(my_drivetrain_queue_.output.FetchLatest());
+ EXPECT_GT(my_drivetrain_queue_.output->left_voltage, -3);
+ EXPECT_GT(my_drivetrain_queue_.output->right_voltage, -3);
+ }
+ VerifyNearGoal();
+}
+
::aos::controls::HPolytope<2> MakeBox(double x1_min, double x1_max,
double x2_min, double x2_max) {
Eigen::Matrix<double, 4, 2> box_H;
diff --git a/frc971/control_loops/drivetrain/ssdrivetrain.cc b/frc971/control_loops/drivetrain/ssdrivetrain.cc
index 049b576..9fdfe1c 100644
--- a/frc971/control_loops/drivetrain/ssdrivetrain.cc
+++ b/frc971/control_loops/drivetrain/ssdrivetrain.cc
@@ -18,98 +18,95 @@
DrivetrainMotorsSS::LimitedDrivetrainLoop::LimitedDrivetrainLoop(
StateFeedbackLoop<4, 2, 2> &&loop)
: StateFeedbackLoop<4, 2, 2>(::std::move(loop)),
- U_Poly_((Eigen::Matrix<double, 4, 2>() << 1, 0, -1, 0, 0, 1, 0, -1)
+ U_poly_((Eigen::Matrix<double, 4, 2>() << 1, 0, -1, 0, 0, 1, 0, -1)
.finished(),
(Eigen::Matrix<double, 4, 1>() << 12.0, 12.0, 12.0, 12.0)
.finished()) {
::aos::controls::HPolytope<0>::Init();
- T << 1, -1, 1, 1;
- T_inverse = T.inverse();
+ T_ << 1, 1, 1, -1;
+ T_inverse_ = T_.inverse();
}
+// This intentionally runs the U-capping code even when it's unnecessary to help
+// make it more deterministic. Only running it when one or both sides want
+// out-of-range voltages could lead to things like running out of CPU under
+// certain situations, which would be bad.
void DrivetrainMotorsSS::LimitedDrivetrainLoop::CapU() {
+ output_was_capped_ = ::std::abs(U(0, 0)) > 12.0 || ::std::abs(U(1, 0)) > 12.0;
+
const Eigen::Matrix<double, 4, 1> error = R() - X_hat();
- if (::std::abs(U(0, 0)) > 12.0 || ::std::abs(U(1, 0)) > 12.0) {
- mutable_U() =
- U() * 12.0 / ::std::max(::std::abs(U(0, 0)), ::std::abs(U(1, 0)));
- LOG_MATRIX(DEBUG, "U is now", U());
- // TODO(Austin): Figure out why the polytope stuff wasn't working and
- // remove this hack.
- output_was_capped_ = true;
- return;
+ LOG_MATRIX(DEBUG, "U at start", U());
+ LOG_MATRIX(DEBUG, "R at start", R());
+ LOG_MATRIX(DEBUG, "Xhat at start", X_hat());
- LOG_MATRIX(DEBUG, "U at start", U());
- LOG_MATRIX(DEBUG, "R at start", R());
- LOG_MATRIX(DEBUG, "Xhat at start", X_hat());
+ Eigen::Matrix<double, 2, 2> position_K;
+ position_K << K(0, 0), K(0, 2), K(1, 0), K(1, 2);
+ Eigen::Matrix<double, 2, 2> velocity_K;
+ velocity_K << K(0, 1), K(0, 3), K(1, 1), K(1, 3);
- Eigen::Matrix<double, 2, 2> position_K;
- position_K << K(0, 0), K(0, 2), K(1, 0), K(1, 2);
- Eigen::Matrix<double, 2, 2> velocity_K;
- velocity_K << K(0, 1), K(0, 3), K(1, 1), K(1, 3);
+ Eigen::Matrix<double, 2, 1> position_error;
+ position_error << error(0, 0), error(2, 0);
+ // drive_error = [total_distance_error, left_error - right_error]
+ const auto drive_error = T_inverse_ * position_error;
+ Eigen::Matrix<double, 2, 1> velocity_error;
+ velocity_error << error(1, 0), error(3, 0);
+ LOG_MATRIX(DEBUG, "error", error);
- Eigen::Matrix<double, 2, 1> position_error;
- position_error << error(0, 0), error(2, 0);
- const auto drive_error = T_inverse * position_error;
- Eigen::Matrix<double, 2, 1> velocity_error;
- velocity_error << error(1, 0), error(3, 0);
- LOG_MATRIX(DEBUG, "error", error);
+ const ::aos::controls::HPolytope<2> pos_poly(U_poly_, position_K * T_,
+ -velocity_K * velocity_error);
- const auto &poly = U_Poly_;
- const Eigen::Matrix<double, 4, 2> pos_poly_H = poly.H() * position_K * T;
- const Eigen::Matrix<double, 4, 1> pos_poly_k =
- poly.k() - poly.H() * velocity_K * velocity_error;
- const ::aos::controls::HPolytope<2> pos_poly(pos_poly_H, pos_poly_k);
+ Eigen::Matrix<double, 2, 1> adjusted_pos_error;
+ {
+ const auto &P = drive_error;
- Eigen::Matrix<double, 2, 1> adjusted_pos_error;
- {
- const auto &P = drive_error;
+ Eigen::Matrix<double, 1, 2> L45;
+ L45 << ::aos::sign(P(1, 0)), -::aos::sign(P(0, 0));
+ const double w45 = 0;
- Eigen::Matrix<double, 1, 2> L45;
- L45 << ::aos::sign(P(1, 0)), -::aos::sign(P(0, 0));
- const double w45 = 0;
+ Eigen::Matrix<double, 1, 2> LH;
+ if (::std::abs(P(0, 0)) > ::std::abs(P(1, 0))) {
+ LH << 0, 1;
+ } else {
+ LH << 1, 0;
+ }
+ const double wh = LH.dot(P);
- Eigen::Matrix<double, 1, 2> LH;
- if (::std::abs(P(0, 0)) > ::std::abs(P(1, 0))) {
- LH << 0, 1;
- } else {
- LH << 1, 0;
- }
- const double wh = LH.dot(P);
+ Eigen::Matrix<double, 2, 2> standard;
+ standard << L45, LH;
+ Eigen::Matrix<double, 2, 1> W;
+ W << w45, wh;
+ const Eigen::Matrix<double, 2, 1> intersection = standard.inverse() * W;
- Eigen::Matrix<double, 2, 2> standard;
- standard << L45, LH;
- Eigen::Matrix<double, 2, 1> W;
- W << w45, wh;
- const Eigen::Matrix<double, 2, 1> intersection = standard.inverse() * W;
-
- bool is_inside_h;
- const auto adjusted_pos_error_h =
- DoCoerceGoal(pos_poly, LH, wh, drive_error, &is_inside_h);
- const auto adjusted_pos_error_45 =
- DoCoerceGoal(pos_poly, L45, w45, intersection, nullptr);
- if (pos_poly.IsInside(intersection)) {
- adjusted_pos_error = adjusted_pos_error_h;
- } else {
- if (is_inside_h) {
- if (adjusted_pos_error_h.norm() > adjusted_pos_error_45.norm() ||
- adjusted_pos_error_45.norm() > intersection.norm()) {
- adjusted_pos_error = adjusted_pos_error_h;
- } else {
- adjusted_pos_error = adjusted_pos_error_45;
- }
+ bool is_inside_h, is_inside_45;
+ const auto adjusted_pos_error_h =
+ DoCoerceGoal(pos_poly, LH, wh, drive_error, &is_inside_h);
+ const auto adjusted_pos_error_45 =
+ DoCoerceGoal(pos_poly, L45, w45, intersection, &is_inside_45);
+ if (pos_poly.IsInside(intersection)) {
+ adjusted_pos_error = adjusted_pos_error_h;
+ } else {
+ if (is_inside_h) {
+ if (adjusted_pos_error_h.norm() > adjusted_pos_error_45.norm() ||
+ adjusted_pos_error_45.norm() > intersection.norm()) {
+ adjusted_pos_error = adjusted_pos_error_h;
} else {
adjusted_pos_error = adjusted_pos_error_45;
}
+ } else {
+ adjusted_pos_error = adjusted_pos_error_45;
}
}
+ }
- LOG_MATRIX(DEBUG, "adjusted_pos_error", adjusted_pos_error);
- mutable_U() =
- velocity_K * velocity_error + position_K * T * adjusted_pos_error;
- LOG_MATRIX(DEBUG, "U is now", U());
- } else {
- output_was_capped_ = false;
+ mutable_U() =
+ velocity_K * velocity_error + position_K * T_ * adjusted_pos_error;
+ LOG_MATRIX(DEBUG, "U is now", U());
+
+ if (!output_was_capped_) {
+ if ((U() - U_uncapped()).norm() > 0.0001) {
+ LOG(FATAL, "U unnecessarily capped\n");
+ }
}
}
diff --git a/frc971/control_loops/drivetrain/ssdrivetrain.h b/frc971/control_loops/drivetrain/ssdrivetrain.h
index 1ed3eb7..193dbb2 100644
--- a/frc971/control_loops/drivetrain/ssdrivetrain.h
+++ b/frc971/control_loops/drivetrain/ssdrivetrain.h
@@ -27,9 +27,15 @@
private:
void CapU() override;
- const ::aos::controls::HPolytope<2> U_Poly_;
- Eigen::Matrix<double, 2, 2> T, T_inverse;
- bool output_was_capped_ = false;;
+ // Reprsents +/- full power on each motor in U-space, aka the square from
+ // (-12, -12) to (12, 12).
+ const ::aos::controls::HPolytope<2> U_poly_;
+
+ // multiplying by T converts [left_error, right_error] to
+ // [left_right_error_difference, total_distance_error].
+ Eigen::Matrix<double, 2, 2> T_, T_inverse_;
+
+ bool output_was_capped_ = false;
};
DrivetrainMotorsSS(const DrivetrainConfig &dt_config);