diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index 586fb033f0ee510de4bc92762de35c2642a5291e..afbb09900dae4a3df7d61a0dbb1e350cf4ed8e87 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -12,3 +12,4 @@ register_executable(iLQR_bimanual)
 register_executable(iLQR_car)
 register_executable(iLQR_manipulator)
 register_executable(iLQR_manipulator_obstacle)
+register_executable(iLQR_obstacle_GPIS)
\ No newline at end of file
diff --git a/cpp/src/iLQR_obstacle_GPIS.cpp b/cpp/src/iLQR_obstacle_GPIS.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8955d77bfff0ae86c46d426d8ca5480a06f53d93
--- /dev/null
+++ b/cpp/src/iLQR_obstacle_GPIS.cpp
@@ -0,0 +1,502 @@
+/*
+    iLQR applied to a 2D point-mass system reaching a target while avoiding 
+    obstacles represented as Gaussian process implicit surfaces (GPIS)
+
+    Copyright (c) 2021 Idiap Research Institute, http://www.idiap.ch/
+    Written by Léane Donzé <leane.donze@epfl.ch>
+    Sylvain Calinon <https://calinon.ch>
+
+    This file is part of RCFS.
+
+    RCFS is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 3 as
+    published by the Free Software Foundation.
+
+    RCFS 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 RCFS. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <Eigen/Core>
+#include <Eigen/Dense>
+#include <eigen3/unsupported/Eigen/KroneckerProduct>
+
+#include <iostream>
+#include <vector>
+
+namespace
+{
+    // define the parameters that influence the behaviour of the algorithm
+    struct Parameters
+    {
+        int num_iterations = 300;  // maximum umber of iterations for iLQR
+        double dt = 1e-2;        // time step size
+        int num_timesteps = 101;  // number of datapoints
+
+        // definition of the viapoints, size <= num_timesteps
+        std::vector<Eigen::Vector3d> viapoints = {
+            { 0.9, 0.9, M_PI / 6 }  //
+        };
+
+        // Control space dimension
+        int nbVarU = 2; //dx1, dx2
+
+        // State space diimension
+        int nbVarX = 2; //x1, x2
+
+        // initial configuration of the robot
+        std::vector<double> initial_state = { 0.3, 0.05};
+
+        // GPIS representation of obstacles
+        std::vector<double> p = {1.4, 1e-5}; //Thin-plate covariance function parameters
+        std::vector<std::vector<double>> x = { {0.2, 0.4, 0.6, -0.4, 0.6, 0.9},
+                                          {0.5, 0.5, 0.5, 0.8, 0.1, 0.6} };
+        std::vector<double> y = {-1, 0, 1, -1, -1, -1};
+
+        // Disc as gemoetric prior
+        double rc = 4e-1;                       // Radius of the disc
+        std::vector<double> xc = {0.55, 0.55};  // Location of the disc
+
+
+        double tracking_weight = 1e2;  // tracking weight term
+        double obstacle_weight = 1e0;  // obstacle weight term
+        double control_weight = 1e-3;  // control weight term
+
+
+    };
+
+    struct Model
+    {
+      public:
+        /**
+         * initialize the model with the given parameter
+         * this calculates the matrices Su and Sx
+         */
+        Model(const Parameters &parameters);
+
+        /**
+         * implementation of the iLQR algorithm
+         * this function calls the other functions as needed
+         */
+        Eigen::MatrixXd ilqr() const;
+
+        /**
+         * perform a trajectory rollout for the given initial joint angles and control commands
+         * return a joint angle trajectory
+         */
+        Eigen::MatrixXd rollout(const Eigen::VectorXd &initial_state, const Eigen::VectorXd &control_commands) const;
+
+        /**
+         * reaching function, called at each iLQR iteration
+         * calculates the error to each viapoint as well as the jacobians
+         */
+        std::pair<Eigen::MatrixXd, Eigen::MatrixXd> reach(const Eigen::MatrixXd &x) const;
+
+        /**
+         * Residuals f and Jacobians J for obstacle avoidance with GPIS representation
+         * (for all time steps)
+         */
+
+        std::tuple<Eigen::MatrixXd, Eigen::MatrixXd, std::vector<int>, std::vector<int> > avoid(const Eigen::MatrixXd &x) const;
+
+        /**
+         * convenience function to extract the viapoints from given joint angle trajectory
+         * also reshapes the viapoints to the expected matrix dimensions
+         */
+        Eigen::MatrixXd viapoints(const Eigen::MatrixXd &joint_angle_trajectory) const;
+
+        /**
+         * return the viapoint_timesteps_
+         * only used for plotting
+         */
+        const Eigen::VectorXi &viatimes() const;
+
+        /**
+         * implementation of the Matlab function "pdist2" 
+         * which seems to have no equivalent in Eigen library
+         */
+        Eigen::MatrixXd pdist2(const Eigen::MatrixXd X, const Eigen::MatrixXd Y) const;
+
+        /**
+         * Error function
+         */
+        Eigen::MatrixXd substr(const Eigen::VectorXd x1, const Eigen::VectorXd x2) const;
+
+        /**
+         * Covariance function in GPIS
+         */
+        std::tuple<Eigen::MatrixXd, std::vector<Eigen::MatrixXd> > covFct(const Eigen::MatrixXd &x1, const Eigen::MatrixXd &x2, const Eigen::VectorXd p, bool flag_noiseObs) const;
+
+        /**
+         * Residuals f and Jacobians J for obstacle avoidance
+         * with GPIS representation (for a given time step) 
+         */
+
+        std::tuple<Eigen::MatrixXd, Eigen::MatrixXd > GPIS(const Eigen::MatrixXd &x) const;
+
+
+      private:
+        /// parameters defined at runtime
+        Eigen::MatrixXd viapoints_;                     // internal viapoint representation
+        Eigen::VectorXi viapoint_timesteps_;            // discrete timesteps of the viapoints, uniformly spread
+
+        double tracking_weight_;
+        double obstacle_weight_;
+        double r_;  // control weight parameter
+
+        int nbVarU_;          // Control space dimension (dx1, dx2)
+        int nbVarX_;          // State space dimension (q1,q2,q3)
+        int num_timesteps_;   // copy the number of timesteps for internal use from the Parameters
+        int num_iterations_;  // maximum number of iterations for the optimization
+
+        Eigen::VectorXd initial_state_;         // internal state 
+        Eigen::MatrixXd control_weight_;        // R matrix, penalizes the control commands
+        Eigen::MatrixXd precision_matrix_;      // Q matrix, penalizes the state error
+        Eigen::MatrixXd mat_Su0_;               // matrix for propagating the control commands for a rollout
+        Eigen::MatrixXd mat_Sx0_;               // matrix for propagating the initial joint angles for a rollout
+        Eigen::MatrixXd mat_Su_;                // matrix for propagating the control commands for a rollout at the viapoints
+     
+
+        Eigen::VectorXd y_;                     // GPIS representation
+        Eigen::VectorXd Mu2_;                   // GPIS representation
+        Eigen::MatrixXd K_;                     // GPIS representation 
+        Eigen::MatrixXd x_;                     // GPIS representation
+        Eigen::VectorXd p_ ;                    // GPIS representation
+
+    };
+
+    ////////////////////////////////////////////////////////////////////////////////
+    // implementation of the iLQR algorithm
+
+    Eigen::MatrixXd Model::ilqr() const
+    {
+        // initial commands, currently all zero
+        // can be modified if a better guess is available
+        Eigen::VectorXd control_commands = Eigen::VectorXd::Zero(nbVarU_* (num_timesteps_ - 1), 1);
+
+        int iter = 1;
+        for (; iter <= num_iterations_; ++iter)  // run the optimization for the maximum number of iterations
+        {
+            std::cout << "iteration " << iter << ":" << std::endl;
+
+            // trajectory rollout, i.e. compute the trajectory for the given control commands starting from the initial state
+            // Sx * x0 + Su * u
+            Eigen::MatrixXd x = rollout(initial_state_, control_commands);
+
+            // Residuals f and Jacobians J (tracking objective)
+            auto [state_error, jacobian_matrix] = reach(viapoints(x));
+
+            // Residuals f and Jacobians J (avoidance objective)
+            auto [obstacle_error, obstacle_jacobian, idx, idt] = avoid(x);
+
+            Eigen::MatrixXd mat_Su2 = Eigen::MatrixXd::Zero(static_cast<int>(idx.size()), mat_Su0_.cols());
+            for (unsigned i = 0; i < idx.size(); ++i)
+            {
+                mat_Su2.row(i) = mat_Su0_.row(idx[i]);
+            }
+
+            // Gauss-Newton update
+            // (Su'*J'*Q*J*Su+Su2'*J2'*J2*Su2*param.q2 + R) \ (-Su'*J'*Q*f(:) - Su2'*J2'*f2(:) * param.q2 - u * param.r)
+            Eigen::MatrixXd control_gradient = (mat_Su_.transpose() * jacobian_matrix.transpose() * tracking_weight_ * jacobian_matrix * mat_Su_        //
+                                                + mat_Su2.transpose() * obstacle_jacobian.transpose() * obstacle_jacobian * mat_Su2 * obstacle_weight_  //
+                                                + control_weight_)                                                                                      //
+                                                 .inverse()                                                                                             //
+                                               * (-mat_Su_.transpose() * jacobian_matrix.transpose() * tracking_weight_ * state_error                   //
+                                                  - mat_Su2.transpose() * obstacle_jacobian.transpose() * obstacle_error * obstacle_weight_             //
+                                                  - r_ * control_commands);
+
+            // calculate the cost of the current state
+            double current_cost = state_error.squaredNorm() * tracking_weight_ + obstacle_error.squaredNorm() * obstacle_weight_ + r_ * control_commands.squaredNorm();
+
+            std::cout << "\t cost = " << current_cost << std::endl;
+
+            // initial step size for the line search
+            double step_size = 1.0;
+            // line search, i.e. find the best step size for updating the control commands with the gradient
+            while (true)
+            {
+                //  calculate the new control commands for the current step size
+                Eigen::MatrixXd tmp_control_commands = control_commands + control_gradient * step_size;
+
+                // calculate a trajectory rollout for the current step size
+                Eigen::MatrixXd tmp_x = rollout(initial_state_, tmp_control_commands);
+
+                // try reaching the viapoints with the current state
+                // we only need the state error here and can disregard the Jacobian, because we are only interested in the cost of the trajectory
+                Eigen::MatrixXd tmp_state_error = reach(viapoints(tmp_x)).first;
+
+                Eigen::MatrixXd tmp_obstacle_error = std::get<0>(avoid(tmp_x));
+
+                // resulting cost when updating the control commands with the current step size
+                double cost =
+                  tmp_state_error.squaredNorm() * tracking_weight_ + tmp_obstacle_error.squaredNorm() * obstacle_weight_ + r_ * tmp_control_commands.squaredNorm();
+
+                // end the line search if the current steps size reduces the cost or becomes too small
+                if (cost < current_cost || step_size < 1e-3)
+                {
+                    control_commands = tmp_control_commands;
+
+                    break;
+                }
+
+                // reduce the step size for the next iteration
+                step_size *= 0.5;
+            }
+
+            std::cout << "\t step_size = " << step_size << std::endl;
+
+            // stop optimizing if the gradient update becomes too small
+            if ((control_gradient * step_size).norm() < 1e-2)
+            {
+                break;
+            }
+        }
+
+        std::cout << "iLQR converged in " << iter << " iterations" << std::endl;
+
+        return rollout(initial_state_, control_commands);
+    }
+    ////////////////////////////////////////////////////////////////////////////////
+    // implementation of all functions used in the iLQR algorithm
+
+    std::pair<Eigen::MatrixXd, Eigen::MatrixXd> Model::reach(const Eigen::MatrixXd &x) const
+    {
+        Eigen::MatrixXd state_error = x - viapoints_.topRows(2); 
+
+        Eigen::MatrixXd jacobian_matrix = Eigen::MatrixXd::Identity(nbVarX_ * x.cols(), nbVarX_ * x.cols());
+
+        state_error = Eigen::Map<Eigen::MatrixXd>(state_error.data(), state_error.size(), 1);
+
+        return std::make_pair(state_error, jacobian_matrix);
+    }
+
+    std::tuple<Eigen::MatrixXd, Eigen::MatrixXd, std::vector<int>, std::vector<int> > Model::avoid(const Eigen::MatrixXd &x) const
+    {
+        auto [ftmp, Jtmp] = GPIS(x);
+
+        std::vector<double> fs;
+        std::vector<Eigen::MatrixXd> js;
+        std::vector<int> idx;
+        std::vector<int> idt;
+
+        int size = 0;
+
+        for (unsigned t = 0; t < x.cols(); ++ t)
+        {
+            if (ftmp(0,t) > 0)
+            {
+                fs.push_back(ftmp(0,t));
+                js.push_back(Jtmp.col(t).transpose());
+
+                size += 1;
+
+                for (unsigned j = 0; j < x.rows(); ++j)
+                {
+                    idx.push_back(static_cast<int>(t * x.rows() + j));
+                }
+                idt.push_back(t);
+            }
+        }
+
+
+        Eigen::MatrixXd f = Eigen::Map<Eigen::MatrixXd>(fs.data(), static_cast<int>(fs.size()), 1);
+        Eigen::MatrixXd j = Eigen::MatrixXd::Zero(size, 2*size);
+
+        int r = 0, c = 0;
+        for (const Eigen::MatrixXd &ji : js)
+        {
+            j.block(r, c, ji.rows(), ji.cols()) = ji;
+            r += static_cast<int>(ji.rows());
+            c += static_cast<int>(ji.cols());
+        }
+
+        return std::make_tuple(f,j,idx,idt);
+        
+    }
+
+    Eigen::MatrixXd Model::rollout(const Eigen::VectorXd &initial_state, const Eigen::VectorXd &control_commands) const
+    {
+        Eigen::MatrixXd x = mat_Su0_ * control_commands + mat_Sx0_ * initial_state;
+        x = Eigen::MatrixXd(Eigen::Map<Eigen::MatrixXd>(x.data(), nbVarX_, num_timesteps_));
+
+        return x;
+    }
+
+    Eigen::MatrixXd Model::viapoints(const Eigen::MatrixXd &joint_angle_trajectory) const
+    {
+        Eigen::MatrixXd via_joint_angles = Eigen::MatrixXd::Zero(joint_angle_trajectory.rows(), viapoint_timesteps_.size());
+
+        for (unsigned t = 0; t < viapoint_timesteps_.size(); ++t)
+        {
+            via_joint_angles.col(t) = joint_angle_trajectory.col(viapoint_timesteps_(t));
+        }
+
+        return via_joint_angles;
+    }
+
+    const Eigen::VectorXi &Model::viatimes() const
+    {
+        return viapoint_timesteps_;
+    }
+
+
+    Eigen::MatrixXd Model::pdist2(const Eigen::MatrixXd X, const Eigen::MatrixXd Y)  const
+    {   
+        Eigen::MatrixXd result(X.rows(), Y.rows());
+        for (unsigned i = 0; i < X.rows(); ++i)
+        {
+            for (unsigned j = 0; j < Y.rows(); ++j){
+                Eigen::VectorXd v1 = X.row(i);
+                Eigen::VectorXd v2 = Y.row(j);
+                result(i,j) = (v1-v2).norm();
+            }
+        }
+        return result;
+    }
+
+    Eigen::MatrixXd Model::substr(const Eigen::VectorXd x1, const Eigen::VectorXd x2) const
+    {
+        return x1.replicate(1,x2.size()) - x2.transpose().replicate(x1.size(), 1);
+    }
+
+    std::tuple<Eigen::MatrixXd, std::vector<Eigen::MatrixXd> > Model::covFct(const Eigen::MatrixXd &x1, const Eigen::MatrixXd &x2, const Eigen::VectorXd p, bool flag_noiseObs) const
+    {
+        // Thin plate covariance function (for 3D implicit shape)
+        Eigen::MatrixXd K = pow(12,-1)*(2*pdist2(x1.transpose(),x2.transpose()).array().pow(3) - 3*p(0)*pdist2(x1.transpose(), x2.transpose()).array().pow(2) + pow(p(0),3)); // Kernel
+        Eigen::MatrixXd dK1 = pow(12,-1)*(6*pdist2(x1.transpose(),x2.transpose()).cwiseProduct(substr(x1.row(0).transpose(), x2.row(0))) - 6*p(0)*substr(x1.row(0).transpose(), x2.row(0))); // Derivative along x1
+        Eigen::MatrixXd dK2 = pow(12,-1)*(6*pdist2(x1.transpose(),x2.transpose()).cwiseProduct(substr(x1.row(1).transpose(), x2.row(1))) - 6*p(0)*substr(x1.row(1).transpose(), x2.row(1))); // Derivative along x2
+
+        std::vector<Eigen::MatrixXd> dK;
+        dK.push_back(dK1);
+        dK.push_back(dK2);
+
+        if (flag_noiseObs == true)
+        {
+            K = K + p(1)*Eigen::MatrixXd::Identity(x1.cols(), x2.cols()); //Consideration of noisy observation y
+        }
+        return std::make_pair(K, dK);
+    }
+
+    std::tuple<Eigen::MatrixXd, Eigen::MatrixXd > Model::GPIS(const Eigen::MatrixXd &x) const
+    {
+        auto [K, dK] = covFct(x, x_, p_, false);
+        Eigen::MatrixXd f = (K*K_.inverse()*y_).transpose();    //GPR with Mu=0
+        Eigen::MatrixXd J(x.rows(), x.cols());
+        J.row(0) = (dK[0]*K_.inverse() * (y_-Mu2_)).transpose();
+        J.row(1) = (dK[1]*K_.inverse() * (y_-Mu2_)).transpose();
+
+        // Reshape gradients
+        Eigen::MatrixXd a = f.cwiseMax(0);  //Amplitude
+        J = 1e2*(a.array().tanh().replicate(2,1).cwiseProduct(J.array()).cwiseQuotient(J.array().square().colwise().sum().sqrt().replicate(2,1))); // Vector moving away from interior of shape
+
+        return std::make_tuple(f, J);
+
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////////
+    // precalculate matrices used in the iLQR algorithm
+
+    Model::Model(const Parameters &parameters)
+    {
+        int num_viapoints = static_cast<int>(parameters.viapoints.size());
+
+        nbVarU_ = parameters.nbVarU;
+        nbVarX_= parameters.nbVarX;
+
+        r_ = parameters.control_weight;
+        tracking_weight_ = parameters.tracking_weight;
+        obstacle_weight_ = parameters.obstacle_weight;
+
+        num_timesteps_ = parameters.num_timesteps;
+        num_iterations_ = parameters.num_iterations;
+
+        viapoints_ = Eigen::MatrixXd::Zero(3, num_viapoints);
+        for (unsigned t = 0; t < parameters.viapoints.size(); ++t)
+        {
+            viapoints_.col(t) = parameters.viapoints[t];
+        }
+        
+        initial_state_ = Eigen::VectorXd::Zero(nbVarX_);
+        for (unsigned i = 0; i < parameters.initial_state.size(); ++i)
+        {
+            initial_state_(i) = parameters.initial_state[i];
+        }
+
+        viapoint_timesteps_ = Eigen::VectorXd::LinSpaced(num_viapoints + 1, 0, num_timesteps_ - 1).bottomRows(num_viapoints).array().round().cast<int>();
+
+        control_weight_ = r_ * Eigen::MatrixXd::Identity((num_timesteps_ - 1) * nbVarU_, (num_timesteps_ - 1) * nbVarU_);
+        precision_matrix_ = tracking_weight_* Eigen::MatrixXd::Identity(nbVarX_ * num_viapoints, nbVarX_ * num_viapoints);
+
+        Eigen::MatrixXi idx = Eigen::VectorXi::LinSpaced(nbVarX_, 0, nbVarX_ - 1).replicate(1, num_viapoints);
+
+        for (unsigned i = 0; i < idx.rows(); ++i)
+        {
+            idx.row(i) += Eigen::VectorXi((viapoint_timesteps_.array()) * nbVarX_).transpose();
+        }
+
+        mat_Su0_ = Eigen::MatrixXd::Zero(nbVarX_* (num_timesteps_), nbVarX_* (num_timesteps_ - 1));
+        mat_Su0_.bottomRows(nbVarX_ * (num_timesteps_ - 1)) = kroneckerProduct(Eigen::MatrixXd::Ones(num_timesteps_ - 1, num_timesteps_ - 1),  //
+                                                                                  parameters.dt * Eigen::MatrixXd::Identity(nbVarX_, nbVarX_))
+                                                                   .eval()
+                                                                   .triangularView<Eigen::Lower>();
+        mat_Sx0_ = kroneckerProduct(Eigen::MatrixXd::Ones(num_timesteps_, 1),  //
+                                    Eigen::MatrixXd::Identity(nbVarX_, nbVarX_))
+                     .eval();
+
+        mat_Su_ = Eigen::MatrixXd::Zero(idx.size(), nbVarX_* (num_timesteps_ - 1));
+        for (unsigned i = 0; i < idx.size(); ++i)
+        {
+            mat_Su_.row(i) = mat_Su0_.row(idx(i));
+        }
+
+        y_ = Eigen::VectorXd::Zero(static_cast<int>(parameters.y.size()));
+        for (unsigned i = 0; i < parameters.y.size(); ++i)
+        {
+            y_(i) = parameters.y[i];
+        }
+
+        Eigen::MatrixXd S = pow(parameters.rc, -2) * Eigen::MatrixXd::Identity(2,2);
+        x_ = Eigen::MatrixXd::Zero(parameters.x.size(), parameters.x[0].size());
+        for (unsigned i = 0; i < parameters.x.size(); ++i)
+        {
+            for (unsigned j = 0; j < parameters.x[0].size(); ++j){
+                x_(i,j) = parameters.x[i][j];
+            }
+        }
+
+        p_ = Eigen::VectorXd::Zero(static_cast<int>(parameters.y.size()));
+        for (unsigned i = 0; i < parameters.p.size(); ++i)
+        {
+            p_(i) = parameters.p[i];
+        }
+
+        Eigen::VectorXd xc = Eigen::VectorXd::Zero(static_cast<int>(parameters.xc.size()));
+        for (unsigned i = 0; i < parameters.xc.size(); ++i)
+        {
+            xc(i) = parameters.xc[i];
+        }
+
+        Mu2_ = 0.5* parameters.rc* (Eigen::MatrixXd::Ones(x_.cols(), x_.cols()) - (x_ - xc.replicate(1,x_.cols())).transpose()*S //
+                                                                                *(x_ - xc.replicate(1,x_.cols()))).diagonal();
+        
+        K_ = std::get<0>(covFct(x_,x_,p_,true));
+
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////
+}
+
+int main()
+{
+    Parameters parameters;
+    Model model(parameters);  // initialize the model with the parameters that were defined at the top
+
+    Eigen::MatrixXd trajectory = model.ilqr();  // calculate the iLQR solution
+
+    return 0;
+}
\ No newline at end of file
diff --git a/matlab/iLQR_manipulator_obstacle.m b/matlab/iLQR_manipulator_obstacle.m
index cd7f986ea825a51531e0ffc5489326046f3273b7..803316391d56810959e69510bc9859c29b01b50f 100644
--- a/matlab/iLQR_manipulator_obstacle.m
+++ b/matlab/iLQR_manipulator_obstacle.m
@@ -2,7 +2,8 @@
 %% 	  (viapoints task with position+orientation including bounding boxes on f(x))
 %%
 %%    Copyright (c) 2021 Idiap Research Institute, http://www.idiap.ch/
-%%    Written by Sylvain Calinon <https://calinon.ch>
+%%    Written by Teguh Lembono <teguh.lembono@idiap.ch>, 
+%%		Sylvain Calinon <https://calinon.ch>
 %%
 %%    This file is part of RCFS.
 %%
@@ -24,7 +25,7 @@ function iLQR_manipulator_obstacle
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 param.dt = 1E-2; %Time step size
 param.nbData = 50; %Number of datapoints
-param.nbIter = 50; %Number of iterations for iLQR
+param.nbIter = 30; %Number of iterations for iLQR
 param.nbPoints = 1; %Number of viapoints
 param.nbObstacles = 2; %Number of obstacles
 param.nbVarX = 3; %State space dimension (q1,q2,q3)
@@ -35,6 +36,7 @@ param.sz2 = [.5, .8]; %Size of obstacles
 param.q = 1E2; %Tracking weight term
 param.q2 = 1E0; %Obstacle avoidance weight term
 param.r = 1E-3; %Control weight term
+param.useBatch = true; %use batch computation for collision cost
 
 param.Mu = [[3; -1; 0]]; %Viapoints (x1,x2,o)
 
@@ -64,11 +66,16 @@ Su0 = [zeros(param.nbVarX, param.nbVarX*(param.nbData-1)); tril(kron(ones(param.
 Sx0 = kron(ones(param.nbData,1), eye(param.nbVarX));
 Su = Su0(idx,:);
 
+tic;
 for n=1:param.nbIter
 	x = reshape(Su0 * u + Sx0 * x0, param.nbVarX, param.nbData); %System evolution
 	[f, J] = f_reach(x(:,tl), param); %Tracking objective
 	
-	[f2, J2, id2] = f_avoid(x, param); %Avoidance objective
+	if param.useBatch
+		[f2, J2, id2] = f_avoid2(x, param); %Avoidance objective
+	else
+		[f2, J2, id2] = f_avoid(x, param); %Avoidance objective
+	end
 	Su2 = Su0(id2,:);
 	
 	du = (Su' * J' * J * Su * param.q + Su2' * J2' * J2 * Su2 * param.q2 + R) \ ...
@@ -81,7 +88,11 @@ for n=1:param.nbIter
 		utmp = u + du * alpha;
 		xtmp = reshape(Su0 * utmp + Sx0 * x0, param.nbVarX, param.nbData);
 		ftmp = f_reach(xtmp(:,tl), param); %Tracking objective
-		ftmp2 = f_avoid(xtmp, param); %Avoidance objective
+		if param.useBatch
+			ftmp2 = f_avoid2(xtmp, param); %Avoidance objective
+		else
+			ftmp2 = f_avoid(xtmp, param); %Avoidance objective
+		end
 		cost = norm(ftmp(:))^2 * param.q + norm(ftmp2(:))^2 * param.q2 + norm(utmp)^2 * param.r;
 		if cost < cost0 || alpha < 1E-3
 			break;
@@ -89,13 +100,14 @@ for n=1:param.nbIter
 		alpha = alpha * 0.5;
 	end
 	u = u + du * alpha;
-	
+	disp(cost)
 	if norm(du * alpha) < 1E-2
 		break; %Stop iLQR when solution is reached
 	end
 end
 disp(['iLQR converged in ' num2str(n) ' iterations.']);
-
+toc
+%disp(tEnd-tStart)
 %Log data
 r.x = x;
 r.f = [];
@@ -192,6 +204,7 @@ function [f, J, id] = f_avoid(x, param)
 					f = [f; ftmp];
 					Jrob = [jkin(x(1:j,t), param.l(1:j)), zeros(param.nbVarF, param.nbVarU-j)];
 					Jtmp = -e' * param.U2(:,:,i)' * Jrob(1:2,:); %quadratic form
+
 					J = blkdiag(J, Jtmp);
 					id = [id, (t-1) * param.nbVarU + [1:param.nbVarU]'];
 				end
@@ -199,3 +212,46 @@ function [f, J, id] = f_avoid(x, param)
 		end
 	end
 end
+
+%%%%%%%%%%%%%%%%%%%%%%
+%Forward kinematics for all robot articulations in batch
+function f = fkin0batch(x, L)
+	T = tril(ones(size(x,1)));
+	T2 = T * diag(L);
+	f = [(T2 * cos(T * x))(:), ...
+			(T2 * sin(T * x))(:)]; 
+end
+
+%%%%%%%%%%%%%%%%%%%%%%
+%Cost and gradient for an obstacle avoidance task with batch computation
+function [f, J, id] = f_avoid2(x, param)
+	f=[]; J=[]; id=[];
+	for i=1:param.nbObstacles
+		xee_batch = fkin0batch(x, param.l);
+		n= size(xee_batch,1);
+		e = (xee_batch - repmat(param.Mu2(1:2,i)', n , 1)) * param.U2(:,:,i);
+		ftmp = ones(n,1) - sum(e.^2, 2); %quadratic form
+		
+		%get the indices where there is collision (ftmp > 0)
+		idx_active = linspace(1,n,n)(ftmp>0);
+		
+		for idx=1:size(idx_active,2)
+			j = idx_active(idx);
+			t = floor((j-1)/param.nbVarX)+1;
+			k = mod(j-1,param.nbVarX)+1;	
+	
+			%compute Jacobian
+			Jrob = [jkin(x(1:k,t), param.l(1:k)), zeros(param.nbVarF, param.nbVarX-k)];
+			Jtmp = -e(j,:) * param.U2(:,:,i)' * Jrob(1:2,:); %quadratic form
+			Jj = zeros(1,param.nbVarX*param.nbData);
+			Jj(:,param.nbVarX*(t-1)+1:param.nbVarX*(t)) = Jtmp;
+			J = [J; Jj];
+			id = [id, (t-1) * param.nbVarX + [1:param.nbVarX]'];
+		end
+		
+		f = [f; ftmp(idx_active(:))];		#keep only active value
+	end
+	
+	id = unique(id); #remove duplicate
+	J = J(:,id); #keep only active value
+end
\ No newline at end of file
diff --git a/python/iLQR_bimanual_manipulability.py b/python/iLQR_bimanual_manipulability.py
new file mode 100644
index 0000000000000000000000000000000000000000..ec0c8bb8f8c3206d3bba5ca7b1fe4802d9c18dc3
--- /dev/null
+++ b/python/iLQR_bimanual_manipulability.py
@@ -0,0 +1,231 @@
+'''
+    iLQR applied to a planar bimanual robot problem with a cost on tracking a desired
+    manipulability ellipsoid at the center of mass (batch formulation)
+
+    Copyright (c) 2021 Idiap Research Institute, http://www.idiap.ch/
+    Written by Boyang Ti <https://www.idiap.ch/~bti/> and
+    Sylvain Calinon <https://calinon.ch>
+
+    This file is part of RCFS.
+
+    RCFS is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 3 as
+    published by the Free Software Foundation.
+
+    RCFS 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 RCFS. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.patches as patches
+import scipy.linalg
+from scipy.linalg import fractional_matrix_power
+
+import numpy.matlib
+# Helper functions
+# ===============================
+
+# Forward kinematics for end-effector (in robot coordinate system)
+def fkin(x, param):
+    L = np.tril(np.ones(3))
+    f = np.vstack((
+        param.l[0:3].T @ np.cos(L @ x[0:3]),
+        param.l[0:3].T @ np.sin(L @ x[0:3]),
+        param.l[[0, 3, 4]].T @ np.cos(L @ x[[0, 3, 4]]),
+        param.l[[0, 3, 4]].T @ np.sin(L @ x[[0, 3, 4]])
+    ))  # f1,f2
+    return f
+
+# Forward kinematics for end-effector (in robot coordinate system)
+def fkin0(x, param): 
+    L = np.tril(np.ones(3))
+    fl = np.vstack((
+        L @ np.diag(param.l[0:3].flatten()) @ np.cos(L @ x[0:3]),
+        L @ np.diag(param.l[0:3].flatten()) @ np.sin(L @ x[0:3])
+    ))
+    fr = np.vstack((
+        L @ np.diag(param.l[[0, 3, 4]].flatten()) @ np.cos(L @ x[[0, 3, 4]]),
+        L @ np.diag(param.l[[0, 3, 4]].flatten()) @ np.sin(L @ x[[0, 3, 4]])
+    ))
+    f = np.hstack((fl[:, ::-1], np.zeros([2, 1]), fr))
+    return f
+
+# Forward kinematics for center of mass of a bimanual robot (in robot coordinate system)
+def fkin_CoM(x, param):
+    L = np.tril(np.ones(3))
+    f = np.vstack((param.l[0:3].T @ L @ np.cos(L @ x[0:3]) + param.l[[0, 3, 4]].T @ L @ np.cos(L @ x[[0, 3, 4]]),
+                   param.l[0:3].T @ L @ np.sin(L @ x[0:3]) + param.l[[0, 3, 4]].T @ L @ np.sin(L @ x[[0, 3, 4]]))) / 6
+
+    return f
+
+# Jacobian with analytical computation (for single time step)
+def Jkin(x, param):
+    L = np.tril(np.ones([param.nbVarX, param.nbVarX]))
+    J = np.vstack([
+        -np.sin(L @ x).T @ np.diag(param.l) @ L,
+         np.cos(L @ x).T @ np.diag(param.l) @ L,
+         np.ones([1, param.nbVarX])
+    ])
+    return J
+
+# Jacobian for center of mass of a robot (in robot coordinate system)
+def Jkin_CoM(x, param):
+    L = np.tril(np.ones(3))
+    Jl = np.vstack((-np.sin(L @ x[0:3].reshape(-1, 1)).T @ L @ np.diag((param.l[0:3].T @ L).flatten()),
+                   np.cos(L @ x[0:3].reshape(-1, 1)).T @ L @ np.diag((param.l[0:3].T @ L).flatten()))) / 6
+    Jr = np.vstack((-np.sin(L @ x[[0, 3, 4]].reshape(-1, 1)).T @ L @ np.diag((param.l[[0, 3, 4]].T @ L).flatten()),
+                   np.cos(L @ x[[0, 3, 4]].reshape(-1, 1)).T @ L @ np.diag((param.l[[0, 3, 4]].T @ L).flatten()))) / 6
+    J = np.hstack(((Jl[:, 0] + Jr[:, 0]).reshape(-1, 1), Jl[:, 1:], Jr[:, 1:]))
+    return J
+
+def rman(x, param):
+    G = fractional_matrix_power(param.MuS, -0.5)
+    f = np.zeros((3, np.size(x, 1)))
+    for i in range(np.size(x, 1)):
+        Jt = Jkin_CoM(x[:, i], param)  # Jacobian for center of mass
+        St = Jt @ Jt.T  # manipulability matrix
+
+        D, V = np.linalg.eig(G @ St @ G)
+        E = V @ np.diag(np.log(D)) @ np.linalg.pinv(V)
+
+        E = np.tril(E) * (np.eye(2) + np.tril(np.ones(2), -1) * np.sqrt(2))
+        f[:, i] = E[np.where(E!=0)]
+    return f
+
+# Jacobian for manipulability tracking with numerical computation
+def Jman_num(x, param):
+    e = 1E-6
+    X = np.matlib.repmat(x, 1, param.nbVarX)
+    F1 = rman(X, param)
+    F2 = rman(X + np.eye(param.nbVarX) * e, param)
+    J = (F2 - F1) / e
+    return J
+
+# Residuals f and Jacobians J for manipulability tracking
+# (c=f'*f is the cost, g=J'*f is the gradient, H=J'*J is the approximated Hessian)
+def f_manipulability(x, param):
+    f = rman(x, param)  # Residuals
+    for t in range(np.size(x, 1)):
+        if t == 0:
+            J = Jman_num(x[:, t].reshape(-1, 1), param)
+        else:
+            J = scipy.linalg.block_diag(J, Jman_num(x[:, t].reshape(-1, 1), param))  # Jacobians
+    return f, J
+
+## Parameters
+param = lambda: None # Lazy way to define an empty class in python
+
+param.dt = 1e0 # Time step length
+param.nbIter = 100 # Maximum number of iterations for iLQR
+param.nbPoints = 1 # Number of viapoints
+param.nbData = 10 # Number of datapoints
+param.nbVarX = 5 # State space dimension ([q1,q2,q3] for left arm, [q1,q4,q5] for right arm)
+param.nbVarU = param.nbVarX # Control space dimension ([dq1, dq2, dq3, dq4, dq5])
+param.nbVarF = 4 # Objective function dimension ([x1,x2] for left arm and [x3,x4] for right arm)
+param.l = np.ones((param.nbVarX, 1)) * 2. # Robot links lengths
+param.r = 1e-6 # Control weight term
+param.MuS = np.asarray([[10, 2], [2, 4]])
+
+# Precision matrix
+Q = np.kron(np.identity(param.nbPoints), np.diag([0., 0., 0., 0.]))
+# Control weight matrix
+R = np.identity((param.nbData-1) * param.nbVarU) * param.r
+# Precision matrix for continuous CoM tracking
+Qc = np.kron(np.identity(param.nbData), np.diag([0., 0.]))
+
+# Time occurrence of viapoints
+tl = np.linspace(0, param.nbData, param.nbPoints+1)
+tl = np.rint(tl[1:]).astype(np.int64) - 1
+idx = np.array([i + np.arange(0, param.nbVarX, 1) for i in (tl*param.nbVarX)])
+
+# iLQR
+# ===============================
+u = np.zeros(param.nbVarU * (param.nbData-1))  # Initial control command
+x0 = np.array([np.pi/3, np.pi/2, np.pi/3, -np.pi/3, -np.pi/4])  # Initial state
+
+# Transfer matrices (for linear system as single integrator)
+Su0 = np.vstack([
+    np.zeros([param.nbVarX, param.nbVarX*(param.nbData-1)]),
+    np.tril(np.kron(np.ones([param.nbData-1, param.nbData-1]), np.eye(param.nbVarX) * param.dt))
+])
+Sx0 = np.kron(np.ones(param.nbData), np.identity(param.nbVarX)).T
+Su = Su0[idx.flatten()]  # We remove the lines that are out of interest
+for i in range(param.nbIter):
+    x = Su0 @ u + Sx0 @ x0  # System evolution
+    x = x.reshape([param.nbVarX, param.nbData], order='F')
+    f, J = f_manipulability(x[:, tl], param)  # Residuals and Jacobians
+    du = np.linalg.inv(Su.T @ J.T @ J @ Su + R) @ (-Su.T @ J.T @ f.flatten('F') - u * param.r)  # Gauss-Newton update
+    # Estimate step size with backtracking line search method
+    alpha = 1
+    cost0 = f.flatten('F').T @ f.flatten('F') + np.linalg.norm(u)**2 * param.r  # Cost
+    while True:
+        utmp = u + du * alpha
+        xtmp = Su0 @ utmp + Sx0 @ x0  # System evolution
+        xtmp = xtmp.reshape([param.nbVarX, param.nbData], order='F')
+        ftmp, _ = f_manipulability(xtmp[:, tl], param)  # Residuals
+        cost = ftmp.flatten('F').T @ ftmp.flatten('F') + np.linalg.norm(utmp)**2 * param.r  # Cost
+        if cost < cost0 or alpha < 1e-3:
+            u = utmp
+            print("Iteration {}, cost: {}".format(i,cost))
+            break
+        alpha /= 2
+    if np.linalg.norm(du * alpha) < 1E-2:
+        break # Stop iLQR iterations when solution is reached
+
+# Plots
+# ===============================
+plt.figure()
+plt.axis('off')
+plt.gca().set_aspect('equal', adjustable='box')
+
+fc = fkin_CoM(x, param)
+al = np.linspace(-np.pi, np.pi, 50)
+ax = plt.gca()
+
+# Plot desired manipulability ellipsoid
+D1, V1 = np.linalg.eig(param.MuS)
+D1 = np.diag(D1)
+R1 = np.real(V1@np.sqrt(D1+0j))
+msh1 = (R1 @ np.array([np.cos(al), np.sin(al)]) * 0.52).T + np.matlib.repmat(fc[:, -1].reshape(-1, 1), 1, 50).T
+p1 = patches.Polygon(msh1, closed=True, alpha=0.9)
+p1.set_facecolor([1, 0.7, 0.7])
+p1.set_edgecolor([1, 0.6, 0.6])
+ax.add_patch(p1)
+
+# Plot robot manipulability ellipsoid
+J = Jkin_CoM(x[:, -1], param)
+S = J @ J.T
+D2, V2 = np.linalg.eig(S)
+D2 = np.diag(D2)
+R2 = np.real(V2@np.sqrt(D2+0j))
+msh2 = (R2 @ np.array([np.cos(al), np.sin(al)]) * 0.5).T + np.matlib.repmat(fc[:, -1].reshape(-1, 1), 1, 50).T
+p2 = patches.Polygon(msh2, closed=True, alpha=0.9)
+p2.set_facecolor([0.4, 0.4, 0.4])
+p2.set_edgecolor([0.3, 0.3, 0.3])
+ax.add_patch(p2)
+
+# Plot CoM
+fc = fkin_CoM(x, param) # Forward kinematics for center of mass
+plt.plot(fc[0, 0], fc[1, 0], marker='o', markerfacecolor='none', markeredgewidth=4, markersize=10, markeredgecolor=[0.5, 0.5, 0.5]) # Plot CoM
+plt.plot(fc[0, tl[-1]], fc[1, tl[-1]], marker='o', markerfacecolor='none', markeredgewidth=4, markersize=10, markeredgecolor=[0.2, 0.2, 0.2]) # Plot CoM
+
+# Plot end-effectors paths
+f01 = fkin0(x[:, 0], param)
+f02 = fkin0(x[:, tl[0]], param)
+
+# Get points of interest
+f = fkin(x, param)
+plt.plot(f01[0, :], f01[1, :], c='black', linewidth=4, alpha=.2)
+plt.plot(f02[0, :], f02[1, :], c='black', linewidth=4, alpha=.4)
+
+plt.plot(f[0, :], f[1, :], c='black', marker='o', markevery=[0]+tl.tolist())
+plt.plot(f[2, :], f[3, :], c='black', marker='o', markevery=[0]+tl.tolist())
+
+
+plt.show()
diff --git a/python/iLQR_manipulator.py b/python/iLQR_manipulator.py
index e216452cc017ec53960efc4fbadfc71322528c9c..2aa963d47157c32745eb03efae5692997e361aa9 100644
--- a/python/iLQR_manipulator.py
+++ b/python/iLQR_manipulator.py
@@ -85,6 +85,8 @@ def f_reach(x, param):
 	return f,J
 
 ## Parameters
+# ===============================
+
 param = lambda: None # Lazy way to define an empty class in python
 param.dt = 1e-2 # Time step length
 param.nbData = 50 # Number of datapoints
diff --git a/python/iLQR_manipulator_dynamics.py b/python/iLQR_manipulator_dynamics.py
index a0db108edde5fa223635a85301bb8d0afcc5f998..471e2f3e018106fa36eb8a4e4c4b3d620303e5d3 100644
--- a/python/iLQR_manipulator_dynamics.py
+++ b/python/iLQR_manipulator_dynamics.py
@@ -21,7 +21,6 @@
 '''
 
 import numpy as np
-import numpy.matlib
 import matplotlib.pyplot as plt
 import matplotlib.patches as patches
 
@@ -36,46 +35,43 @@ def logmap(f,f0):
     return error
 
 # Forward kinematics for E-E
-def fkin(param,x):
-    x = x.T
-    A = np.tril(np.ones([param.nbVarX,param.nbVarX]))
-    f = np.vstack((param.linkLengths @ np.cos(A @ x), 
-                   param.linkLengths @ np.sin(A @ x), 
-                   np.mod(np.sum(x,0)+np.pi, 2*np.pi) - np.pi)) #x1,x2,o (orientation as single Euler angle for planar robot)
-    return f.T
+def fkin(x,param):
+	L = np.tril(np.ones([param.nbVarX, param.nbVarX]))
+	f = np.vstack([
+		param.l @ np.cos(L @ x.T),
+		param.l @ np.sin(L @ x.T),
+		np.mod(np.sum(x.T,0)+np.pi, 2*np.pi) - np.pi
+	]) # f1,f2,f3, where f3 is the orientation (single Euler angle for planar robot)
+	return f.T
 
 # Forward Kinematics for all joints
-def fkin0(param,x):
-    T = np.tril(np.ones([param.nbVarX,param.nbVarX]))
-    T2 = np.tril(np.matlib.repmat(param.linkLengths,len(x),1))
-    f = np.vstack(( 
-        T2 @ np.cos(T@x),
-        T2 @ np.sin(T@x)
-    )).T
-    f = np.vstack(( 
-        np.zeros(2),
-        f
-    ))
-    return f
+def fkin0(x,param):
+	L = np.tril(np.ones([param.nbVarX, param.nbVarX]))
+	f = np.vstack([
+		L @ np.diag(param.l) @ np.cos(L @ x),
+		L @ np.diag(param.l) @ np.sin(L @ x)
+	])
+	f = np.hstack([np.zeros([2,1]), f])
+	return f.T
 
 # Jacobian with analytical computation (for single time step)
-def jkin(param,x):
-    T = np.tril( np.ones((len(x),len(x))) )
+def Jkin(x,param):
+    L = np.tril( np.ones((len(x),len(x))) )
     J = np.vstack((
-        -np.sin(T@x).T @ np.diag(param.linkLengths) @ T,
-        np.cos(T@x).T @ np.diag(param.linkLengths) @ T,
+        -np.sin(L@x).T @ np.diag(param.l) @ L,
+        np.cos(L@x).T @ np.diag(param.l) @ L,
         np.ones(len(x))
     ))
     return J
 
 # Residual and Jacobian
-def f_reach(param,x):
-    f = logmap(fkin(param,x),param.mu)
-    J = np.zeros(( len(x) * param.nbVarF , len(x) * param.nbVarX ))
+def f_reach(x,param):
+    f = logmap(fkin(x,param),param.Mu)
+    J = np.zeros(( param.nbPoints  * param.nbVarF , param.nbPoints  * param.nbVarX ))
 
-    for t in range(x.shape[0]):
+    for t in range(param.nbPoints ):
         f[t,:2] = param.A[t].T @ f[t,:2] # Object oriented fk
-        Jtmp = jkin(param,x[t])
+        Jtmp = Jkin(x[t],param)
         Jtmp[:2] = param.A[t].T @ Jtmp[:2] # Object centered jacobian
 
         if param.useBoundingBox:
@@ -91,74 +87,69 @@ def f_reach(param,x):
 # Forward dynamic to compute
 def forward_dynamics(x, u, param):
     
-    g = 9.81 #Gravity norm
-    kv = 1 #Joints Damping
-    l = np.reshape( param.linkLengths, [1,param.nbVarX] )
+    l = np.reshape( param.l, [1,param.nbVarX] )
     m = np.reshape( param.linkMasses, [1,param.nbVarX] )
-    dt = param.dt
     
-    nbDOFs = l.shape[1]
-    nbData = int(u.shape[0]/nbDOFs + 1)
-    Tm = np.multiply(np.triu(np.ones([nbDOFs,nbDOFs])), np.repeat(m, nbDOFs,0)) 
-    T = np.tril(np.ones([nbDOFs, nbDOFs]))
-    Su = np.zeros([2 * nbDOFs * nbData, nbDOFs * (nbData - 1)])
+    Tm = np.multiply(np.triu(np.ones([param.nbVarX,param.nbVarX])), np.repeat(m, param.nbVarX,0)) 
+    T = np.tril(np.ones([param.nbVarX, param.nbVarX]))
+    Su = np.zeros([2 * param.nbVarX * param.nbData , param.nbVarX * (param.nbData  - 1)])
     
     #Precomputation of mask (in tensor form)
-    S1= np.zeros([nbDOFs, nbDOFs, nbDOFs])
-    J_index = np.ones([1, nbDOFs])
-    for j in range(nbDOFs):
+    S1= np.zeros([param.nbVarX, param.nbVarX, param.nbVarX])
+    J_index = np.ones([1, param.nbVarX])
+    for j in range(param.nbVarX):
         J_index[0,:j] = np.zeros([j])
-        S1[:,:,j] = np.repeat(J_index @ np.eye(nbDOFs), nbDOFs, 0) - np.transpose(np.repeat(J_index @ np.eye(nbDOFs), nbDOFs, 0))
+        S1[:,:,j] = np.repeat(J_index @ np.eye(param.nbVarX), param.nbVarX, 0) - np.transpose(np.repeat(J_index @ np.eye(param.nbVarX), param.nbVarX, 0))
      
     #Initialization of dM and dC tensors and A21 matrix
-    dM = np.zeros([nbDOFs, nbDOFs, nbDOFs])
-    dC = np.zeros([nbDOFs, nbDOFs, nbDOFs])
-    A21 = np.zeros([nbDOFs, nbDOFs])
+    dM = np.zeros([param.nbVarX, param.nbVarX, param.nbVarX])
+    dC = np.zeros([param.nbVarX, param.nbVarX, param.nbVarX])
+    A21 = np.zeros([param.nbVarX, param.nbVarX])
     
     
-    for t in range(nbData-1):        
+    for t in range(param.nbData -1):        
         
         # Computation in matrix form of G,M, and C
-        G =-np.reshape(np.sum(Tm,1), [nbDOFs,1]) * l.T * np.cos(T @ np.reshape(x[t,0:nbDOFs], [nbDOFs,1])) * g
+        G =-np.reshape(np.sum(Tm,1), [param.nbVarX,1]) * l.T * np.cos(T @ np.reshape(x[t,0:param.nbVarX], [param.nbVarX,1])) * param.g
         G = T.T @ G
-        M = (l.T * l) * np.cos(np.reshape(T @ x[t,:nbDOFs], [nbDOFs,1]) - T @ x[t,:nbDOFs]) * (Tm ** .5 @ ((Tm ** .5).T))
+        M = (l.T * l) * np.cos(np.reshape(T @ x[t,:param.nbVarX], [param.nbVarX,1]) - T @ x[t,:param.nbVarX]) * (Tm ** .5 @ ((Tm ** .5).T))
         M = T.T @ M @ T 
-        C = -(l.T * l) * np.sin(np.reshape(T @ x[t,:nbDOFs], [nbDOFs,1]) - T @ x[t,:nbDOFs]) * (Tm ** .5 @ ((Tm ** .5).T))
+        C = -(l.T * l) * np.sin(np.reshape(T @ x[t,:param.nbVarX], [param.nbVarX,1]) - T @ x[t,:param.nbVarX]) * (Tm ** .5 @ ((Tm ** .5).T))
         
         
         # Computation in tensor form of derivatives dG,dM, and dC
-        dG = np.diagflat(np.reshape(np.sum(Tm,1), [nbDOFs,1]) * l.T * np.sin(T @ np.reshape(x[t,0:nbDOFs], [nbDOFs,1])) * g) @ T
-        dM_tmp = (l.T * l) * np.sin(np.reshape(T @ x[t,:nbDOFs], [nbDOFs,1]) - T @ x[t,:nbDOFs]) * (Tm ** .5 @ ((Tm ** .5).T)) 
+        dG = np.diagflat(np.reshape(np.sum(Tm,1), [param.nbVarX,1]) * l.T * np.sin(T @ np.reshape(x[t,0:param.nbVarX], [param.nbVarX,1])) * param.g) @ T
+        dM_tmp = (l.T * l) * np.sin(np.reshape(T @ x[t,:param.nbVarX], [param.nbVarX,1]) - T @ x[t,:param.nbVarX]) * (Tm ** .5 @ ((Tm ** .5).T)) 
         
         for j in range(dM.shape[2]):
             dM[:,:,j] = T.T @ (dM_tmp * S1[:,:,j]) @ T
         
-        dC_tmp = (l.T * l) * np.cos(np.reshape( T @ x[t,:nbDOFs], [nbDOFs,1]) - T @ x[t,:nbDOFs]) * (Tm ** .5 @ ((Tm ** .5).T)) 
+        dC_tmp = (l.T * l) * np.cos(np.reshape( T @ x[t,:param.nbVarX], [param.nbVarX,1]) - T @ x[t,:param.nbVarX]) * (Tm ** .5 @ ((Tm ** .5).T)) 
         for j in range(dC.shape[2]):
             dC[:,:,j] = dC_tmp * S1[:,:,j]   
             
         # update pose 
-        tau = np.reshape(u[(t) * nbDOFs:(t + 1) * nbDOFs], [nbDOFs, 1])
+        tau = np.reshape(u[(t) * param.nbVarX:(t + 1) * param.nbVarX], [param.nbVarX, 1])
         inv_M = np.linalg.inv(M)
-        ddq =inv_M @ (tau + G + T.T @ C @ (T @ np.reshape(x[t,nbDOFs:], [nbDOFs,1])) ** 2) - T @ np.reshape(x[t,nbDOFs:], [nbDOFs,1]) * kv
+        ddq =inv_M @ (tau + G + T.T @ C @ (T @ np.reshape(x[t,param.nbVarX:], [param.nbVarX,1])) ** 2) - T @ np.reshape(x[t,param.nbVarX:], [param.nbVarX,1]) * param.kv
         
         # compute local linear systems
-        x[t+1,:] = x[t,:] + np.hstack([x[t,nbDOFs:], np.reshape(ddq, [nbDOFs,])]) * dt
-        A11 = np.eye(nbDOFs)
-        A12 = A11 * dt
-        A22 = np.eye(nbDOFs) + (2 * inv_M @ T.T @ C @ np.diagflat(T @ x[t,nbDOFs:]) @ T - T * kv) * dt
-        for j in range(nbDOFs):
-            A21[:,j] = (-inv_M @ dM[:,:,j] @ inv_M @ (tau + G + T.T @ C @ (T @ np.reshape(x[t,nbDOFs:], [nbDOFs,1])) ** 2) 
-                        + np.reshape(inv_M @ T.T @ dG[:,j], [nbDOFs,1]) + inv_M @ T .T @ dC[:,:,j] @ (T @ np.reshape(x[t,nbDOFs:], [nbDOFs,1])) ** 2).flatten()
-        A = np.vstack((np.hstack((A11, A12)), np.hstack((A21 * dt, A22))))
-        B = np.vstack((np.zeros([nbDOFs, nbDOFs]), inv_M * dt))
+        x[t+1,:] = x[t,:] + np.hstack([x[t,param.nbVarX:], np.reshape(ddq, [param.nbVarX,])]) * param.dt
+        A11 = np.eye(param.nbVarX)
+        A12 = A11 * param.dt
+        A22 = np.eye(param.nbVarX) + (2 * inv_M @ T.T @ C @ np.diagflat(T @ x[t,param.nbVarX:]) @ T - T * param.kv) * param.dt
+        for j in range(param.nbVarX):
+            A21[:,j] = (-inv_M @ dM[:,:,j] @ inv_M @ (tau + G + T.T @ C @ (T @ np.reshape(x[t,param.nbVarX:], [param.nbVarX,1])) ** 2) 
+                        + np.reshape(inv_M @ T.T @ dG[:,j], [param.nbVarX,1]) + inv_M @ T .T @ dC[:,:,j] @ (T @ np.reshape(x[t,param.nbVarX:], [param.nbVarX,1])) ** 2).flatten()
+        A = np.vstack((np.hstack((A11, A12)), np.hstack((A21 * param.dt, A22))))
+        B = np.vstack((np.zeros([param.nbVarX, param.nbVarX]), inv_M * param.dt))
         
         # compute transformation matrix
-        Su[2 * nbDOFs * (t + 1):2 * nbDOFs * (t + 2),:] = A @ Su[2 * nbDOFs * t:2 * nbDOFs * (t + 1),:]
-        Su[2 * nbDOFs * (t + 1):2 * nbDOFs * (t + 2), nbDOFs * t:nbDOFs * (t + 1)] =B
+        Su[2 * param.nbVarX * (t + 1):2 * param.nbVarX * (t + 2),:] = A @ Su[2 * param.nbVarX * t:2 * param.nbVarX * (t + 1),:]
+        Su[2 * param.nbVarX * (t + 1):2 * param.nbVarX * (t + 2), param.nbVarX * t:param.nbVarX * (t + 1)] =B
     return x, Su
 
-# General param parameters
+# Parameters
 # ===============================
 
 param = lambda: None # Lazy way to define an empty class in python
@@ -169,66 +160,54 @@ param.nbPoints = 2 # Number of viapoints
 param.nbVarX = 3 # State space dimension (x1,x2,x3)
 param.nbVarU = 3 # Control space dimension (dx1,dx2,dx3)
 param.nbVarF = 3 # Objective function dimension (f1,f2,f3, with f3 as orientation)
-param.linkLengths = [2,2,1] # Robot links lengths
+param.l = [2,2,1] # Robot links lengths
 param.linkMasses = [1,1,1]
-param.sizeObj = [.2,.3] # Size of objects
+param.g = 9.81 # Gravity norm
+param.kv = 1 # Joint damping
+param.sz = [.2,.3] # Size of objects
 param.r = 1e-6 # Control weight term
-param.mu = np.asarray([[2,1,-np.pi/6], [3,2,-np.pi/3]]).T # Viapoints 
+param.Mu = np.asarray([[2,1,-np.pi/3], [3,2,-np.pi/3]]) # Viapoints 
 param.useBoundingBox = False
+param.A = np.zeros( (param.nbPoints,2,2) ) # Object orientation matrices
 
-# Task parameters
+# Main program
 # ===============================
 
-# Targets
-param.mu = np.asarray([
-    [2,1,-np.pi/3],                # x , y , orientation
-    [3,2,-np.pi/3]
-])
-
 # Transformation matrices
-param.A = np.zeros( (param.nbPoints,2,2) )
 for i in range(param.nbPoints):
-    orn_t = param.mu[i,-1]
+    orn_t = param.Mu[i,-1]
     param.A[i,:,:] = np.asarray([
         [np.cos(orn_t) , -np.sin(orn_t)],
         [np.sin(orn_t) , np.cos(orn_t)]
     ])
 
-# Regularization matrix
-R = np.identity( (param.nbData-1) * param.nbVarU ) * param.r
-
 # Precision matrix
 Q = np.identity( param.nbVarF  * param.nbPoints)*1e5
 
-# System parameters
-# ===============================
+# Regularization matrix
+R = np.identity( (param.nbData-1) * param.nbVarU ) * param.r
 
 # Time occurence of viapoints
 tl = np.linspace(0,param.nbData,param.nbPoints+1)
 tl = np.rint(tl[1:]).astype(np.int64)-1
 idx = np.array([ i + np.arange(0,param.nbVarX,1) for i in (tl* 2* param.nbVarX)]) 
 
+# iLQR
+# ===============================
+
 u = np.zeros( param.nbVarU * (param.nbData-1) ) # Initial control command
 x0 = np.array( [3 * np.pi/4 , -np.pi/2 , - np.pi/4] ) # Initial position (in joint space)
-v0 = np.array([0,0,0])#initial velocity (in joint space)
+v0 = np.array([0,0,0]) #initial velocity (in joint space)
 x = np.zeros([param.nbData, 2*param.nbVarX])
 x[0,:param.nbVarX] = x0
 x[0,param.nbVarX:] = v0
-# Transfer matrices (for linear system as single integrator)
-Su0 = np.vstack([np.zeros((param.nbVarX, param.nbVarX*(param.nbData-1))), 
-      np.tril(np.kron(np.ones((param.nbData-1, param.nbData-1)), np.eye(param.nbVarX)*param.dt))]) 
-Sx0 = np.kron( np.ones(param.nbData) , np.identity(param.nbVarX) ).T
- # We remove the lines that are out of interest
-
-# Solving iLQR
-# ===============================
 
 for i in range( param.nbIter ):
     # system evolution and Transfer matrix (computed from forward dynamics)
     x, Su0 = forward_dynamics(x, u, param)
     Su = Su0[idx.flatten()]
 
-    f, J = f_reach(param,x[tl,:param.nbVarX])
+    f, J = f_reach(x[tl,:param.nbVarX],param)
     du = np.linalg.inv(Su.T @ J.T @ Q @ J @ Su + R) @ (-Su.T @ J.T @ Q @ f.flatten() - u * param.r)
 
     # Perform line search
@@ -238,7 +217,7 @@ for i in range( param.nbIter ):
     while True:
         utmp = u + du * alpha
         xtmp, _ = forward_dynamics(x, utmp, param)
-        ftmp, _ = f_reach(param,xtmp[tl,:param.nbVarX])
+        ftmp, _ = f_reach(xtmp[tl,:param.nbVarX],param)
         cost = ftmp.flatten() @ Q @ ftmp.flatten() + np.linalg.norm(utmp) * param.r
         
         if cost < cost0 or alpha < 1e-3:
@@ -258,9 +237,9 @@ plt.axis("off")
 plt.gca().set_aspect('equal', adjustable='box')
 
 # Get points of interest
-f = fkin(param,x[:,:param.nbVarX])
-f00 = fkin0(param,x[0,:param.nbVarX])
-fT0 = fkin0(param,x[-1,:param.nbVarX])
+f = fkin(x[:,:param.nbVarX],param)
+f00 = fkin0(x[0,:param.nbVarX],param)
+fT0 = fkin0(x[-1,:param.nbVarX],param)
 
 plt.plot( f00[:,0] , f00[:,1],c='black',linewidth=5,alpha=.2)
 plt.plot( fT0[:,0] , fT0[:,1],c='black',linewidth=5,alpha=.6)
@@ -273,12 +252,12 @@ color_map = ["deepskyblue","darkorange"]
 for i in range(param.nbPoints):
     
     if param.useBoundingBox:
-        rect_origin = param.mu[i,:2] - param.A[i]@np.array(param.sizeObj)
-        rect_orn = param.mu[i,-1]
+        rect_origin = param.Mu[i,:2] - param.A[i]@np.array(param.sizeObj)
+        rect_orn = param.Mu[i,-1]
 
         rect = patches.Rectangle(rect_origin,param.sizeObj[0]*2,param.sizeObj[1]*2,np.degrees(rect_orn),color=color_map[i])
         ax.add_patch(rect)
     else:
-        plt.scatter(param.mu[i,0],param.mu[i,1],s=100,marker="X",c=color_map[i])
+        plt.scatter(param.Mu[i,0],param.Mu[i,1],s=100,marker="X",c=color_map[i])
 
 plt.show()
diff --git a/python/iLQR_manipulator_obstacle.py b/python/iLQR_manipulator_obstacle.py
index 17eafe4518087ef48f9da4cb6c01af4d294bf347..ff77cbb4d52faede7812138a5f85c2a53f178288 100644
--- a/python/iLQR_manipulator_obstacle.py
+++ b/python/iLQR_manipulator_obstacle.py
@@ -3,6 +3,7 @@
 
     Copyright (c) 2021 Idiap Research Institute, http://www.idiap.ch/
     Written by Jeremy Maceiras <jeremy.maceiras@idiap.ch>,
+    Teguh Lembono <teguh.lembono@idiap.ch>,
     Sylvain Calinon <https://calinon.ch>
 
     This file is part of RCFS.
@@ -24,6 +25,8 @@ import numpy as np
 import numpy.matlib
 import matplotlib.pyplot as plt
 import matplotlib.patches as patches
+import time
+
 
 # Helper functions
 # ===============================
@@ -37,15 +40,15 @@ def logmap(f,f0):
 
 # Forward kinematics for E-E
 def fkin(x,ls):
-	x = x.T
-	A = np.tril(np.ones([len(ls),len(ls)]))
-	f = np.vstack((ls @ np.cos(A @ x), 
-				   ls @ np.sin(A @ x), 
-				   np.mod(np.sum(x,0)+np.pi, 2*np.pi) - np.pi)) #x1,x2,o (orientation as single Euler angle for planar robot)
-	return f.T
+    x = x.T
+    A = np.tril(np.ones([len(ls),len(ls)]))
+    f = np.vstack((ls @ np.cos(A @ x), 
+                    ls @ np.sin(A @ x), 
+                    np.mod(np.sum(x,0)+np.pi, 2*np.pi) - np.pi)) #x1,x2,o (orientation as single Euler angle for planar robot)
+    return f.T
 
 # Forward Kinematics for all joints
-def fkin0(param,x):
+def fkin0(x, param):
     T = np.tril(np.ones([param.nbVarX,param.nbVarX]))
     T2 = np.tril(np.matlib.repmat(param.linkLengths,len(x),1))
     f = np.vstack(( 
@@ -58,8 +61,18 @@ def fkin0(param,x):
         ))
     return f
 
+# Forward Kinematics for all joints in batch
+def fkin0batch(x, param):
+    T = np.tril(np.ones([param.nbVarX,param.nbVarX]))
+    T2 = np.tril(np.matlib.repmat(param.linkLengths,x.shape[1],1))
+    f = np.vstack(( 
+        (T2 @ np.cos(T@x.T)).flatten(order='F'),
+        (T2 @ np.sin(T@x.T)).flatten(order='F')
+        )).T
+    return f
+
 # Residual and Jacobian for an obstacle avoidance task
-def f_avoid(param,x):
+def f_avoid(x, param):
     f=[]
     idx=[]
     J=np.zeros((0,0))
@@ -74,7 +87,7 @@ def f_avoid(param,x):
 
                 if ftmp > 0:
                     f += [ftmp]
-                    J_rbt = np.hstack(( jkin(x[t,:(j+1)] , param.linkLengths[:(j+1)]), np.zeros(( param.nbVarF , param.nbVarU-(j+1))) ))
+                    J_rbt = np.hstack(( jkin(x[t,:(j+1)] , param.linkLengths[:(j+1)]), np.zeros(( param.nbVarF , param.nbVarX-(j+1))) ))
                     J_tmp = (-e.T @ param.U_obs[i].T @ J_rbt[:2]).reshape((-1,1))
                     #J_tmp = 1*(J_rbt[:2].T @ param.U_obs[i] @ e).reshape((-1,1))
 
@@ -83,12 +96,45 @@ def f_avoid(param,x):
                     J2[-J_tmp.shape[0]:,-J_tmp.shape[1]:] = J_tmp
                     J = J2 # Numpy does not provid a blockdiag function...
 
-                    idx.append( (t-1)*param.nbVarU + np.array(range(param.nbVarU)) )
+                    idx.append( t*param.nbVarX + np.array(range(param.nbVarX)) )
 
     f = np.array(f)
     idx = np.array(idx)
     return f,J.T,idx
 
+def f_avoid2(x, param):
+    f, J, idx = [], [], []
+    
+    for i in range(param.nbObstacles):
+        f_x = fkin0batch(x, param)                
+        e = (f_x- param.Obs[i][:2])@param.U_obs[i]        
+        ftmp = 1 - np.linalg.norm(e, axis=1)**2
+        
+        #check for value larger than zeros
+        idx_active = np.arange(len(ftmp))[ftmp>0]
+        
+        #compute Jacobian for each idx_active
+        for j in idx_active:
+            t = j//param.nbVarX # which time step
+            k = j%param.nbVarX  # which link
+            
+            J_rbt = np.hstack((jkin(x[t,:(k+1)] , param.linkLengths[:(k+1)]), np.zeros(( param.nbVarF , param.nbVarX-(k+1))) ))
+            J_tmp = (-e[j].T @ param.U_obs[i].T @ J_rbt[:2])
+            J_j = np.zeros(param.nbVarX*param.nbData)
+            J_j[param.nbVarX*t: param.nbVarX*(t+1)] = J_tmp
+            J += [J_j]            
+            
+            #Get indices
+            idx += np.arange(param.nbVarX*t, param.nbVarX*(t+1)).tolist()
+        f += [ftmp[idx_active]]
+    
+    idx = np.array(list(set(idx)))
+    f = np.concatenate(f)
+    if len(idx) > 0:
+        J = np.vstack(J)
+        J = J[:,idx]
+    return f, J,idx
+
 # Jacobian with analytical computation (for single time step)
 def jkin(x,ls):
     T = np.tril( np.ones((len(ls),len(ls))) )
@@ -100,7 +146,7 @@ def jkin(x,ls):
     return J
 
 # Residual and Jacobian for a via-point reaching task
-def f_reach(param,x):
+def f_reach(x, param):
     f = logmap(fkin(x,param.linkLengths),param.mu)
     J = np.zeros(( len(x) * param.nbVarF , len(x) * param.nbVarX ))
 
@@ -125,6 +171,7 @@ param.sizeObstacle = [.5,.8] # Size of objects
 param.r = 1e-3 # Control weight term
 param.Q_track = 1e2
 param.Q_avoid = 1e0
+param.useBatch = False #Batch computation for collision avoidance cost
 
 # Task parameters
 # ===============================
@@ -181,13 +228,16 @@ Su = Su0[idx.flatten()] # We remove the lines that are out of interest
 
 # Solving iLQR
 # ===============================
-
+tic=time.time()
 for i in range( param.nbIter ):
     x = Su0 @ u + Sx0 @ x0
     x = x.reshape( (  param.nbData , param.nbVarX) )
 
-    f, J = f_reach(param,x[tl])  # Tracking objective
-    f2, J2, id2 = f_avoid(param,x)# Avoidance objective
+    f, J = f_reach(x[tl], param)  # Tracking objective
+    if param.useBatch:
+        f2, J2, id2 = f_avoid2(x, param)# Avoidance objective
+    else:
+        f2, J2, id2 = f_avoid(x, param)# Avoidance objective
 
     if len(id2) > 0: # Numpy does not allow zero sized array as Indices
         Su2 = Su0[id2.flatten()]
@@ -204,8 +254,12 @@ for i in range( param.nbIter ):
         utmp = u + du * alpha
         xtmp = Su0 @ utmp + Sx0 @ x0
         xtmp = xtmp.reshape( (  param.nbData , param.nbVarX) )
-        ftmp, _ = f_reach(param,xtmp[tl])
-        f2tmp,_,_ = f_avoid(param,xtmp)
+        ftmp, _ = f_reach(xtmp[tl], param)
+        if param.useBatch:
+            f2tmp,_,_ = f_avoid2(xtmp, param)
+        else:
+            f2tmp,_,_ = f_avoid(xtmp, param)
+            
         cost = np.linalg.norm(ftmp.flatten())**2 * param.Q_track + np.linalg.norm(f2tmp.flatten())**2 * param.Q_avoid + np.linalg.norm(utmp) * param.r
 
         if cost < cost0 or alpha < 1e-3:
@@ -216,8 +270,9 @@ for i in range( param.nbIter ):
         alpha /=2
     
     if np.linalg.norm(alpha * du) < 1e-2: # Early stop condition
-       break
-
+        break
+toc=time.time()
+print('Solving in {} seconds'.format(toc-tic))
 # Ploting
 # ===============================
 
@@ -232,8 +287,8 @@ for i in range(param.nbVarX-1):
     fi = fkin( x[:,:i+1] , param.linkLengths[:i+1] )
     plt.plot(fi[:,0],fi[:,1],c="black",alpha=.5,linestyle='dashed')
 
-f00 = fkin0(param,x[0])
-fT0 = fkin0(param,x[-1])
+f00 = fkin0(x[0], param)
+fT0 = fkin0(x[-1],param)
 
 plt.plot( f00[:,0] , f00[:,1],c='black',linewidth=5,alpha=.2)
 plt.plot( fT0[:,0] , fT0[:,1],c='black',linewidth=5,alpha=.6)