let's start with two fundamental ways of describing a robot:
- Joint space
- Task space
Joint Space
When a robot's current configuration is represented in terms of its joint positions, we call it joint space.
Consider a simple 2-link planar robot.
Suppose:
q₁ = 5°
q₂ = 45°
Then its joint configuration can be represented as:
q = [5°, 45°]
Simple.
Now consider something larger, like a UR10, which has six degrees of freedom.
Its joint configuration can be represented as:
q = [q₁, q₂, q₃, q₄, q₅, q₆]
Each value tells us the position of one joint.
So, in joint space, we describe the robot by asking:
What are the current positions of my joints?
Task Space
There is another way to describe the same robot.
Instead of caring about every individual joint, suppose I only care about the robot's end effector.
Where is it?
How is it oriented?
For a robot operating in 3D space, we can represent the end-effector pose as:
X = [x, y, z, ϕ, θ, ψ]
Here:
x, y, z → Position
ϕ, θ, ψ → Orientation
This is called task space or Cartesian space.
So now we have two different ways of looking at the same robot:
JOINT SPACE TASK SPACE
[q₁, q₂, ... qₙ] [x, y, z, orientation]
Robot joints End effector
This distinction is extremely important because a lot of robot manipulation is essentially about moving between these two representations.
Forward Kinematics
Suppose I know all the joint angles of my robot.
The question is:
Where is my end effector?
That's exactly what Forward Kinematics (FK) answers.
We can write this relationship as:
x = f(q)
In simple terms:
Joint angles ────── FK ──────> End-effector pose
q x
Give forward kinematics the robot's joint configuration, and it calculates where the end effector is.
For example:
q = [q₁, q₂, q₃, q₄, q₅, q₆]
↓
Forward Kinematics
↓
x = [x, y, z, ϕ, θ, ψ]
Inverse Kinematics
Now let's reverse the problem.
Suppose I know where I want the robot's end effector to be.
The question becomes:
What should my joint angles be to reach that pose?
That's the Inverse Kinematics (IK) problem.
Conceptually:
q = f⁻¹(x)
So:
End-effector pose ────── IK ──────> Joint angles
x q
There is an important detail here.
Writing:
q = f⁻¹(x)
is useful for understanding the idea, but inverse kinematics is not always a simple mathematical inverse.
For a particular end-effector pose, a robot may have:
- one solution
- multiple solutions
- infinitely many solutions
- or no solution at all
But conceptually, IK answers:
Where should my joints be if I want my end effector here?
Great. We Know FK and IK.
But there's a problem.
My robot still doesn't move.
BORING.
We know where the robot is.
We know what joint angles can produce a particular pose.
But how do we actually describe motion?
How fast should each joint move?
And if I move my joints at certain velocities, how will my end effector move?
That's where things start getting interesting.
Differential Kinematics
Let's go back to our forward kinematics equation:
x = f(q)
This tells us that the end-effector pose x depends on the robot's joint configuration q.
But now we're interested in how these quantities change with time.
So let's differentiate the forward kinematics equation with respect to time.
Conceptually:
x = f(q)
↓ differentiate with respect to time
ẋ = J(q) q̇
And suddenly, we get one of the most important equations in robot manipulation:
╔══════════════════╗
║ ║
║ ẋ = J(q) q̇ ║
║ ║
╚══════════════════╝
Let's understand what each term means.
q → Joint positions
q̇ → Joint velocities
x → End-effector pose
ẋ → End-effector velocity
J(q) → Jacobian
So this equation:
ẋ = J(q) q̇
answers a very practical question:
If I move my joints at these velocities, how will my end effector move?
This is the key idea behind differential kinematics.
But Where Did This Jacobian Come From?
This was one of the things that initially confused me.
The Jacobian can look like some mysterious matrix that suddenly appears in a robotics textbook.
It isn't.
Let's derive it.
And for that, we'll use one of the simplest robots possible.
A 2-Link Planar Manipulator
Consider a robot with two links:
● End Effector
/
/ l₂
/
● q₂
/
/ l₁
/
● q₁
|
Base
The robot has two joints:
q = [q₁, q₂]
and two link lengths:
l₁
l₂
Because the robot moves in a 2D plane, its end-effector position can be represented using:
x = horizontal position
y = vertical position
Using basic trigonometry, we can calculate the end-effector position.
Forward Kinematics of the 2R Robot
The x-coordinate is:
x = l₁ cos(q₁) + l₂ cos(q₁ + q₂)
The y-coordinate is:
y = l₁ sin(q₁) + l₂ sin(q₁ + q₂)
That's our forward kinematics.
Joint angles go in:
[q₁, q₂]
and the end-effector position comes out:
[x, y]
So far, nothing unusual.
Now comes the interesting part.
What Happens When the Joints Move?
Suppose both joints start moving.
Their angular velocities are:
q̇₁
q̇₂
Because the joints are moving, the end effector also moves.
Its Cartesian velocity becomes:
ẋ
ẏ
So we want to find the relationship:
[q̇₁, q̇₂] ──────────> [ẋ, ẏ]
Let's differentiate our forward kinematics equations.
We had:
x = l₁ cos(q₁) + l₂ cos(q₁ + q₂)
and:
y = l₁ sin(q₁) + l₂ sin(q₁ + q₂)
When we differentiate these equations with respect to time, the result can be arranged as:
┌ ẋ ┐ ┌ ∂x/∂q₁ ∂x/∂q₂ ┐ ┌ q̇₁ ┐
│ │ = │ │ │ │
└ ẏ ┘ └ ∂y/∂q₁ ∂y/∂q₂ ┘ └ q̇₂ ┘
Look carefully at the matrix in the middle.
┌ ∂x/∂q₁ ∂x/∂q₂ ┐
J(q) = │ │
└ ∂y/∂q₁ ∂y/∂q₂ ┘
That's the Jacobian.
Nothing magical happened. We simply differentiated the forward kinematics with respect to the joint variables.
But let's look at what this matrix is actually telling us.
Consider the first column:
┌ ∂x/∂q₁ ┐
│ │
└ ∂y/∂q₁ ┘
It tells us how the end-effector position in x and y changes when joint q₁ changes, while the other joint is held fixed.
Similarly, the second column:
┌ ∂x/∂q₂ ┐
│ │
└ ∂y/∂q₂ ┘
tells us how q₂ influences the end-effector position.
So each column of the Jacobian represents the influence of one joint on the motion of the end effector.
For our 2-link planar robot, we are only interested in motion along x and y, so the Jacobian is a 2 × 2 matrix:
Joints
q₁ q₂
┌ ┐
x direction → │ ∂x/∂q₁ ∂x/∂q₂ │
y direction → │ ∂y/∂q₁ ∂y/∂q₂ │
└ ┘
Now let's generalize this idea.
For a robot with n joints, the joint velocity vector is:
q̇ = [q̇₁, q̇₂, ... , q̇ₙ]
A robot moving freely in 3D space can have an end-effector velocity consisting of:
Linear velocity: [ẋ, ẏ, ż]
Angular velocity: [ωₓ, ωᵧ, ω_z]
So the full geometric Jacobian has the form:
n joints
┌───────────────────────┐
│ │
Linear │ Jᵥ(q) │ 3 rows
│ │
├───────────────────────┤
│ │
Angular │ Jω(q) │ 3 rows
│ │
└───────────────────────┘
6 × n
Therefore, for an n-DOF robot, the full Jacobian is generally a:
6 × n matrix
The n columns correspond to the robot's joints.
The first three rows describe how those joints contribute to the end effector's linear velocity:
[ẋ, ẏ, ż]
The last three rows describe how they contribute to its angular velocity:
[ωₓ, ωᵧ, ω_z]
So we can think of the full Jacobian as:
q̇₁ q̇₂ q̇₃ ... q̇ₙ
↓ ↓ ↓ ↓
┌─────────────────────────┐
ẋ ← │ │
ẏ ← │ Linear part │
ż ← │ Jᵥ │
├─────────────────────────┤
ωₓ ← │ │
ωᵧ ← │ Angular part │
ω_z ← │ Jω │
└─────────────────────────┘
In this article, however, we'll keep things simple.
We are mainly interested in how the joints influence the position of the end effector:
x, y, z
So we'll focus on the linear part of the Jacobian and leave orientation and angular velocity for later.
With that intuition in place, let's actually calculate the Jacobian for our 2-link robot.
Let's Actually Calculate It
From:
x = l₁ cos(q₁) + l₂ cos(q₁ + q₂)
we get:
∂x/∂q₁ = -l₁ sin(q₁) - l₂ sin(q₁ + q₂)
∂x/∂q₂ = -l₂ sin(q₁ + q₂)
And from:
y = l₁ sin(q₁) + l₂ sin(q₁ + q₂)
we get:
∂y/∂q₁ = l₁ cos(q₁) + l₂ cos(q₁ + q₂)
∂y/∂q₂ = l₂ cos(q₁ + q₂)
Putting everything into the matrix:
┌ -l₁sin(q₁)-l₂sin(q₁+q₂) -l₂sin(q₁+q₂) ┐
J(q) = │ │
└ l₁cos(q₁)+l₂cos(q₁+q₂) l₂cos(q₁+q₂) ┘
And that's our Jacobian.
Now our velocity relationship becomes:
┌ ẋ ┐ ┌ q̇₁ ┐
│ │ = J(q) │ │
└ ẏ ┘ └ q̇₂ ┘
or simply:
ẋ = J(q)q̇
This equation now has a very intuitive meaning:
Give me the current robot configuration and its joint velocities, and the Jacobian tells me how the end effector is moving.
Enough With the Equations. Let's Make the Robot Move.
We cannot learn swimming by learning about water.
Let's just dive in.
So far, we've talked about joint space, task space, forward kinematics, and the Jacobian.
Now let's actually use them on a robot.
For this experiment, I'm using:
- ROS 2 Jazzy
- RViz for visualization
- UR10 as the robot
- MPU6050 as a gesture sensor
- Raspberry Pi 4 to read and publish the sensor data
The setup looks roughly like this:
Hand Gesture
│
▼
MPU6050
│
│ Roll / Pitch
▼
Raspberry Pi 4
│
│ ROS 2
▼
Host Machine
│
▼
Differential
Kinematics
│
│ Joint velocities
▼
UR10
│
▼
RViz
The MPU6050 isn't really necessary.
I just had one lying around for a long time and finally wanted to put it to use. 😄
You could achieve exactly the same thing using four keyboard keys:
W → +X
S → -X
A → +Y
D → -Y
The sensor just makes the experiment more interactive.
Converting Gestures Into Cartesian Velocity
For simplicity, I'm only controlling the end effector in the X-Y plane.
I map:
Roll → Velocity along X
Pitch → Velocity along Y
I'm not trying to make the velocity proportional to the exact tilt angle yet.
Instead, once the tilt crosses a threshold, I command a small constant Cartesian velocity.
Conceptually:
Tilt forward → +ẋ
Tilt backward → -ẋ
Tilt left → +ẏ
Tilt right → -ẏ
So after reading the MPU6050, I have something like:
ẋ = 0.05 m/s
ẏ = 0.00 m/s
or:
ẋ = 0.00 m/s
ẏ = -0.05 m/s
Great.
Now I know how I want the end effector to move.
But the UR10 doesn't accept:
Move your end effector at +0.05 m/s in X
as a joint command.
The robot ultimately needs to know:
How should each joint move?
And that brings us straight back to our differential kinematics equation.
Back to the Jacobian
We previously derived:
ẋ = J(q)q̇
Remember what this equation told us:
Joint velocities ───── J(q) ─────> End-effector velocity
q̇ ẋ
But now we have the opposite problem.
From the MPU6050, we already know the desired end-effector velocity:
ẋ
What we need are the corresponding joint velocities:
q̇
In other words, we want:
End-effector velocity ───── ??? ─────> Joint velocities
ẋ q̇
Starting from:
ẋ = J(q)q̇
our goal is to solve for:
q̇
For a simple square and invertible Jacobian, we might be tempted to write:
q̇ = J⁻¹(q)ẋ
But with a real robot like the UR10, things aren't always that simple.
Remember, the UR10 has six joints.
And in our experiment, we're currently asking it to perform only a small part of its possible task-space motion:
Desired motion:
[ẋ, ẏ]
while we have:
Joint velocities:
[q̇₁, q̇₂, q̇₃, q̇₄, q̇₅, q̇₆]
So how do we go from:
[ẋ, ẏ]
to:
[q̇₁, q̇₂, q̇₃, q̇₄, q̇₅, q̇₆]
?
This is where the Jacobian pseudoinverse enters the picture.
J⁺(q)
[ẋ, ẏ] ───────────────────> [q̇₁, q̇₂, q̇₃, q̇₄, q̇₅, q̇₆]
Desired Cartesian Required
velocity joint velocities
And now we're finally ready to write some code.
Instead, we need a pseudoinverse.
In my implementation, I'm using a slightly more robust version called the Damped Least-Squares pseudoinverse.
The equation is:
q̇ = Jᵀ (J Jᵀ + λ²I)⁻¹ v
where:
J → 3 × 6 position Jacobian
Jᵀ → 6 × 3
v → desired Cartesian velocity [ẋ, ẏ, ż]
λ → damping factor
I → 3 × 3 identity matrix
q̇ → resulting 6 joint velocities
And this is what it looks like in Eigen:
const Eigen::Matrix3d regularized =
xyz_jacobian * xyz_jacobian.transpose()
+ damping_squared_ * Eigen::Matrix3d::Identity();
const Eigen::Matrix<double, 6, 1> joint_velocity =
xyz_jacobian.transpose()
* regularized.ldlt().solve(cartesian_velocity);
Let's map the code directly to the equation.
xyz_jacobian
↓
J
xyz_jacobian.transpose()
↓
Jᵀ
xyz_jacobian * xyz_jacobian.transpose()
↓
JJᵀ
damping_squared_ * Identity()
↓
λ²I
cartesian_velocity
↓
v
So these few lines are essentially implementing:
q̇ = Jᵀ (JJᵀ + λ²I)⁻¹ v
That's it.
We Have Joint Velocities. Now What?
At this point, our damped least-squares solution gives us:
q̇ = [q̇₁, q̇₂, q̇₃, q̇₄, q̇₅, q̇₆]
But remember what these values represent.
They are velocities.
They tell us how fast each joint should move, not where the joints should move to.
What we actually need for the next control step is a small change in joint position:
Δq
In other words:
Given these joint velocities, how much should each joint move during this control cycle?
That's straightforward.
Velocity is change in position over time:
q̇ = Δq / Δt
Therefore:
Δq = q̇ Δt
where:
q̇ → Joint velocity
Δt → Time between two control updates
Δq → Change in joint position
Once we know Δq, we can update the current joint configuration:
q_new = q_current + Δq
or:
q_new = q_current + q̇ Δt
For all six joints:
q₁_new = q₁_current + q̇₁ Δt
q₂_new = q₂_current + q̇₂ Δt
q₃_new = q₃_current + q̇₃ Δt
q₄_new = q₄_current + q̇₄ Δt
q₅_new = q₅_current + q̇₅ Δt
q₆_new = q₆_current + q̇₆ Δt
We calculate this repeatedly inside the control loop.
So the robot doesn't make one giant movement.
Instead, it makes many tiny movements:
Current q
│
▼
Calculate J(q)
│
▼
Calculate q̇
│
▼
Δq = q̇ Δt
│
▼
q_new = q + Δq
│
▼
Robot moves slightly
│
└──────────────► Repeat
This is essentially numerical integration.
We're integrating joint velocity over time to obtain joint position.
Now that we can actually update the robot's joints, let's try something interesting.
Experiment 1: Control Only X
Let's start with the simplest possible task.
I only care about the end effector's velocity along the X direction.
Desired:
ẋ = 0.05 m/s
That's it.
I'm not asking the controller to maintain Y.
I'm not asking the controller to maintain Z.
So our task only contains:
[ẋ]
This distinction is important.
It does not mean:
ẋ = 0.05
ẏ = 0
ż = 0
Instead, it means:
X → Controlled
Y → Unconstrained
Z → Unconstrained
The controller's job is simply:
Find joint velocities that produce the requested X velocity.
What happens in Y and Z is not part of the task.
Now let's see what the UR10 actually does.
Result
Watch the end effector carefully.
We asked it to move along X, but we never told it what to do in Y or Z.
So those directions are free to change as a consequence of the joint motion.
This is our first glimpse of what an unconstrained direction actually means.
Experiment 2: Control X and Y
Now let's add another requirement.
Instead of controlling only X, let's control both X and Y.
Our task-space velocity becomes:
┌ ẋ ┐
│ │
└ ẏ ┘
Now:
X → Controlled
Y → Controlled
Z → Unconstrained
This changes our Jacobian too.
Instead of using only the row corresponding to X:
Jx
we now use:
┌ Jx ┐
J = │ │
└ Jy ┘
Our solver must now find joint velocities that satisfy both requested Cartesian velocities.
X constraint
+
Y constraint
│
▼
Jacobian
│
▼
Joint velocities
│
▼
UR10
But Z is still not part of the task.
We haven't said:
ż = 0
We simply haven't constrained Z at all.
Let's see how that changes the robot's motion.
Result
Compare this with the previous experiment.
The solver now has fewer freedoms because it has another Cartesian requirement to satisfy.
But the Z direction is still free.
Experiment 3: Now Constrain Z
Finally, let's add Z to our task.
But here's the interesting part:
I don't actually want the robot to move in Z.
So I command:
ż = 0
Our complete desired linear velocity is now:
┌ ẋ ┐
v = │ ẏ │
└ 0 ┘
Now:
X → Controlled
Y → Controlled
Z → Controlled at zero velocity
This is fundamentally different from simply leaving Z out of the task.
Previously:
Z → Do whatever is necessary
Now:
Z → Do NOT move
The Jacobian used by our controller now contains all three linear velocity rows:
┌ Jx ┐
│ │
J = │ Jy │
│ │
└ Jz ┘
and our desired Cartesian velocity is:
┌ ẋ ┐
v = │ ẏ │
└ 0 ┘
The damped least-squares solver must therefore find joint velocities that produce the requested X and Y motion while simultaneously trying to keep the Z velocity at zero.
Result
Now compare all three experiments:
Experiment 1
X → Controlled
Y → Unconstrained
Z → Unconstrained
Experiment 2
X → Controlled
Y → Controlled
Z → Unconstrained
Experiment 3
X → Controlled
Y → Controlled
Z → Controlled at 0 m/s
And this demonstrates an important idea:
Not controlling a direction is not the same as commanding zero velocity in that direction.
If Z isn't included in our task, the solver is free to produce motion in Z.
If Z is included with:
ż = 0
we are explicitly asking the solver to maintain zero velocity along Z.
The more task-space directions we constrain, the fewer freedoms remain for the robot to use elsewhere.
And that observation leads us toward something much more interesting:
What can the robot do with the freedoms that remain?
That's where the null space starts to become useful.




Top comments (0)