Pedagogical Case Study: State-Space Canonical Forms & Controllability/Observability¶
Linear Time-Invariant (LTI) state-space representation forms the foundation of modern control theory:
$$\begin{aligned} \dot{x}(t) &= A x(t) + B u(t) \\ y(t) &= C x(t) + D u(t) \end{aligned}$$
In this pedagogical case study, we demonstrate ctrlpy.symbolic.state_space.StateSpaceTutor to:
- Compute exact analytical Kalman Controllability ($\mathcal{C}$) and Observability ($\mathcal{O}$) matrices and ranks.
- Perform Popov-Belevitch-Hautus (PBH) eigenvalue mode decomposition and classify modes into the 4 Kalman subspaces.
- Derive analytical similarity transformations for:
- Controllable Canonical Form (Phase-Variable Form): $A_c, B_c, C_c, D, T_c$
- Observable Canonical Form: $A_o, B_o, C_o, D, T_o$
- Jordan / Diagonal Modal Form: $A_d, B_d, C_d, D, V$
- Identify pole-zero cancellations and explain internal unobservable/uncontrollable dynamics step-by-step.
import matplotlib.pyplot as plt
import numpy as np
import ctrlpy as cp
from ctrlpy.symbolic import (
StateSpaceTutor,
state_space_tutor,
)
print(f"ctrlpy version: {cp.__version__}")
1. Standard 2nd-Order System Analysis¶
Consider a 2nd-order system with open-loop poles at $s = -1$ and $s = -2$:
$$A = \begin{bmatrix} 0 & 1 \\ -2 & -3 \end{bmatrix}, \quad B = \begin{bmatrix} 0 \\ 1 \end{bmatrix}, \quad C = \begin{bmatrix} 1 & 0 \end{bmatrix}, \quad D = \begin{bmatrix} 0 \end{bmatrix}$$
The characteristic polynomial is $p(s) = \det(sI - A) = s^2 + 3s + 2 = (s+1)(s+2)$.
A = [[0, 1], [-2, -3]]
B = [[0], [1]]
C = [[1, 0]]
D = [[0]]
tutor = StateSpaceTutor(A, B, C, D)
print(tutor)
Interactive Jupyter LaTeX Breakdown¶
Calling the tutor directly renders formatted LaTeX equation blocks in Jupyter notebooks:
tutor
2. Canonical Transformations & Verification¶
Let us examine the three canonical forms derived by the tutor and verify similarity transformations.
# 1. Controllable Canonical Form (Phase-Variable Form)
ccf = tutor.controllable_canonical_form()
print("=== Controllable Canonical Form ===")
print("Ac =", ccf.A)
print("Bc =", ccf.B)
print("Cc =", ccf.C)
print("Transformation Matrix Tc (x = Tc z_c):", ccf.T)
ccf
# 2. Observable Canonical Form
ocf = tutor.observable_canonical_form()
print("=== Observable Canonical Form ===")
print("Ao =", ocf.A)
print("Bo =", ocf.B)
print("Co =", ocf.C)
print("Transformation Matrix To (x = To z_o):", ocf.T)
ocf
# 3. Jordan / Diagonal Modal Form
jcf = tutor.jordan_canonical_form()
print("=== Jordan Modal Form ===")
print("Ad =", jcf.A)
print("Bd =", jcf.B)
print("Cd =", jcf.C)
print("Modal Matrix V (x = V z_d):", jcf.T)
jcf
Verifying Round-Trip Invariance¶
A similarity transformation preserves eigenvalues and the input-output transfer function $G(s)$:
# Original Transfer Function
G_orig = tutor.to_tf()
print("Original G(s):", G_orig)
# Canonical Form Transfer Functions
G_ccf = ccf.to_ss().to_tf()
G_ocf = ocf.to_ss().to_tf()
G_jcf = jcf.to_ss().to_tf()
print("CCF G(s): ", G_ccf)
print("OCF G(s): ", G_ocf)
print("Jordan G(s): ", G_jcf)
3. Detecting Uncontrollable / Unobservable Modes (Pole-Zero Cancellations)¶
In many real-world systems, sensors or actuators may fail to couple to all internal dynamic modes.
Consider a 2nd-order system where mode $\lambda = -2$ is uncoupled from the input $u(t)$:
$$A = \begin{bmatrix} -1 & 0 \\ 0 & -2 \end{bmatrix}, \quad B = \begin{bmatrix} 1 \\ 0 \end{bmatrix}, \quad C = \begin{bmatrix} 1 & 1 \end{bmatrix}, \quad D = \begin{bmatrix} 0 \end{bmatrix}$$
Here, $\mathcal{C} = [B \quad AB] = \begin{bmatrix} 1 & -1 \\ 0 & 0 \end{bmatrix}$, with $\operatorname{rank}(\mathcal{C}) = 1 < 2$.
A_unctrl = [[-1, 0], [0, -2]]
B_unctrl = [[1], [0]]
C_unctrl = [[1, 1]]
D_unctrl = [[0]]
tutor_unctrl = state_space_tutor(A_unctrl, B_unctrl, C_unctrl, D_unctrl)
print(
f"Is Controllable: {tutor_unctrl.is_controllable} (Rank: {tutor_unctrl.controllability_rank}/2)"
)
print(f"Is Observable: {tutor_unctrl.is_observable} (Rank: {tutor_unctrl.observability_rank}/2)")
print(f"Uncontrollable Modes: {tutor_unctrl.uncontrollable_modes}")
print(f"Transfer Function: G(s) = {tutor_unctrl.transfer_function}")
PBH Modal Decomposition¶
Let us inspect the individual mode analyses generated by the PBH tests:
for mode in tutor_unctrl.modes:
print(f"* Mode λ = {mode.eigenvalue}:")
print(f" Controllable: {mode.is_controllable} (PBH rank: {mode.pbh_c_rank})")
print(f" Observable: {mode.is_observable} (PBH rank: {mode.pbh_o_rank})")
print(f" Kalman Class: {mode.kalman_type.upper()}")
print(f" Description: {mode.description}")
4. Complete Step-by-Step Mathematical Derivations (.explain_steps())¶
StateSpaceTutor.explain_steps() returns an exhaustive markdown and LaTeX trace covering all derivation phases:
steps = tutor.explain_steps()
for idx, step_text in enumerate(steps, 1):
print(f"=== DERIVATION STAGE {idx} ===\n")
print(step_text)
print("\n" + "=" * 60 + "\n")
5. Time-Domain Simulation Comparison¶
Finally, let us simulate the step response of the original state-space system and verify that canonical transformations produce identical input-output trajectories.
# Convert to numerical StateSpace models
sys_orig = tutor.to_ss()
sys_ccf = ccf.to_ss()
sys_ocf = ocf.to_ss()
sys_jcf = jcf.to_ss()
t_span = np.linspace(0, 5, 200)
res_orig = sys_orig.step(t_span)
res_ccf = sys_ccf.step(t_span)
res_jcf = sys_jcf.step(t_span)
plt.figure(figsize=(9, 4.5))
plt.plot(res_orig.time, res_orig.outputs, label="Original System", linewidth=2.5)
plt.plot(res_ccf.time, res_ccf.outputs, "--", label="Controllable Canonical Form", linewidth=2)
plt.plot(res_jcf.time, res_jcf.outputs, ":", label="Jordan Modal Form", linewidth=2)
plt.xlabel("Time [s]")
plt.ylabel("Output y(t)")
plt.title("Invariance of Step Response Under Canonical State Transformations")
plt.grid(True, linestyle="--", alpha=0.6)
plt.legend()
plt.tight_layout()
plt.show()