🧠 Continuous-Time Luenberger Observer Design in Python
In this tutorial, we demonstrate how to design a Luenberger observer for a continuous-time system using Python. This example uses a DC motor modeled in state space and simulates how an observer estimates internal system states from outputs.
🔧 Step 1: Define the System
A DC motor is a mechatronic system containing both electrical and mechanical components. It can be modeled as the following:
\(\frac{d}{dt} \begin{bmatrix} i \\ \theta \\ \dot{\theta} \end{bmatrix} = \begin{bmatrix} -\frac{R}{L} & 0 & -\frac{K}{L} \\ 0 & 0 & 1 \\ \frac{K}{J} & 0 & -\frac{B}{J} \end{bmatrix} \begin{bmatrix} i \\ \theta \\ \dot{\theta} \end{bmatrix} + \begin{bmatrix} \frac{1}{L} \\ 0 \\ 0 \end{bmatrix} V \)
\( y = \begin{bmatrix} 0 & 1 & 0 \end{bmatrix} \begin{bmatrix} i \\ \theta \\ \dot{\theta} \end{bmatrix} \)
Read more here.
In Python, we first construct the system matrices \( A \), \( B \), \( C \), and \( D \):
import numpy as np
L = 1e-3
R = 1
J = 5e-5
B = 1e-4
K = 0.1
A = np.array([[-R/L, 0, -K/L], [0, 0, 1], [K/J, 0, -B/J]])
B = np.array([[1/L], [0], [0]])
C = np.array([[0, 1, 0]])
D = np.array([0])
🟢 Stability Analysis: Eigenvalues of A
Let us first take a look at the system stability by checking the eigenvalues of the “A” matrix:
eigvals = np.linalg.eig(A)
print("Original Eigenvalues:", eigvals[0])
Execute the code and see what you will get.
🔍 Step 2: Check Observability
Before designing the observer, we check that the system is observable:
O = np.linalg.matrix_rank(np.concatenate((C, C @ A, C @ A @ A), axis=0))
print("Observability Rank:", O)
You can also use the np.block function to construct the observability matrix:
O = np.linalg.matrix_rank(np.block([ [C], [C@A], [C@(A@A)] ]))
print("Observability Rank:", O)
You should see the output to be similar to the following:
Observability Rank: 3
The full rank of the observability matrix indicates that the system is observable.
🏀 Step 3: Design the Observer
To design the observer, we select the following desired poles for the estimator error dynamics:
import control.matlab as ct
pole_des = np.array([-500+250j, -500-250j, -1000])
L = ct.place(A.T, C.T, pole_des).T
This way, the matrix \( L \) will ensure that the observer dynamics \( A - LC \) have the desired fast convergence behavior. Let us verify that this is indeed the case by computing the actual eigenvalues of \( A - LC \):
est_poles = np.linalg.eig(A - L @ C)
print("Observer Poles:", est_poles[0])
You should see the following:
Observer Poles: [-500.+250.j -500.-250.j -1000.+0.j]
♻️ Step 4: Simulate the Observer
To simulate the observer, we create an augmented system that includes both the plant and the observer:
# Augmented system
Aaug = np.block([[A, np.zeros((3, 3))], [L @ C, A - L @ C]])
Baug = np.vstack((B, B))
Caug = np.hstack((C, np.zeros((1, 3))))
Daug = np.array([0])
sys = ct.ss(Aaug, Baug, Caug, Daug)
x0 = np.array([10, 2, 10]); xhat0 = np.array([0, 0, 0]); X0 = np.array([x0, xhat0]).reshape((6, 1))
Tend = 0.03; amplitude = 10; initpha = 0; freq = 600
t = np.arange(0, Tend, 1e-4)
u = amplitude * np.sin(freq * t + initpha)
[Y, T, X] = ct.lsim(sys, u, t, X0)
📊 Step 5: Plot the Results
import matplotlib.pyplot as plt
plt.figure()
plt.subplot(3, 1, 1)
plt.plot(t, X[:, 0], t, X[:, 3], '--', linewidth=1.5)
plt.xlabel('time (sec)')
plt.legend(['$x_1 = i_a$', '$\hat x_1$'], fontsize=16)
plt.grid()
plt.ylabel('$x_1$', fontsize=16)
plt.title('States and their estimates')
plt.subplot(3, 1, 2)
plt.plot(t, X[:, 1], t, X[:, 4], '--', linewidth=1.5)
plt.xlabel('time (sec)')
plt.legend(['$x_2 = \\theta$', '$\hat x_2$'], fontsize=16)
plt.grid()
plt.ylabel('$x_2$', fontsize=16)
plt.subplot(3, 1, 3)
plt.plot(t, X[:, 2], t, X[:, 5], '--', linewidth=1.5)
plt.xlabel('time (sec)')
plt.legend(['$x_3 = \dot{\\theta}$', '$\hat x_3$'], fontsize=16)
plt.grid()
plt.ylabel('$x_3$', fontsize=16)
plt.show()
You should see something similar to the following, where the observer accurately tracks the angular position and velocity over time, with rapid convergence.