Updating animation with slider

27 views Asked by At

I'm currently developing an animation featuring a double pendulum, and I'd like to incorporate sliders to enhance user interaction. These sliders are designed to control the two initial angles of the animation. However, I've encountered an issue: when I update the sliders, the animation restarts with the same initial values, despite the slider values being correctly reflected in the updated plot (verified through a print statement). I suspect there might be an issue with how I'm updating the animation.

To animate the double pendulum, I use this code:

def animate_double_pendulum(i, t, y, L1, L2, lines, points, trace):
    theta1, _, theta2, _ = y[:, i]

    x1 = L1 * np.sin(theta1)
    y1 = -L1 * np.cos(theta1)

    x2 = x1 + L2 * np.sin(theta2)
    y2 = y1 - L2 * np.cos(theta2)

    # Update the positions of the pendulum rods
    lines[0].set_data([0, x1], [0, y1])
    lines[1].set_data([x1, x2], [y1, y2])

    # Update the positions of the pendulum masses (points)
    points[0].set_data(x1, y1)
    points[1].set_data(x2, y2)

    # Update the trace of the endpoint
    trace.set_data(np.append(trace.get_xdata(), x2), np.append(trace.get_ydata(), y2))

    return lines + points + [trace]

The initial plot is made using this:

L1 = 1.0  # Length of pendulum 1
L2 = 1.0  # Length of pendulum 2
m1 = 1.0  # Mass of pendulum 1
m2 = 1.0  # Mass of pendulum 2
g = 9.81  # Gravitational acceleration

# Initial conditions
theta1_initial = np.radians(60.0)
theta2_initial = np.radians(5.0)

# Time span and number of points for simulation
t_span = (0, 20)
num_points = 1000

t, y = simulate_double_pendulum(theta1_initial, theta2_initial, L1, L2, m1, m2, g, t_span, num_points)

# Set up the initial plot with pendulum rods, masses, and trace
fig7, ax7 = plt.subplots(figsize=(6, 6))
line1, = ax7.plot([], [], lw=2, color='b')
line2, = ax7.plot([], [], lw=2, color='r')
point1, = ax7.plot([], [], 'bo', label='Mass 1')
point2, = ax7.plot([], [], 'ro', label='Mass 2')
trace, = ax7.plot([], [], lw=1, color='gray', label='Trace')

ax7.legend()
ax7.set_xlim(-2, 2)
ax7.set_ylim(-2, 2)

# Create the initial lines, points, and trace
lines = [line1, line2]
points = [point1, point2]

s_theta1, s_theta2 = get_sliders_double_pendulum()

# Animation parameters
interval = t_span[1] / num_points / 2 * 100  # milliseconds per frame
animation = FuncAnimation(
    fig7, animate_double_pendulum, frames=num_points, fargs=(t, y, L1, L2, lines, points, trace),
    interval=interval, repeat=True
)

plt.show()

And the code that tracks the sliders and the changes regarding those is the following:

def get_sliders_double_pendulum():
    s_theta1 = widgets.FloatSlider(
        description=r"$\theta_1$",
        min=0.0, max=2*np.pi,
        step=0.01,
        value=theta1_initial,
        continuous_update=False)
    
    s_theta2 = widgets.FloatSlider(
        description=r"$\theta_2$",
        min=0.0, max=2*np.pi,
        step=0.01,
        value=theta2_initial,
        continuous_update=False)
    
    s_theta1.observe(on_theta_slider_change, names='value')
    s_theta2.observe(on_theta_slider_change, names='value')
    
    display(s_theta1, s_theta2)
    
    return s_theta1, s_theta2

def on_theta_slider_change(change):
    theta1 = s_theta1.value
    theta2 = s_theta2.value
    update_double_pendulum(theta1, theta2, lines, points, trace, animation)
    
def clear_double_pendulum(lines, points, trace):        
    lines[0].set_data([], [])
    lines[1].set_data([], [])
    points[0].set_data([], [])
    points[1].set_data([], [])
    trace.set_data([], [])

def update_double_pendulum(theta1, theta2, lines, points, trace, animation):
    animation.event_source.stop()

    clear_double_pendulum(lines, points, trace)
    
    t, y = simulate_double_pendulum(theta1, theta2, L1, L2, m1, m2, g, t_span, num_points)
    
    ax7.set(title=r'Double pendulum with starting position $\theta_1$'+f'={np.round(theta1, 2)}, '
             +r'$\theta_2$='+f'{np.round(theta2, 2)}', xlabel='X', ylabel='Y')

    # Update the animation data
    animation = FuncAnimation(
        fig7, animate_double_pendulum, frames=num_points, fargs=(t, y, L1, L2, lines, points, trace),
        interval=interval, repeat=True
    )
    
    animation.frame_seq = animation.new_frame_seq()
    animation._stop = False

    # Restart the animation
    animation.event_source.start()

In summary, my goal is for the animation to restart and showcase a new sequence when the sliders are adjusted. I aim to have the animation reflect the updated values of time (t) and position/angular velocity (y). Any help would be greatly appreciated!

Edit: To copy and paste the code to run it:

%matplotlib notebook
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp

from scipy.integrate import solve_ivp
from matplotlib.animation import FuncAnimation

from ipywidgets import HBox, VBox, Layout, widgets

from IPython.display import display
from sympy import symbols, solve, Matrix
from matplotlib import colors
from matplotlib.patches import FancyArrowPatch

import warnings
warnings.filterwarnings("ignore")

def double_pendulum(t, y, L1, L2, m1, m2, g):
    theta1, omega1, theta2, omega2 = y

    c = np.cos(theta1 - theta2)
    s = np.sin(theta1 - theta2)

    # Equations of motion for the double pendulum
    num1 = -g * (2 * m1 + m2) * np.sin(theta1)
    num2 = -m2 * g * np.sin(theta1 - 2 * theta2)
    num3 = -2 * s * m2 * (omega2 ** 2 * L2 + omega1 ** 2 * L1 * c)
    den = L1 * (2 * m1 + m2 - m2 * np.cos(2 * theta1 - 2 * theta2))
    theta1_prime = (num1 + num2 + num3) / den

    num1 = 2 * s * (omega1 ** 2 * L1 * (m1 + m2) + g * (m1 + m2) * np.cos(theta1) + omega2 ** 2 * L2 * m2 * c)
    den = L2 * (2 * m1 + m2 - m2 * np.cos(2 * theta1 - 2 * theta2))
    theta2_prime = num1 / den

    return [omega1, theta1_prime, omega2, theta2_prime]

def simulate_double_pendulum(theta1, theta2, L1, L2, m1, m2, g, t_span, num_points, omega1=0, omega2=0):
    y0 = [theta1, omega1, theta2, omega2]

    sol = solve_ivp(
        fun=lambda t, y: double_pendulum(t, y, L1, L2, m1, m2, g),
        t_span=t_span,
        y0=y0,
        method='RK45',
        t_eval=np.linspace(t_span[0], t_span[1], num_points)
    )

    return sol.t, sol.y

def animate_double_pendulum(i, t, y, L1, L2, lines, points, trace):
    theta1, _, theta2, _ = y[:, i]

    x1 = L1 * np.sin(theta1)
    y1 = -L1 * np.cos(theta1)

    x2 = x1 + L2 * np.sin(theta2)
    y2 = y1 - L2 * np.cos(theta2)

    # Update the positions of the pendulum rods
    lines[0].set_data([0, x1], [0, y1])
    lines[1].set_data([x1, x2], [y1, y2])

    # Update the positions of the pendulum masses (points)
    points[0].set_data(x1, y1)
    points[1].set_data(x2, y2)

    # Update the trace of the endpoint
    trace.set_data(np.append(trace.get_xdata(), x2), np.append(trace.get_ydata(), y2))

    return lines + points + [trace]

def get_sliders_double_pendulum():
    s_theta1 = widgets.FloatSlider(
        description=r"$\theta_1$",
        min=0.0, max=2*np.pi,
        step=0.01,
        value=theta1,
        continuous_update=False)

    s_theta2 = widgets.FloatSlider(
        description=r"$\theta_2$",
        min=0.0, max=2*np.pi,
        step=0.01,
        value=theta2,
        continuous_update=False)

    s_theta1.observe(on_theta_slider_change, names='value')
    s_theta2.observe(on_theta_slider_change, names='value')

    display(s_theta1, s_theta2)

    return s_theta1, s_theta2

def on_theta_slider_change(change):
    theta1 = s_theta1.value
    theta2 = s_theta2.value
    update_double_pendulum(theta1, theta2, lines, points, trace, animation)

def clear_double_pendulum(lines, points, trace):        
    lines[0].set_data([], [])
    lines[1].set_data([], [])
    points[0].set_data([], [])
    points[1].set_data([], [])
    trace.set_data([], [])

def update_double_pendulum(theta1, theta2, lines, points, trace, animation):
    animation.event_source.stop()

    clear_double_pendulum(lines, points, trace)

    t, y = simulate_double_pendulum(theta1, theta2, L1, L2, m1, m2, g, t_span, num_points)

    ax7.set(title=r'Double pendulum with starting position $\theta_1$'+f'={np.round(theta1, 2)}, '
             +r'$\theta_2$='+f'{np.round(theta2, 2)}', xlabel='X', ylabel='Y')

    # Update the animation data
    animation = FuncAnimation(
        fig7, animate_double_pendulum, frames=num_points, fargs=(t, y, L1, L2, lines, points, trace),
        interval=interval, repeat=True
    )

    animation.frame_seq = animation.new_frame_seq()
    animation._stop = False

    # Restart the animation
    animation.event_source.start()

L1 = 1.0  # Length of pendulum 1
L2 = 1.0  # Length of pendulum 2
m1 = 1.0  # Mass of pendulum 1
m2 = 1.0  # Mass of pendulum 2
g = 9.81  # Gravitational acceleration

# Initial conditions
theta1 = np.radians(100.0)
theta2 = np.radians(45.0)

# Time span and number of points for simulation
t_span = (0, 20)
num_points = 1000

t, y = simulate_double_pendulum(theta1, theta2, L1, L2, m1, m2, g, t_span, num_points)

# Set up the initial plot with pendulum rods, masses, and trace
fig7, ax7 = plt.subplots(figsize=(6, 6))
line1, = ax7.plot([], [], lw=2, color='b')
line2, = ax7.plot([], [], lw=2, color='r')
point1, = ax7.plot([], [], 'bo', label='Mass 1')
point2, = ax7.plot([], [], 'ro', label='Mass 2')
trace, = ax7.plot([], [], lw=1, color='gray', label='Trace')

ax7.legend()
ax7.set_xlim(-2, 2)
ax7.set_ylim(-2, 2)

# Create the initial lines, points, and trace
lines = [line1, line2]
points = [point1, point2]

s_theta1, s_theta2 = get_sliders_double_pendulum()

# Animation parameters
interval = t_span[1] / num_points / 2 * 100  # milliseconds per frame
animation = FuncAnimation(
    fig7, animate_double_pendulum, frames=num_points, fargs=(t, y, L1, L2, lines, points, trace),
    interval=interval, repeat=True
)

plt.show()

This is part of a bigger jupyter notebook, so if not all imports are used in this code, that's why.

0

There are 0 answers