Introduction

A drone knows its motor commands and can read its altitude, but nobody bolted a sensor onto its vertical velocity. A chemical reactor reports temperature at one probe, yet the concentrations racing through its interior stay invisible. In both cases, the quantity you actually need for control is not the one your sensors hand you.

In 1964, the engineer David Luenberger asked a deceptively simple question: if you know exactly how a system evolves — its equations, its inputs — can you compute the state you never measured, just by watching how wrong your guess turns out to be?

His answer was the Luenberger observer: run a simulated copy of the real system inside a computer, compare its predicted output to the real sensor reading, and feed that mismatch back in to correct the simulation. Get the correction right, and the copy's internal state converges onto the true, hidden state of the real system — even though that state was never measured directly.

Watch the Estimate Converge

Below is a simple sliding mass on a spring. The simulation only measures its position — the velocity is hidden, exactly like a real sensor that can't see it directly. A Luenberger observer runs its own internal model side by side, corrects itself using the position error, and estimates the velocity anyway.

<p class="hint">{{hint_para}}</p>
<canvas id="plot" width="640" height="220"></canvas>
<div class="legend">
  <span class="lg lg-true">{{legend_true}}</span>
  <span class="lg lg-est">{{legend_est}}</span>
  <span class="lg lg-meas">{{legend_meas}}</span>
</div>
<div class="slider-row">
  <label for="gain">{{label_gain}}</label>
  <input type="range" id="gain" min="0" max="120" value="20" step="1">
  <span id="gainval" class="gainval">2.0</span>
</div>
<div class="status" id="status">{{status_idle}}</div>
<div class="btns">
  <button id="run" type="button">{{btn_run}}</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; }
canvas { width: 100%; max-width: 640px; height: 220px; display: block; background: #f4f6f8;
         border: 1px solid #e1e6ea; border-radius: 8px; }
.legend { display: flex; gap: 1rem; flex-wrap: wrap; font-size: .8rem; font-weight: 600; margin: .5rem 0; }
.lg { position: relative; padding-left: 1.3rem; }
.lg::before { content: ""; position: absolute; left: 0; top: 50%; width: 1rem; height: 3px; transform: translateY(-50%); border-radius: 2px; }
.lg-true::before { background: #1d3557; }
.lg-est::before { background: #e63946; }
.lg-meas::before { background: #adb1b8; }
.slider-row { display: flex; align-items: center; gap: .6rem; margin: .6rem 0; font-size: .9rem; }
.slider-row input[type="range"] { flex: 1; }
.gainval { font: 700 14px ui-monospace, monospace; min-width: 3em; text-align: right; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.warn { color: #c9962f; }
.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

Set the observer gain L and press Run. With L = 0 the observer is just an open-loop simulation — any mismatch in its starting guess persists forever. Raise L, and the estimated velocity (dashed) bends toward the true, hidden velocity (solid) even though velocity is never fed to the observer directly. Push L too high and you'll see the estimate start to jitter — the correction reacts so hard to sensor noise that it starts fighting itself.

The Real Complexity

The observer looks almost too simple to trust: copy the system, correct it, done. The real content is in why the correction works, and how well.

  • The setup. The real system evolves as x˙=Ax+Bu\dot{x} = Ax + Bu, but you only measure y=Cxy = Cx. The observer keeps its own estimate x^\hat{x} and updates it with x^˙=Ax^+Bu+L(yCx^)\dot{\hat{x}} = A\hat{x} + Bu + L(y - C\hat{x}) — the same dynamics as the real system, plus a correction term driven by the output error yCx^y - C\hat{x}.
  • The error equation is the whole story. Define e=xx^e = x - \hat{x}. Subtracting the two equations above, every term involving the known input uu cancels, leaving e˙=(ALC)e\dot{e} = (A - LC)e. The estimation error obeys its own linear dynamics, completely decoupled from what the system is actually doing.
  • Convergence is a choice, not a hope. Because e˙=(ALC)e\dot{e} = (A - LC)e, the error decays to zero whenever the eigenvalues of ALCA - LC have negative real part — and you get to choose LL. If the pair (A,C)(A, C) is observable (see controllability and observability), a theorem guarantees you can place those eigenvalues anywhere you like by picking LL appropriately, exactly mirroring how state feedback places the eigenvalues of ABKA - BK.
  • Speed versus noise. Pushing the eigenvalues further left makes e0e \to 0 faster, but every real sensor carries noise, and a larger LL amplifies how hard that noise shakes the estimate. Choosing LL is a genuine trade-off between how fast you trust the model and how much you trust the measurement — the same trade-off the Kalman filter resolves optimally under known noise statistics.

So the "real complexity" here is not computational hardness — it's a clean, guaranteed exponential convergence, with a dial you can turn to trade speed for robustness.

Where It Matters

Almost every real control law is designed assuming you know the full state xx, yet almost no real system measures all of it. The observer is the bridge between that assumption and reality:

  • Output feedback control: pair an observer with a state-feedback controller — the celebrated separation principle says you can design each independently, and the combination is still stable.
  • Sensorless motor drives: many electric motors estimate rotor position and speed from voltage and current alone, skipping an expensive physical sensor entirely.
  • Aerospace and robotics: aircraft and drones reconstruct velocities and unmeasured angular rates between the readings of cheaper, noisier sensors.
  • Fault detection: comparing the real output to what an observer predicts is a standard way to flag that a sensor or actuator has failed — a large output error signals something the model didn't expect.

The observer's cousin, the Kalman filter, adds an optimal answer to "what if the model and sensors are noisy" — but the deterministic Luenberger structure at its core is exactly this same feedback-corrected copy.

Conclusion

The Luenberger observer is a small piece of mathematical elegance: take a system you cannot fully see into, run a copy of it in software, and let the error between prediction and measurement do the work of correction. As long as the system is observable, that error is guaranteed to vanish, and with it the gap between your estimate and reality.

It is a reminder that "unmeasured" does not mean "unknowable" — sometimes the missing state is hiding in plain sight, encoded in how the outputs you do have keep changing.

Share this article

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

Comments

Loading comments...

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