diff --git a/README.md b/README.md
index aaa7ccc2728ee1f138d797c7c507f688ca7e3b60..7a99655e345ff105c3f772bb42a7b8b595f791f0 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ RCFS is a collection of source codes to study robotics through simple 2D example
 | LQT | Linear quadratic tracking (LQT) applied to a viapoint task (batch formulation) | ✅ | ✅ |  |  |
 | LQT_tennisServe | LQT in a ballistic task mimicking a bimanual tennis serve problem (batch formulation) | ✅ |  |  |  |
 | LQT_recursive | LQT applied to a viapoint task with a recursive formulation based on augmented state space to find a controller) | ✅ | ✅ |  |  |
+| LQT_nullspace | Batch LQT with nullspace formulation | ✅ | ✅ |  |  |
 | LQT_recursive_LS | LQT applied to a viapoint task with a recursive formulation based on least squares and an augmented state space to find a controller | ✅ | ✅ |  |  |
 | LQT_recursive_LS_multiAgents | LQT applied to a multi-agent system with recursive formulation based on least squares and augmented state, by using a precision matrix with nonzero offdiagonal elements to find a controller in which the two agents coordinate their movements to find an optimal meeting point | ✅ | ✅ |  |  |
 | LQT_CP | LQT with control primitives applied to a viapoint task (batch formulation) | ✅ | ✅ |  |  |
@@ -43,7 +44,6 @@ Additional reading material can be be found as [video lectures](https://tube.swi
 
 | Filename | Main responsible |
 |----------|------------------|
-| Example similar to demo_OC_LQT_nullspace_minimal01.m | Hakan |
 | Example similar to demo_OC_LQT_Lagrangian01.m (inclusion of constraints) | ??? |
 
 
diff --git a/matlab/LQT_nullspace.m b/matlab/LQT_nullspace.m
new file mode 100644
index 0000000000000000000000000000000000000000..7e6631f123349a80f3a5b766143426853914187e
--- /dev/null
+++ b/matlab/LQT_nullspace.m
@@ -0,0 +1,109 @@
+%%    Batch LQT with nullspace formulation
+%%
+%%    Copyright (c) 2021 Idiap Research Institute, http://www.idiap.ch/
+%%    Written by 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/>.
+
+function LQT_nullspace
+
+%% Parameters
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+param.nbData = 50; %Number of datapoints
+param.nbRepros = 60; %Number of stochastic reproductions
+param.nbPoints = 1; %Number of keypoints
+param.nbVar = 2; %Dimension of state vector
+param.dt = 1E-1; %Time step duration
+param.rfactor = 1E-4; %param.dt^nbDeriv;	%Control cost in LQR
+
+
+R = speye((param.nbData-1)*param.nbVar) * param.rfactor;
+x0 = zeros(param.nbVar,1); %Initial point
+
+
+%% Dynamical System settings
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+Su = [zeros(param.nbVar, param.nbVar*(param.nbData-1)); kron(tril(ones(param.nbData-1)), eye(param.nbVar)*param.dt)];
+Sx = kron(ones(param.nbData,1), eye(param.nbVar));
+
+
+%% Task setting
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+tl = linspace(1,param.nbData,param.nbPoints+1);
+tl = round(tl(2:end)); %[param.nbData/2, param.nbData];
+%Mu = rand(param.nbVarPos,param.nbPoints) - 0.5; 
+Mu = [20; 10];
+
+Sigma = repmat(eye(param.nbVar)*1E-3, [1,1,param.nbPoints]);
+MuQ = zeros(param.nbVar*param.nbData,1); 
+Q = zeros(param.nbVar*param.nbData);
+for t=1:length(tl)
+    id(:,t) = [1:param.nbVar] + (tl(t)-1) * param.nbVar;
+    Q(id(:,t), id(:,t)) = inv(Sigma(:,:,t));
+    MuQ(id(:,t)) = Mu(:,t);
+end
+
+
+%% Batch LQR reproduction
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%Precomputation of basis functions to generate control commands u (here, with Bernstein basis functions)
+nbRBF = 10;
+H = buildPhiBernstein(param.nbData-1,nbRBF);
+
+%Reproduction with nullspace planning
+[V,D] = eig(Q);
+U = V * D.^.5;
+J = U' * Su; %Jacobian
+
+%Left pseudoinverse solution
+pinvJ = (J' * J + R) \ J'; %Left pseudoinverse
+N = speye((param.nbData-1)*param.nbVar) - pinvJ * J; %Nullspace projection matrix
+u1 = pinvJ * U' * (MuQ - Sx * x0); %Principal task (least squares solution)
+x = reshape(Sx*x0+Su*u1, param.nbVar, param.nbData); %Reshape data for plotting
+
+%General solutions
+for n=1:param.nbRepros
+    w = randn(param.nbVar,nbRBF) * 1E1; %Random weights	
+    u2 = w * H'; %Reconstruction of control signals by a weighted superposition of basis functions
+    u = u1 + N * u2(:);
+    r(n).x = reshape(Sx*x0+Su*u, param.nbVar, param.nbData); %Reshape data for plotting
+end
+
+
+%% Plot 2D
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+figure('position',[10 10 800 800],'color',[1 1 1],'name','x1-x2 plot'); hold on; axis off;
+for n=1:param.nbRepros
+    plot(r(n).x(1,:), r(n).x(2,:), '-','linewidth',1,'color',.9-[.7 .7 .7].*rand(1));
+end
+plot(x(1,:), x(2,:), '-','linewidth',2,'color',[.8 0 0]);
+plot(x(1,1), x(2,1), 'o','linewidth',2,'markersize',8,'color',[.8 0 0]);
+plot(MuQ(id(1,:)), MuQ(id(2,:)), '.','markersize',30,'color',[.8 0 0]);
+axis equal; 
+%print('-dpng','graphs/LQT_nullspace02.png');
+
+pause(10);
+close all;
+end
+
+%Building Bernstein basis functions
+function phi = buildPhiBernstein(nbData, nbFct)
+	t = linspace(0, 1, nbData);
+	phi = zeros(nbData, nbFct);
+	for i=1:nbFct
+		phi(:,i) = factorial(nbFct-1) ./ (factorial(i-1) .* factorial(nbFct-i)) .* (1-t).^(nbFct-i) .* t.^(i-1);
+	end
+end
+
diff --git a/python/LQT_nullspace.py b/python/LQT_nullspace.py
new file mode 100644
index 0000000000000000000000000000000000000000..2486aed080104dbc5a802634e9931d8c3e5b20e3
--- /dev/null
+++ b/python/LQT_nullspace.py
@@ -0,0 +1,155 @@
+'''
+    Batch LQT with nullspace formulation
+
+    Copyright (c) 2021 Idiap Research Institute, http://www.idiap.ch/
+    Written by Hakan Girgin <hakan.girgin@idiap.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/>.
+'''
+
+import numpy as np
+from math import factorial
+import matplotlib.pyplot as plt
+from scipy.linalg import block_diag
+
+## Precomputation of basis functions to generate structured stochastic u through Bezier curves
+# Building Bernstein basis functions
+def build_phi_bernstein(nb_data, nb_fct):
+    t = np.linspace(0,1,nb_data)
+    phi = np.zeros((nb_data,nb_fct))
+    for i in range(nb_fct):
+        phi[:,i] = factorial(nb_fct-1) / (factorial(i) * factorial(nb_fct-1-i)) * (1-t)**(nb_fct-1-i) * t**i
+    return phi
+
+# Parameters
+# ===============================
+param = lambda: None # Lazy way to define an empty class in python
+param.dt = 1e-1  # Time step length
+param.nbPoints = 1 # Number of targets
+param.nbDeriv = 1 # Order of the dynamical system
+param.nbVarPos = 2 # Number of position variable
+param.nbData = 50  # Number of datapoints
+param.rfactor = 1e-4  # Control weight term
+param.nbRepros = 60 # Number of stochastic reproductions
+param.nbVar = param.nbVarPos * param.nbDeriv # Dimension of state vector
+
+
+# Dynamical System settings (discrete)
+# =====================================
+
+A1d = np.zeros((param.nbDeriv,param.nbDeriv))
+B1d = np.zeros((param.nbDeriv,1))
+
+for i in range(param.nbDeriv):
+    A1d += np.diag( np.ones(param.nbDeriv-i) ,i ) * param.dt**i * 1/factorial(i)
+    B1d[param.nbDeriv-i-1] = param.dt**(i+1) * 1/factorial(i+1)
+
+A = np.kron(A1d,np.identity(param.nbVarPos))
+B = np.kron(B1d,np.identity(param.nbVarPos))
+
+# Build Sx and Su transfer matrices
+Su = np.zeros((param.nbVar*param.nbData,param.nbVarPos * (param.nbData-1)))
+Sx = np.kron(np.ones((param.nbData,1)),np.eye(param.nbVar,param.nbVar))
+
+M = B
+for i in range(1, param.nbData):
+    Sx[i*param.nbVar:param.nbData*param.nbVar,:] = np.dot(Sx[i*param.nbVar:param.nbData*param.nbVar,:], A)
+    Su[param.nbVar*i:param.nbVar*i+M.shape[0],0:M.shape[1]] = M
+    M = np.hstack((np.dot(A,M),B)) # [0,nb_state_var-1]
+
+# Cost function settings
+# =====================================
+
+R = np.identity( (param.nbData-1) * param.nbVarPos ) * param.rfactor  # Control cost matrix
+
+t_list = np.stack([param.nbData-1]) # viapoint time list
+mu_list = np.stack([np.array([20,10.])]) # viapoint list
+Q_list = np.stack([np.eye(param.nbVar)*1e3]) # viapoint precision list
+
+mus = np.zeros((param.nbData, param.nbVar))
+Qs = np.zeros((param.nbData, param.nbVar, param.nbVar))
+for i,t in enumerate(t_list):
+    mus[t] = mu_list[i]
+    Qs[t] = Q_list[i]
+muQ = mus.flatten()
+Q  = block_diag(*Qs)
+
+# Change here off block diagonals of Q
+
+
+# Batch LQR Reproduction
+# =====================================
+
+nbRBF = 10
+H = build_phi_bernstein(param.nbData-1, nbRBF)
+
+
+J = Q @ Su
+N = np.eye(J.shape[-1]) - np.linalg.pinv(J) @ J # nullspace operator of LQT
+
+# Principal Task
+x0 = np.zeros(param.nbVar)
+u1 = np.linalg.solve(Su.T @ Q @ Su + R,  Su.T @ Q @ (muQ - Sx @ x0))
+x1 = (Sx @ x0 + Su @ u1).reshape((-1,param.nbVar))
+
+# Secondary Task
+repr_x = np.zeros((param.nbRepros, param.nbData, param.nbVar))
+for n in range(param.nbRepros):
+    w = np.random.randn(nbRBF, param.nbVarPos) * 1E1 # Random weights
+    u2 = H @ w # Reconstruction of control signals by a weighted superposition of basis functions
+    u = u1 + N @ u2.flatten()
+    repr_x[n] = np.reshape(Sx @ x0 + Su @ u, (-1, param.nbVar)) # Reshape data for plotting
+
+# Plotting
+# =========
+plt.figure()
+plt.title("2D Trajectory")
+plt.scatter(x1[0,0],x1[0,1],c='black',s=100)
+for mu in mu_list:
+    plt.scatter(mu[0], mu[1],c='red',s=100)
+
+plt.plot(x1[:,0] , x1[:,1], c='black')
+for i in range(param.nbRepros):
+    plt.plot(repr_x[i, :, 0], repr_x[i, :, 1], c='blue', alpha=0.1, zorder=0)
+
+plt.axis("off")
+plt.gca().set_aspect('equal', adjustable='box')
+
+fig,axs = plt.subplots(2,1)
+for i,t in enumerate(t_list):
+    axs[0].scatter(t, mu_list[i][0],c='red')
+
+for i in range(param.nbRepros):
+    axs[0].plot(repr_x[i, :, 0], c='blue', alpha=0.1, zorder=0)
+
+axs[0].plot(x1[:,0], c='black')
+axs[0].set_ylabel("$x_1$")
+axs[0].set_xticks([0,param.nbData])
+axs[0].set_xticklabels(["0","T"])
+
+for i,t in enumerate(t_list):
+    axs[1].scatter(t, mu_list[i][1],c='red')
+
+for i in range(param.nbRepros):
+    axs[1].plot(repr_x[i, :, 1], c='blue', alpha=0.1, zorder=0)
+
+axs[1].plot(x1[:,1], c='black')
+axs[1].set_ylabel("$x_2$")
+axs[1].set_xlabel("$t$")
+axs[1].set_xticks([0,param.nbData])
+axs[1].set_xticklabels(["0","T"])
+
+plt.show()