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()