.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "api/gallery/control_systems/state_space/plot_01_kalman_controllability_observability.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code or to run this example in your browser via JupyterLite. .. rst-class:: sphx-glr-example-title .. _sphx_glr_api_gallery_control_systems_state_space_plot_01_kalman_controllability_observability.py: 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 :math:`\dot x = Ax + Bu` can be steered anywhere only if the controllability matrix :math:`[B, AB, \dots, A^{n-1}B]` has full rank, and reconstructed from :math:`y = Cx` only if the observability matrix :math:`[C; CA; \dots; CA^{n-1}]` does (:func:`~engineeringkit.control_systems.is_controllable`, :func:`~engineeringkit.control_systems.is_observable`). A mode that fails either test disappears from the transfer function in a pole-zero cancellation, but it is still there. .. GENERATED FROM PYTHON SOURCE LINES 18-30 .. code-block:: Python 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, ) .. GENERATED FROM PYTHON SOURCE LINES 31-37 An uncontrollable pair of tanks ------------------------------- Two identical tanks, :math:`\dot x_i = -x_i + u`, are filled by the same pump. Whatever the pump does, the difference :math:`x_1 - x_2` decays as :math:`e^{-t}` on its own. .. GENERATED FROM PYTHON SOURCE LINES 37-53 .. code-block:: Python 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) .. rst-class:: sphx-glr-script-out .. code-block:: none rank of [B, AB] = 1 .. GENERATED FROM PYTHON SOURCE LINES 54-62 An unobservable mode hidden by a cancellation --------------------------------------------- For :math:`A = [[0, 1], [-2, -3]]` (modes :math:`-1` and :math:`-2`), :math:`B = [0, 1]^\mathsf{T}` and :math:`C = [1, 1]`, the output never sees the :math:`-1` mode: the transfer function is :math:`(s+1)/((s+1)(s+2)) = 1/(s+2)`. Starting on that mode's eigenvector the state moves while the output stays exactly zero. .. GENERATED FROM PYTHON SOURCE LINES 62-90 .. code-block:: Python A2, B2, C2 = np.array([[0.0, 1.0], [-2.0, -3.0]]), np.array([[0.0], [1.0]]), np.array([[1.0, 1.0]]) assert is_controllable(A2, B2) and not is_observable(A2, C2) print("rank of [C; CA] =", np.linalg.matrix_rank(observability_matrix(A2, C2))) num, den = signal.ss2tf(A2, B2, C2, [[0.0]]) print("transfer function zeros:", np.roots(num[0]).round(6), "poles:", np.roots(den).round(6)) assert np.isclose(np.roots(num[0])[0], -1) and np.min(np.abs(np.roots(den) + 1)) < 1e-9 hidden = solve_ivp(lambda t, x: A2 @ x, (0, 6), [1.0, -1.0], t_eval=tt, rtol=1e-10, atol=1e-12) y_hidden = C2 @ hidden.y assert np.abs(y_hidden).max() < 1e-8 and np.abs(hidden.y).max() > 0.9 C3 = np.array([[1.0, 0.0]]) # measure x1 alone and both modes are visible assert is_observable(A2, C3) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) ax1.plot(tt, tanks.y[0], label="$x_1$") ax1.plot(tt, tanks.y[1], label="$x_2$") ax1.plot(tt, difference, "k--", label="$x_1 - x_2$ (ignores the pump)") ax1.set(xlabel="time [s]", ylabel="level", title="Uncontrollable: one pump, two tanks") ax1.legend() ax2.plot(tt, hidden.y[0], label="$x_1$") ax2.plot(tt, hidden.y[1], label="$x_2$") ax2.plot(tt, y_hidden[0], "k-", lw=2, label="$y = x_1 + x_2$") ax2.set(xlabel="time [s]", title="Unobservable: the output misses a mode") ax2.legend() fig.tight_layout() plt.show() .. image-sg:: /api/gallery/control_systems/state_space/images/sphx_glr_plot_01_kalman_controllability_observability_001.png :alt: Uncontrollable: one pump, two tanks, Unobservable: the output misses a mode :srcset: /api/gallery/control_systems/state_space/images/sphx_glr_plot_01_kalman_controllability_observability_001.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none rank of [C; CA] = 1 transfer function zeros: [-1.] poles: [-2. -1.] .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.073 seconds) .. _sphx_glr_download_api_gallery_control_systems_state_space_plot_01_kalman_controllability_observability.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: lite-badge .. image:: images/jupyterlite_badge_logo.svg :target: ../../../../lite/lab/index.html?path=api/gallery/control_systems/state_space/plot_01_kalman_controllability_observability.ipynb :alt: Launch JupyterLite :width: 150 px .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_01_kalman_controllability_observability.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_01_kalman_controllability_observability.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_01_kalman_controllability_observability.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_