Introduction

Before an engineer writes a single line of controller code for a drone, a car's suspension, or a power grid, two questions decide whether the whole project is even possible. First: using only the knobs I'm allowed to turn — thrust, steering, voltage — can I push the system into any state I want? That's controllability. Second: watching only the sensors I'm allowed to read — a camera, an accelerometer, a voltmeter — can I figure out everything happening inside the system? That's observability.

These sound like philosophical questions, but Rudolf Kálmán showed in 1960 that they have a completely mechanical answer: build a specific matrix out of the system's equations and check its rank. No simulation, no guessing, no trial-and-error controller tuning required.

Get the answer wrong and no amount of clever engineering saves you. If a state is uncontrollable, no input sequence on Earth will move it. If a state is unobservable, no amount of sensor data will ever tell you what it's doing. The state-space model you write down either has these properties baked in, or it doesn't.

Toggle the Coupling

Here is the simplest possible system with two internal states: a "driven" state that the input pushes directly, and a "passenger" state that only moves because it's coupled to the driven one. A slider controls the strength of that coupling.

<p class="hint">{{hint_para}}</p>
<div class="diagram" id="diagram">
  <div class="node input" id="nodeU">{{label_u}}</div>
  <svg class="arrows" viewBox="0 0 220 120" preserveAspectRatio="none">
    <defs>
      <marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
        <path d="M0,0 L6,3 L0,6 Z" fill="currentColor"/>
      </marker>
    </defs>
    <line id="uToX1" x1="20" y1="60" x2="90" y2="60" marker-end="url(#arrow)"/>
    <line id="x1ToX2" x1="130" y1="30" x2="130" y2="90" marker-end="url(#arrow)"/>
    <line id="x2ToY" x1="170" y1="60" x2="200" y2="60" marker-end="url(#arrow)"/>
  </svg>
  <div class="node state driven" id="nodeX1">x1<span class="sub">{{label_driven}}</span></div>
  <div class="node state passenger" id="nodeX2">x2<span class="sub">{{label_passenger}}</span></div>
  <div class="node output" id="nodeY">{{label_y}}</div>
</div>
<div class="slider-row">
  <label for="k">{{label_coupling}}</label>
  <input type="range" id="k" min="0" max="100" value="50" step="1">
  <span id="kval" class="kval">0.50</span>
</div>
<div class="matrices">
  <div class="mat-box">
    <div class="mat-title">{{title_ctrl}}</div>
    <div class="mat" id="ctrlMat"></div>
    <div class="rank" id="ctrlRank"></div>
  </div>
  <div class="mat-box">
    <div class="mat-title">{{title_obs}}</div>
    <div class="mat" id="obsMat"></div>
    <div class="rank" id="obsRank"></div>
  </div>
</div>
<div class="status" id="status">{{status_full}}</div>
<div class="btns">
  <button id="zero" type="button">{{btn_zero}}</button>
  <button id="reset" type="button" class="ghost">{{btn_reset}}</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.diagram { position: relative; height: 140px; margin: .5rem 0 1rem; }
.arrows { position: absolute; top: 0; left: 0; width: 100%; height: 100%; color: #8a97a6; }
.arrows line { stroke: currentColor; stroke-width: 2; transition: stroke .15s, opacity .15s; }
.node { position: absolute; display: flex; flex-direction: column; align-items: center; justify-content: center;
        border-radius: 10px; font: 700 15px ui-monospace, monospace; text-align: center; padding: .3rem .5rem; }
.node .sub { font: 500 10px system-ui, sans-serif; margin-top: 2px; }
.input { left: 0; top: 42px; width: 60px; height: 40px; background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.state { top: 12px; width: 60px; height: 55px; }
.driven { left: 85px; background: #cfe8d8; color: #14532d; border: 1px solid #a8d4b7; }
.passenger { left: 85px; top: 72px; background: #d8dce3; color: #1d3557; border: 1px solid #b9c1cd; transition: opacity .2s, background .2s; }
.output { right: 0; top: 42px; width: 60px; height: 40px; background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.slider-row { display: flex; align-items: center; gap: .6rem; margin: .6rem 0; font-size: .9rem; }
.slider-row input[type="range"] { flex: 1; }
.kval { font: 700 14px ui-monospace, monospace; min-width: 3.2em; text-align: right; }
.matrices { display: flex; gap: 1rem; flex-wrap: wrap; margin: .6rem 0; }
.mat-box { background: #f4f6f8; border: 1px solid #e1e6ea; border-radius: 8px; padding: .5rem .7rem; min-width: 150px; }
.mat-title { font-size: .78rem; font-weight: 700; color: #444; margin-bottom: .25rem; }
.mat { font: 600 13px ui-monospace, monospace; white-space: pre; color: #1d3557; }
.rank { font-size: .8rem; font-weight: 700; margin-top: .3rem; }
.rank.full { color: #0a7d33; }
.rank.deficient { color: #c92f3c; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Code not found

Drag the coupling to zero and watch the controllability matrix collapse from rank 2 to rank 1 — the passenger state becomes a stowaway the input can never reach, no matter how hard you push. Slide it back up and full rank returns instantly. The same coupling term controls whether an output sensor watching only the driven state can ever detect what the passenger state is doing — turn it off and that state goes dark to every sensor, too.

The Real Complexity

Take a linear time-invariant system x˙=Ax+Bu\dot{x} = Ax + Bu, y=Cxy = Cx, where xx is an nn-dimensional state, uu is the input, and yy is the measured output.

  • Controllability. Stack the matrices B,AB,A2B,,An1BB, AB, A^2B, \dots, A^{n-1}B side by side into the controllability matrix C=[B  AB  A2B    An1B]\mathcal{C} = [\,B \; AB \; A^2B \; \cdots \; A^{n-1}B\,]. The system is controllable — some input sequence can drive it from any state to any other state in finite time — if and only if C\mathcal{C} has full rank nn.
  • Observability. Stack C,CA,CA2,,CAn1C, CA, CA^2, \dots, CA^{n-1} on top of each other into the observability matrix O=[C;  CA;  CA2;  ;  CAn1]T\mathcal{O} = [\,C;\; CA;\; CA^2;\; \cdots;\; CA^{n-1}\,]^{\mathsf T}. The system is observable — the full initial state can be reconstructed from a finite window of outputs — if and only if O\mathcal{O} has full rank nn.
  • Why it works. Both conditions ask the same underlying question in two directions: does the input's influence eventually reach every dimension of the state, and does every dimension of the state eventually leave a trace in the output? Multiplying by AA repeatedly "unrolls" the dynamics one step at a time, and Cayley–Hamilton guarantees that nn steps are always enough — you never need to check further.
  • It's cheap. Computing a matrix rank costs O(n3)O(n^3) time via Gaussian elimination. Two deep structural questions about an entire dynamical system — can I steer it, can I see it — reduce to a routine linear-algebra computation, not a search over control strategies.
  • Duality. Controllability of (A,B)(A, B) is exactly observability of (AT,CT ⁣= ⁣BT)(A^{\mathsf T}, C^{\mathsf T}\!=\!B^{\mathsf T}). Kálmán's two tests are really one test, applied twice.

That's the punchline: two questions that sound like they need P vs NP-style exhaustive search — "can any input do this?", "can any output reveal that?" — actually live entirely inside polynomial-time linear algebra, decades before complexity theory had a name for that class.

Where It Matters

These are not textbook curiosities — they are the first checks run before any real control system is trusted with something expensive or dangerous:

  • Aerospace. Before a spacecraft's attitude-control system is finalized, engineers verify controllability so that every axis of rotation can actually be commanded by the available thrusters.
  • State estimation. The Kalman filter that fuses noisy sensor data into a clean state estimate only converges to the truth if the underlying system is observable — otherwise some part of the state is simply invisible, forever.
  • Sensor and actuator placement. Observability tells engineers exactly where a missing sensor would blind them, and controllability tells them where a missing actuator would leave a mode forever out of reach — both drive real hardware layout decisions.
  • Power grids and epidemiology. Large networked systems — electrical grids, epidemic-spread models, even social networks — get analyzed for which subset of nodes you'd need to drive or monitor to control or track the whole network.

Any time someone builds a mathematical model of a system and asks "can I actually control this?" or "can I actually tell what it's doing?", they are running a Kalman rank test, whether they call it that or not.

Conclusion

Controllability and observability turn two dramatic-sounding questions — can I steer this, can I see inside it — into a single mechanical computation: build a matrix from AA, BB or CC, and check its rank. No state hides from a full-rank observability matrix; no state escapes a full-rank controllability matrix.

So the next time a robot arm, a satellite, or a power grid behaves as if some part of it is simply beyond reach, remember that Kálmán's rank conditions could have told the engineers that before they ever built the hardware — the same way state-space models turn dynamics into algebra everywhere else.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/controllability-observability/Content licensed under CC BY-NC 4.0.