Automatic Balancing of a Wheel

Objective

Description

A disc of radius \(\textrm{radius}\) is rotating with a constant angular velocity \(\omega\) around the vertical axis in the horizontal X / Y plane.

It’s center is tied to the origin by a spring with a spring constant \(k\) and a damping coefficient \(\textrm{speed}_{\mu}\). The disc has a fixed imbalance created by a particle of mass \(m_i\) at a distance of \(\textrm{radius}\) from the center of the disc.

A number n_free of particles of mass \(m_f\) are free to move on the perimeter of the disc.

Notes

  • Without adding friction to the attachment of the disc to the origin and to the motion of the free particles, it did not work.

  • With one or two free particles there is only one solution, but with three there are two solutions. One is found.

States

  • \(x, y\) : coordinates of the center of the disc in the inertial frame

  • \(u_x, u_y\) : velocities of the center of the disc in the inertial frame

  • \(q_i\) : angular coordinates of the free particles relative to the disc

  • \(u_i\) : angular velocities of the free particles relative to the disc

Parameters

  • \(m_{\textrm{disc}}\) : mass of the disc

  • \(m_i\) : mass of the fixed particle

  • \(m_f\) : mass of the free particles

  • \(g\) : gravity

  • \(\textrm{speed}_{\mu}\) : damping coefficient for the disc

  • \(\textrm{free}_{\mu}\) : damping coefficient for the free particles

  • \(\omega\) : angular velocity of the disc

  • \(k\) : spring constant of the spring connecting the disc to the origin

  • \(\textrm{radius}\) : radius of the disc

import sympy as sm
import sympy.physics.mechanics as me
import numpy as np
from scipy.integrate import solve_ivp
from scipy.interpolate import CubicSpline
from matplotlib.patches import Circle
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

Equations of Motion, Kanes Method

n_free = 2  # number of free particles
N, A = sm.symbols('N A', cls=me.ReferenceFrame)
O, O_disc, Po = sm.symbols('O, O_disc, Po', cls=me.Point)
Points = sm.symbols(f'P:{n_free}', cls=me.Point)

t = me.dynamicsymbols._t
O.set_vel(N, 0)

# Coordinates and velocities of the center of the disc, w.r.t. N
x, y, ux, uy = me.dynamicsymbols('x, y, ux, uy')
# Coordinates and velocities of the free particles, w.r.t. the disc
q = me.dynamicsymbols(f'q:{n_free}')
u = me.dynamicsymbols(f'u:{n_free}')

m_disc, m_i, m_f, g, speed_mu, free_mu, omega, k, radius = sm.symbols(
    'm_disc, m_i, m_f, g, speed_mu, free_mu, omega, k, radius', real=True)

A.orient_axis(N, omega * t, N.z)
A.set_ang_vel(N, omega * N.z)

# Center of the disc
O_disc.set_pos(O, x * N.x + y * N.y)
O_disc.set_vel(N, ux * N.x + uy * N.y)

# Fixed imbalance
Po.set_pos(O_disc, radius/sm.sqrt(2) * (A.x + A.y))

# Free particles
vel_x = []
vel_y = []
for i in range(n_free):
    Points[i].set_pos(O_disc, radius * (sm.cos(q[i]) * A.x + radius *
                                        sm.sin(q[i]) * A.y))

    # Needed for the dampening of the free particles.
    v_A = Points[i].pos_from(O_disc).diff(t, A)
    vel_x.append(v_A.dot(A.x))
    vel_y.append(v_A.dot(A.y))

Define the bodies.

bodies = []
iZZ = 0.5 * m_disc * radius**2
inertia = me.inertia(A, 0, 0, iZZ)
disc = me.RigidBody('disc', O_disc, A, m_disc, (inertia, O_disc))
bodies.append(disc)
P_fix = me.Particle('P_fix', Po, m_i)
bodies.append(P_fix)  # fixed particle
for i in range(n_free):
    bodies.append(me.Particle(f'P_{i}', Points[i], m_f))

Define the forces.

vektor = O_disc.pos_from(O)
indikator = sm.Piecewise((0, vektor.magnitude() < 1.e-16), (1, True))
forces = [(O_disc, -k * indikator * vektor - speed_mu * (ux * N.x + uy * N.y))]

indikator = sm.Piecewise((0, omega < 1.e-16), (1, True))
for i in range(n_free):
    vektor = Points[i].pos_from(O_disc)
    speed = vektor.cross(indikator * N.z)
    forces.append((Points[i], - free_mu * (vel_x[i] * A.x + vel_y[i] * A.y)))

# Kinematic equations
kd = [ux - x.diff(t), uy - y.diff(t), *[u[i] - q[i].diff(t)
                                        for i in range(n_free)]]

q_ind = [x, y, *q]
u_ind = [ux, uy, *u]

KM = me.KanesMethod(N, q_ind, u_ind, kd)
fr, frstar = KM.kanes_equations(bodies, forces)

MM = KM.mass_matrix_full
force = KM.forcing_full

print('force dynamic symbols', me.find_dynamicsymbols(force))
print('mass matrix dynamic symbols', me.find_dynamicsymbols(MM))
print(f'force contains {sm.count_ops(force)} operations')
print(f'mass matrix contains {sm.count_ops(MM)} operations')
force dynamic symbols {u0(t), u1(t), ux(t), q1(t), uy(t), x(t), q0(t), y(t)}
mass matrix dynamic symbols {q1(t), q0(t)}
force contains 596 operations
mass matrix contains 154 operations

Lambdification

qL = q_ind + u_ind
pL = [m_disc, m_i, m_f, g, speed_mu, free_mu, omega, k, radius, t]
MM_lam = sm.lambdify(qL + pL, MM, cse=True)
force_lam = sm.lambdify(qL + pL, force, cse=True)

Numerical Integration

# Input variables
m_disc1 = 20.0  # mass of the disc
m_i1 = 1.0  # mass of the fixed particle
m_f1 = 1.0  # mass of the free particles
g1 = 9.81  # gravity
radius1 = 1.0  # radius of the disc
speed_mu1 = 1.0  # damping coefficient for the disc
free_mu1 = 0.5  # friction coefficient for the free particles
k1 = 1.e1  # spring constant
omega1 = 1.5  # angular velocity of the disc

x1 = 0.0  # initial x position of the disc
y1 = 0.0  # initial y position of the disc
ux1 = 0.0  # initial x velocity of the disc
uy1 = 0.0  # initial y velocity of the disc
q01 = np.deg2rad(55)
q11 = np.deg2rad(15)
q21 = np.deg2rad(0.0)  # initial angle of the first free particle
u01 = 0.0
u11 = 0.0
u21 = 0.0
t1 = 0.0

intervall = 100
punkte = 100

schritte = int(intervall * punkte)
times = np.linspace(0., intervall, schritte)
t_span = (0., intervall)

pL_vals = [m_disc1, m_i1, m_f1, g1, speed_mu1, free_mu1, omega1, k1,
           radius1, t1]

if n_free == 1:
    y0 = [x1, y1, q01, ux1, uy1, u01]
elif n_free == 2:
    y0 = [x1, y1, q01, q11, ux1, uy1, u01, u11]
elif n_free == 3:
    y0 = [x1, y1, q01, q11, q21, ux1, uy1, u01, u11, u21]
else:
    raise ValueError(f'Unsupported number of free particles: {n_free}',
                     'Set manually in the code above')


def gradient(t, y, args):
    args[-1] = t  # Update time in args
    sol = np.linalg.solve(MM_lam(*y, *args), force_lam(*y, *args))
    return np.array(sol).T[0]


resultat1 = solve_ivp(gradient, t_span, y0, t_eval=times, args=(pL_vals,),
                      method='Radau', atol=1.e-10, rtol=1.e-10)

resultat = resultat1.y.T
print('resultat shape', resultat.shape, '\n')
print(resultat1.message)
resultat shape (10000, 8)

The solver successfully reached the end of the integration interval.

Plot some results.

fig, ax = plt.subplots(2, 1, figsize=(8, 5), layout='constrained',
                       sharex=True)
bezeichnung = ['x', 'y', 'q0', 'q1', 'ux', 'uy', 'u0', 'u1']
for i in range(2):
    ax[0].plot(times, resultat[:, i], label=bezeichnung[i])
ax[0].legend()
ax[0].set_ylabel('distance [m]')
ax[0].set_title('Coordinates of the center of the disc relative to N')
for i in range(2, 2 + n_free):
    ax[1].plot(times, resultat[:, i], label=bezeichnung[i])
ax[1].set_xlabel('time [s]')
ax[1].set_ylabel('angle [rad]')
ax[1].set_title('Angular coordinates of the free particles relative to the '
                ' disc')
_ = ax[1].legend()
Coordinates of the center of the disc relative to N, Angular coordinates of the free particles relative to the  disc

Animation

fps = 10.0

t_arr = np.linspace(0.0, intervall, schritte)
state_sol = CubicSpline(t_arr, resultat)

r_disc = radius1

Pl, Pr, Pu, Pd = sm.symbols('Pl Pr Pu Pd', cls=me.Point)
Pl.set_pos(O_disc, -r_disc*A.x)
Pr.set_pos(O_disc, r_disc*A.x)
Pu.set_pos(O_disc, r_disc*A.y)
Pd.set_pos(O_disc, -r_disc*A.y)


coordinates = O_disc.pos_from(O).to_matrix(N)
for point in (Po, Pl, Pr, Pu, Pd, *Points):
    coordinates = coordinates.row_join(point.pos_from(O).to_matrix(N))

coords_lam = sm.lambdify(qL + pL, coordinates, cse=True)

fig, ax = plt.subplots(figsize=(7, 7))
ax.set_xlim(-r_disc-1, r_disc+1)
ax.set_ylim(-r_disc-1, r_disc+1)
ax.set_aspect('equal')
ax.set_xlabel('x', fontsize=15)
ax.set_ylabel('y', fontsize=15)
ax.axhline(0, color='black', lw=0.5)
ax.axvline(0, color='black', lw=0.5)

# draw the spokes
line1, = ax.plot([], [], lw=1, marker='o', markersize=0, color='black')
line2, = ax.plot([], [], lw=1, marker='o', markersize=0, color='black')

# draw the ball
initial_center = (x1, y1)
ball = Circle(initial_center, r_disc,
              fill=True, color='magenta', alpha=0.5)
ax.add_patch(ball)
# draw the observer
imbalance, = ax.plot([], [], marker='o', markersize=15, color='black')

# The free balls
free_balls = []
farben = ['red', 'yellow', 'blue']  # Colors for the free balls
for i in range(n_free):
    free_balls.append(ax.plot([], [], marker='o', markersize=15,
                              color=farben[i])[0])

# Function to update the plot for each animation frame


def update(t):
    message = ((f'Running time {t:.2f} sec \n The black particle is the '
                f'imbalance. \n The colored particles are free to move to '
               'balance the wheel.'))
    ax.set_title(message, fontsize=12)

    pL_vals[-1] = t  # Update time in pL_vals
    coords = coords_lam(*state_sol(t), *pL_vals)
    line1.set_data([coords[0, 2], coords[0, 3]], [coords[1, 2], coords[1, 3]])
    line2.set_data([coords[0, 4], coords[0, 5]], [coords[1, 4], coords[1, 5]])

    imbalance.set_data([coords[0, 1]], [coords[1, 1]])
    ball.set_center((coords[0, 0], coords[1, 0]))

    for i in range(n_free):
        free_balls[i].set_data([coords[0, 6 + i]], [coords[1, 6 + i]])


# Create the animation
animation = FuncAnimation(fig, update, frames=np.arange(0.0,
                                                        intervall, 1 / fps),
                          interval=400/fps, blit=False)

plt.show()

Total running time of the script: (1 minutes 22.320 seconds)

Gallery generated by Sphinx-Gallery