Project 12 · AME 451
Linear Control Systems — MATLAB Portfolio
AME 451 · Linear Control Systems · University of Southern California
Course
AME 451 — Linear Control Systems
Software
MATLAB / Symbolic Math Toolbox
Topics
Root Locus, Bode, Nyquist, Lead-Lag
Scripts
40+ .m files & .mlx live scripts
Key Tool
Custom Routh-Hurwitz function
Contents
Routh-Hurwitz Stability Analysis
A custom routh_hurwitz()
function was written to automate Routh array construction for both numerical and symbolic polynomials.
It handles both special cases — a zero in the first column (replaced with epsilon,
then the limit taken as ε→0⁺) and a row of zeros (replaced using the auxiliary
polynomial method). The function returns the full Routh array M and the simplified
first column L; sign changes in L equal the number of roots in the right-half plane.
function [M, L] = routh_hurwitz(P, N)
% Returns Routh array M and simplified first column L.
% Handles special cases: zero in first column (epsilon method),
% and row of zeros (auxiliary polynomial method).
% P — numerical or symbolic coefficient array [a_n, a_{n-1}, ..., a_0]
% N — precision digits (default 10); values < 10^-N treated as zero.
if nargin < 2, N = 10; end
n = length(P);
P = sym(P);
syms epsilon
nfil = n; ncol = ceil(n/2);
M = sym(zeros(nfil, ncol));
% Fill first two rows from coefficient array
k = 1;
for j = 1:ncol
for i = 1:2
M(i,j) = P(k); k = k+1;
end
end
% Build remaining rows via 2x2 determinants
for i = 3:nfil
% Special case: first element of row is zero
if abs(double(M(i-1,1))) < 10^(-N)
M(i-1,1) = epsilon; % epsilon replacement
% Special case: entire row is zero
elseif sum(abs(double(M(i-1,:)))) < 10^(-N)
for j = 1:ncol % use auxiliary polynomial
M(i-1,j) = M(i-2,j) * (n-(i-2)-2*(j-1));
end
end
for j = 1:ncol-1
M(i,j) = det([M(i-2,1) M(i-2,j+1); ...
M(i-1,1) M(i-1,j+1)]) / (-M(i-1,1));
end
end
M = vpa(M);
M1 = limit(M, epsilon, 0, 'right'); % resolve epsilon → 0+
L = vpa(zeros(n,1));
for i = 1:n
L(i,1) = M1(i,1); % sign changes in L = # RHP roots
end
Used across homework and final exam to determine stability margins symbolically — solving for the critical gain K at which the first column element crosses zero:
syms s K
den_ol = expand((s+1)*(3*s+1)*(0.4*s+1)); % open-loop denominator
num_ol = sym(9); % gain k = 9
% Characteristic equation: 1 + K·G(s) = 0
CharEq = simplify(den_ol + K*num_ol);
CharCoeff = fliplr(coeffs(CharEq, s)); % [a_n ... a_0]
RouthArray = simplify(routh_hurwitz(CharCoeff));
% Solve for K where first-column element → 0 (stability boundary)
K_critical = solve(RouthArray(3,1)) % K ≈ 15.86
1.2s³ + 4.6s² + 4.4s + 1 + K·0.4·... = 0.
Routh stability boundary: K_critical ≈ 15.86 — the maximum proportional gain
before the closed-loop system becomes unstable. For the operating gain K = 9 all first-column
elements are positive → system is stable with positive phase and gain margins.
Root Locus Analysis & Compensator Design
Root locus techniques were applied to visualize how closed-loop poles migrate as gain K is varied. Performance specifications (max overshoot Mp, natural frequency ωn) were overlaid as constraint curves using sgrid(), enabling graphical compensator placement. The open-loop transfer function for the final exam root locus question included a lead compensator k = 4.35(s+1) designed to pull loci into the acceptable damping/frequency region.
clear; clc; close all
syms s k
% Lead compensator + plant
k = 4.35*(s+1);
num_ol = sym(40*s + 40); % k·(s+1) factor
den_ol = expand((s+10) * (10*s^2) * (0.08*s+1));
g_ol = tf(sym2poly(num_ol), sym2poly(den_ol));
% Root locus with performance constraints overlaid
rlocus(g_ol)
xlim([-3, 1]); ylim([-5, 5])
xline(-1, ':') % Re(s) = -1 boundary
yline([1, -1], ':') % Im(s) = ±1 lines
sgrid(0.4, 2) % ζ = 0.4, ωn = 2 rad/s
Breakaway and break-in points were found symbolically using the backward asymptote equation d/ds[−P(s)/Z(s)] = 0, and Routh-Hurwitz was applied to find exact cross-over gain:
clear; clc; close all
syms s k
num = expand((s+5)*2); % G(s) = 2(s+5) / [(s+1)(s+3)]
den = expand((s+1)*(s+3));
% Breakaway equation: d/ds[-den/num] = 0
bkwyEq = simplify(diff(-den/num));
bkwyPt = double(solve(bkwyEq)) % real breakaway point(s)
% Characteristic equation for Routh analysis
CharEq = simplify(den + k*num);
CharCoeff = fliplr(coeffs(CharEq, s));
RouthArray = simplify(routh_hurwitz(CharCoeff));
g = tf(sym2poly(num), sym2poly(den));
rlocus(g); grid
Bode Plots & Asymptotic Approximation
Frequency response was analyzed by plotting the exact Bode magnitude and phase
alongside manually-constructed asymptotic (straight-line) approximations using a
custom asymp()
helper. A separate BodePaper()
utility generates blank logarithmic graph paper as a MATLAB figure — useful for
overlaying exact vs. approximate curves.
clear; clc; close all
% G(s) = 12 / [(s+4)(s+1/3)]
% Two real poles at s = -4, -1/3
% Low-frequency gain = 12 / (4 × 1/3) = 9 ≈ +19.1 dB
num = [12];
den = conv([1 4], [1 1/3]); % (s+4)(s+1/3)
G = tf(num, den);
% Blank Bode paper (freq: 0.01–100, mag: -60 to +20 dB, phase: -225° to 0°)
BodePaper(0.01, 100, -60, 20, -225, 0);
hold on
% Exact Bode
bode(G); grid on
% Asymptotic (straight-line) approximation overlaid
asymp(G, 0.01, 100, -60, 20);
|G(j0)| = 9 ≈ +19.1 dB. Above each corner the magnitude slope drops −20 dB/decade;
at high frequencies the total slope is −40 dB/decade. Phase transitions from 0° to −180°.
Nyquist, Nichols & Closed-Loop Frequency Response
For Final Q3, a complete multi-domain stability analysis was performed on a third-order system. The same plant was analyzed using four complementary representations: Nyquist plot (encirclement criterion), open-loop Bode plot (gain/phase margins), Nichols chart (closed-loop M-circles), and closed-loop Bode (bandwidth and peak M). Together they paint a full picture of stability robustness.
clear; clc; close all
syms s K
k = 9;
% Transfer functions — open-loop and closed-loop
num_ol = sym(k);
den_ol = expand((s+1)*(3*s+1)*(0.4*s+1)); % three real poles
g_ol = tf(sym2poly(num_ol), sym2poly(den_ol));
num_cl = expand(k*(0.4*s+1));
den_cl = expand(den_ol + k); % 1 + G(s) in denominator
t_cl = tf(sym2poly(num_cl), sym2poly(den_cl));
% ── 1. Nyquist plot ──────────────────────────────
figure
nyquist(g_ol); grid on
xlim([-1.2, 0.4]); ylim([-0.5, 0.5]) % zoom near -1+0j point
% ── 2. Open-loop Bode (gain & phase margins) ────
figure
bode(g_ol); grid on
% ── 3. Nichols chart ─────────────────────────────
figure
nichols(t_cl); grid on % M-circles & N-circles
% ── 4. Closed-loop Bode (bandwidth, peak M) ──────
figure
bode(t_cl); grid on
% ── Routh-Hurwitz critical gain ───────────────────
CharEq = simplify(den_ol + K*num_ol);
CharCoeff = fliplr(coeffs(CharEq, s));
Routh = simplify(routh_hurwitz(CharCoeff));
K_crit = solve(Routh(3,1)) % K_critical ≈ 15.86
K_GM = 15.86 / 9 ≈ 4.9 dB of margin before instability.
Routh-Hurwitz independently confirms critical gain at K ≈ 15.86. All four methods
in agreement — robust stability at the design operating point.
Lead-Lag Compensator Design
HW10 demonstrates the full compensator synthesis workflow: translate time-domain specifications (Mp, tp, Kv) into frequency-domain requirements (phase margin, bandwidth, DC gain), then design lead and lag networks to meet them. The lead compensator adds phase at the crossover frequency; the lag compensator boosts low-frequency gain to satisfy the velocity constant without reducing phase margin.
Plant: G(s) = K / [s(s+8)(s+30)] · Specs: Mp = 10 %, tp = 0.6 s, Kv = 10 s⁻¹
clear; clc; close all
syms s
G_plant_sym = 1/(s*(s+8)*(s+30));
%% ── Step 1: Translate specs to frequency domain ──────────────────────────
M_p = 0.10; t_p = 0.6; K_v = 10;
zeta = abs(log(M_p) / sqrt(pi^2 + log(M_p)^2)) % damping ratio
% Bandwidth from peak-time requirement
omega_B = (pi/(t_p*sqrt(1-zeta^2))) * ...
sqrt((1-2*zeta^2) + sqrt(4*zeta^4 - 4*zeta^2 + 2))
% DC gain K from velocity constant requirement: Kv = lim s→0 [s·K·G]
K = K_v / limit(s*G_plant_sym, s, 0)
%% ── Step 2: Lead compensator parameters ──────────────────────────────────
phi_max = atand(2*zeta / sqrt(-2*zeta^2 + sqrt(1 + 4*zeta^4)))
omega_C = 0.8 * omega_B % design crossover frequency
x = phi_max - (180 - (138.36 + 5)) % required phase addition
alpha = (1 - sind(x)) / (1 + sind(x)) % pole/zero separation ratio
beta = 1/alpha
% Lead zero and pole
T_1 = 1 / (omega_C * sqrt(alpha))
z_lead = 1/T_1
p_lead = z_lead / alpha
G_lead = (1/beta) * ((s + z_lead)/(s + p_lead));
%% ── Step 3: Lag compensator parameters ───────────────────────────────────
z_lag = omega_C / 10 % decade below crossover
p_lag = z_lag / beta
G_lag = beta * ((s + z_lag)/(s + p_lag));
%% ── Step 4: Build compensated open-loop TF ───────────────────────────────
num_plant = K;
den_plant = s*(s+8)*(s+30);
num_comp = K * (s + z_lag) * (s + z_lead);
den_comp = (s + p_lag) * (s + p_lead) * den_plant;
G_comp = tf(sym2poly(num_comp), sym2poly(den_comp));
figure; bode(G_comp); grid on; title('Compensated Open-Loop Bode')
%% ── Step 5: Compare step responses ───────────────────────────────────────
den_uncomp_cl = den_plant + num_plant;
den_comp_cl = den_comp + num_comp;
G_unc_cl = tf(sym2poly(num_plant), sym2poly(den_uncomp_cl));
G_com_cl = tf(sym2poly(num_comp), sym2poly(den_comp_cl));
figure; hold on
step(G_unc_cl); uncompensated = stepinfo(G_unc_cl)
step(G_com_cl); compensated = stepinfo(G_com_cl)
legend('Uncompensated','Compensated', 'Interpreter','latex')
Proportional Gain Tuning & Step Response Comparison
A simple but illuminating exercise: comparing two proportional gains on a first-order plant
to observe the classic speed vs. overshoot trade-off. The closed-loop transfer
function is derived analytically, then step()
responses for Kp = 56 and Kp = 36 are overlaid. Higher proportional gain
accelerates the response but increases overshoot; lower gain is smoother but sluggish —
a fundamental trade-off resolved properly only by adding derivative or integral action.
clear; clc; close all
% Plant: G(s) = 0.5 / (6s + 2) (first-order, time constant τ = 3 s)
% Closed-loop with proportional controller K_p:
% T(s) = 0.5·K_p / (6s + 2 + 0.5·K_p) = K_eff / (τ_eff·s + 1)
K_p = [56, 36];
hold on
for n = 1:length(K_p)
Kp = K_p(n);
num = 0.5*Kp / (2 + 0.5*Kp); % closed-loop DC gain
den = [6/(2 + 0.5*Kp), 1]; % [τ_cl, 1]
T_s = tf(num, den);
step(T_s); grid on
end
legend('$K_p = 56$', '$K_p = 36$', ...
'Interpreter', 'latex', ...
'Location', 'south', ...
'Orientation', 'horizontal')
K_p = 56: τ_cl ≈ 0.18 s, DC gain → 0.93 — fast but higher steady-state offset.
|
K_p = 36: τ_cl ≈ 0.32 s, DC gain → 0.90 — slower, marginally lower gain.
Both remain first-order (no overshoot) but neither eliminates steady-state error — integral
action (Ki) is required to achieve zero offset.