Week 02

Python Kinematics Practice: Building Forward Kinematics from Scratch

Published: 2026-04-11 | Author: Smartotics Learning Journey | Reading Time: 10 min

Figure 1: Python Forward Kinematics: Workspace visualization with code snippet

Quick Summary

Today we write Python code that computes forward kinematics from scratch — no robotics libraries, just NumPy. You’ll build a rotation matrix function, a homogeneous transform function, and a 2-DOF arm forward kinematics solver. By the end, you’ll have a working kinematics library you can extend to 3-DOF and 6-DOF arms.

Setup: What You Need

pip install numpy matplotlib

That’s it. NumPy for matrix math, Matplotlib for visualization.

Step 1: Build Rotation Matrices

These are the three basic rotations around X, Y, and Z axes:

import numpy as np

def rot_x(theta): """Rotation matrix about X axis by theta radians""" c, s = np.cos(theta), np.sin(theta) return np.array([ [1, 0, 0], [0, c, -s], [0, s, c] ])

def rot_y(theta): """Rotation matrix about Y axis by theta radians""" c, s = np.cos(theta), np.sin(theta) return np.array([ [ c, 0, s], [ 0, 1, 0], [-s, 0, c] ])

def rot_z(theta): """Rotation matrix about Z axis by theta radians""" c, s = np.cos(theta), np.sin(theta) return np.array([ [c, -s, 0], [s, c, 0], [0, 0, 1] ])

Test: rotate point [1, 0, 0] by 90 degrees around Z

p = np.array([1.0, 0.0, 0.0]) R_90 = rot_z(np.pi / 2) p_rotated = R_90 @ p print(f”Rotated [1, 0, 0] by 90 deg around Z: {p_rotated}“)

Expected: [0, 1, 0]

@ means matrix multiplication

In Python/NumPy, the @ operator is matrix multiplication. Don’t use * — that does element-wise multiplication. A @ B is the same as np.matmul(A, B).

Step 2: Build Homogeneous Transform

def homogeneous_transform(R, d): """ Create 4x4 homogeneous transform from 3x3 rotation R and 3x1 translation d.

T = [ R  | d ]
    [ 0  | 1 ]
"""
T = np.eye(4)
T[:3, :3] = R
T[:3, 3] = d
return T

Example: rotate 30 degrees around Z, then translate [0.5, 0, 0]

R = rot_z(np.radians(30)) d = np.array([0.5, 0.0, 0.0]) T = homogeneous_transform(R, d) print(“Transform matrix:”) print(T.round(3))

We can extract position and orientation back from any transform:

def extract_pose(T): """Extract (position, rotation_matrix) from 4x4 transform""" position = T[:3, 3] rotation = T[:3, :3] return position, rotation

def invert_transform(T): """Inverse of homogeneous transform (fast: transpose instead of inversion)""" T_inv = np.eye(4) R = T[:3, :3] d = T[:3, 3] T_inv[:3, :3] = R.T T_inv[:3, 3] = -R.T @ d return T_inv

Step 3: 2-DOF Planar Arm Forward Kinematics

Now combine everything. The 2-joint planar arm has two links with lengths L1 and L2:

def fk_2dof(q1, q2, L1, L2): """ Forward kinematics for 2-DOF planar arm.

Args:
    q1, q2: joint angles in radians
    L1, L2: link lengths in meters

Returns:
    x, y: end-effector position
"""
# Transform from base to joint 1
T_0_1 = homogeneous_transform(rot_z(q1), np.array([L1 * np.cos(q1), L1 * np.sin(q1), 0]))

# Transform from joint 1 to end-effector
T_1_2 = homogeneous_transform(rot_z(q2), np.array([L2 * np.cos(q2), L2 * np.sin(q2), 0]))

# Chain: base to end-effector
T_0_2 = T_0_1 @ T_1_2

# Extract position
x, y, z = T_0_2[:3, 3]
return x, y

Test: q1=30 deg, q2=45 deg, L1=0.5m, L2=0.3m

x, y = fk_2dof(np.radians(30), np.radians(45), 0.5, 0.3) print(f”End-effector position: x={x:.4f}m, y={y:.4f}m”)

Expected: x ≈ 0.642m, y ≈ 0.462m

Step 4: Visualize the Arm

Seeing the robot move makes everything click:

def plot_2dof_arm(q1, q2, L1, L2): """Visualize the 2-DOF planar arm using matplotlib""" import matplotlib.pyplot as plt

# Compute joint positions
origin = np.array([0.0, 0.0])
joint1 = np.array([L1 * np.cos(q1), L1 * np.sin(q1)])
ee_x, ee_y = fk_2dof(q1, q2, L1, L2)
end_effector = np.array([ee_x, ee_y])

# Plot
fig, ax = plt.subplots(1, 1, figsize=(8, 8))

# Draw links
ax.plot([origin[0], joint1[0]], [origin[1], joint1[1]], 'b-', linewidth=3, label='Link 1')
ax.plot([joint1[0], end_effector[0]], [joint1[1], end_effector[1]], 'r-', linewidth=3, label='Link 2')

# Draw joints
ax.plot(*origin, 'ko', markersize=10, label='Base')
ax.plot(*joint1, 'go', markersize=8, label='Joint 1 (Elbow)')
ax.plot(*end_effector, 'ro', markersize=8, label='End-Effector')

ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)
ax.set_aspect('equal')
ax.axhline(y=0, color='gray', linewidth=0.5)
ax.axvline(x=0, color='gray', linewidth=0.5)
ax.grid(True, alpha=0.3)
ax.legend()
ax.set_title(f'2-DOF Planar Arm: q1={np.degrees(q1):.0f}, q2={np.degrees(q2):.0f}')

plt.show()

Visualize

q1 = np.radians(30) q2 = np.radians(45) plot_2dof_arm(q1, q2, 0.5, 0.3)

Step 5: General D-H Forward Kinematics

Instead of hardcoding for 2-DOF, let’s use the D-H parameters properly. This function works for any serial robot:

def dh_transform(theta, d, a, alpha): """ Standard Denavit-Hartenberg transform.

Args:
    theta: joint angle (radians) - variable for revolute joints
    d: link offset - variable for prismatic joints
    a: link length
    alpha: link twist

Returns:
    4x4 homogeneous transformation matrix
"""
ct, st = np.cos(theta), np.sin(theta)
ca, sa = np.cos(alpha), np.sin(alpha)

return np.array([
    [ct, -st*ca,  st*sa,  a*ct],
    [st,  ct*ca, -ct*sa,  a*st],
    [0,   sa,     ca,     d   ],
    [0,   0,      0,      1    ]
])

def fk_serial(dh_params, joint_angles): """ Forward kinematics for any serial chain robot.

Args:
    dh_params: list of [a, alpha, d] for each joint (theta varies)
    joint_angles: list of theta values for each joint

Returns:
    4x4 transform from base to end-effector
"""
T = np.eye(4)
for i, (a, alpha, d) in enumerate(dh_params):
    T = T @ dh_transform(joint_angles[i], d, a, alpha)
return T

Example: 2-DOF planar arm using D-H

dh_2dof = [ [0.5, 0, 0], # Joint 1: a=0.5, alpha=0, d=0 [0.3, 0, 0], # Joint 2: a=0.3, alpha=0, d=0 ] T_ee = fk_serial(dh_2dof, [np.radians(30), np.radians(45)]) print(f”End-effector position: [{T_ee[0,3]:.4f}, {T_ee[1,3]:.4f}, {T_ee[2,3]:.4f}]“)

From Here to 6-DOF

The fk_serial() function above works for any number of joints. For a 6-axis industrial robot, just provide 6 D-H parameter sets and 6 joint angles. The math is exactly the same — only the input data changes.

Exercises: Try These

  1. Vary the angles: Try q1=0, q2=0 (fully extended) and q1=90, q2=-90 (folded). Verify the positions match your mental model.
  2. Add a third joint: Extend the DH parameters list to 3 joints. Add a vertical offset (d != 0) to the third joint.
  3. Workspace analysis: Sweep q1 and q2 from -180 to 180 degrees and plot all reachable (x, y) positions. The shape you see is the robot’s workspace.

Exercise 3: Plot workspace

import matplotlib.pyplot.pyplot as plt

workspace_x, workspace_y = [], [] for q1_deg in range(-180, 181, 5): for q2_deg in range(-180, 181, 5): x, y = fk_2dof(np.radians(q1_deg), np.radians(q2_deg), 0.5, 0.3) workspace_x.append(x) workspace_y.append(y)

plt.figure(figsize=(8, 8)) plt.scatter(workspace_x, workspace_y, s=1, alpha=0.5) plt.gca().set_aspect(‘equal’) plt.title(‘2-DOF Planar Arm Workspace’) plt.xlabel(‘X (m)’) plt.ylabel(‘Y (m)’) plt.grid(True, alpha=0.3) plt.show()

FAQ

Q: I don’t have Python or NumPy set up. What do I do?

Install Python from python.org, then run pip install numpy matplotlib. If you need a free environment, use Google Colab — it has NumPy pre-installed and runs in your browser.

Q: Why write from scratch instead of using a robotics library?

You write it once to understand. Then you use a library for production. Understanding is non-negotiable — otherwise you’re just a library user, not an engineer.

Q: How do I extend this to 3D?

Add Z rotations to each DH parameter row. Set alpha to non-zero values and d to non-zero values. The fk_serial() function above already handles 3D — you just need 3D D-H parameters.

Key Takeaways

Disclaimer

For educational purposes only. This article is part of a structured learning curriculum.

Image Credits: AI-generated illustrations. © 2026 Smartotics Learning Journey.