From ace49f32e92e15da10082153723b4f721a5d5953 Mon Sep 17 00:00:00 2001
From: paleziart <paleziart@laas.fr>
Date: Mon, 26 Jul 2021 12:37:55 +0200
Subject: [PATCH] Finish switching to full C++ for the whole-body control part
 + Error is no longer an attribute of the wbc class

---
 include/qrw/QPWBC.hpp          |  84 ++++++++++++++++++++++
 python/gepadd.cpp              |  42 +++++++++++
 scripts/Controller.py          |  52 +++++++-------
 scripts/LoggerControl.py       |   6 +-
 scripts/main_solo12_control.py |   4 +-
 src/QPWBC.cpp                  | 127 +++++++++++++++++++++++++++++++++
 6 files changed, 283 insertions(+), 32 deletions(-)

diff --git a/include/qrw/QPWBC.hpp b/include/qrw/QPWBC.hpp
index 602a8fcb..cdd57f21 100644
--- a/include/qrw/QPWBC.hpp
+++ b/include/qrw/QPWBC.hpp
@@ -14,6 +14,16 @@
 #include "osqp.h"
 #include "other/st_to_cc.hpp"
 
+// For wrapper
+#include "pinocchio/multibody/model.hpp"
+#include "pinocchio/multibody/data.hpp"
+#include "pinocchio/parsers/urdf.hpp"
+#include "pinocchio/algorithm/compute-all-terms.hpp"
+#include "pinocchio/algorithm/jacobian.hpp"
+#include "pinocchio/algorithm/frames.hpp"
+#include "pinocchio/algorithm/rnea.hpp"
+#include "pinocchio/algorithm/crba.hpp"
+
 class QPWBC {
  private:
   
@@ -101,4 +111,78 @@ class QPWBC {
 
 };
 
+class WbcWrapper
+{
+public:
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    ///
+    /// \brief Empty constructor
+    ///
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    WbcWrapper();
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    ///
+    /// \brief Destructor.
+    ///
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    ~WbcWrapper() {}
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    ///
+    /// \brief Initializer
+    ///
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    void initialize(Params& params);
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    ///
+    /// \brief Compute the remaining and total duration of a swing phase or a stance phase based
+    ///        on the content of the gait matrix
+    ///
+    /// \param[in] i considered phase (row of the gait matrix)
+    /// \param[in] j considered foot (col of the gait matrix)
+    /// \param[in] value 0.0 for swing phase detection, 1.0 for stance phase detection
+    ///
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    void compute(VectorN const& q, VectorN const& dq, MatrixN const& f_cmd, MatrixN const& contacts,
+                 MatrixN const& pgoals, MatrixN const& vgoals, MatrixN const& agoals);
+
+    VectorN get_qdes() { return qdes_; }
+    VectorN get_vdes() { return vdes_; }
+    VectorN get_tau_ff() { return tau_ff_; }
+    VectorN get_f_with_delta() { box_qp_->get_f_res(); }
+    MatrixN get_feet_pos() { return MatrixN::Zero(4, 3); }
+    MatrixN get_feet_err() { return MatrixN::Zero(4, 3); }
+    MatrixN get_feet_vel() { return MatrixN::Zero(4, 3); }
+    MatrixN get_feet_pos_target() { return MatrixN::Zero(4, 3); }
+    MatrixN get_feet_vel_target() { return MatrixN::Zero(4, 3); }
+    MatrixN get_feet_acc_target() { return MatrixN::Zero(4, 3); }
+
+private:
+    
+    Params* params_;  // Object that stores parameters
+    QPWBC* box_qp_;  // QP problem solver for the whole body control
+    InvKin* invkin_;  // Inverse Kinematics solver for the whole body control
+
+    pinocchio::Model model_;  // Pinocchio model for frame computations
+    pinocchio::Data data_;  // Pinocchio datas for frame computations
+
+    Eigen::Matrix<double, 18, 18> M_;  // Mass matrix
+    Eigen::Matrix<double, 12, 18> Jc_;  // Jacobian matrix
+    Eigen::Matrix<double, 6, 18> Jc_tmp_;  // Temporary matrix to store result of getFrameJacobian
+    Eigen::Matrix<double, 1, 4> k_since_contact_;
+    Vector12 qdes_;  // Desired actuator positions
+    Vector12 vdes_;  // Desired actuator velocities
+    Vector12 tau_ff_;  // Desired actuator torques (feedforward)
+
+    Vector18 ddq_cmd_;  // Actuator accelerations computed by Inverse Kinematics
+    Vector19 q_default_;  // Default configuration vector to compute the mass matrix
+    Vector12 f_with_delta_;  // Contact forces with deltas found by QP solver
+    Vector18 ddq_with_delta_;  // Actuator accelerations with deltas found by QP solver
+
+    int k_log_;  // Counter for logging purpose
+    int indexes_[4] = {10, 18, 26, 34};  // Indexes of feet frames in this order: [FL, FR, HL, HR]
+};
+
 #endif  // QPWBC_H_INCLUDED
diff --git a/python/gepadd.cpp b/python/gepadd.cpp
index b84ed8a0..7f302d41 100644
--- a/python/gepadd.cpp
+++ b/python/gepadd.cpp
@@ -245,6 +245,47 @@ struct QPWBCPythonVisitor : public bp::def_visitor<QPWBCPythonVisitor<QPWBC>>
 };
 void exposeQPWBC() { QPWBCPythonVisitor<QPWBC>::expose(); }
 
+/////////////////////////////////
+/// Binding WbcWrapper class
+/////////////////////////////////
+template <typename WbcWrapper>
+struct WbcWrapperPythonVisitor : public bp::def_visitor<WbcWrapperPythonVisitor<WbcWrapper>>
+{
+    template <class PyClassWbcWrapper>
+    void visit(PyClassWbcWrapper& cl) const
+    {
+        cl.def(bp::init<>(bp::arg(""), "Default constructor."))
+
+            .def("initialize", &WbcWrapper::initialize, bp::args("params"), "Initialize WbcWrapper from Python.\n")
+
+            .def("get_qdes", &WbcWrapper::get_qdes, "Get qdes_.\n")
+            .def("get_vdes", &WbcWrapper::get_vdes, "Get vdes_.\n")
+            .def("get_tau_ff", &WbcWrapper::get_tau_ff, "Get tau_ff_.\n")
+
+            .def_readonly("qdes", &WbcWrapper::get_qdes)
+            .def_readonly("vdes", &WbcWrapper::get_vdes)
+            .def_readonly("tau_ff", &WbcWrapper::get_tau_ff)
+            .def_readonly("f_with_delta", &WbcWrapper::get_f_with_delta)
+            .def_readonly("feet_pos", &WbcWrapper::get_feet_pos)
+            .def_readonly("feet_err", &WbcWrapper::get_feet_err)
+            .def_readonly("feet_vel", &WbcWrapper::get_feet_vel)
+            .def_readonly("feet_pos_target", &WbcWrapper::get_feet_pos_target)
+            .def_readonly("feet_vel_target", &WbcWrapper::get_feet_vel_target)
+            .def_readonly("feet_acc_target", &WbcWrapper::get_feet_acc_target)
+
+            // Run WbcWrapper from Python
+            .def("compute", &WbcWrapper::compute, bp::args("q", "dq", "f_cmd", "contacts", "pgoals", "vgoals", "agoals"), "Run WbcWrapper from Python.\n");
+    }
+
+    static void expose()
+    {
+        bp::class_<WbcWrapper>("WbcWrapper", bp::no_init).def(WbcWrapperPythonVisitor<WbcWrapper>());
+
+        ENABLE_SPECIFIC_MATRIX_TYPE(matXd);
+    }
+};
+void exposeWbcWrapper() { WbcWrapperPythonVisitor<WbcWrapper>::expose(); }
+
 /////////////////////////////////
 /// Binding Estimator class
 /////////////////////////////////
@@ -364,6 +405,7 @@ BOOST_PYTHON_MODULE(libquadruped_reactive_walking)
     exposeFootTrajectoryGenerator();
     exposeInvKin();
     exposeQPWBC();
+    exposeWbcWrapper();
     exposeEstimator();
     exposeParams();
 }
\ No newline at end of file
diff --git a/scripts/Controller.py b/scripts/Controller.py
index be42e404..05f1e950 100644
--- a/scripts/Controller.py
+++ b/scripts/Controller.py
@@ -134,6 +134,9 @@ class Controller:
         self.estimator = lqrw.Estimator()
         self.estimator.initialize(params)
 
+        self.wbcWrapper = lqrw.WbcWrapper()
+        self.wbcWrapper.initialize(params)
+
         # Wrapper that makes the link with the solver that you want to use for the MPC
         self.mpc_wrapper = MPC_Wrapper.MPC_Wrapper(params, self.q)
 
@@ -141,10 +144,6 @@ class Controller:
         # import ForceMonitor
         # myForceMonitor = ForceMonitor.ForceMonitor(pyb_sim.robotId, pyb_sim.planeId)
 
-        # Define the default controller
-        self.myController = wbc_controller(params)
-        self.myController.qdes[:] = q_init.ravel()
-
         self.envID = params.envID
         self.velID = params.velID
         self.dt_wbc = params.dt_wbc
@@ -173,6 +172,7 @@ class Controller:
         self.feet_v_cmd = np.zeros((3, 4))
         self.feet_p_cmd = np.zeros((3, 4))
 
+        self.error = False  # True if something wrong happens in the controller
         self.error_flag = 0
         self.q_security = np.array([np.pi*0.4, np.pi*80/180, np.pi] * 4)
 
@@ -278,12 +278,12 @@ class Controller:
         # Target state for the whole body control
         self.x_f_wbc = (self.x_f_mpc[:24, 0]).copy()
         if not self.gait.getIsStatic():
-            self.x_f_wbc[0] = self.myController.dt * xref[6, 1]
-            self.x_f_wbc[1] = self.myController.dt * xref[7, 1]
+            self.x_f_wbc[0] = self.dt_wbc * xref[6, 1]
+            self.x_f_wbc[1] = self.dt_wbc * xref[7, 1]
             self.x_f_wbc[2] = self.h_ref
             self.x_f_wbc[3] = 0.0
             self.x_f_wbc[4] = 0.0
-            self.x_f_wbc[5] = self.myController.dt * xref[11, 1]
+            self.x_f_wbc[5] = self.dt_wbc * xref[11, 1]
         else:  # Sort of position control to avoid slow drift
             self.x_f_wbc[0:3] = self.planner.q_static[0:3, 0]  # TODO: Adapt to new code
             self.x_f_wbc[3:6] = self.planner.RPY_static[:, 0]
@@ -291,17 +291,17 @@ class Controller:
 
         # Whole Body Control
         # If nothing wrong happened yet in the WBC controller
-        if (not self.myController.error) and (not self.joystick.stop):
+        if (not self.error) and (not self.joystick.stop):
 
             self.q_wbc = np.zeros((19, 1))
             self.q_wbc[2, 0] = self.h_ref  # at position (0.0, 0.0, h_ref)
             self.q_wbc[6, 0] = 1.0  # with orientation (0.0, 0.0, 0.0)
-            self.q_wbc[7:, 0] = self.myController.qdes[:]  # with reference angular positions of previous loop
+            self.q_wbc[7:, 0] = self.wbcWrapper.qdes[:]  # with reference angular positions of previous loop
 
             # Get velocity in base frame for Pinocchio (not current base frame but desired base frame)
             self.b_v = self.v.copy()
             self.b_v[:6, 0] = self.v_ref[:6, 0]  # Base at reference velocity (TODO: add hRb once v_ref is considered in base frame)
-            self.b_v[6:, 0] = self.myController.vdes[:]  # with reference angular velocities of previous loop
+            self.b_v[6:, 0] = self.wbcWrapper.vdes[:]  # with reference angular velocities of previous loop
 
             # Feet command position, velocity and acceleration in base frame
             self.feet_a_cmd = self.footTrajectoryGenerator.getFootAccelerationBaseFrame(oRh.transpose(), self.v_ref[3:6, 0:1])
@@ -309,18 +309,18 @@ class Controller:
             self.feet_p_cmd = self.footTrajectoryGenerator.getFootPositionBaseFrame(oRh.transpose(), np.array([[0.0], [0.0], [self.h_ref]]) + oTh)
 
             # Run InvKin + WBC QP
-            self.myController.compute(self.q_wbc, self.b_v,
-                                      self.x_f_wbc[12:], cgait[0, :],
-                                      self.feet_p_cmd,
-                                      self.feet_v_cmd,
-                                      self.feet_a_cmd)
+            self.wbcWrapper.compute(self.q_wbc, self.b_v,
+                                    self.x_f_wbc[12:], np.array([cgait[0, :]]),
+                                    self.feet_p_cmd,
+                                    self.feet_v_cmd,
+                                    self.feet_a_cmd)
 
             # Quantities sent to the control board
             self.result.P = 6.0 * np.ones(12)
             self.result.D = 0.3 * np.ones(12)
-            self.result.q_des[:] = self.myController.qdes[:]
-            self.result.v_des[:] = self.myController.vdes[:]
-            self.result.tau_ff[:] = 0.8 * self.myController.tau_ff
+            self.result.q_des[:] = self.wbcWrapper.qdes[:]
+            self.result.v_des[:] = self.wbcWrapper.vdes[:]
+            self.result.tau_ff[:] = 0.8 * self.wbcWrapper.tau_ff
 
             # Display robot in Gepetto corba viewer
             """if self.k % 5 == 0:
@@ -353,19 +353,19 @@ class Controller:
 
     def security_check(self):
 
-        if (self.error_flag == 0) and (not self.myController.error) and (not self.joystick.stop):
-            self.error_flag = self.estimator.security_check(self.myController.tau_ff)
+        if (self.error_flag == 0) and (not self.error) and (not self.joystick.stop):
+            self.error_flag = self.estimator.security_check(self.wbcWrapper.tau_ff)
             if (self.error_flag != 0):
-                self.myController.error = True
+                self.error = True
                 if (self.error_flag == 1):
                     self.error_value = self.estimator.getQFilt()[7:] * 180 / 3.1415
                 elif (self.error_flag == 2):
                     self.error_value = self.estimator.getVSecu()
                 else:
-                    self.error_value = self.myController.tau_ff
+                    self.error_value = self.wbcWrapper.tau_ff
 
         # If something wrong happened in the controller we stick to a security controller
-        if self.myController.error or self.joystick.stop:
+        if self.error or self.joystick.stop:
 
             # Quantities sent to the control board
             self.result.P = np.zeros(12)
@@ -381,8 +381,6 @@ class Controller:
         self.t_list_mpc[self.k] = t_mpc - t_planner
         self.t_list_wbc[self.k] = t_wbc - t_mpc
         self.t_list_loop[self.k] = time.time() - tic
-        self.t_list_InvKin[self.k] = self.myController.tac - self.myController.tic
-        self.t_list_QPWBC[self.k] = self.myController.toc - self.myController.tac
 
     def updateState(self):
 
@@ -397,13 +395,13 @@ class Controller:
             Ryaw = np.array([[math.cos(self.yaw_estim), -math.sin(self.yaw_estim)],
                              [math.sin(self.yaw_estim), math.cos(self.yaw_estim)]])
 
-            self.q[0:2, 0:1] = self.q[0:2, 0:1] + Ryaw @ self.v_ref[0:2, 0:1] * self.myController.dt
+            self.q[0:2, 0:1] = self.q[0:2, 0:1] + Ryaw @ self.v_ref[0:2, 0:1] * self.dt_wbc
 
             # Mix perfect x and y with height measurement
             self.q[2, 0] = self.estimator.getQFilt()[2]
 
             # Mix perfect yaw with pitch and roll measurements
-            self.yaw_estim += self.v_ref[5, 0] * self.myController.dt
+            self.yaw_estim += self.v_ref[5, 0] * self.dt_wbc
             self.q[3:7, 0] = pin.Quaternion(pin.rpy.rpyToMatrix(self.estimator.getRPY()[0], self.estimator.getRPY()[1], self.yaw_estim)).coeffs()
 
             # Actuators measurements
diff --git a/scripts/LoggerControl.py b/scripts/LoggerControl.py
index 70962a24..4023d3bc 100644
--- a/scripts/LoggerControl.py
+++ b/scripts/LoggerControl.py
@@ -160,11 +160,11 @@ class LoggerControl():
         self.wbc_tau_ff[self.i] = loop.result.tau_ff
         self.wbc_f_ctc[self.i] = wbc.f_with_delta[:, 0]
         self.wbc_feet_pos[self.i] = wbc.feet_pos
-        self.wbc_feet_pos_target[self.i] = wbc.log_feet_pos_target[:, :, self.i+1]
+        self.wbc_feet_pos_target[self.i] = wbc.feet_pos_target
         self.wbc_feet_err[self.i] = wbc.feet_err
         self.wbc_feet_vel[self.i] = wbc.feet_vel
-        self.wbc_feet_vel_target[self.i] = wbc.log_feet_vel_target[:, :, self.i+1]
-        self.wbc_feet_acc_target[self.i] = wbc.log_feet_acc_target[:, :, self.i+1]
+        self.wbc_feet_vel_target[self.i] = wbc.feet_vel_target
+        self.wbc_feet_acc_target[self.i] = wbc.feet_acc_target
         #self.wbc_feet_pos_invkin[self.i] = wbc.invKin.cpp_posf.transpose() # TODO: Adapt logging
         #self.wbc_feet_vel_invkin[self.i] = wbc.invKin.cpp_vf.transpose()
 
diff --git a/scripts/main_solo12_control.py b/scripts/main_solo12_control.py
index 07d0be99..e22bb582 100644
--- a/scripts/main_solo12_control.py
+++ b/scripts/main_solo12_control.py
@@ -172,7 +172,7 @@ def control_loop(name_interface, name_interface_clone=None, des_vel_analysis=Non
     t = 0.0
     t_max = (params.N_SIMULATION-2) * params.dt_wbc
 
-    while ((not device.hardware.IsTimeout()) and (t < t_max) and (not controller.myController.error)):
+    while ((not device.hardware.IsTimeout()) and (t < t_max) and (not controller.error)):
 
         # Update sensor data (IMU, encoders, Motion capture)
         device.UpdateMeasurment()
@@ -316,7 +316,7 @@ def control_loop(name_interface, name_interface_clone=None, des_vel_analysis=Non
         # Disconnect the PyBullet server (also close the GUI)
         device.Stop()
 
-    if controller.myController.error:
+    if controller.error:
         if (controller.error_flag == 1):
             print("-- POSITION LIMIT ERROR --")
         elif (controller.error_flag == 2):
diff --git a/src/QPWBC.cpp b/src/QPWBC.cpp
index 4d1c442e..f8338c3f 100644
--- a/src/QPWBC.cpp
+++ b/src/QPWBC.cpp
@@ -547,3 +547,130 @@ void QPWBC::update_PQ() {
   my_print_csc_matrix(P, t_char);*/
 
 }
+
+
+
+WbcWrapper::WbcWrapper()
+    : M_(Eigen::Matrix<double, 18, 18>::Zero())
+    , Jc_(Eigen::Matrix<double, 12, 18>::Zero())
+    , Jc_tmp_(Eigen::Matrix<double, 6, 18>::Zero())
+    , k_since_contact_(Eigen::Matrix<double, 1, 4>::Zero())
+    , qdes_(Vector12::Zero())
+    , vdes_(Vector12::Zero())
+    , tau_ff_(Vector12::Zero())
+    , ddq_cmd_(Vector18::Zero())
+    , q_default_(Vector19::Zero())
+    , f_with_delta_(Vector12::Zero())
+    , ddq_with_delta_(Vector18::Zero())
+    , k_log_(0)
+{}
+
+void WbcWrapper::initialize(Params& params) 
+{
+  // Params store parameters
+  params_ = &params;
+
+  // Path to the robot URDF (TODO: Automatic path)
+  const std::string filename = std::string("/opt/openrobots/share/example-robot-data/robots/solo_description/robots/solo12.urdf");
+
+  // Build model from urdf (base is not free flyer)
+  pinocchio::urdf::buildModel(filename, pinocchio::JointModelFreeFlyer(), model_, false);
+
+  // Construct data from model
+  data_ = pinocchio::Data(model_);
+
+  // Update all the quantities of the model
+  pinocchio::computeAllTerms(model_, data_ , VectorN::Zero(model_.nq), VectorN::Zero(model_.nv));
+
+  // Initialize inverse kinematic and box QP solvers
+  invkin_ = new InvKin();
+  invkin_->initialize(params);
+  box_qp_ = new QPWBC();
+  box_qp_->initialize(params);
+
+  // Initialize quaternion
+  q_default_(6, 0) = 1.0;
+}
+
+void WbcWrapper::compute(VectorN const& q, VectorN const& dq, MatrixN const& f_cmd, MatrixN const& contacts,
+                         MatrixN const& pgoals, MatrixN const& vgoals, MatrixN const& agoals)
+{
+  //  Update nb of iterations since contact
+  k_since_contact_ += contacts;  // Increment feet in stance phase
+  k_since_contact_ = k_since_contact_.cwiseProduct(contacts);  // Reset feet in swing phase
+
+  // Compute Inverse Kinematics
+  invkin_->run_InvKin(q.tail(12), dq.tail(12), contacts, pgoals.transpose(), vgoals.transpose(), agoals.transpose());
+  ddq_cmd_.tail(12) = invkin_->get_ddq_cmd();
+
+  // TODO: Adapt logging of feet_pos, feet_err, feet_vel
+
+  // Compute the upper triangular part of the joint space inertia matrix M by using the Composite Rigid Body Algorithm
+  // Result is stored in data_.M
+  pinocchio::crba(model_, data_, q_default_);
+
+  // Make mass matrix symetric
+  data_.M.triangularView<Eigen::StrictlyLower>() = data_.M.transpose().triangularView<Eigen::StrictlyLower>();
+
+  // TODO: Check if needed because crbaMinimal may allow to directly get the jacobian
+  // TODO: Check if possible to use the model of InvKin to avoid computations
+  pinocchio::computeJointJacobians(model_, data_, q);
+
+  // TODO: Check if we can save time by switching MatrixXd to defined sized vector since they are
+  // not called from python anymore
+
+  // Retrieve feet jacobian
+  Jc_.setZero();
+  for (int i = 0; i < 4; i++) 
+  {
+    if (contacts(0, i))
+    {
+      Jc_tmp_.setZero();
+      pinocchio::getFrameJacobian(model_, data_, indexes_[i], pinocchio::LOCAL_WORLD_ALIGNED, Jc_tmp_);
+      Jc_.block(3 * i, 0, 3, 18) = Jc_tmp_.block(0, 0, 3, 18);
+    }
+  }
+
+  // Compute the inverse dynamics, aka the joint torques according to the current state of the system,
+  // the desired joint accelerations and the external forces, using the Recursive Newton Euler Algorithm.
+  // Result is stored in data_.tau
+  pinocchio::rnea(model_, data_, q, dq, ddq_cmd_);
+
+  /*std::cout << "M" << std::endl;
+  std::cout << data_.M << std::endl;
+  std::cout << "Jc" << std::endl;
+  std::cout << Jc_ << std::endl;
+  std::cout << "f_cmd" << std::endl;
+  std::cout << f_cmd << std::endl;
+  std::cout << "rnea" << std::endl;
+  std::cout << data_.tau.head(6) << std::endl;
+  std::cout << "k_since" << std::endl;
+  std::cout << k_since_contact_ << std::endl;*/
+
+  // Solve the QP problem
+  box_qp_->run(data_.M, Jc_, Eigen::Map<const VectorN>(f_cmd.data(), f_cmd.size()), data_.tau.head(6), k_since_contact_);
+
+  // Add to reference quantities the deltas found by the QP solver
+  f_with_delta_ = box_qp_->get_f_res();
+  ddq_with_delta_.head(6) = ddq_cmd_.head(6) + box_qp_->get_ddq_res();
+  ddq_with_delta_.tail(12) = ddq_cmd_.tail(12);
+
+  // Compute joint torques from contact forces and desired accelerations
+  pinocchio::rnea(model_, data_, q, dq, ddq_with_delta_);
+
+  /*std::cout << "rnea delta" << std::endl;
+  std::cout << data_.tau.tail(12) << std::endl;
+  std::cout << "ddq del" << std::endl;
+  std::cout << ddq_with_delta_ << std::endl;
+  std::cout << "f del" << std::endl;
+  std::cout << f_with_delta_ << std::endl;*/
+
+  tau_ff_ = data_.tau.tail(12) - Jc_.block(0, 6, 12, 12).transpose() * f_with_delta_;
+
+  // Retrieve desired positions and velocities
+  vdes_ = invkin_->get_dq_cmd();
+  qdes_ = invkin_->get_q_cmd();
+
+  // Increment log counter
+  k_log_++;
+}
-- 
GitLab