Rolling Axle, Controlled

Description

This is a modification of the rolling axle on an uneven street example, see https://pydy.org/pst-notebooks/examples/plot_rolling_axle_uneven_street.html

There, with the no slip conditions enforced, the system has only one degree of freedom, so controlling it to reach some fixed point is not possible.

Now, I want it to move to some final position by applying torques to the wheels. Hence the no slip conditions are relaxed, using opnty’s eom_bounds keyword.

Notes

  • It takes quite long to get an acceptable solution, presumably because the jacobian is not very sparse.

  • I am not very sure about the ‘mechanical meaning’ of the relaxed no slip conditions.

  • The animation is a bit ‘jumpy’ to save space. It may be improved by using a larger value for fps.

States

  • \(q_L, q_R\) : rotation angles of the wheels

  • \(x_L, y_L\) : coordinates of the contact point of the left wheel

  • \(q_2, q_3\) : rotation angles of the axle

  • \(x_R, y_R\) : coordinates of the contact point of the right wheel

  • \(l_y, l_z\) : components of the vector from the contact point to the center of the left wheel

  • \(r_y, r_z\) : components of the vector from the contact point to the center of the right wheel

  • \(u_L, u_R\) : angular speeds of the wheels

  • \(u_2, u_3\) : angular speeds of the axle

  • \(ux_L, uy_L\) : speeds of the contact point of the left wheel

  • \(ux_R, uy_R\) : speeds of the contact point of the right wheel

  • \(ul_y, ul_z\) : speeds of the vector from the contact point to the center of the left wheel

  • \(ur_y, ur_z\) : speeds of the vector from the contact point to the center of the right wheel

  • \(T_L, T_R\) : torques applied to the wheels. Controls of opty

Parameters

  • \(m_L, m_R\) : masses of the wheels

  • \(m_o\) : mass of the particle attached to the wheels

  • \(g\) : gravity

  • \(r_L, r_R\) : radii of the wheels

  • \(l\) : distance between the wheels

  • \(amplitude, frequenz\) : parameters of the street

  • \(reibung\) : friction between the wheels and the axle

Further symbols

  • \(N\) : inertial frame

  • \(AX\) : frame attached to the axle

  • \(AL\) : frame attached to the left wheel

  • \(AR\) : frame attached to the right wheel

  • \(O\) : reference point, fixed in N

  • \(CPL\) : contact point of the left wheel

  • \(CPR\) : contact point of the right wheel

  • \(Dmc_L\) : center of mass of the left wheel

  • \(Dmc_R\) : center of mass of the right wheel

  • \(m_{Dmc_L}\) : particle attached to the left wheel

  • \(m_{Dmc_R}\) : particle attached to the right wheel

import sympy as sm
import sympy.physics.mechanics as me
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
from scipy.optimize import root, minimize
from opty import Problem
from matplotlib.animation import FuncAnimation
from matplotlib.patches import Ellipse
from matplotlib.transforms import Affine2D

If True, print some information about the eom.

info = True

Rotation angles of the wheels and the body, and their speeds.

qL, qR, q2, q3 = me.dynamicsymbols('qL qR q2 q3')
uL, uR, u2, u3 = me.dynamicsymbols('uL uR  u2 u3')

Coordinates of the contact points of the left / right wheel.

xL, yL, xR, yR = me.dynamicsymbols('xL yL xR yR')
uxL, uyL, uxR, uyR = me.dynamicsymbols('uxL uyL uxR uyR')  # their 'speeds'

Components of the vectors from the contact points to the centers of mass, in N. Their speeds.

ly, lz, ry, rz = me.dynamicsymbols('ly lz ry rz')
uly, ulz, ury, urz = me.dynamicsymbols('uly ulz ury urz')

Torques on the wheels. Controls for opty.

TL, TR = me.dynamicsymbols('TL TR')

Parameters of the system: masses, gravity, radii of the wheels, and distance between the wheels.

mL, mR, mo, g, rL, rR, l = sm.symbols(
    'mL mR mo g rL rR l')

Parameters for the surface.

amplitude, frequenz, reibung = sm.symbols('amplitude frequenz reibung')

Define some frames, points, etc.

N, AX, AL, AR = sm.symbols('N, AX, AL, AR', cls=me.ReferenceFrame)
O, CPL, CPR, DmcL, DmcR = sm.symbols('O, CPL, CPR, DmcL, DmcR',
                                     cls=me.Point)
m_DmcL, m_DmcR = sm.symbols('m_DmcL, m_DmcR', cls=me.Point)

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

The axle does not rotate around itself.

AX.orient_body_fixed(N, [q3, q2, 0], 'ZYX')
rot = AX.ang_vel_in(N)
AX.set_ang_vel(N, u2*AX.y + u3*AX.z)
rot1 = AX.ang_vel_in(N)

The left wheel rotates around the axle, that is, around AX.x, similarly for the right wheel.

AL.orient_axis(AX, qL, AX.x)
AL.set_ang_vel(AX, uL*AX.x)
AR.orient_axis(AX, qR, AX.x)
AR.set_ang_vel(AX, uR*AX.x)

Particles attached to the wheels.

m_DmcL.set_pos(DmcL, rL*AL.y)
m_DmcR.set_pos(DmcR, rR*AR.y)

Here the street is modelled. rumpel must be an integer.

x_h, y_h = sm.symbols('x_h y_h')
rumpel = 2


def gesamt(x, y, amplitude, frequenz, rumpel):
    strasse = sum([amplitude/j * (sm.sin(j*frequenz*sm.pi * x) +
                                  sm.sin(j*frequenz*sm.pi * y))
                   for j in range(1, rumpel)])
    return strasse


def gesamt_plot(x_h, y_h, amplitude, frequenz):
    return sum([amplitude/j * (sm.sin(j*frequenz*sm.pi * x_h) +
                               sm.sin(j*frequenz*sm.pi * y_h))
                for j in range(1, rumpel)])

Create the dictionary to replace \(\dfrac{d}{dt}(\textrm{gen. coord})\) with the corresponding symbols.

kin_dict = {
    xL.diff(t): uxL,
    yL.diff(t): uyL,
    xR.diff(t): uxR,
    yR.diff(t): uyR,
    qL.diff(t): uL,
    qR.diff(t): uR,
    ly.diff(t): uly,
    lz.diff(t): ulz,
    ry.diff(t): ury,
    rz.diff(t): urz,
    q2.diff(t): u2,
    q3.diff(t): u3,
}

kin_dict
{Derivative(xL(t), t): uxL(t), Derivative(yL(t), t): uyL(t), Derivative(xR(t), t): uxR(t), Derivative(yR(t), t): uyR(t), Derivative(qL(t), t): uL(t), Derivative(qR(t), t): uR(t), Derivative(ly(t), t): uly(t), Derivative(lz(t), t): ulz(t), Derivative(ry(t), t): ury(t), Derivative(rz(t), t): urz(t), Derivative(q2(t), t): u2(t), Derivative(q3(t), t): u3(t)}

Configuration Constraints

\(CP_L, CP_R\) are the contact points where the wheels touch the street.

CPL.set_pos(O, xL*N.x + yL*N.y +
            gesamt(xL, yL, amplitude, frequenz, rumpel)*N.z)
CPL.set_vel(N, uxL * N.x + uyL * N.y +
            gesamt(xL, yL, amplitude, frequenz, rumpel).diff(t) * N.z)

Define the vectors pointing from the contact point to the corresponding center of the wheel. \(\text{vector}_L \perp A.x\) and \(\text{vector}_R \perp A.x\), so they have no component in A.x direction.

vectorL = ly * AX.y + lz * AX.z
vectorR = ry * AX.y + rz * AX.z

\(\text{vector}_L\) and \(\text{vector}_R\) must have magnitude equal to the respective wheel \(r_L\) and \(r_R\).

constr_length = sm.Matrix([
    vectorL.magnitude() - rL,
    vectorR.magnitude() - rR,
])

constr_length
Matrix([
[-rL + sqrt(ly(t)**2 + lz(t)**2)],
[-rR + sqrt(ry(t)**2 + rz(t)**2)]])

Set centers of mass of wheels, second contact point CPR.

DmcL.set_pos(CPL, vectorL)
DmcL.v2pt_theory(CPL, N, AX)
DmcR.set_pos(DmcL, l * AX.x)
CPR.set_pos(DmcR, -vectorR)

\(\text{vector}_L\) must be in the plane formed by the gradient \(n_L\) at the point (xL, yL) on the surface and by \(A.x\) the direction of the axle, that is \(\text{vector}_L \circ (n_L \times A.x) = 0\) Same for \(\text{vector}_R\).

nL = (-gesamt(xL, yL, amplitude, frequenz, rumpel).diff(xL) * N.x -
      gesamt(xL, yL, amplitude, frequenz, rumpel).diff(yL) * N.y +
      N.z).normalize()

nR = (-gesamt(xR, yR, amplitude, frequenz, rumpel).diff(xR) * N.x -
      gesamt(xR, yR, amplitude, frequenz, rumpel).diff(yR) * N.y +
      N.z).normalize()

perpL = nL.cross(AX.x)
perpR = nR.cross(AX.x)

constrT = sm.Matrix([
    perpL.dot(vectorL),
    perpR.dot(vectorR),
])

Determine the constraints for \(x_R, y_R, q_2\) for the location of CPR.

CPR_pos = xR*N.x + yR*N.y + gesamt(xR, yR, amplitude, frequenz, rumpel)*N.z
delta_loc = CPR.pos_from(O) - CPR_pos

constr_CPR = sm.Matrix([
    delta_loc.dot(N.x),
    delta_loc.dot(N.y),
    delta_loc.dot(N.z),
])

Combine the configuration constraints.

config_constr = constr_length.col_join(constrT).col_join(constr_CPR)
if info:
    print(f"config_constr contains {sm.count_ops(config_constr)} operations")
    print("DS", me.find_dynamicsymbols(config_constr))
    print("FS", config_constr.free_symbols, "shape = ", config_constr.shape)
config_constr contains 484 operations
DS {ly(t), xL(t), yR(t), q3(t), lz(t), q2(t), ry(t), xR(t), yL(t), rz(t)}
FS {l, rR, frequenz, t, rL, amplitude} shape =  (7, 1)

No Slip Constraints

Set the speeds of the centers of the wheels and of the particles attached to it.

DmcL.v2pt_theory(CPL, N, AL)
DmcR.v2pt_theory(CPR, N, AR)

vDmcL = DmcL.pos_from(O).diff(t, N)
vDmcR = DmcR.pos_from(O).diff(t, N)

deltaL_vel = vDmcL - DmcL.vel(N)
deltaR_vel = vDmcR - DmcR.vel(N)

frame = N
constr_no_slip = sm.Matrix([
    deltaL_vel.dot(frame.x),
    deltaL_vel.dot(frame.y),
    deltaR_vel.dot(frame.x),
    deltaR_vel.dot(frame.y),
])


m_DmcL.v2pt_theory(DmcL, N, AL)
_ = m_DmcR.v2pt_theory(DmcR, N, AR)

Kane’s Equations

iXXL = 0.5 * mL * rL**2
iYYL = 0.25 * mL * rL**2
iZZL = 0.25 * mL * rL**2
iXXR = 0.5 * mR * rR**2
iYYR = 0.25 * mR * rR**2
iZZR = 0.25 * mR * rR**2

IL = me.inertia(AL, iXXL, iYYL, iZZL)
IR = me.inertia(AR, iXXR, iYYR, iZZR)

BodyL = me.RigidBody('BodyL', DmcL, AL, mL, (IL, DmcL))
BodyR = me.RigidBody('BodyR', DmcR, AR, mR, (IR, DmcR))
partL = me.Particle('partL', m_DmcL, mo)
partR = me.Particle('partR', m_DmcR, mo)
BODY = [BodyL, BodyR, partL, partR]

Set the external forces acting on the system.

FL1 = [(DmcL, -mL*g*N.z), (DmcR, -mR*g*N.z),
       (m_DmcL, -mo*g*N.z), (m_DmcR, -mo*g*N.z)]
Torque = [(AL, -reibung * uL*AX.x), (AR, -reibung * uR*AX.x)]

The control for opty.

TorqueC = [(AL, TL*AL.x), (AR, TR*AR.x)]

Combine the forces and torques.

FL = FL1 + Torque + TorqueC

Kane’s method.

speed_constr = config_constr.diff(t)

kd = sm.Matrix([key - value for key, value in kin_dict.items()])

q_ind = [qL, qR, xL, yL, q3]
q_dep = [xR, yR, q2, ly, lz, ry, rz]
u_ind = [uL, uR, uxL, uyL, u3]
u_dep = [uxR, uyR, u2, uly, ulz, ury, urz]


kane = me.KanesMethod(
    N,
    q_ind=q_ind,
    q_dependent=q_dep,
    u_ind=u_ind,
    u_dependent=u_dep,
    kd_eqs=kd,
    velocity_constraints=speed_constr,
    configuration_constraints=config_constr,
)

fr, frstar = kane.kanes_equations(BODY, FL)


eom1 = kd.col_join(fr + frstar)

Append the configuration constraints.

eom2 = eom1.col_join(config_constr)

Append the non-slip constraints. They will be loosened in the optimization problem.

eom = eom2.col_join(constr_no_slip)

# Print some information about the eom.
if info:
    print(f"eom have {sm.count_ops(eom):,} operations, "
          f"{sm.count_ops(sm.cse(eom)):,} after cse, "
          f"shape = {eom.shape}, \n")
    print("eom dynamic symbols", me.find_dynamicsymbols(eom))
    print("shapes of eom", eom.shape)
eom have 2,035,388 operations, 1,868 after cse, shape = (28, 1),

eom dynamic symbols {Derivative(uxL(t), t), uyL(t), Derivative(qL(t), t), Derivative(q3(t), t), Derivative(uyL(t), t), Derivative(rz(t), t), Derivative(yR(t), t), qR(t), uL(t), Derivative(ry(t), t), uR(t), rz(t), ulz(t), xL(t), TL(t), Derivative(q2(t), t), q2(t), Derivative(uL(t), t), u3(t), urz(t), yL(t), ry(t), Derivative(xR(t), t), Derivative(uR(t), t), ly(t), yR(t), Derivative(u2(t), t), uxL(t), Derivative(yL(t), t), q3(t), lz(t), TR(t), Derivative(ly(t), t), u2(t), xR(t), Derivative(lz(t), t), uly(t), Derivative(qR(t), t), Derivative(ury(t), t), Derivative(xL(t), t), Derivative(urz(t), t), uyR(t), qL(t), ury(t), uxR(t), Derivative(u3(t), t)}
shapes of eom (28, 1)

Set parameters.

par_map = {}
par_map[mL] = 1.0
par_map[mR] = 0.25
par_map[mo] = 0.1
par_map[g] = 9.81
par_map[rL] = 2.0
par_map[rR] = 1.0
par_map[l] = 5.0
par_map[amplitude] = 0.15
par_map[frequenz] = 0.25
par_map[reibung] = 1.0

Set the independent gen. coordinates.

qL1 = 0.0
qR1 = 0.0
xL1 = 1.0
yL1 = 0.0
q31 = 0.0

Final position of the contact point of the left wheel.

xL_end = 20.0
yL_end = 20.0

Calculate consistent initial dependent generalized coordinates. All generalized speeds are set to zero initially.

pL = [key for key in par_map.keys()]
pL_vals = [par_map[key] for key in pL]

config_constr_lam = sm.lambdify(q_dep + q_ind + pL, config_constr, cse=True)


def func(y, args):
    return config_constr_lam(*y, *args).squeeze()


y0 = np.zeros(len(q_dep))
args = [qL1, qR1, xL1, yL1, q31] + pL_vals

res = root(func, y0, args=args)
xR1, yR1, q21, ly1, lz1, ry1, rz1 = res.x
if res.x[4] <= 0 or res.x[6] <= 0:
    raise ValueError(f"use different initial guess, lz = {res.x[4]}, "
                     f"rz = {res.x[6]} meaning at least one wheel is "
                     "below the street")

for i, j in zip(q_dep, res.x):
    print(f"{i} = {j:.3f}")
xR(t) = 6.090
yR(t) = -0.126
q2(t) = 0.249
ly(t) = -0.246
lz(t) = 1.985
ry(t) = -0.120
rz(t) = 0.993

Set up Problem

h = sm.symbols('h')
num_nodes = 300
t0, tf = 0.0, h * (num_nodes - 1)
interval_value = h
state_symbols = q_ind + q_dep + u_ind + u_dep

instance_constraints = [
    qL.func(t0) - qL1,
    qR.func(t0) - qR1,
    xL.func(t0) - xL1,
    yL.func(t0) - yL1,
    q3.func(t0) - q31,
    xR.func(t0) - xR1,
    yR.func(t0) - yR1,
    q2.func(t0) - q21,
    ly.func(t0) - ly1,
    lz.func(t0) - lz1,
    ry.func(t0) - ry1,
    rz.func(t0) - rz1,
    *[speed.func(t0) - 0.0 for speed in u_ind + u_dep],
    TL.func(t0) - 0.0,
    TR.func(t0) - 0.0,
    xL.func(tf) - xL_end,
    yL.func(tf) - yL_end,
    *[speed.func(tf) - 0.0 for speed in u_ind + u_dep],
]


def obj(free):
    # Minimize the duration.
    return free[-1]


def obj_grad(free):
    grad = np.zeros_like(free)
    grad[-1] = 1.0
    return grad


limit = 50.0
bounds = {
    h: (0.0, 0.1),
    TL: (-limit, limit),
    TR: (-limit, limit),
    lz: (0.0, par_map[rL]),  # left hub must remain above the street
    rz: (0.0, par_map[rR]),  # right hub must remain above the street
}

Loosen the non slip constraints.

delta_eom = 7.5  # Arbitrary. Not sure about the mechanical meaning.
eom_bounds = {
    24: (-delta_eom, delta_eom),
    25: (-delta_eom, delta_eom),
    26: (-delta_eom, delta_eom),
    27: (-delta_eom, delta_eom),
}


prob = Problem(
    obj,
    obj_grad,
    eom,
    state_symbols,
    num_nodes,
    h,
    known_parameter_map=par_map,
    instance_constraints=instance_constraints,
    time_symbol=t,
    bounds=bounds,
    eom_bounds=eom_bounds,
)

Solve the Problem

initial_guess = np.ones(prob.num_free)
initial_guess[-1] = 0.005

for i in range(3):
    if i == 0:
        prob.add_option('max_iter', 50)
    elif i == 1:
        prob.add_option('max_iter', 1000)
    else:
        prob.add_option('max_iter', 25000)
    solution, info = prob.solve(initial_guess)
    print(info['status_msg'])
    initial_guess = solution
b'Maximum number of iterations exceeded (can be specified by an option).'
b'Maximum number of iterations exceeded (can be specified by an option).'
b'Algorithm terminated successfully at a locally optimal point, satisfying the convergence tolerances (can be specified by options).'

Plot the constraint violations.

_ = prob.plot_constraint_violations(solution, subplots=True, show_bounds=True)
Constraint violations  Values of bounded EoMs

Plot the trajectories.

_ = prob.plot_trajectories(solution, show_bounds=True)
State Trajectories, Input Trajectories

Plot the objective value over iterations.

_ = prob.plot_objective_value()
Objective Value

Plot the sparsity pattern of the Jacobian.

_ = prob.plot_jacobian_sparsity()
plot axle rolling controlled

Find the minimum curvature of the surface and check it is larger than the wheels. (Formula from the internet)

x_h, y_h = sm.symbols('x_h y_h')
fx = gesamt_plot(x_h, y_h, amplitude, frequenz).diff(x_h)
fy = gesamt_plot(x_h, y_h, amplitude, frequenz).diff(y_h)
fxx = gesamt_plot(x_h, y_h, amplitude, frequenz).diff(x_h, 2)
fyy = gesamt_plot(x_h, y_h, amplitude, frequenz).diff(y_h, 2)
fxy = gesamt_plot(x_h, y_h, amplitude, frequenz).diff(x_h, y_h)

E = 1 + fx**2
F = fx * fy
G = 1 + fy**2

L = fxx / sm.sqrt(1 + fx**2 + fy**2)
M = fxy / sm.sqrt(1 + fx**2 + fy**2)
NN = fyy / sm.sqrt(1 + fx**2 + fy**2)

I1 = sm.Matrix([[E, F],
                [F, G]])

II = sm.Matrix([[L, M],
                [M, NN]])

# Shape operator
S = I1.inv() * II

# Eigenvalues = principal curvatures
k1, k2 = S.eigenvals().keys()

k1_lam = sm.lambdify([x_h, y_h, amplitude, frequenz], k1, cse=True)
k2_lam = sm.lambdify([x_h, y_h, amplitude, frequenz], k2, cse=True)


def func1(x0, args):
    # just needed to get the arguments matching for minimuze
    return np.abs(1.0 / k1_lam(*x0, *args))


def func2(x0, args):
    # just needed to get the arguments matching for minimuze
    return np.abs(1.0 / k2_lam(*x0, *args))


x0 = np.array((5.0, 10.0))      # initial guess
args = np.array((par_map[amplitude], par_map[frequenz]))

for _ in range(10):
    minimal1 = minimize(func1, x0, args, tol=1e-6)
    x0 = minimal1.x
for _ in range(10):
    minimal2 = minimize(func2, x0, args, tol=1e-6)
    x0 = minimal2.x
print("minimal1:", minimal1.message)
print("minimal2:", minimal2.message)

min_radius = min(minimal1.fun, minimal2.fun)

print('maximally admissible radius = {:.4f}'.format(min_radius))
if min_radius < max(par_map[rL], par_map[rR]):
    raise ValueError("The initial conditions are not viable, because "
                     "the radius of the wheels is larger than the maximally "
                     "admissible radius.")
minimal1: Optimization terminated successfully.
minimal2: Optimization terminated successfully.
maximally admissible radius = 10.8076

Distance between \(Dmc_L\) and \(Dmc_R\) should be constant. Distance of \(CP_L\), \(CP_R\) from the surface of the street should be zero.

resultat, *_, h_act = prob.parse_free(solution)
resultat = resultat.T
sys_times = np.linspace(t0, num_nodes * h_act, num_nodes)

delta_CPL_z = CPL.pos_from(O).dot(N.z) - gesamt(xL, yL, amplitude, frequenz,
                                                rumpel)
delta_CPL_z = me.msubs(delta_CPL_z, kin_dict)
delta_CPL_z_lam = sm.lambdify(q_ind + q_dep + u_ind + u_dep + pL, delta_CPL_z,
                              cse=True)
delta_CPR_z = CPR.pos_from(O).dot(N.z) - gesamt(xR, yR, amplitude, frequenz,
                                                rumpel)
delta_CPR_z = me.msubs(delta_CPR_z, kin_dict)
delta_CPR_z_lam = sm.lambdify(q_ind + q_dep + u_ind + u_dep + pL, delta_CPR_z,
                              cse=True)

Dmc_dist = DmcL.pos_from(DmcR).magnitude()
Dmc_dist = me.msubs(Dmc_dist, kin_dict)
Dmc_dist_lam = sm.lambdify(q_ind + q_dep + u_ind + u_dep + pL, Dmc_dist,
                           cse=True)

dist_np = np.empty(resultat.shape[0])
CPL_z_np = np.empty(resultat.shape[0])
CPR_z_np = np.empty(resultat.shape[0])

for i in range(resultat.shape[0]):
    CPL_z_np[i] = delta_CPL_z_lam(*[resultat[i, j] for j in range(24)],
                                  *pL_vals)
    CPR_z_np[i] = delta_CPR_z_lam(*[resultat[i, j] for j in range(24)],
                                  *pL_vals)
    dist_np[i] = Dmc_dist_lam(*[resultat[i, j] for j in range(24)],
                              *pL_vals)

fig, ax = plt.subplots(1, 1, figsize=(10, 3))
ax.plot(sys_times[0: resultat.shape[0]], dist_np,
        label='distance between centers of mass')
ax.plot(sys_times[0: resultat.shape[0]], CPL_z_np,
        label='CPL z coordinate')
ax.plot(sys_times[0: resultat.shape[0]], CPR_z_np,
        label='CPR z coordinate')
ax.legend(loc='upper left')
ax.set_title('Distance between centers of mass and '
             'z coordinates of CPL and CPR')
ax.set_xlabel('Time [s]')
ax.set_ylabel('Distance [m]')

max_dist = np.max(dist_np)
min_dist = np.min(dist_np)
print("Error in distance between centers of mass from being constant: "
      f"{(max_dist - min_dist) / min_dist:.4e}")
print(F"error in CPL z coordinate from being equal to the road height: "
      f"{(np.max(CPL_z_np) - np.min(CPL_z_np)):.4e}")
print(F"error in CPR z coordinate from being equal to the road height: "
      f"{(np.max(CPR_z_np) - np.min(CPR_z_np)):.4e}")
Distance between centers of mass and z coordinates of CPL and CPR
Error in distance between centers of mass from being constant: 0.0000e+00
error in CPL z coordinate from being equal to the road height: 0.0000e+00
error in CPR z coordinate from being equal to the road height: 2.1768e-11

Animation

fps = 7

rL1 = par_map[rL]
rR1 = par_map[rR]
amplitude1 = par_map[amplitude]
frequenz1 = par_map[frequenz]

resultat, *_, h_act = prob.parse_free(solution)
resultat = resultat.T
print(f"actual time step h = {h_act:.4f}")

t_arr = np.linspace(t0, num_nodes*h_act, num_nodes)
state_sol = interp1d(t_arr, resultat, kind='cubic', axis=0)
coordinates = DmcL.pos_from(O).to_matrix(N)
for point in (DmcR, m_DmcL, m_DmcR):
    coordinates = coordinates.row_join(point.pos_from(O).to_matrix(N))

coords_lam = sm.lambdify(state_symbols + pL, coordinates, cse=True)
max_x = np.max(np.concatenate((resultat[:, 2], resultat[:, 5])))
max_y = np.max(np.concatenate((resultat[:, 3], resultat[:, 6])))
min_x = np.min(np.concatenate((resultat[:, 2], resultat[:, 5])))
min_y = np.min(np.concatenate((resultat[:, 3], resultat[:, 6])))

gesamt_plot_lam = sm.lambdify([x_h, y_h, amplitude, frequenz],
                              gesamt_plot(x_h, y_h, amplitude, frequenz),
                              cse=True)

max_radius = 2.0 * max(rL1, rR1)
xx = np.linspace(min_x-max_radius, max_x+max_radius, 100)
yy = np.linspace(min_y-max_radius, max_y+max_radius, 100)
XX, YY = np.meshgrid(xx, yy)
ZZ = gesamt_plot_lam(XX, YY, amplitude1, frequenz1)


fig, ax = plt.subplots(figsize=(7, 7))
ax.set_xlim(min_x-max_radius, max_x+max_radius)
ax.set_ylim(min_y-max_radius, max_y+max_radius)
ax.set_aspect('equal')
ax.set_xlabel('x', fontsize=15)
ax.set_ylabel('y', fontsize=15)

cf = ax.contourf(XX, YY, ZZ, levels=50, cmap='viridis')
fig.colorbar(cf, label='z value [m]', shrink=0.8)

# axle
line1, = ax.plot([], [], lw=1, marker='o', markersize=0, color='red')
# particles attached to the wheels
line4 = ax.scatter([], [], color='black', s=20)
line5 = ax.scatter([], [], color='black', s=20)
# startpoint, endpoint
ax.scatter([xL1], [yL1], color='red', s=50, edgecolor='black')
ax.scatter([xL_end], [yL_end], color='green', s=50, edgecolor='black')

# ellipses defined in local frame AX
winkel_q2 = state_sol(0)[7]
ellipseL = Ellipse((0, 0), width=2.0*rL1*np.sin(winkel_q2),
                   height=2.0*rL1,
                   fill=True, lw=2, color='red', alpha=0.5)
ax.add_patch(ellipseL)
ellipseR = Ellipse((0, 0), width=2.0*rR1*np.sin(winkel_q2),
                   height=2.0*rR1,
                   fill=True, lw=2, color='magenta', alpha=0.5)
ax.add_patch(ellipseR)


def update(t):
    message = (f'Running time {t:.2f} sec. \n'
               f'The left wheel is red with radius {rL1}, the '
               f'right wheel is magenta \n with radius {rR1}.'
               f' The black dots are the particles attached \n to the wheels')
    ax.set_title(message, fontsize=11)
    coords = coords_lam(*state_sol(t), *pL_vals)

    line1.set_data([coords[0, 0], coords[0, 1]], [coords[1, 0], coords[1, 1]])
    line4.set_offsets([coords[0, 2], coords[1, 2]])
    line5.set_offsets([coords[0, 3], coords[1, 3]])

    # transform from AX → inertial frame
    theta = state_sol(t)[4]
    X = coords[0, 0]
    Y = coords[1, 0]
    transform = Affine2D().rotate(theta).translate(X, Y) + ax.transData
    ellipseL.set_width(2.0*rL1*np.sin(state_sol(t)[7]))
    ellipseL.set_transform(transform)
    X = coords[0, 1]
    Y = coords[1, 1]
    transform = Affine2D().rotate(theta).translate(X, Y) + ax.transData
    ellipseR.set_width(2.0*rR1*np.sin(state_sol(t)[7]))
    ellipseR.set_transform(transform)

    return line1, line4, line5, ellipseL, ellipseR


# Create the animation
animation = FuncAnimation(fig, update,
                          frames=np.concatenate([np.arange(
                              0, t_arr[-1], 1.0/fps), [t_arr[-1]]]),
                          interval=1000/fps, blit=False)
plt.show()
actual time step h = 0.0289

Total running time of the script: (29 minutes 6.670 seconds)

Gallery generated by Sphinx-Gallery