Skip to content

Latest commit

 

History

History
489 lines (357 loc) · 18.4 KB

2d_pendulum_white_noise.rst

File metadata and controls

489 lines (357 loc) · 18.4 KB
.. jupyter-execute::

   #====================
   # 2d_pendulum_white_noise
   #====================

Note

You can download this example as a Python script: :jupyter-download:script:`2d_pendulum_white_noise` or Jupyter notebook: :jupyter-download📓`2d_pendulum_white_noise`.

.. jupyter-execute::


Basic 2D n particle pendulum, hanging from the ceiling. The point on the ceiling may be fixed or move along the X and Y directions in a predescribed manner. On each particle, white noise external forces are applied in X and in Y direction.

sdeint is a library, which performs ITO (and Stratonovich) numerical integration of stochastic differential equations. In the description it says, it is still a ‘beta’ version. Playing around with it, it seems to give ‘reasonable’ results.

.. jupyter-execute::

    import sympy.physics.mechanics as me
    import sympy as sm

    from scipy.optimize import fsolve
    import numpy as np

    import matplotlib.pyplot as plt
    %matplotlib inline
    from matplotlib import animation
    from IPython.display import HTML
    import matplotlib
    matplotlib.rcParams['animation.embed_limit'] = 2**128
    from matplotlib.patches import Rectangle

    import time

    import sdeint as sd

n is the number of particles. For simplicity, they all have the same mass m, and the massless rods connecting them all have the same length l.

Antrieb = True means the point on the ceiling moves in some predescribed manner. I let it go around in a circle, other drives may be done. Antrieb = False means the point on the ceiling is fixed.

All else is just standard sympy.physics.mechanics. - FX, FY hold the external forces on each particle in X / Y direction. - reibung is the friction in the joints. - kraft is the factor with which the external forces may be multiplied to make them ‘stronger’. Same factor for all particles. - rhs: to get the reaction forces one needs $ rhs = MM^{-1} * force $. This will be calculated numerically, hence these are just ‘substitutes’.

.. jupyter-execute::

    #----------------------------------------------------------------------------------------------------------
    n = 5                          #Number of particles.
    Antrieb = False                # if True the ceiling point moves in in pre described manner, else fixed
    #----------------------------------------------------------------------------------------------------------------------------------
    start = time.time()

    # Generalized coordinates and velocities
    q = []
    qd = []
    u = []
    rhs = []    # needed for the reaction force
    FX = []
    FY = []

    for i in range(1, n+1):                              #NOTE: data for particle i are at list place i-1
        q.append(me.dynamicsymbols('q'+str(i)))          # rotation of particle i relative to N
        u.append(me.dynamicsymbols('u'+str(i)))
        qd.append(me.dynamicsymbols('q'+str(i), 1))
        rhs.append(me.dynamicsymbols('rhs'+str(i)))      # needed later for the reaction force
        FX.append(me.dynamicsymbols('F' + str(i)+ 'x'))  # external forces
        FY.append(me.dynamicsymbols('F' + str(i) + 'y')) #     dto.


    ux01, uy01, fx, fy = me.dynamicsymbols('ux01, uy01, fx, fy')          # reaction forces on suspension point
    aux = [ux01, uy01]

    g, l, m, reibung, kraft, t = sm.symbols('g, l, m, reibung, kraft, t') #kraft = intensity of white noise

    # of  course  any other 'reasonable' functions will also work
    if Antrieb == True:
        antriebx = l * sm.sin(sm.pi/2. * t)
        antriebxdt = antriebx.diff(t)
        antrieby = l * sm.cos(sm.pi/2. * t)
        antriebydt = antrieby.diff(t)
    else:
        antriebx = sm.S(0.)
        antriebxdt = sm.S(0)
        antrieby = sm.S(0.)
        antriebydt = sm.S(0.)

    #Reference frame, reference point
    N = me.ReferenceFrame('N')
    P0 = me.Point('P0')
    P0.set_vel(N, 0)

    P01 = P0.locatenew('P01', antriebx * N.x + antrieby * N.y)             # Base point on ceiling,
    P01.set_vel(N, antriebxdt*N.x + antriebydt*N.y + ux01*N.x + uy01*N.y)  # ux01, uy01 for reaction forces

    # Frame points, particles for each Pendulum, numbered P01a, 1, 2, ..., n
    A = [N]
    P = [P01]
    particles = [me.Particle('P01a', P01, m)]                      # Particle fixed to ceiling

    for i in range(1, n+1):
        Ai = N.orientnew('A' + str(i), 'Axis', [q[i-1], N.z])
        Ai.set_ang_vel(N, u[i-1] * N.z)
        A.append(Ai)

        Pi = P[i-1].locatenew('P' + str(i), (l + (i-1)/n) * A[i].y)
        Pi.v2pt_theory(P[i-1], N, A[i])
        P.append(Pi)

        # Create a new particle of mass m at this point
        Pai = me.Particle('Pa' + str(i), Pi, m)
        particles.append(Pai)

    # kinematic equations
    kd = []
    for i in range(n):
        kd.append((qd[i] - u[i]))

    # forces
    FL = [(P01, - m*g*N.y + fx*N.x + fy*N.y)]  # point on ceiling different from the rest.
    for i in range(1, n+1):
        kraft_auf_punkt =(P[i], -m*g*N.y + kraft*(FX[i-1]*A[i].x + FY[i-1]*A[i].y))
        torque = (A[i], -reibung*u[i-1]*A[i].z)
        FL.append(kraft_auf_punkt)
        FL.append(torque)

    # Kane's equations
    KM = me.KanesMethod(N, q_ind=q, u_ind=u, u_auxiliary=aux, kd_eqs=kd)
    (fr, frstar) = KM.kanes_equations(particles, FL)

    MM = KM.mass_matrix_full
    force = KM.forcing_full



A stochastic differential equation of the Ito type can have the random forces only in a linearized manner, see any textbook on stochastic differential equations. Hence the force vector must be linearized around (FX, FY).

I think, this makes physical sense: if the force k is small, then f(x, k) =_{approx} f(x, 0) + d/dk(f(x, 0) * k. Now here we have: d/dt(x) = f(x, 0) + d/dkf(x, k) * k or, as usually written in stochastic textbooks $ dx_t = f(x, 0) * dt + d/dkf(x, 0) * dk_t$, with dk_t = white noise

forceito = force(FX=0, FY=0) + d/d(FX+FY)(force(FX, FY). Note, that FX, FY are lists, and the diffentiation is done using the Jacobian.

In the code below, d/d(FX+FY)(force(FX, FY) is called B, force(FX=0, FY=0) is called force, and forceito is not needed explicitly anywhere. Also some notational inconsitency on my part: Kane’s formalism gives MM, and force, so that: MM * d/dt(x) = force. But in the integration we need $ d/dt(x) = MM^{-1} * (force + B) $

As FX, FY are needed explicitly to get the rection forces at the point on the ceiling, the mass matrix MM, and the force vectors have to be enlarged properly.

.. jupyter-execute::

    # expand MM properly
    Hilfs = sm.Matrix.zeros(len(force), len(FX + FY))
    MM = sm.Matrix.hstack(MM, Hilfs)
    Hilfs = sm.Matrix.hstack(sm.Matrix.zeros(len(FX + FY), len(force)), sm.eye(len(FX + FY)))
    MM = sm.Matrix.vstack(MM, Hilfs)
    print('MM shape:', MM.shape)
    print('MM DS:', me.find_dynamicsymbols(MM))
    print('MM free symbols', MM.free_symbols)
    print('MM has {} operations'.format(np.sum(np.array([MM[i, j].count_ops(visual=False) for i in range(MM.shape[0]) for j in range(MM.shape[1])]))), '\n')

    # Linearize force around F = 0, so ITO's formalism may be applied
    B = force.jacobian(sm.Matrix([FX+FY]))
    #Extension as Fx, Fy are explizitly needed for the reaction forces on P01: fx, fy
    B = sm.Matrix.vstack(B, sm.Matrix(sm.eye(len(FX + FY))))
    print('B shape:', B.shape)
    print('B DS:', me.find_dynamicsymbols(B))
    print('B free symbols', B.free_symbols)
    print('B has {} operations'.format(np.sum(np.array([B[i, j].count_ops(visual=False) for i in range(B.shape[0]) for j in range(B.shape[1])]))), '\n')

    subs_dict = {i: sm.S(0.) for i in FX + FY}
    force = sm.Matrix.vstack(force, sm.Matrix([sm.S(0.) for i in range(len(FX+FY))])).subs(subs_dict)
    print('force shape:', force.shape)
    print('force DS', me.find_dynamicsymbols(force))
    print('force has {} operations'.format(np.sum(np.array([force[i].count_ops(visual=False) for i in range(len(force))]))), '\n')


Set up the reaction forces. $ rhs = MM^{-1} * force $ is needed for the reaction forces. This will be calculated numerically later.

orte_x, orte_y are needed for the animation only.

I always find it interesting to look at the energy of the system. Strange behaviour may point to mistakes in setting up Kane’s equations of motion.

Lastly the sympy functions are converted into numpy functions, using sm.lambdify(…)

.. jupyter-execute::

    # Reaction force
    subs_dict = {i.diff(t): rhs[j] for j, i in enumerate(u)}
    eingepraegt = KM.auxiliary_eqs.subs(subs_dict)
    print('eingepraegt shape:', eingepraegt.shape)
    print('eingepraegt DS:', me.find_dynamicsymbols(eingepraegt))
    print('eingepraegt free symbols', eingepraegt.free_symbols)
    print('eingepraegt has {} operations'.format(np.sum(np.array([eingepraegt[i].count_ops(visual=False) for i in range(eingepraegt.shape[0])]))), '\n')


    # needed for animation only
    orte_x = []
    orte_y = []

    for i in range(n+1):
        orte_x.append(me.dot(P[i].pos_from(P0), N.x))
        orte_y.append(me.dot(P[i].pos_from(P0), N.y))


    pot_energie = sum([m*g*me.dot(P[i].pos_from(P0), N.y) for i in range(n+1)])
    kin_energie = sum([particles[i].kinetic_energy(N).subs({i: 0. for i in aux}) for i in range(n+1)])

    #Lambdification
    qL = q + u + FX + FY
    pL = [g, m, l, reibung, kraft]
    F = [fx, fy]

    force_lam = sm.lambdify(qL + [t] + pL, force, cse=True)
    MM_lam = sm.lambdify(qL + [t] + pL, MM, cse = True)
    B_lam = sm.lambdify(qL + [t] + pL, B, cse=True)

    eingepraegt_lam = sm.lambdify(F + qL + [t] + pL + rhs, eingepraegt, cse=True)

    orte_x_lam = sm.lambdify(qL + [t] + pL, orte_x, cse=True)
    orte_y_lam = sm.lambdify(qL + [t] + pL, orte_y, cse=True)

    pot_lam = sm.lambdify(qL + [t] + pL, pot_energie, cse=True)
    kin_lam = sm.lambdify(qL + [t] + pL, kin_energie, cse=True)

    print("It took {:.3f} sec to establish Kane's formalism".format(time.time() - start))

For integrating this Ito type differential equation, I use the sdeint library. It is the only one I know. It seems to be basically a ‘one man enterprise’, not updated frequently, but at the moment it works. As to the meaning of the parameters of itoint, consult the documentation of sdeint.

sdeint is not optimized for speed, as the documentation says. Based on my limited experience with it, I think, about 1,000 steps per second of integration is about right. ( I asked the developer this question, but never received a reply)

Input variables: - m1: mass of each particle - l1: length of the massless rod - reibung1: is the friction in the joints - kraft1: is the factor by which the ‘white noise’ is multiplied

.. jupyter-execute::

    # Numerical integration
    start = time.time()

    # Input variables
    #==================================================================================================

    intervall = 3.                       # integration runs from 0. to intervall
    # number of evaluations of sdeint. One should have at least 500 evaluations per time unit.
    schritte = 3000
    m1 = 1.                             # mass of each particle
    l1 = 1.                             # distance from one particle to the next
    reibung1 = 0.                       # friction
    kraft1 = 3.                         # 'strength' of forces
    #==================================================================================================

    times = np.linspace(0, intervall, schritte)

    pL_vals = [9.8, m1, l1, reibung1, kraft1]

    # for simplicity, the initial conditions are always the same:
    # pendulum is hanging straight down, external forces are 0. at t = 0.
    y0 = [np.pi for i in range(n)] + [0. for i in range(n, 2*n)] + [0.for i in range(len(FX + FY))]                      #Anfangsbed.

    # the 'meaning' of f, G may be learned from sdeint site.
    def f(x, t):
        A = np.linalg.solve(MM_lam(*x, t, *pL_vals), force_lam(*x, t, *pL_vals)).reshape(len(force))
        return A

    def G(x, t):
        B = np.linalg.solve(MM_lam(*x, t, *pL_vals), B_lam(*x, t, *pL_vals))
        return B

    resultat = sd.itoint(f, G, y0, times)

    print('it took {:.3f} sec to integrate {} steps'.format(time.time() - start, schritte))
    print('resultat.shape: ', resultat.shape)



    #====================================================================================


Print the angular speeds. I guess, all it shows is that they look ‘random’, as they should.

.. jupyter-execute::

    #==================================================================================
    # print generalized angular velocities
    fig, ax = plt.subplots(figsize=(10, 6))
    for i, j in zip(range(n, 2*n), [i for i in range(1, n+1)]):
        ax.plot(times, resultat[:, i], label='Angular velocity of node {}'.format(j))
        ax.set_title('Generalized angular velocities')
    ax.legend();


Print the externally applied forces. They should look like Brownian motion, integrated white noise, and they seem to look o.k.

.. jupyter-execute::

    start = time.time()

    #Print externally applied forces
    fig, ax = plt.subplots(figsize=(10, 6))

    for i, j in zip(range(2*n, 4*n), FX + FY):
        ax.plot(times, kraft1*resultat[:, i], label=' {}'.format(j))
        ax.set_title('Integrated externally applied forces, that is Brownian Motion')
    ax.legend();


Calculate the reaction forces on the suspension point on the ceiling. - numerically calculate rhs - numerically solve eingepraegt for fx, fy - plot them

.. jupyter-execute::

    # print reaction forces on P01
    # 1. numerically calculate the RHS, as it is needed for the reaction forces.
    RHS = np.zeros((schritte, len(force)))
    for i in range(schritte):
        zeit = times[i]
        RHS[i, :] = np.linalg.solve(MM_lam(*[resultat[i, j]for j in range(resultat.shape[1])], zeit, *pL_vals),
                                   force_lam(*[resultat[i, j] for j in range(resultat.shape[1])], zeit, *pL_vals)).reshape(resultat.shape[1])

    print('RHS shape', RHS.shape)

    # 2. calculate reaction forces numerically: solve eingepraegt for fx, fy
    kraft_x = np.zeros(schritte)
    kraft_y = np.zeros(schritte)

    def func (x, *args):
    # just needed to 'convert the arguments' properly
        return eingepraegt_lam(*x, *args).reshape(2)

    x0 = tuple([1., 1.])   #initial guess

    for i in range(schritte):
        zeit = times[i]
        y0 = [resultat[i, j] for j in range(resultat.shape[1])]
        rhs = [RHS[i, j] for j in range(n, 2*n)]
        pL_vals = list(pL_vals)

        args = tuple(y0 + [zeit] + pL_vals + rhs)
        A = fsolve(func, x0, args=args).reshape(2)
        x0 = tuple(A)      # new guess, should speed up convergence.
        kraft_x[i] = A[0]
        kraft_y[i] = A[1]




    fig, ax = plt.subplots(figsize=(10, 6))
    ax.plot(times, kraft_x, label='Reaction force on P0 in X direction')
    ax.plot(times, kraft_y, label='Reaction force on P0 in Y direction')
    ax.set_title('Reaction forces on P01')
    ax.legend();


Plot the energies of the system. As is to be expected, they increase / and decrease randomly. As is to be expected, the potential energy is smoother than the kinetic energy.

.. jupyter-execute::

    pot_np = np.empty(schritte)
    kin_np = np.empty(schritte)
    total_np = np.empty(schritte)

    for i in range(schritte):
        zeit = times[i]
        pot_np[i] = pot_lam(*[resultat[i, j] for j in range(resultat.shape[1])], zeit, *pL_vals)
        kin_np[i] = kin_lam(*[resultat[i, j] for j in range(resultat.shape[1])], zeit, *pL_vals)
        total_np[i] = pot_np[i] + kin_np[i]

    fig, ax = plt.subplots(figsize=(10, 6))
    ax.plot(times, pot_np, label='Potential Energy')
    ax.plot(times, kin_np, label='Kinetic Energy')
    ax.plot(times, total_np, label='Total Energy')

    ax.set_title('Energy of the System of {} bodies'.format(n+1))
    ax.legend();


    print('it took {:.3f} sec to plot the graphs'.format(time.time() - start))

Create an animation.

The last comand HTML(..) is needed on my iPad to show an animation. I do not know, whether needed on other machines.

.. jupyter-execute::


    # This is only to reduce the number of framesfor the animation, else it takes forever on my iPad
    # with HTML
    if schritte > 1000:
        N = int(schritte / 200)  #to make the animation faster
    else:
        N = 1


    x_coords = np.zeros((int(schritte/N)+1, n+1))
    y_coords = np.zeros((int(schritte/N)+1, n+1))

    k = -1
    for j in range(len(times)):
        zeit= times[j]
        if j % N == 0:
            k += 1
            x_coords[k] = orte_x_lam(*[resultat[j, i] for i in range(resultat.shape[1])], zeit, *pL_vals )
            y_coords[k] = orte_y_lam(*[resultat[j, i] for i in range(resultat.shape[1])], zeit, *pL_vals )

    def animate_pendulum(times, x, y):

        fig, ax = plt.subplots(figsize=(8, 8))
        ax.axis('on')
        lim1 = max([np.abs(y_coords[i, n]) for i in range(int(schritte/N))]) + l1/5.
        lim2 = max([np.abs(y_coords[i, n]) for i in range(int(schritte/N))]) + l1/5.
        lim = max(lim1, lim2)
        ax.set(xlim=(-lim, lim), ylim=(-lim, lim))

        line, = ax.plot([], [], 'o-', lw=0.5)

        cart_width = 0.2 * lim
        cart_height = 0.1 * lim
        rect = Rectangle([(x[0, 0]) - cart_width/2, y[0, 0] - cart_height/2],
                         cart_width, cart_height, fill=True, color='red', ec='black')
        ax.add_patch(rect)

        def init():
            line.set_data([], [])
            return line,

        def animate(i):
            rect.set_xy((x[i, 0] - cart_width/2., y[i, 0] - cart_height/2.))
            line.set_data(x[i], y[i])
            zeit = i * N * intervall / schritte
            ax.set_title('stochastically exited pendulum at time {:.2f} sec'.format(zeit), fontsize=15)
            return line,

        anim = animation.FuncAnimation(fig, animate, frames=int(schritte/N),
                                       interval=1000*times.max() / (4*N) ,
                                       blit=True, init_func=init)
        plt.close(fig)
        return anim

    anim = animate_pendulum(times, x_coords, y_coords)
    HTML(anim.to_jshtml())