I have been playing around with a simple n+1 particle pendulum. The first particle may only move in X direction.
The particles are connected by (unbendable) springs and dampers.
Using velocity constraints, the particles may be forced to only move in X or in Y direction.
Now, if I force the LAST particle to move only in a prescribed direction, all seems to work fine. Total energy (without friction) remains constant, the animation looks 'reasonable'.
However, if I try to constrain ANY OTHER particle the numerical integration fails badly.
On mechanical grounds, I see no reason, why the last particle should be special.
I wonder what is going on here.
Any help / hints will be greatly appreciated.
The program is attached below.
Thanks! Peter
import sympy.physics.mechanics as me
import sympy as sm
from scipy.integrate import odeint, solve_ivp
import numpy as np
from scipy.optimize import fsolve
import matplotlib.pyplot as plt
from matplotlib import animation
from IPython.display import HTML
import matplotlib
matplotlib.rcParams['animation.embed_limit'] = 2**128
import time
'''
n+1 body 2D pendulum. Bodies in n_rest are restricted non - holonomically to move either only
vertically or only horizontally. P0 may only move horizontally.
For some reason, only the last body can be restricted, else numerical issues appear.
'''
# Input
#================================================================================================
n = 6 # number of bodies, called P0.....Pn. P0 is fixed
cse = True # cse=True is about 10 times faster than cse=False
# n_rest holds the 'particles' which are to be cnstraint by speed constraints.
n_rest = [6] # Only if the last particle is restricted it will work
# wie contains whether the particle can move horizontally only or vertically only
wie = ['v']
#================================================================================================
start = time.time()
# Defining the variables needed
m, g, l, l0, reibung, feder = sm.symbols('m g l l0 reibung feder')
t = sm.symbols('t')
auxy, fy = me.dynamicsymbols(' auxy fy') # reaction forces for first point
q = [] # angle of rotation for frame_i wrt frame N
u = []
x = [] # location of point P_i wrt frame_i
ux = []
A = []
P = []
aux = [] # for the reaction forces
KRAFT = []
for i in range(0, n):
q.append(me.dynamicsymbols('q' + str(i)))
u.append(me.dynamicsymbols('u' + str(i)))
x.append(me.dynamicsymbols('x' + str(i)))
ux.append(me.dynamicsymbols('ux' + str(i)))
for i in range(n+1):
A.append(me.ReferenceFrame('A' + str(i)))
P.append(me.Point('P' + str(i)))
aux.append(me.dynamicsymbols('aux' + str(i) + 'xy'))
KRAFT.append(me.dynamicsymbols('F' + str(i) + 'xy'))
auxy, fy = me.dynamicsymbols(' auxy fy') # for the reaction forces on P[0]
x00, ux00 = me.dynamicsymbols('x00, ux00') # P[0] may move in horizontal direction
#represent the rhs, needed for reaction forces, calculated numerically later. KM.rhs() is way too large!
rhs = [sm.symbols('rhs' + str(i)) for i in range(4*n + 2)]
#setting up the geometry
BODY = []
speed_store = []
# Inertial frame
N = me.ReferenceFrame('N')
P0 = me.Point('P0')
P0.set_vel(N, 0)
A[0] = N
P[0].set_pos(P0, x00 * N.x)
P[0].set_vel(N, ux00*N.x + auxy*N.y) #P[0] is fixed, these are virtual velocities
BODY.append(me.Particle('P0a', P[0], m))
speed_store.append(P[0].vel(N))
for i in range(1, n+1):
A[i].orient_axis(N, q[i-1], N.z)
A[i].set_ang_vel(A[i-1], u[i-1] * A[i].z)
P[i].set_pos(P[i-1], x[i-1]*A[i].x)
P[i].set_vel(A[i], ux[i-1]*A[i].x)
P[i].v1pt_theory(P[i-1], N, A[i])
BODY.append(me.Particle('P' + str(i) + 'a', P[i], m))
speed_store.append(P[i].vel(N))
#kimenatic equations
kd = [*[u[i] - q[i].diff(t) for i in range(n)]] + [*[ux[i] - x[i].diff(t)
for i in range(n)]] + [ux00 - x00.diff(t)]
#speed constraints, reaction forces
speed_constr = []
Kraft1 = [(P[0], fy*N.y)]
auxecht = [auxy]
for i in n_rest:
k = n_rest.index(i) #find out whether horizontal or vertical restriction
richtung = wie[k]
if richtung == 'h': # P[i] can only move horizontally
speed_constr.append(me.dot(P[i].vel(N), N.y))
P[i].set_vel(N, speed_store[i] + aux[i] * N.y)
Kraft1.append((P[i], KRAFT[i] * N.y))
auxecht.append(aux[i])
elif richtung == 'v':
speed_constr. append(me.dot(P[i].vel(N), N.x))
P[i].set_vel(N, speed_store[i] + aux[i] * N.x)
Kraft1.append((P[i], KRAFT[i] * N.x))
auxecht.append(aux[i])
else:
raise Exception('wrong entry in wie')
# forces, torques
Moment = []
Reibung = []
for i in range(n+1):
Moment.append((A[i], -reibung*u[i-1]*A[i].z))
Reibung.append((P[i], -reibung*ux[i-1]*A[i].x))
# forces due to the springs. P0, P[n] must be treated separately
Kraft2 = []
Kraft2.append((P[0], -m*g*N.y + feder*(x[0] - l0)*A[1].x ))
for i in range(1, n):
Kraft2.append((P[i], -m*g*N.y - feder*(x[i-1] - l0)*A[i].x + feder*(x[i] - l0)*A[i+1].x))
Kraft2.append((P[n], -m*g*N.y - feder*(x[n-1] - l0) * A[n].x))
FL = Kraft1 + Kraft2 + Reibung + Moment
q_ind = [x00] + x + q
u_ind = [ux00] + ux + [u[k-1] for k in range(1, n+1) if k not in n_rest]
u_dep = [u[k-1] for k in n_rest]
KM = me.KanesMethod(N, q_ind=q_ind, u_ind=u_ind, u_dependent=u_dep, kd_eqs=kd, u_auxiliary=auxecht,
velocity_constraints=speed_constr)
(fr, frstar) = KM.kanes_equations(BODY, FL)
MM = KM.mass_matrix_full
print('MM shape:', MM.shape)
print('MM free symbols', MM.free_symbols)
print('MM DS', me.find_dynamicsymbols(MM))
print('MM contains {} operations'.format(sum([MM[i, j].count_ops(visual=False)
for i in range(MM.shape[0]) for j in range(MM.shape[1])])), '\n')
force_dict = {KRAFT[i]: sm.S(0) for i in range(len(KRAFT))} # as per J. Moore this is correct
aux_dict = {i: sm.S(0) for i in auxecht}
Daux_dict = {i.diff(t): sm.S(0) for i in auxecht}
force = KM.forcing_full.subs(force_dict).subs(Daux_dict).subs(aux_dict)
print('force shape:', force.shape)
print('force free symbols', force.free_symbols)
print('force DS', me.find_dynamicsymbols(force))
print('force contains {} operations'.format(sum([force[i].count_ops(visual=False)
for i in range(force.shape[0])])), '\n')
rhs_dict = {sm.Derivative(i, t): rhs[j] for j, i in enumerate([x00] + x + q + [ux00] + ux + u)} #rhs to be calculated numerically later
eingepraegt = KM.auxiliary_eqs.subs(rhs_dict)
print('eingepraegt shape:', eingepraegt.shape)
print('eingepraegt free symbold', eingepraegt.free_symbols)
print('eingepraegt DS', me.find_dynamicsymbols(eingepraegt))
print('eingepraegt contains {} operations'.format(sum([eingepraegt[i].count_ops(visual=False)
for i in range(eingepraegt.shape[0])])), '\n')
# Carthesian coordinates of the points
orte = [[me.dot(P[i].pos_from(P0), uv) for uv in (N.x, N.y)] for i in range(n+1)]
# absent any friction, the total energy must be constant, a necessary condition for the equations to
# be correct
kin_dict1 = {q[i].diff(t): u[i] for i in range(n)}
kin_dict2 = {i: 0. for i in auxecht}
pot_energie = m * g * sum([me.dot(P[k].pos_from(P0), N.y) for k in range(n+1)])
kin_energie = sum(([BODY[k].kinetic_energy(N) for k in range(n+1)])).subs(kin_dict1).subs(kin_dict2)
feder_energie = 0.5 * feder * sum([(x[i] - l0)**2 for i in range(n)])
#Lambdification
qL = [x00] + x + q + [ux00] + ux + u
pL = [m, g, l, l0, reibung, feder]
F1 = [fy] + [KRAFT[i] for i in n_rest]
MM_lam = sm.lambdify(qL + pL, MM, cse=cse)
force_lam = sm.lambdify(qL + pL, force, cse=cse)
eingepraegt_lam = sm.lambdify(F1 + qL + pL + rhs, eingepraegt, cse=cse)
orte_lam = sm.lambdify(qL + pL, orte)
pot_lam = sm.lambdify(qL + pL, pot_energie)
kin_lam = sm.lambdify(qL + pL, kin_energie)
feder_lam = sm.lambdify(qL + pL, feder_energie)
print('it took {:.3f} sec to establish Kanes equations'.format(time.time() - start))
# Numerical Integration
start = time.time()
#qL = x + q + uvar + u_ind + u_dep
#pL = [m, g, l, l0, reibung, feder]
# Input variables
#============================================================================
m1 = 1. # mass of particle
l1 = 1. # initial distance of particles from eeach other
l01 = 1. # natural length of spring
reibung1 = 0. # friction
feder1 = 250. # spring constant
intervall = 15
schritte = 300
pL_vals = [1., 9.8, 1., 1., 0., 100.]
x1 = [ l1 for i in range(n+1)]
ux1 = [0. for i in range(n+1)]
q1 = [np.pi/4. for i in range(n)]
u1 = [0. for i in range(n)]
#=====================================================================================
y0 = x1 + q1 + ux1 + u1
pL_vals = [m1, 9.8, l1, l01, reibung1, feder1 ]
#
height_start = []
#height_start = [(orte_lam(*y0, *pL_vals)[0][1], 1)] #Start Y - coordinate on attachment point P0
for i in n_rest:
k = n_rest.index(i)
if wie[k] == 'h':
zeiger = 1
else:
zeiger =0
height_start.append((orte_lam(*y0, *pL_vals)[i][zeiger], zeiger))
times = np.linspace(0, intervall, schritte)
t_span = (0., intervall)
def gradient(t, y, args):
sol = np.linalg.solve(MM_lam(*y, *args), force_lam(*y, *args))
return np.array(sol).T[0]
# method = 'radau' give the best results in that total energy = constant, if reibung = 0.
resultat1 = solve_ivp(gradient, t_span, y0, t_eval = times, args=(pL_vals,), method='Radau')
resultat = resultat1.y.T
event_dict = {-1: 'Integration failed', 0: 'Integration finished successfully', 1: 'some termination event'}
print(event_dict[resultat1.status])
print("To numerically integrate an intervall of {} sec with cse = {}, the routine cycled {} times and it took {:.3f} sec "
.format(intervall, cse, resultat1.nfev, time.time() - start), '\n')
print('resultat shape', resultat.shape)
# A holonomic constraint (X = const) has to be approximated by a non-holonomic constraint(d/dt(X) = 0).
# This give the maximum error due to this.
max_deviation = []
for k, ll in enumerate(n_rest):
max_deviation.append(max([np.abs(orte_lam(*[resultat[i,j]
for j in range(resultat.shape[1])] , *pL_vals)[ll][height_start[k][1]] - height_start[k][0])
for i in range(resultat.shape[0])]))
for i, ll in enumerate(n_rest):
print('largest deviation of point {} was {:.3f} distance units'.format(ll, np.abs(max_deviation[i])))
#Print coordinate
fig, ax = plt.subplots(figsize=(15, 10))
for i in range(0, resultat.shape[1]):
ax.plot(times[: resultat.shape[0]], resultat[:, i], label = str(i))
ax.legend();
#energies
pot_np = np.empty(resultat.shape[0])
kin_np = np.empty(resultat.shape[0])
feder_np = np.empty(resultat.shape[0])
total_np = np.empty(resultat.shape[0])
for i in range(resultat.shape[0]):
pot_np[i] = pot_lam(*[resultat[i, j] for j in range(resultat.shape[1])], *pL_vals)
kin_np[i] = kin_lam(*[resultat[i, j] for j in range(resultat.shape[1])], *pL_vals)
feder_np[i] = feder_lam(*[resultat[i, j] for j in range(resultat.shape[1])], *pL_vals)
total_np[i] = pot_np[i] + kin_np[i] + feder_np[i]
fig, ax = plt.subplots(figsize=(15, 10))
ax.plot(times[: resultat.shape[0]], pot_np, label='potential energy')
ax.plot(times[: resultat.shape[0]], kin_np, label='kinetic energy')
ax.plot(times[: resultat.shape[0]], feder_np, label='spring energy')
ax.plot(times[: resultat.shape[0]], total_np, label='total energy')
ax.set_title('Energies of {} bodies, friction = {} units'.format(n+1, reibung1))
ax.legend();
# Print forces
# rhs calculated numerically, needed for reaction forces ( = eingepraegt)
start = time.time()
RHS = np.zeros((resultat.shape[0], resultat.shape[1]))
for i in range(resultat.shape[0]):
zeit = times[i]
RHS[i, :] = np.linalg.solve(MM_lam(*[resultat[i, j]for j in range(resultat.shape[1])], *pL_vals),
force_lam(*[resultat[i, j] for j in range(resultat.shape[1])], *pL_vals)).reshape(resultat.shape[1])
print('RHS shape', RHS.shape)
#calculate reaction forces numerically
def func (x, *args):
kraft11 = eingepraegt_lam(*x, *args)
return kraft11.reshape(len(F1))
KRAFT1 = np.empty((resultat.shape[0], len(auxecht)))
x0 = tuple(([100. for i in range(len(auxecht))])) #erster Startwert
for i in range(resultat.shape[0]):
y0 = [resultat[i, j] for j in range(resultat.shape[1])]
rhs = [RHS[i, j] for j in range(resultat.shape[1])]
args = tuple(y0 + pL_vals + rhs)
A = fsolve(func, x0, args=args)#.reshape(len(auxecht))
x0 = tuple(A) #neuer Startwert = vorheriges Resultat, sollte Konvergenz beschleunigen
KRAFT1[i] = A
fig, ax = plt.subplots(figsize=(12,6))
for i, j in enumerate(('P[0] in Y - direction', *[n_rest[k] for k in range(len(n_rest))])):
plt.plot(times[: resultat.shape[0]], KRAFT1[:, i], label='force on body ' + str(j))
plt.legend();
print('it took {:.3f} sec to calulate and plot the reaction forces'.format(time.time() - start))
#Animation
start = time.time()
# get Carthesian coordinates
x_coords = np.zeros((resultat.shape[0], n+1))
y_coords = np.zeros((resultat.shape[0], n+1))
for j in range(resultat.shape[0]):
x_coords[j] = [orte_lam(*[resultat[j, k] for k in range(resultat.shape[1])], *pL_vals)[l][0] for l in range(n+1)]
y_coords[j] = [orte_lam(*[resultat[j, k] for k in range(resultat.shape[1])], *pL_vals)[l][1] for l in range(n+1)]
max_x = max([abs(x_coords[i, j])for i in range(resultat.shape[0]) for j in range(n+1)])
max_y = max([abs(y_coords[i, j])for i in range(resultat.shape[0]) for j in range(n+1)])
max_xy = max(max_x, max_y) + 0.5
# Ab hier das eigentliche Animationsprogramm
fig, ax = plt.subplots(figsize=(12, 12))
ax.axis('on')
ax.set(xlim=(-max_xy, max_xy), ylim=(-max_xy, max_xy))
plt.hlines(y_coords[0, 0], -max_xy, max_xy, linestyles='dashed') #für den Aufhängepunkt
for i in range(len(n_rest)):
if wie[i] == 'h':
plt.hlines(y_coords[0, n_rest[i]], -max_xy, max_xy, linestyles='dashed')
else:
plt.vlines(x_coords[0, n_rest[i]], -max_xy, max_xy, linestyles='dashed')
Lines = []
for i in range(n+1): #markiert die Punkte, die roten Punkte sind restriktiert
if i in n_rest or i == 0:
color = 'red'
else:
color = 'blue'
line, = ax.plot([], [], 'o-', lw=0.5, color=color, markersize=10)
Lines.append(line)
line, = ax.plot([], [], 'o-', lw=0.5, color='blue', markersize=0) #connects the points
Lines.append(line)
def animate_pendulum(times, x, y):
def animate(i):
for j in range(n+1):
Lines[j].set_data(x[i, j], y[i, j])
Lines[-1].set_data(x[i], y[i])
ax.set_title('running time {:.2f} sec'.format(i/schritte*intervall), fontsize=20)
return Lines
anim = animation.FuncAnimation(fig, animate, frames=resultat.shape[0],
interval=1000*times[: resultat.shape[0]].max() / resultat.shape[0],
blit=True) #, init_func=init)
plt.close(fig)
return anim
anim = animate_pendulum(times[: resultat.shape[0]], x_coords, y_coords)
HTML(anim.to_jshtml())
I have been playing around with a simple n+1 particle pendulum. The first particle may only move in X direction. The particles are connected by (unbendable) springs and dampers. Using velocity constraints, the particles may be forced to only move in X or in Y direction.
Now, if I force the LAST particle to move only in a prescribed direction, all seems to work fine. Total energy (without friction) remains constant, the animation looks 'reasonable'.
However, if I try to constrain ANY OTHER particle the numerical integration fails badly.
On mechanical grounds, I see no reason, why the last particle should be special. I wonder what is going on here.
Any help / hints will be greatly appreciated.
The program is attached below.
Thanks! Peter