From b30861a097a3eda09149745e8c993ec7b6c63273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Gr=C3=B6ger?= <tobias.groeger@student.kit.edu> Date: Mon, 5 Sep 2022 19:17:10 +0200 Subject: [PATCH] Some kalman filter updates --- source/armarx/navigation/human/CMakeLists.txt | 35 +- .../human/UnscentedKalmanFilter_impl.cpp | 3 + .../navigation/human/test/manifKalmanTest.cpp | 462 ++++++++++++++++++ .../human/test/se2KalmanFilterTest.cpp | 295 +++++++++++ .../human/test/se2xVkalmanFilterTest.cpp | 295 +++++++++++ ...FilterTest.cpp => so2kalmanFilterTest.cpp} | 11 +- 6 files changed, 1094 insertions(+), 7 deletions(-) create mode 100644 source/armarx/navigation/human/test/manifKalmanTest.cpp create mode 100644 source/armarx/navigation/human/test/se2KalmanFilterTest.cpp create mode 100644 source/armarx/navigation/human/test/se2xVkalmanFilterTest.cpp rename source/armarx/navigation/human/test/{kalmanFilterTest.cpp => so2kalmanFilterTest.cpp} (98%) diff --git a/source/armarx/navigation/human/CMakeLists.txt b/source/armarx/navigation/human/CMakeLists.txt index f3b7cc38..2ffaa3d1 100644 --- a/source/armarx/navigation/human/CMakeLists.txt +++ b/source/armarx/navigation/human/CMakeLists.txt @@ -30,9 +30,9 @@ armarx_add_library(teb_human find_package(sciplot) -armarx_add_test(kalman_test +armarx_add_test(so2_kalman_test TEST_FILES - test/kalmanFilterTest.cpp + test/so2kalmanFilterTest.cpp DEPENDENCIES PUBLIC ArmarXCore @@ -40,6 +40,37 @@ armarx_add_test(kalman_test sciplot::sciplot ) +armarx_add_test(se2_kalman_test + TEST_FILES + test/se2KalmanFilterTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::teb_human + sciplot::sciplot +) + +armarx_add_test(se2xV_kalman_test + TEST_FILES + test/se2xVkalmanFilterTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::teb_human + sciplot::sciplot +) + +armarx_add_test(manif_kalman_test + TEST_FILES + test/manifKalmanTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::teb_human + sciplot::sciplot +) + + armarx_add_test(se3_kalman_test TEST_FILES test/UnscentedKalmanFilterTest.cpp diff --git a/source/armarx/navigation/human/UnscentedKalmanFilter_impl.cpp b/source/armarx/navigation/human/UnscentedKalmanFilter_impl.cpp index 672e487f..168a9e03 100644 --- a/source/armarx/navigation/human/UnscentedKalmanFilter_impl.cpp +++ b/source/armarx/navigation/human/UnscentedKalmanFilter_impl.cpp @@ -7,3 +7,6 @@ template class UnscentedKalmanFilter<armarx::navigation::components::dynamic_sce kalman_filter::SystemModelSO2xR2<float>>; template class UnscentedKalmanFilter<armarx::navigation::components::dynamic_scene_provider:: kalman_filter::SystemModelSO2xR2<double>>; + +template class UnscentedKalmanFilter< + armarx::navigation::components::dynamic_scene_provider::kalman_filter::SystemModelSE2<double>>; diff --git a/source/armarx/navigation/human/test/manifKalmanTest.cpp b/source/armarx/navigation/human/test/manifKalmanTest.cpp new file mode 100644 index 00000000..4940ab7c --- /dev/null +++ b/source/armarx/navigation/human/test/manifKalmanTest.cpp @@ -0,0 +1,462 @@ +/** + * \file se2_localization_ukfm.cpp + * + * Created on: Dec 10, 2018 + * \author: artivis + * + * --------------------------------------------------------- + * This file is: + * (c) 2021 Jeremie Deray + * + * This file is part of `manif`, a C++ template-only library + * for Lie theory targeted at estimation for robotics. + * Manif is: + * (c) 2018 Jeremie Deray @ IRI-UPC, Barcelona + * --------------------------------------------------------- + * + * --------------------------------------------------------- + * Demonstration example: + * + * 2D Robot localization based on fixed beacons. + * + * See se3_localization.cpp for the 3D equivalent. + * See se3_sam.cpp for a more advanced example performing smoothing and mapping. + * --------------------------------------------------------- + * + * This demo showcases an application of an Unscented Kalman Filter on Manifold, + * based on the paper + * 'A Code for Unscented Kalman Filtering on Manifolds (UKF-M)' + * [https://arxiv.org/pdf/2002.00878.pdf], M. Brossard, A. Barrau and S. Bonnabel. + * + * The following is an abstract of the example hereafter. + * Please consult the aforemention paper for better UKF-M reference + * and the paper Sola-18, [https://arxiv.org/abs/1812.01537] for general + * Lie group reference. + * + * + * We consider a robot in the plane surrounded by a small + * number of punctual landmarks or _beacons_. + * The robot receives control actions in the form of axial + * and angular velocities, and is able to measure the location + * of the beacons w.r.t its own reference frame. + * + * The robot pose X is in SE(2) and the beacon positions b_k in R^2, + * + * | cos th -sin th x | + * X = | sin th cos th y | // position and orientation + * | 0 0 1 | + * + * b_k = (bx_k, by_k) // lmk coordinates in world frame + * + * The control signal u is a twist in se(2) comprising longitudinal + * velocity v and angular velocity w, with no lateral velocity + * component, integrated over the sampling time dt. + * + * u = (v*dt, 0, w*dt) + * + * The control is corrupted by additive Gaussian noise u_noise, + * with covariance + * + * Q = diagonal(sigma_v^2, sigma_s^2, sigma_w^2). + * + * This noise accounts for possible lateral slippage u_s + * through a non-zero value of sigma_s, + * + * At the arrival of a control u, the robot pose is updated + * with X <-- X * Exp(u) = X + u. + * + * Landmark measurements are of the range and bearing type, + * though they are put in Cartesian form for simplicity. + * Their noise n is zero mean Gaussian, and is specified + * with a covariances matrix R. + * We notice the rigid motion action y = h(X,b) = X^-1 * b + * (see appendix C), + * + * y_k = (brx_k, bry_k) // lmk coordinates in robot frame + * + * We consider the beacons b_k situated at known positions. + * We define the pose to estimate as X in SE(2). + * The estimation error dx and its covariance P are expressed + * in the tangent space at X. + * + * All these variables are summarized again as follows + * + * X : robot pose, SE(2) + * u : robot control, (v*dt ; 0 ; w*dt) in se(2) + * Q : control perturbation covariance + * b_k : k-th landmark position, R^2 + * y : Cartesian landmark measurement in robot frame, R^2 + * R : covariance of the measurement noise + * + * The motion and measurement models are + * + * X_(t+1) = f(X_t, u) = X_t * Exp ( w ) // motion equation + * y_k = h(X, b_k) = X^-1 * b_k // measurement equation + * + * The algorithm below comprises first a simulator to + * produce measurements, then uses these measurements + * to estimate the state, using a Lie-based error-state Kalman filter. + * + * This file has plain code with only one main() function. + * There are no function calls other than those involving `manif`. + * + * Printing simulated state and estimated state together + * with an unfiltered state (i.e. without Kalman corrections) + * allows for evaluating the quality of the estimates. + */ + +#include <iomanip> +#include <iostream> +#include <tuple> +#include <vector> + +#include <Eigen/Cholesky> + +#include "manif/SE2.h" +#include <sciplot/sciplot.hpp> + +using std::cout; +using std::endl; + +using namespace Eigen; + +typedef Array<double, 2, 1> Array2d; +typedef Array<double, 3, 1> Array3d; + +template <typename Scalar> +struct Weights +{ + Weights() = default; + ~Weights() = default; + + Weights(const Scalar l, const Scalar alpha) + { + using std::sqrt; + + const Scalar m = (alpha * alpha - 1) * l; + const Scalar ml = m + l; + + sqrt_d_lambda = sqrt(ml); + wj = Scalar(1) / (Scalar(2) * (ml)); + wm = m / (ml); + w0 = m / (ml) + Scalar(3) - alpha * alpha; + } + + Scalar sqrt_d_lambda; + Scalar wj; + Scalar wm; + Scalar w0; +}; + +using Weightsd = Weights<double>; + +template <typename Scalar> +std::tuple<Weights<Scalar>, Weights<Scalar>, Weights<Scalar>> +compute_sigma_weights(const Scalar state_size, + const Scalar propagation_noise_size, + const Scalar alpha_0, + const Scalar alpha_1, + const Scalar alpha_2) +{ + assert(state_size > 0); + assert(propagation_noise_size > 0); + assert(alpha_0 >= 1e-3 && alpha_0 <= 1); + assert(alpha_1 >= 1e-3 && alpha_1 <= 1); + assert(alpha_2 >= 1e-3 && alpha_2 <= 1); + + return std::make_tuple(Weights<Scalar>(state_size, alpha_0), + Weights<Scalar>(propagation_noise_size, alpha_1), + Weights<Scalar>(state_size, alpha_2)); +} + +int +main() +{ + std::srand((unsigned int)time(0)); + + // START CONFIGURATION + // + // + const int NUMBER_OF_LMKS_TO_MEASURE = 3; + constexpr int DoF = manif::SE2d::DoF; + constexpr int SystemNoiseSize = manif::SE2d::DoF; + // Measurement Dim + constexpr int Rp = 2; + + // Define the robot pose element and its covariance + manif::SE2d X = manif::SE2d::Identity(), X_simulation = manif::SE2d::Identity(), + X_unfiltered = manif::SE2d::Identity(); + Matrix3d P = Matrix3d::Identity() * 1e-6; + + // Define a control vector and its noise and covariance + manif::SE2Tangentd u_simu, u_est, u_unfilt; + Vector3d u_nom, u_noisy, u_noise; + Array3d u_sigmas; + Matrix3d U, Uchol; + + u_nom << 0.1, 0.0, 0.05; + u_sigmas << 0.1, 0.1, 0.1; + U = (u_sigmas * u_sigmas).matrix().asDiagonal(); + Uchol = U.llt().matrixL(); + + // Define three landmarks in R^2 + Eigen::Vector2d b; + const std::vector<Eigen::Vector2d> landmarks{ + Eigen::Vector2d(2.0, 0.0), Eigen::Vector2d(2.0, 1.0), Eigen::Vector2d(2.0, -1.0)}; + + // Define the beacon's measurements + Vector2d y, y_bar, y_noise; + Matrix<double, Rp, 2 * DoF> yj; + Array2d y_sigmas; + Matrix2d R; + std::vector<Vector2d> measurements(landmarks.size()); + + y_sigmas << 0.01, 0.01; + R = (y_sigmas * y_sigmas).matrix().asDiagonal(); + + + // Declare UFK variables + Array3d alpha; + alpha << 1e-3, 1e-3, 1e-3; + + Weightsd w_d, w_q, w_u; + std::tie(w_d, w_q, w_u) = compute_sigma_weights<double>(DoF, Rp, alpha(0), alpha(1), alpha(2)); + + // Declare some temporaries + + manif::SE2d X_new; + Matrix3d P_new; + manif::SE2d s_j_p, s_j_m; + Vector3d xi_mean; + Vector3d w_p, w_m; + + Matrix2d P_yy; + Matrix<double, DoF, 2 * DoF> xij; + Matrix<double, DoF, 2> P_xiy; + + Vector2d e, z; // expectation, innovation + Matrix<double, 3, 2> K; // Kalman gain + manif::SE2Tangentd dx; // optimal update step, or error-state + + Matrix<double, DoF, DoF> xis; + Matrix<double, DoF, DoF * 2> xis_new; + Matrix<double, DoF, SystemNoiseSize * 2> xis_new2; + + // + // + // CONFIGURATION DONE + + + // DEBUG + cout << std::fixed << std::setprecision(4) << std::showpos << endl; + cout << "X STATE : X Y THETA" << endl; + cout << "----------------------------------" << endl; + cout << "X initial : " << X_simulation.log().coeffs().transpose() << endl; + cout << "----------------------------------" << endl; + // END DEBUG + + + std::vector<double> X_simulated_x, X_simulated_y, X_simulated_theta; + std::vector<double> X_estimated_x, X_estimated_y, X_estimated_theta; + std::vector<double> X_unfiltered_x, X_unfiltered_y, X_unfiltered_theta; + + // START TEMPORAL LOOP + // + // + + // Make 10 steps. Measure up to three landmarks each time. + for (int t = 0; t < 10; t++) + { + //// I. Simulation ############################################################################### + + /// simulate noise + u_noise = u_sigmas * Array3d::Random(); // control noise + u_noisy = u_nom + u_noise; // noisy control + + u_simu = u_nom; + u_est = u_noisy; + u_unfilt = u_noisy; + + /// first we move - - - - - - - - - - - - - - - - - - - - - - - - - - - - + X_simulation = X_simulation + u_simu; // overloaded X.rplus(u) = X * exp(u) + + /// then we measure all landmarks - - - - - - - - - - - - - - - - - - - - + for (std::size_t i = 0; i < landmarks.size(); i++) + { + b = landmarks[i]; // lmk coordinates in world frame + + /// simulate noise + y_noise = y_sigmas * Array2d::Random(); // measurement noise + + y = X_simulation.inverse().act(b); // landmark measurement, before adding noise + y = y + y_noise; // landmark measurement, noisy + measurements[i] = y; // store for the estimator just below + } + + + //// II. Estimation ############################################################################### + + /// First we move - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + X_new = X + u_est; // X * exp(u) + + // set sigma points + xis = w_d.sqrt_d_lambda * P.llt().matrixL().toDenseMatrix(); + + // sigma points on manifold + for (int i = 0; i < DoF; ++i) + { + s_j_p = X + manif::SE2Tangentd(xis.col(i)); + s_j_m = X + manif::SE2Tangentd(-xis.col(i)); + + xis_new.col(i) = (X_new.lminus(s_j_p + u_est)).coeffs(); + xis_new.col(i + DoF) = (X_new.lminus(s_j_m + u_est)).coeffs(); + } + + // compute covariance + xi_mean = w_d.wj * xis_new.rowwise().sum(); + xis_new.colwise() -= xi_mean; + + P_new = w_d.wj * xis_new * xis_new.transpose() + w_d.w0 * xi_mean * xi_mean.transpose(); + + // sigma points on manifold + for (int i = 0; i < SystemNoiseSize; ++i) + { + w_p = w_q.sqrt_d_lambda * Uchol.col(i); + w_m = -w_q.sqrt_d_lambda * Uchol.col(i); + + xis_new2.col(i) = (X_new.lminus(X + (u_est + w_p))).coeffs(); + xis_new2.col(i + SystemNoiseSize) = (X_new.lminus(X + (u_est + w_m))).coeffs(); + } + + xi_mean = w_q.wj * xis_new2.rowwise().sum(); + xis_new2.colwise() -= xi_mean; + + U = w_q.wj * xis_new2 * xis_new2.transpose() + w_q.w0 * xi_mean * xi_mean.transpose(); + + P = P_new + U; + + X = X_new; + + /// Then we correct using the measurements of each lmk - - - - - - - - - + for (int i = 0; i < NUMBER_OF_LMKS_TO_MEASURE; i++) + { + // landmark + b = landmarks[i]; // lmk coordinates in world frame + + // measurement + y = measurements[i]; // lmk measurement, noisy + + // expectation + e = X.inverse().act(b); + + // set sigma points + xis = w_u.sqrt_d_lambda * P.llt().matrixL().toDenseMatrix(); + + // compute measurement sigma points + for (int d = 0; d < DoF; ++d) + { + s_j_p = X + manif::SE2Tangentd(xis.col(d)); + s_j_m = X + manif::SE2Tangentd(-xis.col(d)); + + yj.col(d) = s_j_p.inverse().act(b); + yj.col(d + DoF) = s_j_m.inverse().act(b); + } + + // measurement mean + y_bar = w_u.wm * e + w_u.wj * yj.rowwise().sum(); + + yj.colwise() -= y_bar; + e -= y_bar; + + // compute covariance and cross covariance matrices + P_yy = w_u.w0 * e * e.transpose() + w_u.wj * yj * yj.transpose() + R; + + xij << xis, -xis; + P_xiy = w_u.wj * xij * yj.transpose(); + + // Kalman gain + K = P_yy.colPivHouseholderQr().solve(P_xiy.transpose()).transpose(); + + // innovation + z = y - y_bar; + + // Correction step + dx = K * z; // dx is in the tangent space at X + + // Update + X = X + dx; // overloaded X.rplus(dx) = X * exp(dx) + P = P - K * P_yy * K.transpose(); + } + + + //// III. Unfiltered ############################################################################## + + // move also an unfiltered version for comparison purposes + X_unfiltered = X_unfiltered + u_unfilt; + + + //// IV. Results ############################################################################## + + // DEBUG + cout << "X simulated : " << X_simulation.log().coeffs().transpose() << "\n"; + cout << "X estimated : " << X.log().coeffs().transpose() << "\n"; + cout << "X unfilterd : " << X_unfiltered.log().coeffs().transpose() << "\n"; + cout << "----------------------------------" << endl; + + const auto& X_simu = X_simulation.log().coeffs().transpose(); + const auto& X_est = X.log().coeffs().transpose(); + const auto& X_unf = X_unfiltered.log().coeffs().transpose(); + X_simulated_x.push_back(X_simu(0)); + X_simulated_y.push_back(X_simu(1)); + X_simulated_theta.push_back(X_simu(2)); + X_estimated_x.push_back(X_est(0)); + X_estimated_y.push_back(X_est(1)); + X_estimated_theta.push_back(X_est(2)); + X_unfiltered_x.push_back(X_unf(0)); + X_unfiltered_y.push_back(X_unf(1)); + X_unfiltered_theta.push_back(X_unf(2)); + // END DEBUG + } + + sciplot::Plot2D pos_plot; + pos_plot.xlabel("x"); + pos_plot.ylabel("y"); + pos_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + pos_plot.xrange("*", "*").yrange("*", "*"); + + pos_plot.drawCurve(X_simulated_x, X_simulated_y).label("Simulated"); + pos_plot.drawCurve(X_estimated_x, X_estimated_y).label("Estimated"); + pos_plot.drawCurve(X_unfiltered_x, X_unfiltered_y).label("Unfiltered"); + + + sciplot::Plot2D orientation_plot; + orientation_plot.xlabel("t"); + orientation_plot.ylabel("rot"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange("*", "*").yrange("*", "*"); + + sciplot::Vec x = sciplot::linspace(0.0, 1.0, X_estimated_theta.size()); + orientation_plot.drawCurve(x, X_simulated_theta).label("Simulated"); + orientation_plot.drawCurve(x, X_estimated_theta).label("Estimated"); + orientation_plot.drawCurve(x, X_unfiltered_theta).label("Unfiltered"); + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot, orientation_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(1000, 500); + + // Show the plot in a pop-up window + canvas.show(); + + // Save the plot to a PDF file + canvas.save("manif_kalman_output.pdf"); + + + // + // + // END OF TEMPORAL LOOP. DONE. + + return 0; +} diff --git a/source/armarx/navigation/human/test/se2KalmanFilterTest.cpp b/source/armarx/navigation/human/test/se2KalmanFilterTest.cpp new file mode 100644 index 00000000..61d37530 --- /dev/null +++ b/source/armarx/navigation/human/test/se2KalmanFilterTest.cpp @@ -0,0 +1,295 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::human +#define ARMARX_BOOST_TEST + +#include <cstdlib> /* srand, rand */ +#include <ctime> /* time */ + +#include <ArmarXCore/core/exceptions/local/ExpressionException.h> +#include <ArmarXCore/core/logging/Logging.h> +#include <ArmarXCore/util/time.h> + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <armarx/navigation/Test.h> +#include <armarx/navigation/human/HumanSystemModel.h> +#include <manif/SO2.h> +#include <sciplot/sciplot.hpp> + +namespace armarx::navigation::components::dynamic_scene_provider +{ + using T = double; //TODO double or float? + using Vector = Eigen::Matrix<T, 2, 1>; + using SystemModelT = kalman_filter::SystemModelSE2<T>; + + constexpr T RAD = 1; + constexpr long num_timesteps = 3000; + constexpr T max_time = 1; + constexpr T dt = max_time / num_timesteps; + constexpr T c = (1 / max_time) * 2 * M_PI; // rotation per time + + constexpr T rot_noise_std = 0.1; + constexpr T pos_noise_std = 0.1; + constexpr T obs_rot_noise_std = 0.1; + constexpr T obs_pos_noise_std = 0.1; + constexpr T initial_offset_angle = 1 * 10 * M_PI / 180; + const Vector initial_offet_pos = 0.4 * RAD * Vector(1, 0.5); + + + void + simulate_trajectory(std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ControlT>& omegas) + { + constexpr T startAngle = 0; + + SystemModelT::StateT state; + state.pose = manif::SE2<T>(RAD, 0, startAngle); + states.push_back(state); + + for (int i = 1; i < num_timesteps; i++) + { + const T t = dt * i; + const T angle = t * c; + + const Vector last_pos = states.at(i - 1).pose.translation(); + //const Vector pos(angle, std::cos(angle)); + const Vector pos = RAD * Vector(std::cos(angle), std::sin(angle)); + + BOOST_TEST(((last_pos - pos).norm() < 0.1 * RAD), + "position differs too much from last step: " << (last_pos - pos).norm()); + + Eigen::Vector2d euclidean_velocity = Eigen::Vector2d{0, c}; + SystemModelT::ControlT control; + //control.velocity.coeffs() << (pos - last_pos) / dt, c; + control.velocity.coeffs() << euclidean_velocity, c; + + const SystemModelT::StateT propagated = SystemModelT::propagationFunction( + state, control, SystemModelT::ControlNoiseT::Zero(), dt); + + state.pose = manif::SE2<T>(pos.x(), pos.y(), angle + startAngle); + + T pos_diff = (propagated.pose.translation() - state.pose.translation()).norm(); + BOOST_TEST(pos_diff < 2e-10, + "propagated position differs too much from real position: " << pos_diff); + + T rot_diff = propagated.pose.angle() - state.pose.angle(); + BOOST_TEST(rot_diff < 2e-10, + "propagated rotation differs too much from real orientation: " << rot_diff); + + states.push_back(propagated); + + + // add noise + control.velocity.coeffs().segment(0, 2) += pos_noise_std * Vector::Random(); + control.velocity.coeffs().segment(2, 1) += + rot_noise_std * Eigen::Matrix<T, 1, 1>::Random(); + omegas.push_back(control); + } + } + + void + simulate_observation(const std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ObsT>& observations) + { + for (const auto& state : states) + { + SystemModelT::ObsT obs = SystemModelT::observationFunction(state); + + SystemModelT::ObsT noise = SystemModelT::ObsT::Random(); + noise.segment(0, 2) *= obs_pos_noise_std; + noise.segment(2, 1) *= obs_rot_noise_std; + + BOOST_TEST_MESSAGE("SALT TEST: noise " << noise); + + observations.push_back(obs + noise); + + BOOST_TEST_MESSAGE("SALT TEST: state:\n" + << state.pose.x() << "\n" + << state.pose.y() << "\n" + << state.pose.angle()); + + BOOST_TEST_MESSAGE("SALT TEST: state.log:\n" << state.pose.log().coeffs()); + + BOOST_TEST_MESSAGE("SALT TEST: noisy obs:\n" << (obs + noise)); + + SystemModelT::ObsT diff = + (obs + noise) - + SystemModelT::ObsT(state.pose.x(), state.pose.y(), state.pose.angle()); + + BOOST_TEST_MESSAGE("\nSALT TEST: diff:\n" << diff); + } + } + + + BOOST_AUTO_TEST_CASE(se2KalmanFilterTest) + { + srand(time(NULL)); + + std::vector<SystemModelT::StateT> states; + std::vector<SystemModelT::ControlT> omegas; + std::vector<SystemModelT::ObsT> observations; + simulate_trajectory(states, omegas); + simulate_observation(states, observations); + + ARMARX_INFO << "Num States: " << states.size() << " Num Controls: " << omegas.size() + << " Num Obs: " << observations.size(); + + UnscentedKalmanFilter<SystemModelT>::PropCovT Q = + UnscentedKalmanFilter<SystemModelT>::PropCovT::Identity(); + Q.block<2, 2>(0, 0) *= pos_noise_std * pos_noise_std; + Q.block<1, 1>(2, 2) *= rot_noise_std * rot_noise_std; + + UnscentedKalmanFilter<SystemModelT>::ObsCovT R = + UnscentedKalmanFilter<SystemModelT>::ObsCovT::Identity(); + R.block<2, 2>(0, 0) *= obs_pos_noise_std * obs_pos_noise_std; + R.block<1, 1>(2, 2) *= obs_rot_noise_std * obs_rot_noise_std; + + UnscentedKalmanFilter<SystemModelT>::StateCovT P0 = + UnscentedKalmanFilter<SystemModelT>::StateCovT::Identity(); + P0.block<2, 2>(0, 0) *= initial_offet_pos.norm() * initial_offet_pos.norm(); + P0.block<1, 1>(2, 2) *= initial_offset_angle * initial_offset_angle; + + UnscentedKalmanFilter<SystemModelT>::AlphaT alpha; + alpha(0) = 0.5; //0.8; // state + alpha(1) = 0.5; //0.1; // control + alpha(2) = 0.5; //0.1; // update + + SystemModelT::StateT state0; + const Vector startPos = states.at(0).pose.translation() + initial_offet_pos; + state0.pose = manif::SE2<T>{ + startPos.x(), startPos.y(), states.at(0).pose.angle() + initial_offset_angle}; + + UnscentedKalmanFilter<SystemModelT> ukf(Q, R, alpha, state0, P0); + + std::vector<SystemModelT::StateT> ukf_states; + std::vector<UnscentedKalmanFilter<SystemModelT>::StateCovT> ukf_Ps; + ukf_states.push_back(state0); + ukf_Ps.push_back(P0); + + std::vector<T> x_true, y_true, x_obs, y_obs, x_ukf, y_ukf; + std::vector<T> a_true, a_obs, a_ukf; + std::vector<T> x_ukf_full, y_ukf_full, a_ukf_full; + std::vector<T> ukf_pos_var, ukf_rot_var, ukf_var; + + for (int i = 1; i < num_timesteps; i++) + { + // propagate + TIMING_START(LOOP); + TIMING_START(PROPAGATION); + ukf.propagation(omegas.at(i - 1), dt); + TIMING_END(PROPAGATION); + if ((i - 1) % 50 == 0) + { + TIMING_START(UPDATE); + ukf.update(observations.at(i)); + TIMING_END(UPDATE); + TIMING_START(REST); + + BOOST_TEST_MESSAGE("Max Cov " + << ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + BOOST_TEST_MESSAGE("Diag: " << (ukf.getCurrentStateCovariance() - + Eigen::Matrix<T, 3, 3>::Identity()) + .norm()); + + x_obs.push_back(observations.at(i)(0)); + y_obs.push_back(observations.at(i)(1)); + a_obs.push_back(observations.at(i)(2)); + + x_ukf.push_back(ukf.getCurrentState().pose.x()); + y_ukf.push_back(ukf.getCurrentState().pose.y()); + a_ukf.push_back(ukf.getCurrentState().pose.angle()); + + TIMING_END(REST); + } + x_true.push_back(states.at(i).pose.x()); + y_true.push_back(states.at(i).pose.y()); + a_true.push_back(states.at(i).pose.angle()); + + x_ukf_full.push_back(ukf.getCurrentState().pose.x()); + y_ukf_full.push_back(ukf.getCurrentState().pose.y()); + a_ukf_full.push_back(ukf.getCurrentState().pose.angle()); + + ukf_states.push_back(ukf.getCurrentState()); + ukf_Ps.push_back(ukf.getCurrentStateCovariance()); + + ukf_pos_var.push_back( + ukf.getCurrentStateCovariance().block<2, 2>(0, 0).cwiseAbs().maxCoeff()); + ukf_rot_var.push_back( + ukf.getCurrentStateCovariance().block<1, 1>(2, 2).cwiseAbs().maxCoeff()); + ukf_var.push_back(ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + + TIMING_END(LOOP); + } + + sciplot::Plot2D pos_plot; + pos_plot.xlabel("x"); + pos_plot.ylabel("y"); + pos_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + pos_plot.xrange(-RAD * 1.1, RAD * 1.1).yrange(-RAD * 1.1, RAD * 1.1); + //pos_plot.xrange(-4, 4).yrange(-4, 4); + + pos_plot.drawCurve(x_true, y_true).label("True"); + pos_plot.drawCurve(x_obs, y_obs).label("Obs"); + pos_plot.drawCurveWithPoints(x_ukf, y_ukf).label("UKF"); + pos_plot.drawCurve(x_ukf_full, y_ukf_full).label("Full UKF"); + + + sciplot::Plot2D orientation_plot; + orientation_plot.xlabel("x"); + orientation_plot.ylabel("y"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange(-0.1, 1.1).yrange(-4, 4); + + sciplot::Vec x = sciplot::linspace(0.0, 1.0, a_obs.size()); + sciplot::Vec x_t = sciplot::linspace(0.0, 1.0, a_true.size()); + orientation_plot.drawCurve(x_t, a_true).label("True").lineWidth(1); + orientation_plot.drawCurve(x, a_obs).label("Obs").lineWidth(1); + orientation_plot.drawCurve(x, a_ukf).label("UKF").lineWidth(1); + orientation_plot.drawCurve(x_t, a_ukf_full).label("Full UKF").lineWidth(1); + + + sciplot::Plot2D error_plot; + error_plot.xlabel("x"); + error_plot.ylabel("y"); + error_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + error_plot.xrange(-0.1, 1.1).yrange("*", "*"); + + error_plot.drawCurve(x_t, ukf_pos_var).label("Pos variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_rot_var).label("Rot variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_var).label("Total variance").lineWidth(1); + + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot, orientation_plot}, {error_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(1000, 1000); + + // Show the plot in a pop-up window + canvas.show(); + + // Save the plot to a PDF file + canvas.save("se2_kalman_output.pdf"); + } + +} // namespace armarx::navigation::components::dynamic_scene_provider diff --git a/source/armarx/navigation/human/test/se2xVkalmanFilterTest.cpp b/source/armarx/navigation/human/test/se2xVkalmanFilterTest.cpp new file mode 100644 index 00000000..5187e87a --- /dev/null +++ b/source/armarx/navigation/human/test/se2xVkalmanFilterTest.cpp @@ -0,0 +1,295 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::human +#define ARMARX_BOOST_TEST + +#include <cstdlib> /* srand, rand */ +#include <ctime> /* time */ + +#include <ArmarXCore/core/exceptions/local/ExpressionException.h> +#include <ArmarXCore/core/logging/Logging.h> +#include <ArmarXCore/util/time.h> + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <armarx/navigation/Test.h> +#include <armarx/navigation/human/HumanSystemModel.h> +#include <manif/SO2.h> +#include <sciplot/sciplot.hpp> + +namespace armarx::navigation::components::dynamic_scene_provider +{ + using T = double; //TODO double or float? + using Vector = Eigen::Matrix<T, 2, 1>; + using SystemModelT = kalman_filter::SystemModelSE2xV<T>; + + constexpr T RAD = 1; + constexpr long num_timesteps = 3000; + constexpr T max_time = 1; + constexpr T dt = max_time / num_timesteps; + constexpr T c = (1 / max_time) * 2 * M_PI; // rotation per time + + constexpr T obs_rot_noise_std = 0.01; + constexpr T obs_pos_noise_std = 0.01; + constexpr T initial_offset_angle = 1 * 10 * M_PI / 180; + const Vector initial_offet_pos = 0.8 * RAD * Vector(1, 0.5); + const Vector initial_offet_vel = 0.8 * RAD * Vector(1, 0.5); + + + void + simulate_trajectory(std::vector<SystemModelT::StateT>& states) + { + constexpr T startAngle = 0; + + SystemModelT::StateT state; + state.pose = manif::SE2<T>(RAD, 0, startAngle); + state.velocity = manif::SE2Tangent<T>(0, c, c); + states.push_back(state); + + for (int i = 1; i < num_timesteps; i++) + { + const T t = dt * i; + const T angle = t * c; + + const Vector last_pos = states.at(i - 1).pose.translation(); + //const Vector pos(angle, std::cos(angle)); + const Vector pos = RAD * Vector(std::cos(angle), std::sin(angle)); + + BOOST_TEST(((last_pos - pos).norm() < 0.1 * RAD), + "position differs too much from last step: " << (last_pos - pos).norm()); + + SystemModelT::ControlT control; + + const SystemModelT::StateT propagated = SystemModelT::propagationFunction( + state, control, SystemModelT::ControlNoiseT::Zero(), dt); + + state.pose = manif::SE2<T>(pos.x(), pos.y(), angle + startAngle); + state.velocity = manif::SE2Tangent<T>(0, c, c); + + T pos_diff = (propagated.pose.translation() - state.pose.translation()).norm(); + BOOST_TEST(pos_diff < 2e-10, + "propagated position differs too much from real position: " << pos_diff); + + T rot_diff = propagated.pose.angle() - state.pose.angle(); + BOOST_TEST(rot_diff < 2e-10, + "propagated rotation differs too much from real orientation: " << rot_diff); + + T vel_diff = (propagated.velocity.coeffs() - state.velocity.coeffs()).norm(); + BOOST_TEST(vel_diff < 2e-10, + "propagated velocity differs too much from real velocity: " << vel_diff); + + states.push_back(state); + } + } + + void + simulate_observation(const std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ObsT>& observations) + { + for (const auto& state : states) + { + SystemModelT::ObsT obs = SystemModelT::observationFunction(state); + + SystemModelT::ObsT noise = SystemModelT::ObsT::Random(); + noise.segment(0, 2) *= obs_pos_noise_std; + noise.segment(2, 1) *= obs_rot_noise_std; + + observations.push_back(obs + noise); + } + } + + + BOOST_AUTO_TEST_CASE(se2xVkalmanFilterTest) + { + srand(time(NULL)); + + std::vector<SystemModelT::StateT> states; + std::vector<SystemModelT::ObsT> observations; + simulate_trajectory(states); + simulate_observation(states, observations); + + ARMARX_INFO << "Num States: " << states.size() << " Num Obs: " << observations.size(); + + UnscentedKalmanFilter<SystemModelT>::PropCovT Q = + UnscentedKalmanFilter<SystemModelT>::PropCovT::Identity(); + + UnscentedKalmanFilter<SystemModelT>::ObsCovT R = + UnscentedKalmanFilter<SystemModelT>::ObsCovT::Identity(); + R.block<2, 2>(0, 0) *= obs_pos_noise_std * obs_pos_noise_std; + R.block<1, 1>(2, 2) *= obs_rot_noise_std * obs_rot_noise_std; + + UnscentedKalmanFilter<SystemModelT>::StateCovT P0 = + UnscentedKalmanFilter<SystemModelT>::StateCovT::Identity(); + P0.block<2, 2>(0, 0) *= initial_offet_pos.norm() * initial_offet_pos.norm(); + P0.block<1, 1>(2, 2) *= initial_offset_angle * initial_offset_angle; + P0.block<3, 3>(3, 3) *= initial_offet_vel.norm() * initial_offet_vel.norm(); + + UnscentedKalmanFilter<SystemModelT>::AlphaT alpha; + alpha(0) = 0.001; // state + alpha(1) = 0.001; // control + alpha(2) = 0.001; // update + + SystemModelT::StateT state0; + const Vector startPos = states.at(0).pose.translation() + initial_offet_pos; + state0.pose = manif::SE2<T>{ + startPos.x(), startPos.y(), states.at(0).pose.angle() + initial_offset_angle}; + state0.velocity = states.at(0).velocity + initial_offet_vel; + + UnscentedKalmanFilter<SystemModelT> ukf(Q, R, alpha, state0, P0); + + std::vector<SystemModelT::StateT> ukf_states; + std::vector<UnscentedKalmanFilter<SystemModelT>::StateCovT> ukf_Ps; + ukf_states.push_back(state0); + ukf_Ps.push_back(P0); + + std::vector<T> x_true, y_true, x_obs, y_obs, x_ukf, y_ukf; + std::vector<T> a_true, a_obs, a_ukf; + std::vector<T> vel_x_true, vel_y_true, vel_a_true; + std::vector<T> vel_x_ukf_full, vel_y_ukf_full, vel_a_ukf_full; + std::vector<T> x_ukf_full, y_ukf_full, a_ukf_full; + std::vector<T> ukf_pos_var, ukf_rot_var, ukf_var; + + for (int i = 1; i < num_timesteps; i++) + { + // propagate + TIMING_START(LOOP); + TIMING_START(PROPAGATION); + ukf.propagation(SystemModelT::ControlT{}, dt); + TIMING_END(PROPAGATION); + if ((i - 1) % 5 == 0) + { + TIMING_START(UPDATE); + ukf.update(observations.at(i)); + TIMING_END(UPDATE); + TIMING_START(REST); + const SystemModelT::StateT& current_state = ukf.getCurrentState(); + + BOOST_TEST_MESSAGE("Max Cov " + << ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + BOOST_TEST_MESSAGE("Diag: " << (ukf.getCurrentStateCovariance() - + Eigen::Matrix<T, 3, 3>::Identity()) + .norm()); + + x_obs.push_back(observations.at(i)(0)); + y_obs.push_back(observations.at(i)(1)); + a_obs.push_back(observations.at(i)(2)); + + x_ukf.push_back(ukf.getCurrentState().pose.x()); + y_ukf.push_back(ukf.getCurrentState().pose.y()); + a_ukf.push_back(ukf.getCurrentState().pose.angle()); + + TIMING_END(REST); + } + + x_true.push_back(states.at(i).pose.x()); + y_true.push_back(states.at(i).pose.y()); + a_true.push_back(states.at(i).pose.angle()); + + vel_x_true.push_back(states.at(i).velocity.x()); + vel_y_true.push_back(states.at(i).velocity.y()); + vel_a_true.push_back(states.at(i).velocity.angle()); + + x_ukf_full.push_back(ukf.getCurrentState().pose.x()); + y_ukf_full.push_back(ukf.getCurrentState().pose.y()); + a_ukf_full.push_back(ukf.getCurrentState().pose.angle()); + + vel_x_ukf_full.push_back(ukf.getCurrentState().velocity.x()); + vel_y_ukf_full.push_back(ukf.getCurrentState().velocity.y()); + vel_a_ukf_full.push_back(ukf.getCurrentState().velocity.angle()); + + ukf_states.push_back(ukf.getCurrentState()); + ukf_Ps.push_back(ukf.getCurrentStateCovariance()); + + ukf_pos_var.push_back( + ukf.getCurrentStateCovariance().block<2, 2>(0, 0).cwiseAbs().maxCoeff()); + ukf_rot_var.push_back( + ukf.getCurrentStateCovariance().block<1, 1>(2, 2).cwiseAbs().maxCoeff()); + ukf_var.push_back(ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + + TIMING_END(LOOP); + } + + sciplot::Plot2D pos_plot; + pos_plot.xlabel("x"); + pos_plot.ylabel("y"); + pos_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + pos_plot.xrange(-RAD * 1.1, RAD * 1.1).yrange(-RAD * 1.1, RAD * 1.1); + + pos_plot.drawCurve(x_true, y_true).label("True"); + pos_plot.drawCurve(x_obs, y_obs).label("Obs"); + pos_plot.drawCurveWithPoints(x_ukf, y_ukf).label("UKF"); + pos_plot.drawCurve(x_ukf_full, y_ukf_full).label("Full UKF"); + + + sciplot::Plot2D orientation_plot; + orientation_plot.xlabel("t"); + orientation_plot.ylabel("rot"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange(-0.1, 1.1).yrange(-4, 4); + + sciplot::Vec x = sciplot::linspace(0.0, 1.0, a_obs.size()); + sciplot::Vec x_t = sciplot::linspace(0.0, 1.0, a_true.size()); + orientation_plot.drawCurve(x_t, a_true).label("True").lineWidth(1); + orientation_plot.drawCurve(x, a_obs).label("Obs").lineWidth(1); + orientation_plot.drawCurve(x, a_ukf).label("UKF").lineWidth(1); + orientation_plot.drawCurve(x_t, a_ukf_full).label("Full UKF").lineWidth(1); + + + sciplot::Plot2D velocity_plot; + orientation_plot.xlabel("t"); + orientation_plot.ylabel("vel"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange(-0.1, 1.1).yrange("*", "*"); + + orientation_plot.drawCurve(x_t, vel_x_true).label("x True").lineWidth(1); + orientation_plot.drawCurve(x_t, vel_y_true).label("y True").lineWidth(1); + orientation_plot.drawCurve(x_t, vel_a_true).label("a True").lineWidth(1); + orientation_plot.drawCurve(x_t, vel_x_ukf_full).label("x Ukf").lineWidth(1); + orientation_plot.drawCurve(x_t, vel_y_ukf_full).label("y Ukf").lineWidth(1); + orientation_plot.drawCurve(x_t, vel_a_ukf_full).label("a Ukf").lineWidth(1); + + + sciplot::Plot2D error_plot; + error_plot.xlabel("t"); + error_plot.ylabel("err"); + error_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + error_plot.xrange(-0.1, 1.1).yrange("*", "*"); + + error_plot.drawCurve(x_t, ukf_pos_var).label("Pos variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_rot_var).label("Rot variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_var).label("Total variance").lineWidth(1); + + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot, orientation_plot}, {velocity_plot, error_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(1000, 1000); + + // Show the plot in a pop-up window + //canvas.show(); + + // Save the plot to a PDF file + canvas.save("kalman_output.pdf"); + } + +} // namespace armarx::navigation::components::dynamic_scene_provider diff --git a/source/armarx/navigation/human/test/kalmanFilterTest.cpp b/source/armarx/navigation/human/test/so2kalmanFilterTest.cpp similarity index 98% rename from source/armarx/navigation/human/test/kalmanFilterTest.cpp rename to source/armarx/navigation/human/test/so2kalmanFilterTest.cpp index 9a80c9ec..d6778e2e 100644 --- a/source/armarx/navigation/human/test/kalmanFilterTest.cpp +++ b/source/armarx/navigation/human/test/so2kalmanFilterTest.cpp @@ -42,7 +42,7 @@ namespace armarx::navigation::components::dynamic_scene_provider using Vector = Eigen::Matrix<T, 2, 1>; using SystemModelT = kalman_filter::SystemModelSO2xR2<T>; - constexpr T RAD = 8000; + constexpr T RAD = 40; constexpr long num_timesteps = 3000; constexpr T max_time = 1; constexpr T dt = max_time / num_timesteps; @@ -134,7 +134,7 @@ namespace armarx::navigation::components::dynamic_scene_provider } - BOOST_AUTO_TEST_CASE(kalmanFilterTest) + BOOST_AUTO_TEST_CASE(so2KalmanFilterTest) { srand(time(NULL)); @@ -266,13 +266,14 @@ namespace armarx::navigation::components::dynamic_scene_provider orientation_plot.drawCurve(x, a_ukf).label("UKF").lineWidth(1); orientation_plot.drawCurve(x_t, a_ukf_full).label("Full UKF").lineWidth(1); + sciplot::Plot2D error_plot; error_plot.xlabel("x"); error_plot.ylabel("y"); error_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + error_plot.xrange(-0.1, 1.1).yrange("*", "*"); - - error_plot.drawCurve(x_t, ukf_pos_var).label("Pos cariance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_pos_var).label("Pos variance").lineWidth(1); error_plot.drawCurve(x_t, ukf_rot_var).label("Rot variance").lineWidth(1); error_plot.drawCurve(x_t, ukf_var).label("Total variance").lineWidth(1); @@ -284,7 +285,7 @@ namespace armarx::navigation::components::dynamic_scene_provider canvas.size(1000, 1000); // Show the plot in a pop-up window - canvas.show(); + //canvas.show(); // Save the plot to a PDF file canvas.save("kalman_output.pdf"); -- GitLab