Figure 5.19: Stability plots for a bicycle moving at constant velocity
Jump to navigation
Jump to search
| Chapter | Dynamic Behavior |
|---|---|
| Figure number | 5.19 |
| Figure title | Stability plots for a bicycle moving at constant velocity |
| GitHub URL | https://github.com/murrayrm/fbs2e-python/blob/main/example-5.17-bicycle stability.py |
| Requires | python-control, bicycle.py |
Figure 5.19: Stability plots for a bicycle moving at constant velocity. The plot in (a) shows the real part of the system eigenvalues as a function of the bicycle velocity . The system is stable when all eigenvalues have negative real part (shaded region). The plot in (b) shows the locus of eigenvalues on the complex plane as the velocity is varied and gives a different view of the stability of the system. This type of plot is called a root locus diagram.
Note: The line styles used in this plot are slightly different than in the book. Solid lines are used for real-valued eigenvalues and dashed lines are used for the real part of complex-valued eigenvalues.
# example-5.17-bicycle_stability.py - Root locus diagram for a bicycle model
# RMM, 24 Nov 2024
#
# Figure 5.19: Stability plots for a bicycle moving at constant
# velocity. The plot in (a) shows the real part of the system eigenvalues
# as a function of the bicycle velocity v0. The system is stable when all
# eigenvalues have negative real part (shaded region). The plot in (b)
# shows the locus of eigenvalues on the complex plane as the velocity v is
# varied and gives a different view of the stability of the system. This
# type of plot is called a root locus diagram.
#
# Notes:
#
# 1. The line styles used in this plot are slightly different than in the
# book. Solid lines are used for real-valued eigenvalues and dashed
# lines are used for the real part of complex-valued eigenvalues.
#
# 2. This code relies on features on python-control-0.10.2, which is
# currently under development.
import control as ct
import numpy as np
import matplotlib.pyplot as plt
from math import isclose
ct.use_fbs_defaults()
#
# System dynamics
#
from bicycle import whipple_A
# Set up the plotting grid to match the layout in the book
fig = plt.figure(constrained_layout=True)
gs = fig.add_gridspec(2, 2)
#
# (a) Stability diagram
#
ax = fig.add_subplot(gs[0, 0]) # first row, first column
ax.set_title("(a) Stability diagram")
# Compute the eigenvalues as a function of velocity
v0_vals = np.linspace(-15, 15, 500)
eig_vals = []
for v0 in v0_vals:
A = whipple_A(v0)
eig_vals.append(np.sort(np.linalg.eig(A).eigenvalues))
# Initialize lists to categorize eigenvalues
eigs_real_stable = []
eigs_complex_stable = []
eigs_real_unstable = []
eigs_complex_unstable = []
# Keep track of region in which all eigenvalues are stable
stable_beg = stable_end = None
# Process each set of eigenvalues
for i, eig_set in enumerate(eig_vals):
# Create arrays filled with NaN for each category
real_stable = np.full(eig_set.shape, np.nan)
complex_stable = np.full(eig_set.shape, np.nan)
real_unstable = np.full(eig_set.shape, np.nan)
complex_unstable = np.full(eig_set.shape, np.nan)
# Classify eigenvalues
for j, eig in enumerate(eig_set):
if isclose(eig.imag, 0): # Real eigenvalue
if eig.real < 0:
real_stable[j] = eig.real
else:
real_unstable[j] = eig.real
else: # Complex eigenvalue
if eig.real < 0:
complex_stable[j] = eig.real
else:
complex_unstable[j] = eig.real
# Append categorized arrays to respective lists
eigs_real_stable.append(real_stable)
eigs_complex_stable.append(complex_stable)
eigs_real_unstable.append(real_unstable)
eigs_complex_unstable.append(complex_unstable)
# Look for regions where everything is stable
if stable_beg is None and all(eig_set.real < 0):
stable_beg = i
elif stable_beg and stable_end is None and any(eig_set.real > 0):
stable_end = i
# Plot the stability diagram
ax.plot(v0_vals, eigs_real_stable, 'b-')
ax.plot(v0_vals, eigs_real_unstable, 'r-')
ax.plot(v0_vals, eigs_complex_stable, 'b--')
ax.plot(v0_vals, eigs_complex_unstable, 'r--')
# Add in the coordinate axes
ax.axhline(color='k', linewidth=0.5)
ax.axvline(color='k', linewidth=0.5)
# Label and shade stable and unstable regions
ax.text(-12, 8, "Unstable")
ax.fill_betweenx(
[-15, 15], [v0_vals[stable_beg], v0_vals[stable_beg]],
[v0_vals[stable_end], v0_vals[stable_end]], color='0.9')
ax.text(7.2, 6, "Stable", rotation=90)
ax.text(11.7, 5, "Unstable", rotation=90)
# Label the axes
ax.set_xlabel(r"Velocity $v_0$ [m/s]")
ax.set_ylabel(r"$\text{Re}\,\lambda$")
ax.axis('scaled')
ax.axis([-15, 15, -15, 15])
#
# (b) Root locus diagram
#
ax = fig.add_subplot(gs[0, 1]) # first row, second column
ax.set_title("(b) Root locus diagram")
# Generate the root locus diagram via the root_locus_plot functionality
pos_idx = np.argmax(v0_vals >= 0)
poles = eig_vals[pos_idx]
loci = np.array(eig_vals[pos_idx:])
rl_map = ct.PoleZeroData(poles, [], v0_vals[pos_idx:], loci)
rl_map.plot(ax=ax)
# Add in the coordinate axes
ax.axhline(color='k', linewidth=0.5)
ax.axvline(color='k', linewidth=0.5)
# Label the real axes of the plot
ax.text(-12.5, -2, r"$\leftarrow v_0$")
ax.text(-3.5, -2, r"$v_0 \rightarrow$")
# Label the crossover points
xo_idx = np.argmax(rl_map.loci[:, 3].real < 0)
ax.plot(0, rl_map.loci[xo_idx, 2].imag, 'bo', markersize=3)
ax.plot(0, rl_map.loci[xo_idx, 3].imag, 'bo', markersize=3)
ax.text(1, rl_map.loci[xo_idx, 2].imag, r"$v_0 = 6.1$")
ax.text(1, rl_map.loci[xo_idx, 3].imag, r"$v_0 = 6.1$")
# Label the axes
ax.set_xlabel(r"$\text{Re}\,\lambda$")
ax.set_ylabel(r"$\text{Im}\,\lambda$")
ax.set_box_aspect(1)
ax.axis([-15, 15, -10, 10])
# Save the figure
plt.savefig("figure-5.19-bicycle_stability.png", bbox_inches='tight')
