12. Observers and Observer-State Feedback
State feedback assumes you can measure every state directly — rarely true in practice. This chapter shows how to estimate the full state vector from the available sensor outputs, and combine that estimate with feedback control.
Lecture videos
Section titled “Lecture videos”Slides
Section titled “Slides”PowerPoint handouts: Dark · Light
Interactive example
Section titled “Interactive example”Observer gain via pole placement
Section titled “Observer gain via pole placement”A 2nd-order system with A = [[0, 1], [-4, -0.2]] and only the first state measured (C = [1, 0]) has open-loop eigenvalues at -0.1 (+/-) 1.997j — lightly damped. We want an observer whose estimation-error dynamics A - LC settle faster, at poles -2 and -3.
The trick: A - LC and its transpose A^T - C^T L^T share the same eigenvalues. So the duality between control and estimation lets us reuse ct.place() — built for state-feedback gain K in A - BK — by feeding it A^T and C^T instead of A and B, then transposing the result back to get the observer gain L.
import control as ctimport numpy as np
A = np.array([[0, 1], [-4, -0.2]])C = np.array([[1], [0]]).T
print("Original eigenvalues of A:", np.linalg.eigvals(A))
# A - LC and A^T - C^T L^T share the same eigenvalues, so ct.place() (built# for state feedback A - BK) doubles as an observer-gain solver hereL = ct.place(A.T, C.T, [-2, -3]).Tprint("Observer gain L =")print(L)
print("\nEigenvalues of A - L*C:", np.linalg.eigvals(A - L @ C))print("(moved from the original poles to the target -2, -3)")Observer gain via pole placement (mcimp/codes/observer/simple2ndorder_obs.py)
Click "Run" to execute.
Motor position observer simulation
Section titled “Motor position observer simulation”A DC motor’s state is armature current i_a, angular position theta, and angular velocity theta_dot — but a practical sensor typically only measures theta. Can we still estimate the full state? The observability matrix built from C, CA, CA^2 has full rank 3, so yes.
We design an observer gain L via the same A^T/C^T pole-placement duality, targeting fast estimation-error poles at -500 (+/-) 250j and -1000 (much faster than the plant’s own dynamics). Stacking the true state and the estimated state into one 6-dimensional augmented system lets a single lsim() call simulate the real motor and its observer side by side, driven by the same sinusoidal input.
The plot below shows all three true states (solid) against their observer estimates (dashed), starting from a deliberately wrong initial guess. Watch how quickly the dashed curves lock onto the solid ones — that’s the observer error converging to zero, exactly as its assigned poles predict.
import numpy as npimport matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as pltimport control.matlab as ctimport io, base64
# Motor position control: states are armature current i_a, angular# position theta, and angular velocity theta_dot. Only theta is measured.L_ind = 1e-3R = 1J = 5e-5Bf = 1e-4K = 0.1
A = np.array([[-R / L_ind, 0, -K / L_ind], [0, 0, 1], [K / J, 0, -Bf / J]])Bm = np.array([1 / L_ind, 0, 0]).reshape((3, 1))C = np.array([0, 1, 0]).reshape((1, 3))
print("Plant eigenvalues:", np.linalg.eigvals(A))
O = np.linalg.matrix_rank(np.block([[C], [C @ A], [C @ (A @ A)]]))print("Observability matrix rank:", O, "(system is observable)" if O == 3 else "")
pole_des = np.array([-500 + 250j, -500 - 250j, -1000])Lgain = ct.place(A.T, C.T, pole_des).Tprint("\nObserver gain L =")print(Lgain)print("Resulting estimation-error poles:", np.linalg.eigvals(A - Lgain @ C))
# Augmented [true state; estimated state] system so a single lsim() call# simulates the plant and the observer togetherAaug = np.block([[A, np.zeros((3, 3))], [Lgain @ C, A - Lgain @ C]])Baug = np.block([[Bm], [Bm]])Caug = np.block([C, np.zeros((1, 3))])sys = ct.ss(Aaug, Baug, Caug, np.array([0]))
x0 = np.array([10, 2, 10]) # true initial state (deliberately wrong)xhat0 = np.array([0, 0, 0]) # observer starts with no informationX0 = np.array([x0, xhat0]).reshape((6, 1))
Tend = 0.03t = np.arange(0, Tend, 1e-4)u = 10 * np.sin(600 * t) # sinusoidal drive input
_, _, X = ct.lsim(sys, u, t, X0)
fig, axs = plt.subplots(3, 1, figsize=(7, 8), sharex=True)labels = ["i_a (current)", "theta (position)", "theta_dot (velocity)"]for i in range(3): axs[i].plot(t, X[:, i], label="true state") axs[i].plot(t, X[:, i + 3], "--", label="observer estimate") axs[i].set_ylabel(labels[i]) axs[i].grid(True)axs[0].legend()axs[0].set_title("True states vs. observer estimates")axs[-1].set_xlabel("time (sec)")plt.tight_layout()
buf = io.BytesIO()plt.savefig(buf, format="png", dpi=90)plot_b64 = base64.b64encode(buf.getvalue()).decode()
final_error = X[-1, :3] - X[-1, 3:]print(f"\nFinal estimation error at t={Tend}s:", final_error)Motor position observer simulation (mcimp/codes/observer/motorobs.py)
Click "Run" to execute.