Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Kalman’s controllability and observability (1960)#
A transfer function describes only the part of a system that the input
reaches and the output sees. Rudolf Kalman (1960) split the state into
these parts. The state of \(\dot x = Ax + Bu\) can be steered anywhere
only if the controllability matrix \([B, AB, \dots, A^{n-1}B]\) has full
rank, and reconstructed from \(y = Cx\) only if the observability matrix
\([C; CA; \dots; CA^{n-1}]\) does
(is_controllable(),
is_observable()). A mode that fails
either test disappears from the transfer function in a pole-zero
cancellation, but it is still there.
import matplotlib.pyplot as plt
import numpy as np
from scipy import signal
from scipy.integrate import solve_ivp
from engineeringkit.control_systems import (
controllability_matrix,
is_controllable,
is_observable,
observability_matrix,
)
An uncontrollable pair of tanks#
Two identical tanks, \(\dot x_i = -x_i + u\), are filled by the same pump. Whatever the pump does, the difference \(x_1 - x_2\) decays as \(e^{-t}\) on its own.
A1, B1 = -np.eye(2), np.array([[1.0], [1.0]])
print("rank of [B, AB] =", np.linalg.matrix_rank(controllability_matrix(A1, B1)))
assert not is_controllable(A1, B1)
def pump(t):
return 2 + np.sin(3 * t) + np.sign(np.sin(0.7 * t))
tt = np.linspace(0, 6, 601)
x0 = [1.0, -0.5]
tanks = solve_ivp(lambda t, x: A1 @ x + B1[:, 0] * pump(t), (0, 6), x0, t_eval=tt, rtol=1e-10, atol=1e-12)
difference = tanks.y[0] - tanks.y[1]
assert np.allclose(difference, (x0[0] - x0[1]) * np.exp(-tt), atol=1e-8)
rank of [B, AB] = 1
