Pedagogical Case Study: Analytical Root Locus Rules¶
The Evans Root Locus Method provides deep qualitative and quantitative insight into how closed-loop poles migrate in the complex $s$-plane as the feedback gain $K$ varies from $0$ to $+\infty$.
In this notebook, we demonstrate how ctrlpy.symbolic.root_locus.RootLocusRules:
- Performs exact analytical calculations of all 7 classical Evans rules.
- Provides step-by-step mathematical explanations suitable for classroom lecture slides and homework grading.
- Perfectly aligns with numerical root locus plots generated by
ctrlpy.plotting.plot_root_locus.
import matplotlib.pyplot as plt
import numpy as np
import ctrlpy as cp
from ctrlpy.symbolic import RootLocusRules
print(f"ctrlpy version: {cp.__version__}")
1. Third-Order Canonical System: $G(s) = \frac{1}{s(s+1)(s+2)}$¶
Consider the classical open-loop plant: $$G(s) = \frac{1}{s(s+1)(s+2)} = \frac{1}{s^3 + 3s^2 + 2s}$$
G1 = cp.tf([1], [1, 3, 2, 0])
rlr1 = RootLocusRules(G1)
# Print ASCII summary
print(rlr1)
# Render LaTeX table in Jupyter
rlr1
Detailed Step-by-Step Mathematical Derivations¶
We can inspect the analytical derivation for each rule:
for step in rlr1.explain_steps():
print(f"- {step}")
Visual Verification with Numerical Root Locus Plot¶
We plot the numerical root locus alongside the analytically calculated asymptote centroid $\sigma_a = -1.0$, asymptote lines ($\pm 60^\circ, 180^\circ$), breakaway point ($s = -0.423$), and $j\omega$ crossings ($s = \pm j\sqrt{2}$ at $K_{\text{crit}} = 6$):
fig, ax = plt.subplots(figsize=(8, 6))
cp.plot_root_locus(G1, gains=np.logspace(-2, 2, 1000), ax=ax)
# Overlay analytical centroid and asymptotes
sigma = rlr1.centroid
ax.plot(sigma, 0, "y*", markersize=12, label=f"Centroid $\\sigma_a = {sigma:.2f}$")
# Asymptote lines
s_line = np.linspace(0, 4, 100)
ax.plot(
sigma + s_line * np.cos(np.radians(60)),
s_line * np.sin(np.radians(60)),
"k--",
alpha=0.5,
label="Asymptotes",
)
ax.plot(
sigma + s_line * np.cos(np.radians(-60)), s_line * np.sin(np.radians(-60)), "k--", alpha=0.5
)
# Overlay breakaway point
bp = rlr1.breakaway_points[0]
ax.plot(bp["s"], 0, "mo", markersize=9, label=f"Breakaway: s = {bp['s']:.3f}")
# Overlay jw crossings
cross = rlr1.imag_axis_crossings[0]
ax.plot(
0, cross["omega"], "r^", markersize=9, label=f"$j\\omega$ Crossing: s = +j{cross['omega']:.3f}"
)
ax.plot(0, -cross["omega"], "r^", markersize=9)
ax.set_title("Root Locus with Analytical Evans Geometric Features", fontsize=12, fontweight="bold")
ax.legend(loc="upper right", fontsize=9)
plt.tight_layout()
plt.show()
2. System with Finite Open-Loop Zero: $G(s) = \frac{s+3}{s(s+1)(s+2)(s+4)}$¶
Adding a zero at $s = -3$ pulls locus branches into the left-half plane, shifting the centroid and altering the real-axis segments.
G2 = cp.tf([1, 3], np.poly([0, -1, -2, -4]))
rlr2 = RootLocusRules(G2)
rlr2
fig, ax = plt.subplots(figsize=(8, 5))
cp.plot_root_locus(G2, gains=np.logspace(-2, 3, 1000), ax=ax)
ax.set_title("Root Locus with Finite Zero at s = -3", fontsize=12, fontweight="bold")
plt.tight_layout()
plt.show()
3. Complex Conjugate Poles and Angle of Departure¶
Consider a plant with underdamped complex poles: $$G(s) = \frac{1}{s(s^2 + 2s + 2)}$$
Open-loop poles are located at $s = 0$ and $s = -1 \pm j$.
G3 = cp.tf([1], [1, 2, 2, 0])
rlr3 = RootLocusRules(G3)
print("Departure angles from complex poles:")
for p, deg in rlr3.departure_angles.items():
print(f" Pole {p}: {deg:.2f}°")
rlr3
fig, ax = plt.subplots(figsize=(8, 5))
cp.plot_root_locus(G3, gains=np.logspace(-2, 2, 1000), ax=ax)
ax.set_title("Root Locus with Complex Poles (Departure Angles)", fontsize=12, fontweight="bold")
plt.tight_layout()
plt.show()