blob: 79454ab7f610c983a377a35d2d6cb88df1c0691d [file] [log] [blame]
#include "y2023/localizer/localizer.h"
#include "absl/flags/declare.h"
#include "absl/flags/flag.h"
#include "absl/flags/reflection.h"
#include "gtest/gtest.h"
#include "aos/events/logging/log_writer.h"
#include "aos/events/simulated_event_loop.h"
#include "frc971/control_loops/drivetrain/drivetrain_test_lib.h"
#include "frc971/control_loops/drivetrain/localizer_generated.h"
#include "frc971/control_loops/pose.h"
#include "frc971/vision/target_map_generated.h"
#include "frc971/vision/target_map_utils.h"
#include "y2023/constants/simulated_constants_sender.h"
#include "y2023/control_loops/drivetrain/drivetrain_base.h"
#include "y2023/localizer/status_generated.h"
ABSL_FLAG(std::string, output_folder, "",
"If set, logs all channels to the provided logfile.");
ABSL_DECLARE_FLAG(double, max_distance_to_target);
namespace y2023::localizer::testing {
using frc971::control_loops::drivetrain::Output;
class LocalizerTest : public ::testing::Test {
protected:
static constexpr uint64_t kTargetId = 1;
LocalizerTest()
: configuration_(aos::configuration::ReadConfig("y2023/aos_config.json")),
event_loop_factory_(&configuration_.message()),
roborio_node_([this]() {
// Get the constants sent before anything else happens.
// It has nothing to do with the roborio node.
SendSimulationConstants(&event_loop_factory_, 7971,
"y2023/constants/test_constants.json");
return aos::configuration::GetNode(&configuration_.message(),
"roborio");
}()),
imu_node_(
aos::configuration::GetNode(&configuration_.message(), "imu")),
camera_node_(
aos::configuration::GetNode(&configuration_.message(), "pi1")),
dt_config_(frc971::control_loops::drivetrain::testing::
GetTestDrivetrainConfig()),
localizer_event_loop_(
event_loop_factory_.MakeEventLoop("localizer", imu_node_)),
localizer_(localizer_event_loop_.get(), dt_config_),
drivetrain_plant_event_loop_(event_loop_factory_.MakeEventLoop(
"drivetrain_plant", roborio_node_)),
drivetrain_plant_imu_event_loop_(
event_loop_factory_.MakeEventLoop("drivetrain_plant", imu_node_)),
drivetrain_plant_(drivetrain_plant_event_loop_.get(),
drivetrain_plant_imu_event_loop_.get(), dt_config_,
std::chrono::microseconds(500)),
roborio_test_event_loop_(
event_loop_factory_.MakeEventLoop("test", roborio_node_)),
imu_test_event_loop_(
event_loop_factory_.MakeEventLoop("test", imu_node_)),
camera_test_event_loop_(
event_loop_factory_.MakeEventLoop("test", camera_node_)),
logger_test_event_loop_(
event_loop_factory_.GetNodeEventLoopFactory("logger")
->MakeEventLoop("test")),
constants_fetcher_(imu_test_event_loop_.get()),
output_sender_(
roborio_test_event_loop_->MakeSender<Output>("/drivetrain")),
target_sender_(
camera_test_event_loop_->MakeSender<frc971::vision::TargetMap>(
"/camera")),
control_sender_(roborio_test_event_loop_->MakeSender<
frc971::control_loops::drivetrain::LocalizerControl>(
"/drivetrain")),
output_fetcher_(
roborio_test_event_loop_
->MakeFetcher<frc971::controls::LocalizerOutput>("/localizer")),
status_fetcher_(
imu_test_event_loop_->MakeFetcher<Status>("/localizer")) {
absl::SetFlag(&FLAGS_max_distance_to_target, 100.0);
{
aos::TimerHandler *timer = roborio_test_event_loop_->AddTimer([this]() {
{
auto builder = output_sender_.MakeBuilder();
auto output_builder = builder.MakeBuilder<Output>();
output_builder.add_left_voltage(output_voltages_(0));
output_builder.add_right_voltage(output_voltages_(1));
builder.CheckOk(builder.Send(output_builder.Finish()));
}
});
roborio_test_event_loop_->OnRun([timer, this]() {
timer->Schedule(roborio_test_event_loop_->monotonic_now(),
std::chrono::milliseconds(5));
});
}
{
// Sanity check that the test calibration files look like what we
// expect.
CHECK_EQ("pi1", constants_fetcher_.constants()
.cameras()
->Get(0)
->calibration()
->node_name()
->string_view());
const Eigen::Matrix<double, 4, 4> H_robot_camera =
frc971::control_loops::drivetrain::FlatbufferToTransformationMatrix(
*constants_fetcher_.constants()
.cameras()
->Get(0)
->calibration()
->fixed_extrinsics());
CHECK_EQ(kTargetId, constants_fetcher_.constants()
.target_map()
->target_poses()
->Get(0)
->id());
const Eigen::Matrix<double, 4, 4> H_field_target = PoseToTransform(
constants_fetcher_.constants().target_map()->target_poses()->Get(0));
// For reference, the camera should pointed straight forwards on the
// robot, offset by 1 meter.
aos::TimerHandler *timer = camera_test_event_loop_->AddTimer(
[this, H_robot_camera, H_field_target]() {
if (!send_targets_) {
return;
}
const frc971::control_loops::Pose robot_pose(
{drivetrain_plant_.GetPosition().x(),
drivetrain_plant_.GetPosition().y(), 0.0},
drivetrain_plant_.state()(2, 0) + implied_yaw_error_);
const Eigen::Matrix<double, 4, 4> H_field_camera =
robot_pose.AsTransformationMatrix() * H_robot_camera;
const Eigen::Matrix<double, 4, 4> H_camera_target =
H_field_camera.inverse() * H_field_target;
const Eigen::Quaterniond quat(H_camera_target.block<3, 3>(0, 0));
const Eigen::Vector3d translation(
H_camera_target.block<3, 1>(0, 3));
auto builder = target_sender_.MakeBuilder();
frc971::vision::Quaternion::Builder quat_builder(*builder.fbb());
quat_builder.add_w(quat.w());
quat_builder.add_x(quat.x());
quat_builder.add_y(quat.y());
quat_builder.add_z(quat.z());
auto quat_offset = quat_builder.Finish();
frc971::vision::Position::Builder position_builder(*builder.fbb());
position_builder.add_x(translation.x());
position_builder.add_y(translation.y());
position_builder.add_z(translation.z());
auto position_offset = position_builder.Finish();
frc971::vision::TargetPoseFbs::Builder target_builder(
*builder.fbb());
target_builder.add_id(send_target_id_);
target_builder.add_position(position_offset);
target_builder.add_orientation(quat_offset);
target_builder.add_pose_error(pose_error_);
target_builder.add_pose_error_ratio(pose_error_ratio_);
auto target_offset = target_builder.Finish();
auto targets_offset = builder.fbb()->CreateVector({target_offset});
frc971::vision::TargetMap::Builder map_builder(*builder.fbb());
map_builder.add_target_poses(targets_offset);
map_builder.add_monotonic_timestamp_ns(
std::chrono::duration_cast<std::chrono::nanoseconds>(
camera_test_event_loop_->monotonic_now().time_since_epoch())
.count());
builder.CheckOk(builder.Send(map_builder.Finish()));
});
camera_test_event_loop_->OnRun([timer, this]() {
timer->Schedule(camera_test_event_loop_->monotonic_now(),
std::chrono::milliseconds(50));
});
}
localizer_control_send_timer_ =
roborio_test_event_loop_->AddTimer([this]() {
auto builder = control_sender_.MakeBuilder();
auto control_builder = builder.MakeBuilder<
frc971::control_loops::drivetrain::LocalizerControl>();
control_builder.add_x(localizer_control_x_);
control_builder.add_y(localizer_control_y_);
control_builder.add_theta(localizer_control_theta_);
control_builder.add_theta_uncertainty(0.01);
control_builder.add_keep_current_theta(false);
builder.CheckOk(builder.Send(control_builder.Finish()));
});
// Get things zeroed.
event_loop_factory_.RunFor(std::chrono::seconds(10));
CHECK(status_fetcher_.Fetch());
CHECK(status_fetcher_->imu()->zeroed());
if (!absl::GetFlag(FLAGS_output_folder).empty()) {
logger_event_loop_ =
event_loop_factory_.MakeEventLoop("logger", imu_node_);
logger_ = std::make_unique<aos::logger::Logger>(logger_event_loop_.get());
logger_->StartLoggingOnRun(absl::GetFlag(FLAGS_output_folder));
}
}
void SendLocalizerControl(double x, double y, double theta) {
localizer_control_x_ = x;
localizer_control_y_ = y;
localizer_control_theta_ = theta;
localizer_control_send_timer_->Schedule(
roborio_test_event_loop_->monotonic_now());
}
::testing::AssertionResult IsNear(double expected, double actual,
double epsilon) {
if (std::abs(expected - actual) < epsilon) {
return ::testing::AssertionSuccess();
} else {
return ::testing::AssertionFailure()
<< "Expected " << expected << " but got " << actual
<< " with a max difference of " << epsilon
<< " and an actual difference of " << std::abs(expected - actual);
}
}
::testing::AssertionResult VerifyEstimatorAccurate(double eps) {
const Eigen::Matrix<double, 5, 1> true_state = drivetrain_plant_.state();
::testing::AssertionResult result(true);
status_fetcher_.Fetch();
if (!(result = IsNear(status_fetcher_->state()->x(), true_state(0), eps))) {
return result;
}
if (!(result = IsNear(status_fetcher_->state()->y(), true_state(1), eps))) {
return result;
}
if (!(result =
IsNear(status_fetcher_->state()->theta(), true_state(2), eps))) {
return result;
}
return result;
}
aos::FlatbufferDetachedBuffer<aos::Configuration> configuration_;
aos::SimulatedEventLoopFactory event_loop_factory_;
const aos::Node *const roborio_node_;
const aos::Node *const imu_node_;
const aos::Node *const camera_node_;
const frc971::control_loops::drivetrain::DrivetrainConfig<double> dt_config_;
std::unique_ptr<aos::EventLoop> localizer_event_loop_;
Localizer localizer_;
std::unique_ptr<aos::EventLoop> drivetrain_plant_event_loop_;
std::unique_ptr<aos::EventLoop> drivetrain_plant_imu_event_loop_;
frc971::control_loops::drivetrain::testing::DrivetrainSimulation
drivetrain_plant_;
std::unique_ptr<aos::EventLoop> roborio_test_event_loop_;
std::unique_ptr<aos::EventLoop> imu_test_event_loop_;
std::unique_ptr<aos::EventLoop> camera_test_event_loop_;
std::unique_ptr<aos::EventLoop> logger_test_event_loop_;
frc971::constants::ConstantsFetcher<Constants> constants_fetcher_;
aos::Sender<Output> output_sender_;
aos::Sender<frc971::vision::TargetMap> target_sender_;
aos::Sender<frc971::control_loops::drivetrain::LocalizerControl>
control_sender_;
aos::Fetcher<frc971::controls::LocalizerOutput> output_fetcher_;
aos::Fetcher<Status> status_fetcher_;
Eigen::Vector2d output_voltages_ = Eigen::Vector2d::Zero();
aos::TimerHandler *localizer_control_send_timer_;
bool send_targets_ = false;
double localizer_control_x_ = 0.0;
double localizer_control_y_ = 0.0;
double localizer_control_theta_ = 0.0;
std::unique_ptr<aos::EventLoop> logger_event_loop_;
std::unique_ptr<aos::logger::Logger> logger_;
uint64_t send_target_id_ = kTargetId;
double pose_error_ = 1e-7;
double pose_error_ratio_ = 0.1;
double implied_yaw_error_ = 0.0;
absl::FlagSaver flag_saver_;
};
// Test a simple scenario with no errors where the robot should just drive
// straight forwards.
TEST_F(LocalizerTest, Nominal) {
output_voltages_ << 1.0, 1.0;
event_loop_factory_.RunFor(std::chrono::seconds(2));
CHECK(output_fetcher_.Fetch());
CHECK(status_fetcher_.Fetch());
// The two can be different because they may've been sent at different
// times.
EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
1e-6);
// Confirm that we did indeed drive forwards (and straight), as expected.
EXPECT_LT(0.1, output_fetcher_->x());
EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
// And check that we actually think that we are near where the simulator
// says we are.
EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
}
// Confirm that when the robot drives backwards that we localize correctly.
TEST_F(LocalizerTest, NominalReverse) {
output_voltages_ << -1.0, -1.0;
event_loop_factory_.RunFor(std::chrono::seconds(2));
CHECK(output_fetcher_.Fetch());
CHECK(status_fetcher_.Fetch());
// The two can be different because they may've been sent at different
// times.
EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
1e-6);
// Confirm that we did indeed drive backwards (and straight), as expected.
EXPECT_GT(-0.1, output_fetcher_->x());
EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
// And check that we actually think that we are near where the simulator
// says we are.
EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
}
// Confirm that when the robot turns counter-clockwise that we localize
// correctly.
TEST_F(LocalizerTest, NominalSpinInPlace) {
output_voltages_ << -1.0, 1.0;
// Go 1 ms over 2 sec to make sure we actually see relatively recent messages
// on each channel.
event_loop_factory_.RunFor(std::chrono::milliseconds(2001));
CHECK(output_fetcher_.Fetch());
CHECK(status_fetcher_.Fetch());
// The two can be different because they may've been sent at different
// times.
EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-6);
EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-6);
EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
1e-2);
// Confirm that we did indeed turn counter-clockwise.
EXPECT_NEAR(0.0, output_fetcher_->x(), 1e-10);
EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
EXPECT_LT(0.1, output_fetcher_->theta());
EXPECT_NEAR(0.0, status_fetcher_->state()->left_voltage_error(), 1e-1);
EXPECT_NEAR(0.0, status_fetcher_->state()->right_voltage_error(), 1e-1);
// And check that we actually think that we are near where the simulator
// says we are.
EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
}
// Confirm that when the robot drives in a curve that we localize
// successfully.
TEST_F(LocalizerTest, NominalCurve) {
output_voltages_ << 2.0, 3.0;
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(output_fetcher_.Fetch());
CHECK(status_fetcher_.Fetch());
// The two can be different because they may've been sent at different
// times.
EXPECT_NEAR(output_fetcher_->x(), status_fetcher_->state()->x(), 1e-2);
EXPECT_NEAR(output_fetcher_->y(), status_fetcher_->state()->y(), 1e-2);
EXPECT_NEAR(output_fetcher_->theta(), status_fetcher_->state()->theta(),
1e-2);
// Confirm that we did indeed drive in a rough, counter-clockwise, curve.
EXPECT_LT(0.1, output_fetcher_->x());
EXPECT_LT(0.1, output_fetcher_->y());
EXPECT_LT(0.1, output_fetcher_->theta());
// And check that we actually think that we are near where the simulator
// says we are.
EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
}
// Tests that, in the presence of a non-zero voltage error, that we correct
// for it.
TEST_F(LocalizerTest, VoltageErrorDisabled) {
output_voltages_ << 0.0, 0.0;
drivetrain_plant_.set_left_voltage_offset(2.0);
drivetrain_plant_.set_right_voltage_offset(2.0);
event_loop_factory_.RunFor(std::chrono::seconds(2));
CHECK(output_fetcher_.Fetch());
CHECK(status_fetcher_.Fetch());
// We should've just ended up driving straight forwards.
EXPECT_LT(0.1, output_fetcher_->x());
EXPECT_NEAR(0.0, output_fetcher_->y(), 1e-10);
EXPECT_NEAR(0.0, output_fetcher_->theta(), 1e-10);
EXPECT_NEAR(2.0, status_fetcher_->state()->left_voltage_error(), 1.0);
EXPECT_NEAR(2.0, status_fetcher_->state()->right_voltage_error(), 1.0);
// And check that we actually think that we are near where the simulator
// says we are.
EXPECT_TRUE(VerifyEstimatorAccurate(0.05));
}
// Tests that image corrections in the nominal case (no errors) causes no
// issues.
TEST_F(LocalizerTest, NominalImageCorrections) {
output_voltages_ << 3.0, 2.0;
send_targets_ = true;
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(status_fetcher_.Fetch());
EXPECT_TRUE(VerifyEstimatorAccurate(1e-2));
ASSERT_TRUE(status_fetcher_->has_statistics());
ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
ASSERT_EQ(status_fetcher_->statistics()->Get(0)->total_candidates(),
status_fetcher_->statistics()->Get(0)->total_accepted());
ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
}
// Tests that image corrections when there is an error at the start results
// in us actually getting corrected over time.
TEST_F(LocalizerTest, ImageCorrections) {
output_voltages_ << 0.0, 0.0;
drivetrain_plant_.mutable_state()->x() = 2.0;
drivetrain_plant_.mutable_state()->y() = 2.0;
SendLocalizerControl(5.0, 3.0, 0.0);
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(output_fetcher_.Fetch());
ASSERT_NEAR(5.0, output_fetcher_->x(), 1e-5);
ASSERT_NEAR(3.0, output_fetcher_->y(), 1e-5);
ASSERT_NEAR(0.0, output_fetcher_->theta(), 1e-5);
send_targets_ = true;
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(status_fetcher_.Fetch());
EXPECT_TRUE(VerifyEstimatorAccurate(0.1));
ASSERT_TRUE(status_fetcher_->has_statistics());
ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
ASSERT_EQ(status_fetcher_->statistics()->Get(0)->total_candidates(),
status_fetcher_->statistics()->Get(0)->total_accepted());
ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
}
// Tests that we correctly reject an invalid target.
TEST_F(LocalizerTest, InvalidTargetId) {
output_voltages_ << 0.0, 0.0;
send_targets_ = true;
send_target_id_ = 100;
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(status_fetcher_.Fetch());
ASSERT_TRUE(status_fetcher_->has_statistics());
ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
ASSERT_EQ(status_fetcher_->statistics()
->Get(0)
->rejection_reasons()
->Get(static_cast<size_t>(RejectionReason::NO_SUCH_TARGET))
->count(),
status_fetcher_->statistics()->Get(0)->total_candidates());
}
// Tests that we correctly reject a detection with a high pose error.
TEST_F(LocalizerTest, HighPoseError) {
output_voltages_ << 0.0, 0.0;
send_targets_ = true;
// Send the minimum pose error to be rejected
constexpr double kEps = 1e-9;
pose_error_ = 1e-6 + kEps;
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(status_fetcher_.Fetch());
ASSERT_TRUE(status_fetcher_->has_statistics());
ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
ASSERT_EQ(status_fetcher_->statistics()
->Get(0)
->rejection_reasons()
->Get(static_cast<size_t>(RejectionReason::HIGH_POSE_ERROR))
->count(),
status_fetcher_->statistics()->Get(0)->total_candidates());
}
// Tests that we correctly reject a detection with a high implied yaw error.
TEST_F(LocalizerTest, HighImpliedYawError) {
output_voltages_ << 0.0, 0.0;
send_targets_ = true;
implied_yaw_error_ = 31.0 * M_PI / 180.0;
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(status_fetcher_.Fetch());
ASSERT_TRUE(status_fetcher_->has_statistics());
ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
ASSERT_EQ(
status_fetcher_->statistics()
->Get(0)
->rejection_reasons()
->Get(static_cast<size_t>(RejectionReason::HIGH_IMPLIED_YAW_ERROR))
->count(),
status_fetcher_->statistics()->Get(0)->total_candidates());
}
// Tests that we correctly reject a detection with a high pose error ratio.
TEST_F(LocalizerTest, HighPoseErrorRatio) {
output_voltages_ << 0.0, 0.0;
send_targets_ = true;
// Send the minimum pose error to be rejected
constexpr double kEps = 1e-9;
pose_error_ratio_ = 0.4 + kEps;
event_loop_factory_.RunFor(std::chrono::seconds(4));
CHECK(status_fetcher_.Fetch());
ASSERT_TRUE(status_fetcher_->has_statistics());
ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
ASSERT_EQ(
status_fetcher_->statistics()
->Get(0)
->rejection_reasons()
->Get(static_cast<size_t>(RejectionReason::HIGH_POSE_ERROR_RATIO))
->count(),
status_fetcher_->statistics()->Get(0)->total_candidates());
}
} // namespace y2023::localizer::testing