from typing import DefaultDict
from manim import *
import subprocess, sys
import numpy as np
import math

config.media_embed = True

from collections import defaultdict

#############################################
# Generalized N pendulum
#############################################
class Pendulum:
    def __init__(self, N, theta=None):
        self.N = N;
        self.gravity = 9.81
        self.L = np.ones(shape=(N, )) # lengths
        self.m = np.ones(shape=(N, )) # masses
        
        self.theta = np.zeros(shape=(N, )) if theta is None else theta
        self.theta_dot = np.zeros(shape=(N, ))

    def step(self, delta_t):
        equations = np.zeros(shape=(4 * self.N, 4 * self.N + 1))
        theta_dd = 0
        a_xn = theta_dd + self.N
        a_yn = a_xn + self.N
        T_n = a_yn + self.N
        for p in range(0, 4 * self.N, 4):
            n = p // 4
            # First equation x_dd
            equations[p, theta_dd + n] = self.L[n] * math.cos(self.theta[n])
            equations[p, a_xn + n] = -1.0;
            if n > 0:
                equations[p, a_xn + n - 1] = 1
            equations[p, -1] = self.theta_dot[n]**2 * math.sin(self.theta[n])

            # Second equation y_dd
            equations[p + 1, theta_dd + n] = -1.0 * self.L[n] * math.sin(self.theta[n])
            equations[p + 1, a_yn + n] = -1.0
            if n > 0:
                equations[p + 1, a_yn + n - 1] = 1.0
            equations[p + 1, -1] = self.theta_dot[n]**2 * math.cos(self.theta[n])

            # Third equation horizontal tension
            equations[p + 2, a_xn + n] = self.m[n]
            equations[p + 2, T_n + n] = math.sin(self.theta[n])
            if n + 1 < self.N:
                equations[p + 2, T_n + n + 1] = -1.0 * math.sin(self.theta[n + 1])
            equations[p + 2, -1] = 0

            # Fourth equation vertical tension
            equations[p + 3, a_yn + n] = self.m[n]
            equations[p + 3, T_n + n] = math.cos(self.theta[n])
            if n + 1 < self.N:
                equations[p + 3, T_n + n + 1] = -1.0 * math.cos(self.theta[n + 1])
            equations[p + 3, -1] = self.m[n] * self.gravity

        solution = np.linalg.solve(equations[:, 0:-1], equations[:, -1])
        self.theta_dot += delta_t * solution[0:self.N]
        self.theta += delta_t * self.theta_dot

class DoublePendulum():
    """ Creates a collection of 2-Pendulums
        Parameters:
        theta: np.ndarray with shape (N, 2)
        lengths (optional): np.ndarray with shape (N, 2) default is all ones
        masses  (optional): np.ndarray with shape (N, 2) default is all ones
    """
    def __init__(self, theta, lengths=None, masses=None):
        self.t = theta
        self.t_d = np.zeros(theta.shape)
        self.l = np.ones(theta.shape) if lengths is None else lengths
        self.m = np.ones(theta.shape) if masses is None else masses
        self.g = 9.81 # gravity

    def step(self, delta_t, step_size=0.0005):
        for i in range(math.ceil(delta_t / step_size)):
            self.smol_step(step_size)

    def smol_step(self, delta_t):
        m_1, m_2 = self.m[:, 0], self.m[:, 1]
        t_1, t_2 = self.t[:, 0], self.t[:, 1]
        l_1, l_2 = self.l[:, 0], self.l[:, 1]
        t_d1, t_d2 = self.t_d[:, 0], self.t_d[:, 1]
        bottom = (2 * m_1) + m_2 - (m_2 * np.cos(2 * (t_1 - t_2)))
        theta_dd1 = (-self.g * (2 * m_1 + m_2) * np.sin(t_1) - m_2 * self.g * np.sin(t_1 - 2 * t_2) - 2 * np.sin(t_1 - t_2) * m_2 * (t_d2**2 * l_2 + t_d1**2 * l_1 * np.cos(t_1 - t_2))) / (l_1 * bottom)
        theta_dd2 = (2 * np.sin(t_1 - t_2) * (t_d1**2 * l_1 * (m_1 + m_2) + self.g * (m_1 + m_2) * np.cos(t_1) + t_d2**2 * l_2 * m_2 * np.cos(t_1 - t_2))) / (l_2 * bottom)
        self.t_d[:, 0] += delta_t * theta_dd1
        self.t_d[:, 1] += delta_t * theta_dd2
        self.t += delta_t * self.t_d

    def get_nth_pendulum(self, n):
        return self.t[n, 0], self.t[n, 1]
class PhysicalNPendulum(Scene):
    def construct(self):
        p = Pendulum(N=2, theta=[math.pi / 2] * 2)
        scale = 2
        x_c, y_c = 0, 3

        def get_pendulum(dot_radius=DEFAULT_DOT_RADIUS):
            points = [[x_c, y_c, 0]] + [[scale * p.L[n] * math.sin(p.theta[n]), -scale * p.L[n] * math.cos(p.theta[n]), 0] for n in range(p.N)]
            for i in range(1, len(points)):
                for k in range(3):
                    points[i][k] += points[i - 1][k]
            return [ Line(points[i - 1], points[i]) for i in range(1, len(points)) ] + [ Dot(point, radius=dot_radius) for point in points]

        def step(pendulum, dt):
            for obj in pendulum:
                pendulum.remove(obj)
            p.step(dt / 2)
            for obj in get_pendulum(dot_radius=0.06):
                pendulum.add(obj)

        pendulum = VGroup()
        for obj in get_pendulum():
            pendulum.add(obj)
        pendulum.add_updater(step)
        self.add(pendulum)
        self.wait(5)
        pendulum.remove_updater(step)


param= "-v WARNING  --progress_bar None   -r  500,200 --fps=20 --disable_caching PhysicalNPendulum"
paramgif= "-v WARNING  --progress_bar None --format=gif  -r  500,200  --disable_caching PhysicalNPendulum"
%manim $param
Loading...
class Physical2Pendulum(Scene):
    def construct(self):
        ps = [0, 3, 0]
        scale = 2.5
        angles = True
        history = defaultdict(lambda: [])
        theta_hist = defaultdict(lambda: [])


        def mod_clamp(x, low=-math.pi, high=math.pi):
            return ((x-((high-low)/2)) % (high-low)) + low
        
        def lin_scale(x, start, end):
            return (x - start) / (end - start)


        def get_pendulum(p, pid, tracers=False, color=BLUE, dot_radius=DEFAULT_DOT_RADIUS):
            t_1, t_2 = p.get_nth_pendulum(0)
            p0 = [ps[0] + scale * math.sin(t_1), ps[1] - scale * math.cos(t_1), 0]
            p1 = [p0[0] + scale * math.sin(t_2), p0[1] - scale * math.cos(t_2), 0]
            x0, x1 = DashedLine([ps[0], ps[1], 0], [ps[0], ps[1] - 1, 0]), DashedLine([p0[0], p0[1], 0], [p0[0], p0[1] - 1, 0])
            l0, l1 = Line(ps, p0), Line(p0, p1)
            x0.set_color(color)
            x1.set_color(color)
            l0.set_color(color)
            l1.set_color(color)

            theta_hist[pid].append(np.array([lin_scale(mod_clamp(t_1), -math.pi, math.pi), lin_scale(mod_clamp(t_2), -math.pi, math.pi), 0]))
            history[pid].append(p1)
            if len(history[pid]) > 10:
                history[pid] = history[pid][1:]

            res = [ l0, l1, Dot(ps, radius=dot_radius, color=WHITE), Dot(p0, radius=dot_radius, color=color), Dot(p1, radius=dot_radius, color=color)]
            if angles:
                flip0, flip1 = p0[0] < ps[0], p1[0] < p0[0]
                res += [ x0, x1, Angle(x0, l0, other_angle=flip0), Angle(x1, l1, other_angle=flip1),
                    MathTex(r"\theta_1").move_to(Angle(x0, l0, other_angle=flip0, radius=0.4 + 3 * SMALL_BUFF).point_from_proportion(0.5)),
                    MathTex(r"\theta_2").move_to(Angle(x1, l1, other_angle=flip1, radius=0.4 + 3 * SMALL_BUFF).point_from_proportion(0.5))]
            if tracers:
                res += [ Line(history[pid][i-1], history[pid][i], color=color) for i in range(1, len(history[pid])) ]
            return res


        def create_pendulum(theta, pid=0, color=BLUE, rate=1, add_updater=True, tracers=True):
            p = DoublePendulum(theta)
            pendulum = VGroup()
            history[pid].clear()
            for obj in get_pendulum(p, pid, color=color, dot_radius=0.06):
                pendulum.add(obj)

            def step(pendulum, dt):
                clear(pendulum)
                p.step(dt * rate)
                for obj in get_pendulum(p, pid, color=color, tracers=tracers, dot_radius=0.06):
                    pendulum.add(obj)

            if add_updater:
                pendulum.add_updater(step)
            return p, pendulum, step

        def clear(*objs):
            for obj in objs:
                for o in obj:
                    obj.remove(o)

        def create_pendulum_path(pid, color, s, e):
            path = VGroup()

            def update(pth, dt):
                clear(pth)
                for obj in get_plot_line(pid, s, e, color):
                    pth.add(obj)
            path.add_updater(update)
            return path, update

        def get_plot_line(pid, domain_start, domain_end, color):
            res = []
            for i in range(1, len(theta_hist[pid])):
                p0 = domain_start + theta_hist[pid][i - 1] * (domain_end - domain_start)
                p1 = domain_start + theta_hist[pid][i] * (domain_end - domain_start)
                if np.linalg.norm(p1 - p0) <= 4:
                    res.append(Line(p0, p1, color=color))
            return res


        #######################################################################
        angles = False
        p1, pendulum1, s1 = create_pendulum(np.ones((1, 2)) * math.pi / 2.0, pid=1, color=BLUE, add_updater=False)
        p2, pendulum2, s2 = create_pendulum(np.ones((1, 2)) * math.pi / 1.9, pid=2, color=ORANGE, add_updater=False)
        self.add(pendulum1, pendulum2)
        self.play( Create(pendulum1), Create(pendulum2))
        pendulum1.add_updater(s1)
        pendulum2.add_updater(s2)
        self.wait(4)
        pendulum1.remove_updater(s1)
        pendulum2.remove_updater(s2)
        self.play(FadeOut(pendulum1), FadeOut(pendulum2))
        self.remove(pendulum1, pendulum2)

        #######################################################################
        # plot = Square(side_length=6.5).to_corner(RIGHT + UP)
        # t1, t2 = MathTex(r"\theta_1").shift(3.3611 * RIGHT + 3.5 * DOWN), MathTex(r"\theta_2").move_to([-0.3, 0.25, 0])
        # p_pi1, m_pi1 = MathTex(r"\pi").move_to([-0.2, 3.5, 0]), MathTex(r"-\pi").move_to([0.1111, -3.25, 0])
        # p_pi2 = MathTex(r"\pi").move_to([6.61111, -3.25, 0])
        # self.add(plot, t1, t2)
        # self.play(  Create(plot),
        #     Create(t1), Create(t2),
        #     Create(p_pi1), Create(m_pi1),
        #     Create(p_pi2)
        # )

        # p_start = np.array([0.11111, -3, 0])
        # p_end = np.array([6.6111, 3.5, 0])
        # ps = [-3.5, 3, 0]
        # scale = 1.7
        # p1, pendulum1, s1 = create_pendulum(np.ones((1, 2)) * math.pi / 2.0, pid=3, color=BLUE, add_updater=False)
        # p2, pendulum2, s2 = create_pendulum(np.ones((1, 2)) * math.pi / 1.9, pid=4, color=ORANGE, add_updater=False)
        # path1, p_s1 = create_pendulum_path(3, BLUE, p_start, p_end)
        # path2, p_s2 = create_pendulum_path(4, ORANGE, p_start, p_end)
        # self.add(pendulum1, pendulum2, path1, path2)
        # self.play( Create(pendulum1) )
        # self.wait(1)
        # pendulum1.add_updater(s1)
        # pendulum2.add_updater(s2)
        # self.wait(18)
        # pendulum1.remove_updater(s1)
        # pendulum2.remove_updater(s2)
        # path1.remove_updater(p_s1)
        # path2.remove_updater(p_s2)
        # self.play(FadeOut(pendulum1), FadeOut(pendulum2), FadeOut(path1), FadeOut(path2))
        # self.play( FadeOut(p_pi1), FadeOut(p_pi2), FadeOut(m_pi1), FadeOut(t1), FadeOut(t2))
        # self.remove(pendulum1, pendulum2, path1, path2)





param= "-v WARNING  --progress_bar None   -r  500,400 --fps=24 --disable_caching Physical2Pendulum"
# paramgif= "-v WARNING  --progress_bar None --format=gif  -r  500,200  --disable_caching Physical2Pendulum"
%manim $param
Loading...