multi-link pendulum - python simulation and animation of a multi-link pendulum

This jupyter notebook uses Lagrange formalism to derive the dynamics of a multi-link pendulum, simulates it and shows an animation of resulting solution. Unfortunately, it seems to be not very time-performant, so the best I could do in a reasonable amount of time was 6 links. That is why the dynamic equations exported earlier are simulated in rust here and the results are re-imported to python to be shown and animated.

[1] 1994 - Murray, Li, Sastry: A Mathematical Introduction to Robotic Manipulation (https://www.ce.cit.tum.de/fileadmin/w00cgn/rm/pdf/murray-li-sastry-94-complete.pdf)

Notes:

  • indexing starts with zero
  • angular positions and velocities are defined absolutely (in general joint based measurements of a link orientation is relative to the previous link)

(Benjamin Jahn, TU Ilmenau, 2025)

Initilization¶

initialization: clear workspace, define latex use for display

In [1]:
# clear workspace
%reset -f
In [2]:
# latex math display
from IPython.display import Math, Latex

def latexDisplay(latex_obj): display(Math(latex_obj))
# def latexDisplay(latex_obj): display(Latex(r'$$' + (latex_obj) + r'$$'))

Run Rust for Solution¶

In [3]:
! cargo run -q 2>/dev/null
########## Robotic Equation Solver ##########
Time elapsed for numerical integration is: 10.324547584s

Import Solution¶

In [4]:
class Solution(object):
    def __init__(self,**kwargs):
        self.__dict__.update(kwargs)

sol = Solution(x=None, y = None)
In [5]:
# read csv
import pandas
import numpy as np
df = pandas.read_csv('multi_link_pendulum_solution.csv', sep=',', header=None)
N = df.shape[1] - 1

sol.t = df.iloc[:, 0].values
tmp = df.iloc[:, 1:N+1].values
sol.y = np.asarray(np.transpose(np.array(tmp)))
In [6]:
# implicte knowledge about the link lengths
l_num = np.linspace(0.5, 0.35, N) # length of links [m]

Plots¶

In [7]:
# plot imports
import plotly.graph_objs as go
import plotly.io as pio
pio.templates.default = "none"
pio.renderers.default = "notebook_connected"
In [8]:
# define generic plot function for angular position of triple pendulum
from colour import Color

def plot_pendulum(sol):
    start_color = '#0000ff'; end_color = '#00ff80'
    colorscale = [x.hex for x in list(Color(start_color).range_to(Color(end_color), N))]

    fig = go.Figure()
    for idx in range(0, N):
        fig.add_trace(go.Scatter(x=sol.t, y=sol.y[idx, :] / np.pi, name=r"$q_{" + str(idx) + "}$", 
            line=dict(width=2, color=colorscale[idx])))

    fig.update_layout(height=600, width=900, margin=go.layout.Margin(t=20), 
        xaxis=dict(title=r"$\text{time in seconds}$", mirror=True, ticks='outside', showline=True),
        yaxis=dict(title=r"$\text{angular position in } \pi \text{ rad}$", 
            tickmode="linear", tick0=0.0, dtick=1, mirror=True, ticks='outside', showline=True))

    fig.show()
In [9]:
# plot angular positions
plot_pendulum(sol)

Animation¶

In [10]:
## import matplotlib for animations
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import animation, rc, colormaps
matplotlib.rcParams['animation.embed_limit'] = 2**128
from math import *
rc('animation', html='jshtml')
In [11]:
# define pendulum animation function
def animate_pendulum(sol, fps=30, colormap='winter'):

    # colors = cm.get_cmap(colormap, N)
    colors = colormaps[colormap].resampled(N)

    # frame number and index distance for given fps (tried to avoid interpolation to not slow down animation even further)
    T2anim = sol.t[-1]
    frameNumber = fps*T2anim
    didx = floor(len(sol.t)/frameNumber)

    # create and setup figure
    px = 1/plt.rcParams['figure.dpi']  # pixel in inches
    fig, ax = plt.subplots(figsize=(900*px,540*px))
    ax.set_aspect('equal')
    # plt.axis('equal'); 
    plt.close()
    l_sum = sum(l_num)
    ax.set_xlim((-l_sum*1.65, l_sum*1.65)); ax.set_ylim((-l_sum*1.35, l_sum*1.35))

    # create line objects
    line_base, = ax.plot([], [], lw=3, c=(0, 0, 0))
    lines = [None]*N
    for idx in range(0,N):
        # lines[idx], = ax.plot([], [], lw=4, c=colors(idx/N))
        lines[idx], = ax.plot([], [], lw=4, c=colors(idx/N), solid_capstyle='round')

    # initialization function: plot the background of each frame
    def init():
        # draw rail
        line_base.set_data(np.linspace(-l_sum*1.65, l_sum*1.65, 10), 0*np.linspace(-2, 2, 10)) 
        
        return (line_base,*lines, ) # return line objects

    # animation function: this is called sequentially
    def animate(i):

        # current configuration of pendulum
        p = np.zeros((2,N+1))
        for idx in range(1,N+1):
            p[:,idx] = p[:,idx-1] + np.array([-l_num[idx-1]*sin(sol.y[idx-1,i*didx]), l_num[idx-1]*cos(sol.y[idx-1,i*didx])])

        for idx in range(0,N):
            lines[idx].set_data([p[0,idx], p[0, idx+1]],[p[1,idx], p[1, idx+1]])

        return (*lines,) # return line objects

    # return animation object
    return animation.FuncAnimation(fig, animate, init_func=init, frames=floor(frameNumber), interval=1/fps*1000, blit=True)
In [12]:
anim = animate_pendulum(sol)
anim 
Out[12]:
No description has been provided for this image