Week 05 | Day 02

Lagrangian Dynamics: Energy-Based Equations of Motion

Published: April 21, 2026 | Author: Smartotics Learning Journey | Reading Time: 14 min

TL;DR: Instead of balancing forces on every body, the Lagrangian method computes the equations of motion from kinetic and potential energy. It’s cleaner, less error-prone, and scales to robots with dozens of joints. L = T - V, then apply Euler-Lagrange.


The Problem with Force Balance

Yesterday, we wrote Newton-Euler equations for individual links. For a 6-DOF industrial robot, that means:

The Lagrangian approach eliminates constraint forces entirely. It works at the system level, not the component level.


Core Idea: Energy Never Lies

If you can write the total kinetic energy T and total potential energy V of the robot as functions of joint angles q and velocities , you can derive the equations of motion directly.

The Lagrangian

L(q, q̇) = T(q, q̇) - V(q)

L is not energy — it’s a mathematical object whose derivatives produce the equations of motion.

Euler-Lagrange Equation

For each joint i:

d/dt(∂L/∂q̇ᵢ) - ∂L/∂qᵢ = τᵢ

Where τᵢ is the generalized force (torque for revolute joints, force for prismatic joints).

Energy Decomposition


Worked Example: Single Pendulum

Before tackling multi-link robots, let’s verify the method on a simple pendulum.

Setup

Step 1: Kinetic Energy

The mass moves in a circle. Its velocity is v = L·θ̇.

T = ½·m·v² = ½·m·L²·θ̇²

Step 2: Potential Energy

Height above lowest point: h = L(1 - cos θ)

V = m·g·h = m·g·L(1 - cos θ)

Step 3: Lagrangian

L = T - V = ½·m·L²·θ̇² - m·g·L(1 - cos θ)

Step 4: Apply Euler-Lagrange

import sympy as sp

def derive_pendulum_dynamics():
    """
    Derive pendulum equations of motion using symbolic Lagrangian mechanics
    """
    # Symbols
    t = sp.Symbol('t')
    theta = sp.Function('theta')(t)
    m, L, g = sp.symbols('m L g', positive=True)
    
    # Derivatives
    theta_dot = sp.diff(theta, t)
    theta_ddot = sp.diff(theta, t, 2)
    
    # Kinetic energy
    T = sp.Rational(1,2) * m * L**2 * theta_dot**2
    
    # Potential energy
    V = m * g * L * (1 - sp.cos(theta))
    
    # Lagrangian
    Lagrangian = T - V
    
    print("Lagrangian:")
    print(Lagrangian)
    
    # Euler-Lagrange: d/dt(dL/dθ̇) - dL/dθ = τ
    dL_dtheta_dot = sp.diff(Lagrangian, theta_dot)
    d_dt_dL = sp.diff(dL_dtheta_dot, t)
    dL_dtheta = sp.diff(Lagrangian, theta)
    
    # Equation of motion
    eom = sp.Eq(d_dt_dL - dL_dtheta, 0)
    
    print("\nEquation of motion:")
    print(eom)
    
    # Simplify
    eom_simplified = sp.simplify(eom.lhs)
    print(f"\nSimplified: {eom_simplified} = τ")
    
    return eom

eom = derive_pendulum_dynamics()
# Output: m·L²·θ̈ + m·g·L·sin(θ) = τ
# Which gives: θ̈ = (τ - m·g·L·sin(θ)) / (m·L²)

This matches what we get from Newton’s second law: τ = I·α + m·g·L·sin(θ), where I = m·L².


2-DOF Planar Arm: Full Derivation

Now let’s apply Lagrangian mechanics to our standard 2-DOF arm.

import numpy as np
import sympy as sp

def derive_2dof_lagrangian():
    """
    Full symbolic derivation of 2-DOF planar arm dynamics
    """
    # Define symbols
    q1, q2 = sp.symbols('q1 q2')           # Joint angles
    dq1, dq2 = sp.symbols('dq1 dq2')       # Joint velocities
    m1, m2 = sp.symbols('m1 m2', positive=True)
    L1, L2 = sp.symbols('L1 L2', positive=True)
    g = sp.Symbol('g', positive=True)
    
    # Link 1 COM position
    x1 = (L1/2) * sp.cos(q1)
    y1 = (L1/2) * sp.sin(q1)
    
    # Link 2 COM position
    x2 = L1 * sp.cos(q1) + (L2/2) * sp.cos(q1 + q2)
    y2 = L1 * sp.sin(q1) + (L2/2) * sp.sin(q1 + q2)
    
    # Velocities (time derivatives)
    dx1 = sp.diff(x1, q1) * dq1
    dy1 = sp.diff(y1, q1) * dq1
    dx2 = sp.diff(x2, q1) * dq1 + sp.diff(x2, q2) * dq2
    dy2 = sp.diff(y2, q1) * dq1 + sp.diff(y2, q2) * dq2
    
    # Kinetic energy (translational + rotational)
    T1 = sp.Rational(1,2) * m1 * (dx1**2 + dy1**2) + sp.Rational(1,2) * (m1*L1**2/12) * dq1**2
    T2 = sp.Rational(1,2) * m2 * (dx2**2 + dy2**2) + sp.Rational(1,2) * (m2*L2**2/12) * (dq1 + dq2)**2
    T = sp.simplify(T1 + T2)
    
    # Potential energy
    V1 = m1 * g * y1
    V2 = m2 * g * y2
    V = sp.simplify(V1 + V2)
    
    # Lagrangian
    L = sp.simplify(T - V)
    
    print("Kinetic Energy T:")
    print(T)
    print("\nPotential Energy V:")
    print(V)
    
    return L, T, V

L, T, V = derive_2dof_lagrangian()

Extracting the Mass Matrix

The kinetic energy of a robot always has the quadratic form:

T = ½ · q̇ᵀ · M(q) · q̇

This means we can read the mass matrix directly from the kinetic energy:

def extract_mass_matrix(T, dq_vars):
    """
    Extract mass matrix M(q) from kinetic energy T = ½·q̇ᵀ·M·q̇
    
    Args:
        T: Kinetic energy expression
        dq_vars: List of velocity symbols [dq1, dq2, ...]
    
    Returns:
        Mass matrix as SymPy Matrix
    """
    n = len(dq_vars)
    M = sp.zeros(n, n)
    
    for i in range(n):
        for j in range(n):
            # M[i,j] = coefficient of dq_i * dq_j
            # For diagonal: coefficient of dq_i² / 2
            if i == j:
                coeff = sp.expand(T).coeff(dq_vars[i], 2)
                M[i, j] = 2 * coeff if coeff != 0 else 0
            else:
                coeff = sp.expand(T).coeff(dq_vars[i] * dq_vars[j], 1)
                M[i, j] = coeff if coeff != 0 else 0
    
    return sp.simplify(M)

# Extract for our 2-DOF arm
dq1, dq2 = sp.symbols('dq1 dq2')
M = extract_mass_matrix(T, [dq1, dq2])
print("Mass Matrix M(q):")
print(M)

Computing Coriolis and Gravity Terms

def compute_coriolis_matrix(M, q_vars, dq_vars):
    """
    Compute Coriolis/centrifugal matrix C(q, q̇) from mass matrix
    
    Uses Christoffel symbols:
    C[i,j] = Σₖ ½ (∂M[i,j]/∂qₖ + ∂M[i,k]/∂qⱼ - ∂M[j,k]/∂qᵢ) · q̇ₖ
    """
    n = len(q_vars)
    C = sp.zeros(n, n)
    
    for i in range(n):
        for j in range(n):
            c_ij = 0
            for k in range(n):
                term = (sp.diff(M[i,j], q_vars[k]) + 
                       sp.diff(M[i,k], q_vars[j]) - 
                       sp.diff(M[j,k], q_vars[i])) * dq_vars[k] / 2
                c_ij += term
            C[i, j] = sp.simplify(c_ij)
    
    return C

def compute_gravity_vector(V, q_vars):
    """
    Compute gravity vector G(q) = ∂V/∂q
    """
    G = sp.Matrix([sp.diff(V, q) for q in q_vars])
    return sp.simplify(G)

q1, q2 = sp.symbols('q1 q2')
C = compute_coriolis_matrix(M, [q1, q2], [dq1, dq2])
G = compute_gravity_vector(V, [q1, q2])

print("Coriolis Matrix C(q, q̇):")
print(C)
print("\nGravity Vector G(q):")
print(G)

The Complete Equations of Motion

τ = M(q)·q̈ + C(q, q̇)·q̇ + G(q)

Let’s verify this is equivalent to the Lagrangian result:

def verify_equations_of_motion(M, C, G, q_vars, dq_vars):
    """
    Verify that M·q̈ + C·q̇ + G matches Euler-Lagrange result
    """
    n = len(q_vars)
    ddq_vars = sp.symbols([f'ddq{i+1}' for i in range(n)])
    
    # Compute left-hand side: M·q̈ + C·q̇ + G
    ddq_vec = sp.Matrix(ddq_vars)
    dq_vec = sp.Matrix(dq_vars)
    
    lhs = M * ddq_vec + C * dq_vec + G
    lhs = sp.simplify(lhs)
    
    print("Equations of motion (τ = M·q̈ + C·q̇ + G):")
    for i in range(n):
        print(f{i+1} = {lhs[i]}")
    
    return lhs

tau = verify_equations_of_motion(M, C, G, [q1, q2], [dq1, dq2])

Numerical Simulation

Now let’s simulate the 2-DOF arm using our derived dynamics:

def simulate_2dof_arm(q0, dq0, tau_func, m1, m2, L1, L2, duration=5.0, dt=0.001):
    """
    Simulate 2-DOF arm dynamics using derived equations
    
    Args:
        q0: Initial joint angles [q1, q2]
        dq0: Initial joint velocities [dq1, dq2]
        tau_func: Function(t, q, dq) -> [tau1, tau2]
        duration: Simulation duration (seconds)
        dt: Time step
    
    Returns:
        times, q_trajectory, dq_trajectory
    """
    g = 9.81
    num_steps = int(duration / dt)
    
    # Storage
    times = np.zeros(num_steps)
    q = np.zeros((num_steps, 2))
    dq = np.zeros((num_steps, 2))
    
    # Initial conditions
    q[0] = q0
    dq[0] = dq0
    
    for i in range(num_steps - 1):
        t = i * dt
        times[i] = t
        
        q1_i, q2_i = q[i]
        dq1_i, dq2_i = dq[i]
        
        # Compute mass matrix
        c2 = np.cos(q2_i)
        s2 = np.sin(q2_i)
        
        M11 = m1*L1**2/3 + m2*(L1**2 + L1*L2*c2 + L2**2/3)
        M12 = m2*(L1*L2*c2/2 + L2**2/3)
        M22 = m2*L2**2/3
        
        M_num = np.array([[M11, M12], [M12, M22]])
        
        # Compute Coriolis terms
        h = -m2 * L1 * L2 * s2 / 2
        C11 = h * dq2_i
        C12 = h * (dq1_i + dq2_i)
        C21 = -h * dq1_i
        C22 = 0
        
        C_num = np.array([[C11, C12], [C21, C22]])
        
        # Compute gravity
        c1 = np.cos(q1_i)
        c12 = np.cos(q1_i + q2_i)
        
        G1 = m1*g*(L1/2)*c1 + m2*g*(L1*c1 + (L2/2)*c12)
        G2 = m2*g*(L2/2)*c12
        
        G_num = np.array([G1, G2])
        
        # Get control torques
        tau = tau_func(t, q[i], dq[i])
        
        # Solve for accelerations: q̈ = M⁻¹(τ - C·q̇ - G)
        rhs = tau - C_num @ dq[i] - G_num
        ddq = np.linalg.solve(M_num, rhs)
        
        # Integrate (semi-implicit Euler)
        dq[i+1] = dq[i] + ddq * dt
        q[i+1] = q[i] + dq[i+1] * dt
    
    times[-1] = duration
    return times, q, dq

# Example: free fall with gravity compensation
compensation = lambda t, q, dq: np.array([
    1.0*9.81*(0.6*np.cos(q[0]) + 0.4*np.cos(q[0]+q[1])),
    0.8*9.81*0.25*np.cos(q[0]+q[1])
])

times, q_traj, dq_traj = simulate_2dof_arm(
    q0=[np.pi/4, 0],
    dq0=[0, 0],
    tau_func=compensation,
    m1=1.0, m2=0.8, L1=0.6, L2=0.5,
    duration=3.0, dt=0.001
)

print(f"Simulation complete: {len(times)} steps")
print(f"Final position: q1={q_traj[-1,0]:.3f}, q2={q_traj[-1,1]:.3f}")

When to Use Lagrangian vs. Newton-Euler

ScenarioRecommended MethodWhy
Single rigid bodyNewton-EulerDirect, intuitive
Few links, force analysis neededNewton-EulerConstraint forces are visible
Many links (6+ DOF)LagrangianScales better, no constraint bookkeeping
Symbolic derivationLagrangianEnergy expressions are easier to differentiate
Real-time control (1 kHz)Recursive Newton-EulerO(n) complexity vs. O(n²) for Lagrangian
Optimal control / learningLagrangianClean parameterization of dynamics

Key Takeaways


Next Steps

Tomorrow, we’ll explore the Newton-Euler recursive algorithm — an O(n) method that computes dynamics efficiently for real-time control, used in virtually every industrial robot controller.

Practice Exercise: Derive the Lagrangian dynamics for a cart-pole system (inverted pendulum on a moving cart). Simulate it under PD control and observe how the cart must actively move to stabilize the pole.


Disclaimer

For educational purposes only. This article is part of a structured learning curriculum and does not constitute professional engineering advice.