Chapter 13: Observability and Detectability – Concepts

Lesson 3: Detectability and Stable Unobservable Modes

This lesson explains detectability as a weaker but extremely important property than observability. A system can fail to reveal every component of the initial state and still be usable for estimation, provided that the hidden components decay by themselves. We formalize this idea using unobservable subspaces, stable hidden dynamics, and observer-error intuition.

1. Motivation: When Full Observability Is More Than We Need

In Lesson 1, observability was introduced as the ability to determine the initial state from output measurements. In Lesson 2, the observable and unobservable parts of the state were interpreted as subspaces. We now ask a more engineering-oriented question: if some state components are invisible at the output, do they necessarily cause a serious estimation problem?

The answer is no. If every unobservable mode is internally stable, then the invisible part of the state decays without needing to be measured. Such a system is called detectable. For the continuous-time autonomous output model \( \dot{\mathbf{x}}=\mathbf{A}\mathbf{x} \), \( \mathbf{y}=\mathbf{C}\mathbf{x} \), detectability says:

\[ \begin{aligned} &\text{unobservable initial-state components may exist, but their}\\ &\text{free response must converge to zero as } t \to \infty. \end{aligned} \]

Thus, observability is about exact state reconstruction, while detectability is about asymptotic state-error suppression. Detectability is the property needed before one can hope to build a stable asymptotic observer in later chapters.

flowchart TD
  A["Start with output y(t) = C x(t)"] --> B["Some state directions affect y(t)?"]
  B -->|"yes"| C["Visible directions can be \nreconstructed from outputs"]
  B -->|"no"| D["Hidden directions form \nunobservable subspace"]
  D --> E["Do hidden modes decay by themselves?"]
  E -->|"yes"| F["System is detectable"]
  E -->|"no"| G["System is not detectable"]
  F --> H["Observer error can be \nmade asymptotically stable"]
  G --> I["A hidden unstable error cannot be \ncorrected from output"]
        

2. Unobservable Subspace and Hidden Initial Conditions

Consider the continuous-time homogeneous system \( \dot{\mathbf{x}}=\mathbf{A}\mathbf{x} \), \( \mathbf{y}=\mathbf{C}\mathbf{x} \). The output generated by an initial state \( \mathbf{x}_0 \) is

\[ \mathbf{y}(t)=\mathbf{C}e^{\mathbf{A}t}\mathbf{x}_0. \]

The unobservable subspace is the set of initial states that produce zero output for all future time:

\[ \mathcal{N}_o=\left\{\mathbf{x}_0\in\mathbb{R}^n: \mathbf{C}e^{\mathbf{A}t}\mathbf{x}_0=\mathbf{0}\;\text{for all}\; t\ge 0\right\}. \]

Using the power-series expansion of the matrix exponential, an equivalent finite-dimensional description is

\[ \mathcal{N}_o= \bigcap_{k=0}^{n-1}\ker(\mathbf{C}\mathbf{A}^k). \]

A state in \( \mathcal{N}_o \) is not merely difficult to measure; it is completely invisible to the output channel. If \( \mathcal{N}_o=\{\mathbf{0}\} \), the system is observable. If \( \mathcal{N}_o\ne\{\mathbf{0}\} \), the system is not observable, but it may still be detectable.

Invariance of the unobservable subspace.

Let \( \mathbf{x}_0\in\mathcal{N}_o \). Then \( \mathbf{C}\mathbf{A}^k\mathbf{x}_0=\mathbf{0} \) for all \( k=0,\dots,n-1 \). By the Cayley-Hamilton theorem, all higher powers of \( \mathbf{A} \) are linear combinations of \( \mathbf{I},\mathbf{A},\dots,\mathbf{A}^{n-1} \). Therefore

\[ \mathbf{C}\mathbf{A}^k(\mathbf{A}\mathbf{x}_0) =\mathbf{C}\mathbf{A}^{k+1}\mathbf{x}_0=\mathbf{0},\quad k=0,\dots,n-1. \]

Hence \( \mathbf{A}\mathbf{x}_0\in\mathcal{N}_o \). The unobservable subspace is therefore \( \mathbf{A} \)-invariant. This fact lets us speak rigorously about the dynamics of the hidden part of the state.

3. Definition of Detectability

Detectability requires that all motion inside the unobservable subspace be stable. Let \( \mathbf{N} \) be a full-column-rank matrix whose columns form a basis for \( \mathcal{N}_o \). Since \( \mathcal{N}_o \) is invariant under \( \mathbf{A} \), there exists a matrix \( \mathbf{A}_u \) such that

\[ \mathbf{A}\mathbf{N}=\mathbf{N}\mathbf{A}_u. \]

The matrix \( \mathbf{A}_u \) is the hidden or unobservable dynamics matrix. The pair \( (\mathbf{A},\mathbf{C}) \) is detectable in continuous time if

\[ \operatorname{Re}(\lambda_i(\mathbf{A}_u))<0 \quad \text{for every unobservable mode } \lambda_i(\mathbf{A}_u). \]

For a discrete-time system \( \mathbf{x}_{k+1}=\mathbf{A}\mathbf{x}_k \), \( \mathbf{y}_k=\mathbf{C}\mathbf{x}_k \), the corresponding definition is

\[ |\lambda_i(\mathbf{A}_u)|<1 \quad \text{for every unobservable mode } \lambda_i(\mathbf{A}_u). \]

Therefore, observability implies detectability, but detectability does not imply observability:

\[ \mathcal{N}_o=\{\mathbf{0}\} \quad \Longrightarrow \quad (\mathbf{A},\mathbf{C})\text{ is detectable}. \]

The reverse implication fails because \( \mathcal{N}_o \) may be nonzero while all modes in \( \mathcal{N}_o \) are stable.

4. Decomposition View of Visible and Hidden Dynamics

Choose a nonsingular coordinate transformation \( \mathbf{x}=\mathbf{T}\mathbf{z} \) whose last block of coordinates spans the unobservable subspace. Then, in appropriate coordinates, the system can be written conceptually as

\[ \begin{aligned} \dot{\mathbf{z}}_o &= \mathbf{A}_{oo}\mathbf{z}_o+\mathbf{A}_{ou}\mathbf{z}_u,\\ \dot{\mathbf{z}}_u &= \mathbf{A}_{uu}\mathbf{z}_u,\\ \mathbf{y} &= \mathbf{C}_o\mathbf{z}_o. \end{aligned} \]

The variable \( \mathbf{z}_o \) is the observable coordinate block, while \( \mathbf{z}_u \) is the unobservable coordinate block. The output does not directly contain \( \mathbf{z}_u \). Detectability requires \( \mathbf{A}_{uu} \) to be asymptotically stable.

flowchart TD
  X["State x"] --> T["Change coordinates x = T z"]
  T --> ZO["Observable block z_o"]
  T --> ZU["Unobservable block z_u"]
  ZO --> Y["Output y depends on z_o"]
  ZU --> H["Hidden dynamics z_u_dot = A_uu z_u"]
  H --> S["Detectable if hidden modes are stable"]
        

This block picture is central: hidden modes are acceptable only when their own dynamics remove their effect over time. A hidden unstable mode is dangerous because no output correction can see it.

5. Why Detectability Matters for Observer Error

Although observer design will be studied later, it is useful to see why detectability appears naturally. A full-order Luenberger-style observer has the structure

\[ \dot{\hat{\mathbf{x}}} =\mathbf{A}\hat{\mathbf{x}}+\mathbf{B}\mathbf{u} +\mathbf{L}\left(\mathbf{y}-\mathbf{C}\hat{\mathbf{x}}\right). \]

The estimation error \( \mathbf{e}=\mathbf{x}-\hat{\mathbf{x}} \) satisfies

\[ \dot{\mathbf{e}}=(\mathbf{A}-\mathbf{L}\mathbf{C})\mathbf{e}. \]

If an error component lies in the unobservable subspace, then \( \mathbf{C}\mathbf{e}=\mathbf{0} \), so the output injection term cannot directly correct it. In the hidden coordinates, the error component behaves like

\[ \dot{\mathbf{e}}_u=\mathbf{A}_{uu}\mathbf{e}_u. \]

Thus, if \( \mathbf{A}_{uu} \) contains an unstable eigenvalue, the observer cannot remove that hidden error. If \( \mathbf{A}_{uu} \) is stable, the hidden error decays naturally. This is the fundamental reason detectability is weaker than observability but still sufficient for stable asymptotic estimation.

6. Main Theorem: Stable Unobservable Modes Characterize Detectability

Theorem. For the continuous-time LTI pair \( (\mathbf{A},\mathbf{C}) \), the following statements are equivalent:

\[ \begin{aligned} \text{(i)}\;&(\mathbf{A},\mathbf{C})\text{ is detectable},\\ \text{(ii)}\;&\text{every unobservable mode of }\mathbf{A} \text{ has negative real part},\\ \text{(iii)}\;&\mathbf{C}e^{\mathbf{A}t}\mathbf{x}_0=\mathbf{0} \text{ for all } t\ge 0 \Longrightarrow e^{\mathbf{A}t}\mathbf{x}_0\to \mathbf{0}. \end{aligned} \]

Proof.

First, suppose that every unobservable mode has negative real part. If \( \mathbf{x}_0\in\mathcal{N}_o \), write \( \mathbf{x}_0=\mathbf{N}\boldsymbol{\xi}_0 \), where the columns of \( \mathbf{N} \) span \( \mathcal{N}_o \). Since \( \mathbf{A}\mathbf{N}=\mathbf{N}\mathbf{A}_u \), the hidden motion is

\[ e^{\mathbf{A}t}\mathbf{x}_0 =\mathbf{N}e^{\mathbf{A}_u t}\boldsymbol{\xi}_0. \]

If all eigenvalues of \( \mathbf{A}_u \) have negative real parts, then \( e^{\mathbf{A}_u t}\to\mathbf{0} \). Therefore \( e^{\mathbf{A}t}\mathbf{x}_0\to\mathbf{0} \). This proves that every invisible initial condition decays.

Conversely, suppose there exists an unobservable mode \( \lambda \) with \( \operatorname{Re}(\lambda)\ge 0 \). Then the hidden dynamics contain a nondecaying component. In Jordan form, a hidden trajectory associated with that mode contains terms of the form

\[ t^q e^{\lambda t}\mathbf{v},\quad q\ge 0. \]

Such a term cannot converge to zero when \( \operatorname{Re}(\lambda)\ge 0 \). Since it is hidden, it produces zero output but a nondecaying state response. Hence the system is not detectable. This proves the equivalence.

The discrete-time proof is identical after replacing \( e^{\mathbf{A}t} \) by \( \mathbf{A}^k \) and replacing the stability region \( \operatorname{Re}(\lambda)<0 \) by \( |\lambda|<1 \).

7. Worked Examples

Example 1: detectable but not observable. Let

\[ \mathbf{A}= \begin{bmatrix} -1&0&0\\ 0&2&0\\ 0&0&-0.5 \end{bmatrix},\quad \mathbf{C}=\begin{bmatrix}0&1&0\end{bmatrix}. \]

The output measures only the second state: \( y=x_2 \). The hidden states are \( x_1 \) and \( x_3 \), with modes \( -1 \) and \( -0.5 \). They are stable in continuous time. Therefore the system is not observable but is detectable.

Example 2: not detectable. Let

\[ \mathbf{A}= \begin{bmatrix} 1&0&0\\ 0&-2&0\\ 0&0&-0.5 \end{bmatrix},\quad \mathbf{C}=\begin{bmatrix}0&1&0\end{bmatrix}. \]

The first state is hidden and has mode \( \lambda=1 \). Since \( \operatorname{Re}(1)>0 \), a hidden initial error in the \( x_1 \) direction grows as \( e^t \) while producing zero output. Hence the pair is not detectable.

Example 3: discrete-time detectable but not observable. Let

\[ \mathbf{A}= \begin{bmatrix} 0.3&0&0\\ 0&1.2&0\\ 0&0&-0.7 \end{bmatrix},\quad \mathbf{C}=\begin{bmatrix}0&1&0\end{bmatrix}. \]

The unobservable modes are \( 0.3 \) and \( -0.7 \), both inside the unit disk. The visible mode \( 1.2 \) may be unstable, but it is measured and can later be corrected by output injection. Therefore the pair is detectable but not observable.

8. Python Implementation — Chapter13_Lesson3.py

The following program constructs the finite observability matrix, computes a numerical basis for the unobservable subspace using the SVD, restricts \( \mathbf{A} \) to that subspace, and tests whether the hidden modes are stable.

# Chapter13_Lesson3.py
# Detectability and Stable Unobservable Modes
#
# Requirements:
#   pip install numpy
#
# This script implements detectability from the unobservable-subspace viewpoint.
# Continuous time: all unobservable modes must satisfy Re(lambda) < 0.
# Discrete time: all unobservable modes must satisfy |lambda| < 1.

import numpy as np


def observability_matrix(A: np.ndarray, C: np.ndarray) -> np.ndarray:
    """Build O = [C; C A; ...; C A^(n-1)]."""
    A = np.asarray(A, dtype=float)
    C = np.asarray(C, dtype=float)
    n = A.shape[0]

    blocks = []
    Ak = np.eye(n)
    for _ in range(n):
        blocks.append(C @ Ak)
        Ak = Ak @ A
    return np.vstack(blocks)


def nullspace(M: np.ndarray, tol: float = 1e-10) -> np.ndarray:
    """Return an orthonormal basis for ker(M), using the SVD."""
    U, s, Vt = np.linalg.svd(M, full_matrices=True)
    if s.size == 0:
        rank = 0
    else:
        rank = int(np.sum(s > tol * max(M.shape) * max(s)))
    return Vt[rank:].T


def unobservable_modes(A: np.ndarray, C: np.ndarray, tol: float = 1e-10) -> np.ndarray:
    """Return eigenvalues of A restricted to the unobservable subspace."""
    A = np.asarray(A, dtype=float)
    C = np.asarray(C, dtype=float)

    O = observability_matrix(A, C)
    N = nullspace(O, tol=tol)

    if N.shape[1] == 0:
        return np.array([], dtype=complex)

    # For an LTI system, ker(O) is A-invariant. With orthonormal N,
    # the hidden dynamics matrix is A_N = N^T A N.
    A_hidden = N.T @ A @ N
    return np.linalg.eigvals(A_hidden)


def is_detectable(A: np.ndarray, C: np.ndarray, system: str = "continuous",
                  tol: float = 1e-9) -> bool:
    """Check detectability from the unobservable modes."""
    lambdas_hidden = unobservable_modes(A, C, tol=tol)

    if lambdas_hidden.size == 0:
        return True

    if system.lower().startswith("cont"):
        return bool(np.all(np.real(lambdas_hidden) < -tol))

    if system.lower().startswith("disc"):
        return bool(np.all(np.abs(lambdas_hidden) < 1.0 - tol))

    raise ValueError("system must be 'continuous' or 'discrete'")


def report(A: np.ndarray, C: np.ndarray, name: str, system: str = "continuous") -> None:
    O = observability_matrix(A, C)
    hidden = unobservable_modes(A, C)
    print(f"\n{name}")
    print("-" * len(name))
    print("A =\n", A)
    print("C =\n", C)
    print("rank(O) =", np.linalg.matrix_rank(O), "of n =", A.shape[0])
    print("unobservable modes =", hidden)
    print("detectable =", is_detectable(A, C, system=system))


if __name__ == "__main__":
    # Example 1: not observable, but detectable.
    # The unstable mode +2 is measured; hidden modes -1 and -0.5 are stable.
    A1 = np.diag([-1.0, 2.0, -0.5])
    C1 = np.array([[0.0, 1.0, 0.0]])
    report(A1, C1, "Continuous-time example: detectable but not observable")

    # Example 2: not detectable.
    # The hidden mode +1 is unstable.
    A2 = np.diag([1.0, -2.0, -0.5])
    C2 = np.array([[0.0, 1.0, 0.0]])
    report(A2, C2, "Continuous-time example: not detectable")

    # Example 3: discrete-time detectable.
    # Hidden modes 0.3 and -0.7 lie inside the unit disk; measured mode 1.2 is visible.
    A3 = np.diag([0.3, 1.2, -0.7])
    C3 = np.array([[0.0, 1.0, 0.0]])
    report(A3, C3, "Discrete-time example: detectable but not observable",
           system="discrete")

9. C++ Implementation — Chapter13_Lesson3.cpp

This C++ version uses the Eigen library for SVD, rank computation, and eigenvalue calculation. The algorithm is the same as in the Python version.

// Chapter13_Lesson3.cpp
// Detectability and Stable Unobservable Modes
//
// Dependency: Eigen 3
// Compile example:
//   g++ -std=c++17 Chapter13_Lesson3.cpp -I /path/to/eigen -O2 -o detectability
//
// Continuous time: all unobservable modes must satisfy Re(lambda) < 0.
// Discrete time: all unobservable modes must satisfy |lambda| < 1.

#include <Eigen/Dense>
#include <Eigen/Eigenvalues>
#include <iostream>
#include <complex>
#include <vector>
#include <string>

using Matrix = Eigen::MatrixXd;
using VectorC = Eigen::VectorXcd;

Matrix observabilityMatrix(const Matrix& A, const Matrix& C) {
    const int n = static_cast<int>(A.rows());
    const int p = static_cast<int>(C.rows());

    Matrix O(p * n, n);
    Matrix Ak = Matrix::Identity(n, n);

    for (int k = 0; k < n; ++k) {
        O.block(k * p, 0, p, n) = C * Ak;
        Ak = Ak * A;
    }
    return O;
}

Matrix nullspace(const Matrix& M, double tol = 1e-10) {
    Eigen::JacobiSVD<Matrix> svd(M, Eigen::ComputeFullV);
    const auto& s = svd.singularValues();
    int rank = 0;
    double scale = (s.size() == 0) ? 0.0 : s(0);
    for (int i = 0; i < s.size(); ++i) {
        if (s(i) > tol * std::max(M.rows(), M.cols()) * scale) {
            ++rank;
        }
    }

    Matrix V = svd.matrixV();
    return V.rightCols(M.cols() - rank);
}

VectorC unobservableModes(const Matrix& A, const Matrix& C, double tol = 1e-10) {
    Matrix O = observabilityMatrix(A, C);
    Matrix N = nullspace(O, tol);

    if (N.cols() == 0) {
        return VectorC(0);
    }

    Matrix Ahidden = N.transpose() * A * N;
    Eigen::EigenSolver<Matrix> es(Ahidden);
    return es.eigenvalues();
}

bool isDetectable(const Matrix& A, const Matrix& C,
                  const std::string& system = "continuous",
                  double tol = 1e-9) {
    VectorC hidden = unobservableModes(A, C, tol);

    if (hidden.size() == 0) {
        return true;
    }

    if (system == "continuous") {
        for (int i = 0; i < hidden.size(); ++i) {
            if (hidden(i).real() >= -tol) {
                return false;
            }
        }
        return true;
    }

    if (system == "discrete") {
        for (int i = 0; i < hidden.size(); ++i) {
            if (std::abs(hidden(i)) >= 1.0 - tol) {
                return false;
            }
        }
        return true;
    }

    throw std::runtime_error("system must be continuous or discrete");
}

void report(const Matrix& A, const Matrix& C,
            const std::string& name,
            const std::string& system = "continuous") {
    Matrix O = observabilityMatrix(A, C);
    VectorC hidden = unobservableModes(A, C);

    std::cout << "\n" << name << "\n";
    std::cout << std::string(name.size(), '-') << "\n";
    std::cout << "A =\n" << A << "\n";
    std::cout << "C =\n" << C << "\n";
    std::cout << "rank(O) = "
              << Eigen::FullPivLU<Matrix>(O).rank()
              << " of n = " << A.rows() << "\n";

    std::cout << "unobservable modes = ";
    for (int i = 0; i < hidden.size(); ++i) {
        std::cout << hidden(i) << " ";
    }
    std::cout << "\n";

    std::cout << "detectable = "
              << (isDetectable(A, C, system) ? "true" : "false")
              << "\n";
}

int main() {
    Matrix A1 = Matrix::Zero(3, 3);
    A1(0, 0) = -1.0;
    A1(1, 1) = 2.0;
    A1(2, 2) = -0.5;
    Matrix C1(1, 3);
    C1 << 0.0, 1.0, 0.0;
    report(A1, C1, "Continuous-time: detectable but not observable");

    Matrix A2 = Matrix::Zero(3, 3);
    A2(0, 0) = 1.0;
    A2(1, 1) = -2.0;
    A2(2, 2) = -0.5;
    Matrix C2(1, 3);
    C2 << 0.0, 1.0, 0.0;
    report(A2, C2, "Continuous-time: not detectable");

    Matrix A3 = Matrix::Zero(3, 3);
    A3(0, 0) = 0.3;
    A3(1, 1) = 1.2;
    A3(2, 2) = -0.7;
    Matrix C3(1, 3);
    C3 << 0.0, 1.0, 0.0;
    report(A3, C3, "Discrete-time: detectable but not observable", "discrete");

    return 0;
}

10. Java Implementation — Chapter13_Lesson3.java

This Java implementation uses Apache Commons Math for matrix decompositions. It is useful when building control-analysis tools in a Java backend or desktop application.

// Chapter13_Lesson3.java
// Detectability and Stable Unobservable Modes
//
// Dependency: Apache Commons Math 3
// Compile example:
//   javac -cp commons-math3-3.6.1.jar Chapter13_Lesson3.java
// Run example:
//   java -cp .:commons-math3-3.6.1.jar Chapter13_Lesson3
//
// On Windows PowerShell, use:
//   java -cp ".;commons-math3-3.6.1.jar" Chapter13_Lesson3

import org.apache.commons.math3.linear.Array2DRowRealMatrix;
import org.apache.commons.math3.linear.EigenDecomposition;
import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;
import org.apache.commons.math3.linear.SingularValueDecomposition;

public class Chapter13_Lesson3 {
    public static RealMatrix observabilityMatrix(RealMatrix A, RealMatrix C) {
        int n = A.getRowDimension();
        int p = C.getRowDimension();

        RealMatrix O = new Array2DRowRealMatrix(p * n, n);
        RealMatrix Ak = MatrixUtils.createRealIdentityMatrix(n);

        for (int k = 0; k < n; k++) {
            RealMatrix block = C.multiply(Ak);
            O.setSubMatrix(block.getData(), k * p, 0);
            Ak = Ak.multiply(A);
        }
        return O;
    }

    public static RealMatrix nullspace(RealMatrix M, double tol) {
        SingularValueDecomposition svd = new SingularValueDecomposition(M);
        double[] s = svd.getSingularValues();
        int n = M.getColumnDimension();

        int rank = 0;
        double scale = s.length == 0 ? 0.0 : s[0];
        for (double value : s) {
            if (value > tol * Math.max(M.getRowDimension(), M.getColumnDimension()) * scale) {
                rank++;
            }
        }

        RealMatrix V = svd.getV();
        int nullity = n - rank;
        if (nullity == 0) {
            return new Array2DRowRealMatrix(n, 0);
        }

        RealMatrix N = new Array2DRowRealMatrix(n, nullity);
        for (int j = 0; j < nullity; j++) {
            N.setColumnVector(j, V.getColumnVector(rank + j));
        }
        return N;
    }

    public static double[][] unobservableModes(RealMatrix A, RealMatrix C, double tol) {
        RealMatrix O = observabilityMatrix(A, C);
        RealMatrix N = nullspace(O, tol);

        if (N.getColumnDimension() == 0) {
            return new double[][] { new double[0], new double[0] };
        }

        RealMatrix Ahidden = N.transpose().multiply(A).multiply(N);
        EigenDecomposition eig = new EigenDecomposition(Ahidden);
        return new double[][] { eig.getRealEigenvalues(), eig.getImagEigenvalues() };
    }

    public static boolean isDetectable(RealMatrix A, RealMatrix C,
                                       String system, double tol) {
        double[][] hidden = unobservableModes(A, C, tol);
        double[] re = hidden[0];
        double[] im = hidden[1];

        if (re.length == 0) {
            return true;
        }

        if (system.equalsIgnoreCase("continuous")) {
            for (double value : re) {
                if (value >= -tol) {
                    return false;
                }
            }
            return true;
        }

        if (system.equalsIgnoreCase("discrete")) {
            for (int i = 0; i < re.length; i++) {
                double modulus = Math.hypot(re[i], im[i]);
                if (modulus >= 1.0 - tol) {
                    return false;
                }
            }
            return true;
        }

        throw new IllegalArgumentException("system must be continuous or discrete");
    }

    public static void report(double[][] Adata, double[][] Cdata,
                              String name, String system) {
        RealMatrix A = new Array2DRowRealMatrix(Adata);
        RealMatrix C = new Array2DRowRealMatrix(Cdata);
        RealMatrix O = observabilityMatrix(A, C);

        SingularValueDecomposition svd = new SingularValueDecomposition(O);
        int rankO = svd.getRank();

        double[][] hidden = unobservableModes(A, C, 1e-10);
        double[] re = hidden[0];
        double[] im = hidden[1];

        System.out.println("\n" + name);
        System.out.println("-".repeat(name.length()));
        System.out.println("rank(O) = " + rankO + " of n = " + A.getRowDimension());

        System.out.print("unobservable modes = ");
        for (int i = 0; i < re.length; i++) {
            System.out.print("(" + re[i] + " + " + im[i] + "i) ");
        }
        System.out.println();

        System.out.println("detectable = " + isDetectable(A, C, system, 1e-9));
    }

    public static void main(String[] args) {
        double[][] A1 = {
            {-1.0, 0.0, 0.0},
            { 0.0, 2.0, 0.0},
            { 0.0, 0.0, -0.5}
        };
        double[][] C1 = { {0.0, 1.0, 0.0} };
        report(A1, C1, "Continuous-time: detectable but not observable", "continuous");

        double[][] A2 = {
            { 1.0, 0.0, 0.0},
            { 0.0, -2.0, 0.0},
            { 0.0, 0.0, -0.5}
        };
        double[][] C2 = { {0.0, 1.0, 0.0} };
        report(A2, C2, "Continuous-time: not detectable", "continuous");

        double[][] A3 = {
            {0.3, 0.0, 0.0},
            {0.0, 1.2, 0.0},
            {0.0, 0.0, -0.7}
        };
        double[][] C3 = { {0.0, 1.0, 0.0} };
        report(A3, C3, "Discrete-time: detectable but not observable", "discrete");
    }
}

11. MATLAB/Simulink Implementation — Chapter13_Lesson3.m

The MATLAB code uses null, rank, and eig directly. If Simulink is installed, it also creates an optional state-space block model to visualize the plant output.

% Chapter13_Lesson3.m
% Detectability and Stable Unobservable Modes
%
% This script uses the unobservable-subspace viewpoint.
% Continuous time: all unobservable modes satisfy real(lambda) < 0.
% Discrete time: all unobservable modes satisfy abs(lambda) < 1.
%
% Optional Control System Toolbox functions such as obsv are not required.

clear; clc;

A1 = diag([-1, 2, -0.5]);
C1 = [0 1 0];
reportDetectability(A1, C1, "Continuous-time: detectable but not observable", "continuous");

A2 = diag([1, -2, -0.5]);
C2 = [0 1 0];
reportDetectability(A2, C2, "Continuous-time: not detectable", "continuous");

A3 = diag([0.3, 1.2, -0.7]);
C3 = [0 1 0];
reportDetectability(A3, C3, "Discrete-time: detectable but not observable", "discrete");

% Optional Simulink demonstration:
% If Simulink is installed, the following block creates a simple model with a
% State-Space block. The detectability test itself remains in MATLAB because
% it is an algebraic property of (A,C).
if exist("new_system", "file") == 4
    mdl = "Chapter13_Lesson3_Detectability_Model";
    if ~bdIsLoaded(mdl)
        new_system(mdl);
        open_system(mdl);

        add_block("simulink/Sources/Step", mdl + "/zero input");
        add_block("simulink/Continuous/State-Space", mdl + "/plant");
        add_block("simulink/Sinks/Scope", mdl + "/output scope");

        set_param(mdl + "/plant", ...
            "A", mat2str(A1), ...
            "B", mat2str(zeros(3,1)), ...
            "C", mat2str(C1), ...
            "D", mat2str(0));

        add_line(mdl, "zero input/1", "plant/1");
        add_line(mdl, "plant/1", "output scope/1");
        save_system(mdl);
        disp("Created optional Simulink model: " + mdl);
    end
end

function O = observabilityMatrix(A, C)
    n = size(A, 1);
    O = [];
    Ak = eye(n);
    for k = 1:n
        O = [O; C * Ak]; %#ok<AGROW>
        Ak = Ak * A;
    end
end

function lambdasHidden = unobservableModes(A, C)
    O = observabilityMatrix(A, C);
    N = null(O, "r");

    if isempty(N)
        lambdasHidden = [];
        return;
    end

    Ahidden = N' * A * N;
    lambdasHidden = eig(Ahidden);
end

function tf = isDetectable(A, C, systemType)
    tol = 1e-9;
    lambdasHidden = unobservableModes(A, C);

    if isempty(lambdasHidden)
        tf = true;
        return;
    end

    if systemType == "continuous"
        tf = all(real(lambdasHidden) < -tol);
    elseif systemType == "discrete"
        tf = all(abs(lambdasHidden) < 1 - tol);
    else
        error("systemType must be continuous or discrete");
    end
end

function reportDetectability(A, C, name, systemType)
    O = observabilityMatrix(A, C);
    lambdasHidden = unobservableModes(A, C);

    fprintf("\n%s\n", name);
    fprintf("%s\n", repmat("-", 1, strlength(name)));
    fprintf("rank(O) = %d of n = %d\n", rank(O), size(A,1));

    disp("unobservable modes:");
    disp(lambdasHidden.');

    fprintf("detectable = %d\n", isDetectable(A, C, systemType));
end

12. Wolfram Mathematica Implementation — Chapter13_Lesson3.nb

Mathematica is especially convenient for exact symbolic examples, null spaces, and eigenvalue expressions. The downloadable notebook contains this implementation.

(* Chapter13_Lesson3.nb *)
(* Detectability and Stable Unobservable Modes *)

ClearAll[ObservabilityMatrix, NullSpaceBasis, UnobservableModes, DetectableQ];

ObservabilityMatrix[A_, C_] := Module[{n = Length[A]},
  Join @@ Table[C.MatrixPower[A, k], {k, 0, n - 1}]
];

NullSpaceBasis[M_] := Transpose[NullSpace[M]];

UnobservableModes[A_, C_] := Module[{O, N, Ahidden},
  O = ObservabilityMatrix[A, C];
  N = NullSpaceBasis[O];

  If[Length[N] == 0 || Dimensions[N][[2]] == 0,
    {},
    Ahidden = Transpose[N].A.N;
    Eigenvalues[Ahidden]
  ]
];

DetectableQ[A_, C_, systemType_: "Continuous"] := Module[{hidden},
  hidden = UnobservableModes[A, C];

  If[hidden == {},
    True,
    If[systemType == "Continuous",
      And @@ Thread[Re[hidden] < 0],
      And @@ Thread[Abs[hidden] < 1]
    ]
  ]
];

A1 = DiagonalMatrix[{-1, 2, -0.5}];
C1 = {{0, 1, 0}};
Print["Continuous-time: detectable but not observable"];
Print["Observability rank = ", MatrixRank[ObservabilityMatrix[A1, C1]]];
Print["Unobservable modes = ", UnobservableModes[A1, C1]];
Print["Detectable = ", DetectableQ[A1, C1, "Continuous"]];

A2 = DiagonalMatrix[{1, -2, -0.5}];
C2 = {{0, 1, 0}};
Print["Continuous-time: not detectable"];
Print["Observability rank = ", MatrixRank[ObservabilityMatrix[A2, C2]]];
Print["Unobservable modes = ", UnobservableModes[A2, C2]];
Print["Detectable = ", DetectableQ[A2, C2, "Continuous"]];

A3 = DiagonalMatrix[{0.3, 1.2, -0.7}];
C3 = {{0, 1, 0}};
Print["Discrete-time: detectable but not observable"];
Print["Observability rank = ", MatrixRank[ObservabilityMatrix[A3, C3]]];
Print["Unobservable modes = ", UnobservableModes[A3, C3]];
Print["Detectable = ", DetectableQ[A3, C3, "Discrete"]];

13. Problems and Solutions

Problem 1 (Observable implies detectable): Prove that if \( (\mathbf{A},\mathbf{C}) \) is observable, then it is detectable.

Solution: If the pair is observable, then \( \mathcal{N}_o=\{\mathbf{0}\} \). Therefore there are no unobservable modes. The condition that all unobservable modes be stable is vacuously true. Hence the pair is detectable.

Problem 2 (Continuous-time hidden mode): Consider

\[ \mathbf{A}= \begin{bmatrix} -3&0\\ 0&4 \end{bmatrix},\quad \mathbf{C}=\begin{bmatrix}1&0\end{bmatrix}. \]

Is the pair observable? Is it detectable?

Solution: The output is \( y=x_1 \). The second state is hidden. Its mode is \( \lambda=4 \), which has positive real part. Thus the pair is not observable and not detectable. The hidden initial condition \( \mathbf{x}_0=[0\;1]^T \) produces zero output but grows as \( e^{4t} \).

Problem 3 (Detectable but not observable): Construct a two-state continuous-time system that is not observable but is detectable.

Solution: One example is

\[ \mathbf{A}= \begin{bmatrix} -2&0\\ 0&1 \end{bmatrix},\quad \mathbf{C}=\begin{bmatrix}0&1\end{bmatrix}. \]

The hidden state is \( x_1 \), and its mode is \( -2 \), which is stable. The measured state \( x_2 \) has the mode \( 1 \). Since the only unobservable mode is stable, the pair is detectable but not observable.

Problem 4 (Discrete-time condition): For

\[ \mathbf{A}= \begin{bmatrix} 0.8&0\\ 0&1.1 \end{bmatrix},\quad \mathbf{C}=\begin{bmatrix}0&1\end{bmatrix}, \]

determine detectability.

Solution: The hidden state is \( x_1 \), whose mode is \( 0.8 \). Since \( |0.8|<1 \), the hidden mode decays. The visible mode \( 1.1 \) is outside the unit disk, but it is measured. Therefore the pair is detectable but not observable.

Problem 5 (Proof using hidden coordinates): Suppose the hidden block of a continuous-time realization is \( \dot{\mathbf{z}}_u=\mathbf{A}_{uu}\mathbf{z}_u \). Show that if \( \mathbf{A}_{uu} \) has an eigenvalue with nonnegative real part, the pair cannot be detectable.

Solution: If \( \mathbf{A}_{uu}\mathbf{v}=\lambda\mathbf{v} \) and \( \operatorname{Re}(\lambda)\ge 0 \), choose \( \mathbf{z}_u(0)=\mathbf{v} \) and \( \mathbf{z}_o(0)=\mathbf{0} \). The output remains zero for the purely hidden component, but the hidden state contains \( e^{\lambda t}\mathbf{v} \), which does not converge to zero. Therefore an invisible nondecaying error exists, so the pair is not detectable.

14. Summary

Detectability is the requirement that all unobservable modes be stable. It is weaker than observability because it permits hidden state components, but only if those components decay naturally. In continuous time, hidden modes must satisfy \( \operatorname{Re}(\lambda)<0 \); in discrete time, they must satisfy \( |\lambda|<1 \). This property explains why stable observer error dynamics are possible even when full state reconstruction is impossible.

15. References

  1. Kalman, R.E. (1960). On the general theory of control systems. Proceedings of the First International Congress on Automatic Control, Moscow, 481–492.
  2. Kalman, R.E. (1963). Mathematical description of linear dynamical systems. Journal of the Society for Industrial and Applied Mathematics, Series A: Control, 1(2), 152–192.
  3. Gilbert, E.G. (1963). Controllability and observability in multivariable control systems. Journal of the Society for Industrial and Applied Mathematics, Series A: Control, 1(2), 128–151.
  4. Luenberger, D.G. (1964). Observing the state of a linear system. IEEE Transactions on Military Electronics, 8(2), 74–80.
  5. Luenberger, D.G. (1966). Observers for multivariable systems. IEEE Transactions on Automatic Control, 11(2), 190–197.
  6. Kalman, R.E., & Bucy, R.S. (1961). New results in linear filtering and prediction theory. Journal of Basic Engineering, 83(1), 95–108.
  7. Hautus, M.L.J. (1969). Controllability and observability conditions of linear autonomous systems. Nederl. Akad. Wetensch. Proc. Ser. A, 72, 443–448.
  8. Wonham, W.M. (1968). On pole assignment in multi-input controllable linear systems. IEEE Transactions on Automatic Control, 12(6), 660–665.