Skip to content

Commit f4edb8e

Browse files
add luenberger observer
1 parent cbda3ab commit f4edb8e

12 files changed

Lines changed: 508 additions & 177 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
1818
|--------------------------------------------------------------------|----------------------------------------------------------------------|
1919
| [Analysis](doc/analysis/README.md) | FFT, Power Spectral Density, DCT, Window Functions, Signal Detectors, Convolution & Correlation, Goertzel Algorithm |
2020
| [Control Analysis](doc/control_analysis/README.md) | Frequency Response, Root Locus |
21-
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF, Gain-Scheduled Controller |
21+
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF, Gain-Scheduled Controller, Luenberger Observer |
2222
| [Dynamics](doc/dynamics/README.md) | Euler-Lagrange, Newton-Euler, Recursive Newton-Euler, ABA |
2323
| [Estimators](doc/estimators/README.md) | Linear Regression, Polynomial Fitting, Yule-Walker (offline), Recursive Least Squares (online) |
2424
| [Filters](doc/filters/README.md) | Kalman, Extended Kalman, Unscented Kalman, Alpha-Beta/Alpha-Beta-Gamma, FIR, IIR, Exponential Moving Average, Moving Average, Complementary, Median Filter, CIC (Cascaded Integrator-Comb), Notch/Comb Filter |

ROADMAP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ Difficulty legend:
2929
|----|------------------------------------------------------|---------------------------|------------|
3030
| 15 | Biquad / Second-Order-Section cascade | `filters/passive` | ★★★☆☆ |
3131
| 17 | Lead-lag compensator | `controllers` | ★★★☆☆ |
32-
| 19 | Luenberger observer + pole placement (Ackermann) | `controllers` | ★★★☆☆ |
3332
| 20 | Integral / servo state feedback (LQI) | `controllers` | ★★★☆☆ |
3433
| 21 | LMS / NLMS adaptive filter | `estimators/online` | ★★★☆☆ |
3534
| 22 | Savitzky-Golay filter | `filters/passive` | ★★★☆☆ |
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Luenberger Observer
2+
3+
## Overview & Motivation
4+
5+
State-feedback controllers require the complete state vector, but physical systems typically expose only a few measured outputs. The **Luenberger observer** reconstructs unmeasured states from input and output measurements using a copy of the plant model continuously corrected by the difference between predicted and actual outputs.
6+
7+
Unlike the Kalman filter, the Luenberger observer is deterministic — it requires no noise statistics and no covariance propagation. For embedded systems with well-characterized models and deterministic environments, it offers the same state reconstruction at a fraction of the computational cost.
8+
9+
## Mathematical Theory
10+
11+
### Discrete-Time State-Space System
12+
13+
$$x[k+1] = A x[k] + B u[k]$$
14+
$$y[k] = C x[k] + D u[k]$$
15+
16+
### Observer Update Law
17+
18+
The observer maintains a state estimate $\hat{x}[k]$ updated by:
19+
20+
$$\hat{y}[k] = C \hat{x}[k] + D u[k]$$
21+
$$\hat{x}[k+1] = A \hat{x}[k] + B u[k] + L (y[k] - \hat{y}[k])$$
22+
23+
The term $y[k] - \hat{y}[k]$ is the **innovation** (output prediction error). The gain matrix $L \in \mathbb{R}^{n \times p}$ scales how aggressively the estimate is corrected.
24+
25+
### Error Dynamics
26+
27+
Defining the estimation error $e[k] = x[k] - \hat{x}[k]$:
28+
29+
$$e[k+1] = (A - LC)\, e[k]$$
30+
31+
The error decays to zero if and only if all eigenvalues of $(A - LC)$ lie strictly inside the unit circle. Choosing $L$ to place those eigenvalues at desired locations is the **pole placement** problem for observers.
32+
33+
### Ackermann's Formula (SISO Output)
34+
35+
For single-output systems ($p = 1$), the observer gain that places eigenvalues of $(A - LC)$ at $\{\mu_1, \dots, \mu_n\}$ is:
36+
37+
$$L = \varphi_d(A)\, \mathcal{O}^{-1}\, e_n$$
38+
39+
where:
40+
- $\varphi_d(z) = \prod_{i=1}^n (z - \mu_i)$ is the desired characteristic polynomial, evaluated at $A$
41+
- $\mathcal{O} = \begin{bmatrix} C \\ CA \\ \vdots \\ CA^{n-1} \end{bmatrix}$ is the observability matrix
42+
- $e_n = [0, \dots, 0, 1]^T$ is the last standard basis vector
43+
44+
This is the dual of Ackermann's controller placement formula.
45+
46+
### Observability Condition
47+
48+
Ackermann's formula requires $\mathcal{O}$ to be invertible, which holds if and only if the pair $(A, C)$ is **observable**: every state affects the output through some combination of shifts. An unobservable pair makes the formula degenerate — the gain cannot force arbitrary error convergence.
49+
50+
## Complexity Analysis
51+
52+
| Phase | Time | Space | Notes |
53+
|--------------------|----------------|-----------|-----------------------------------------------------|
54+
| Design (Ackermann) | $O(n^3)$ | $O(n^2)$ | Observability matrix build + linear solve |
55+
| Update (per step) | $O(n^2 + np)$ | $O(n^2)$ | Matrix-vector products; dominant cost is $A\hat{x}$ |
56+
57+
The design phase is offline. The real-time cost per sample is dominated by the $n \times n$ state-transition multiply.
58+
59+
## Step-by-Step Walkthrough
60+
61+
**System:** Double integrator, $n=2$, $p=1$, desired observer poles $\{\mu_1, \mu_2\} = \{0.2,\, 0.3\}$
62+
63+
$$A = \begin{bmatrix}1 & 1\\0 & 1\end{bmatrix}, \quad B = \begin{bmatrix}0\\1\end{bmatrix}, \quad C = \begin{bmatrix}1 & 0\end{bmatrix}$$
64+
65+
**Step 1 — Build observability matrix:**
66+
67+
$$\mathcal{O} = \begin{bmatrix}C\\CA\end{bmatrix} = \begin{bmatrix}1 & 0\\1 & 1\end{bmatrix}$$
68+
69+
**Step 2 — Evaluate desired polynomial at $A$:**
70+
71+
$$\varphi_d(z) = (z - 0.2)(z - 0.3) = z^2 - 0.5z + 0.06$$
72+
73+
$$\varphi_d(A) = A^2 - 0.5A + 0.06I = \begin{bmatrix}0.56 & 1.5\\0 & 0.56\end{bmatrix}$$
74+
75+
**Step 3 — Solve for gain:**
76+
77+
$$\mathcal{O}^{-1} = \begin{bmatrix}1 & 0\\-1 & 1\end{bmatrix}, \quad \mathcal{O}^{-1} e_2 = \begin{bmatrix}0\\1\end{bmatrix}$$
78+
79+
$$L = \varphi_d(A) \cdot \begin{bmatrix}0\\1\end{bmatrix} = \begin{bmatrix}1.5\\0.56\end{bmatrix}$$
80+
81+
**Verification:** eigenvalues of $A - LC = \begin{bmatrix}-0.5 & 1\\-0.56 & 1\end{bmatrix}$ are $\{0.2, 0.3\}$. ✓
82+
83+
## Pitfalls & Edge Cases
84+
85+
- **Unobservable pair.** If $\mathcal{O}$ is rank-deficient, the linear solve in Ackermann's formula fails. Verify observability with a rank test before design.
86+
- **Pole placement too aggressive.** Observer poles much faster than the controller poles amplify measurement noise, since every output error is fed back through $L$. Typical practice: observer poles 2–5× faster than controller poles.
87+
- **Deadbeat design** (all poles at zero) converges in exactly $n$ steps but maximizes noise sensitivity and requires large $L$ entries, risking numerical overflow in fixed-point.
88+
- **MIMO output limitation.** Ackermann's formula applies to SISO output ($p=1$). Multi-output systems require alternative pole-placement methods (e.g., Brogan's formula or numerical optimization).
89+
- **Model mismatch.** If the observer plant differs from the true plant, the error dynamics no longer satisfy $e[k+1] = (A-LC)e[k]$ exactly; a bounded steady-state error results rather than zero convergence.
90+
91+
## Variants & Generalizations
92+
93+
| Variant | Key Difference |
94+
|---------------------------------|-----------------------------------------------------------------------------------------------|
95+
| **Kalman Filter** | Stochastic design — minimizes covariance rather than placing poles; handles noise statistics |
96+
| **Extended Luenberger Observer**| Linearizes a nonlinear plant around the estimate for quasi-linear operation |
97+
| **Unknown-Input Observer** | Estimates states in the presence of unmeasured disturbances |
98+
| **Reduced-Order Observer** | Estimates only the unmeasured states, using measured outputs directly |
99+
| **Continuous-time observer** | Uses $\dot{\hat{x}} = A\hat{x} + Bu + L(y - C\hat{x})$; same structure, continuous pole placement |
100+
101+
## Applications
102+
103+
- **Motor control** — estimating velocity and back-EMF from position and current sensors.
104+
- **Automotive suspension** — reconstructing unsprung mass velocity from chassis accelerometers.
105+
- **Satellite attitude estimation** — inferring angular rates from gyros and star trackers.
106+
- **Observer-based compensator** — pairing with LQR to form the deterministic equivalent of LQG when noise statistics are unavailable.
107+
108+
## Connections to Other Algorithms
109+
110+
```mermaid
111+
graph LR
112+
LO["Luenberger Observer"]
113+
LTI["LinearTimeInvariant"]
114+
GE["Gaussian Elimination"]
115+
LQR["LQR Controller"]
116+
KF["Kalman Filter"]
117+
LTI -->|"plant model"| LO
118+
GE -->|"solve O^{-1}"| LO
119+
LO -->|"state estimate"| LQR
120+
KF -.->|"stochastic counterpart"| LO
121+
```
122+
123+
| Algorithm | Relationship |
124+
|----------------------------------------------------------------|---------------------------------------------------------------------------|
125+
| [Linear Time-Invariant Model](LinearTimeInvariant.md) | Supplies the $(A, B, C, D)$ matrices used in both design and update steps |
126+
| [Gaussian Elimination](../solvers/GaussianElimination.md) | Solves $\mathcal{O} x = e_n$ inside Ackermann's formula |
127+
| [LQR Controller](Lqr.md) | Primary consumer of the observer's state estimate |
128+
| [Kalman Filter](../filters/active/KalmanFilter.md) | Stochastic counterpart; adds noise covariance propagation |
129+
130+
## References & Further Reading
131+
132+
- Luenberger, D.G., "An Introduction to Observers," *IEEE Transactions on Automatic Control*, 16(6), 1971.
133+
- Franklin, G.F., Powell, J.D. and Emami-Naeini, A., *Feedback Control of Dynamic Systems*, 8th ed., Pearson, 2019 — Chapter 7.
134+
- Chen, C.-T., *Linear System Theory and Design*, 4th ed., Oxford University Press, 2013 — Chapter 8.

doc/controllers/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Feedback control algorithms for regulating dynamic systems in real time.
1313
| [MPC Controller](Mpc.md) | Model Predictive Controller — receding-horizon optimal control with constraint handling |
1414
| [Linear Time-Invariant Model](LinearTimeInvariant.md) | Discrete-time state-space plant model (A, B, C, D) shared across controllers and filters |
1515
| [Feedforward / 2-DOF Controller](Feedforward2Dof.md) | Feedforward + feedback wrapper for decoupled reference tracking and disturbance rejection |
16+
| [Luenberger Observer](LuenbergerObserver.md) | Deterministic state estimator using pole placement (Ackermann's formula) to reconstruct unmeasured states |

numerical/controllers/implementations/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ target_sources(numerical.controllers.implementations PRIVATE
1717
GainScheduledController.hpp
1818
Lqg.hpp
1919
Lqr.hpp
20+
LuenbergerObserver.hpp
2021
Mpc.hpp
2122
PidIncremental.hpp
2223
SaturationRateLimiter.hpp
@@ -28,6 +29,7 @@ numerical_add_coverage_sources(numerical.controllers.implementations
2829
GainScheduledController.cpp
2930
Lqg.cpp
3031
Lqr.cpp
32+
LuenbergerObserver.cpp
3133
Mpc.cpp
3234
PidIncremental.cpp
3335
SaturationRateLimiter.cpp
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include "numerical/controllers/implementations/LuenbergerObserver.hpp"
2+
3+
namespace controllers
4+
{
5+
template class LuenbergerObserver<float, 2, 1, 1>;
6+
template class LuenbergerObserver<float, 3, 1, 1>;
7+
template class LuenbergerObserver<float, 2, 2, 1>;
8+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#pragma once
2+
3+
#if defined(__GNUC__) || defined(__clang__)
4+
#pragma GCC optimize("O3", "fast-math")
5+
#endif
6+
7+
#include "numerical/math/CompilerOptimizations.hpp"
8+
#include "numerical/math/LinearTimeInvariant.hpp"
9+
#include "numerical/solvers/GaussianElimination.hpp"
10+
#include <array>
11+
#include <cstddef>
12+
#include <type_traits>
13+
14+
namespace controllers
15+
{
16+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
17+
class LuenbergerObserver
18+
{
19+
static_assert(std::is_floating_point_v<T>, "LuenbergerObserver supports floating-point types");
20+
static_assert(StateSize > 0, "StateSize must be positive");
21+
static_assert(InputSize > 0, "InputSize must be positive");
22+
static_assert(OutputSize > 0, "OutputSize must be positive");
23+
24+
public:
25+
using Plant = math::LinearTimeInvariant<T, StateSize, InputSize, OutputSize>;
26+
using StateMatrix = math::SquareMatrix<T, StateSize>;
27+
using GainMatrix = math::Matrix<T, StateSize, OutputSize>;
28+
using StateVector = math::Vector<T, StateSize>;
29+
using InputVector = math::Vector<T, InputSize>;
30+
using OutputVector = math::Vector<T, OutputSize>;
31+
32+
LuenbergerObserver(const Plant& plant, const GainMatrix& observerGain);
33+
34+
static GainMatrix AckermannGain(const Plant& plant, const std::array<T, StateSize>& desiredPoles);
35+
36+
OPTIMIZE_FOR_SPEED StateVector Update(const InputVector& u, const OutputVector& y);
37+
38+
[[nodiscard]] const StateVector& Estimate() const;
39+
40+
void Reset(const StateVector& x0);
41+
42+
private:
43+
static StateMatrix BuildObservabilityMatrix(const StateMatrix& A, const math::Matrix<T, OutputSize, StateSize>& C);
44+
static StateMatrix EvaluateCharacteristicPoly(const StateMatrix& A, const std::array<T, StateSize>& poles);
45+
46+
Plant plant;
47+
GainMatrix L;
48+
StateVector xhat;
49+
};
50+
51+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
52+
LuenbergerObserver<T, StateSize, InputSize, OutputSize>::LuenbergerObserver(
53+
const Plant& plantModel, const GainMatrix& observerGain)
54+
: plant{ plantModel }
55+
, L{ observerGain }
56+
, xhat{}
57+
{}
58+
59+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
60+
OPTIMIZE_FOR_SPEED
61+
typename LuenbergerObserver<T, StateSize, InputSize, OutputSize>::StateVector
62+
LuenbergerObserver<T, StateSize, InputSize, OutputSize>::Update(
63+
const InputVector& u, const OutputVector& y)
64+
{
65+
OutputVector yhat = plant.C * xhat + plant.D * u;
66+
OutputVector innovation = y - yhat;
67+
xhat = plant.A * xhat + plant.B * u + L * innovation;
68+
return xhat;
69+
}
70+
71+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
72+
const typename LuenbergerObserver<T, StateSize, InputSize, OutputSize>::StateVector&
73+
LuenbergerObserver<T, StateSize, InputSize, OutputSize>::Estimate() const
74+
{
75+
return xhat;
76+
}
77+
78+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
79+
void LuenbergerObserver<T, StateSize, InputSize, OutputSize>::Reset(const StateVector& x0)
80+
{
81+
xhat = x0;
82+
}
83+
84+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
85+
typename LuenbergerObserver<T, StateSize, InputSize, OutputSize>::StateMatrix
86+
LuenbergerObserver<T, StateSize, InputSize, OutputSize>::BuildObservabilityMatrix(
87+
const StateMatrix& A, const math::Matrix<T, OutputSize, StateSize>& C)
88+
{
89+
static_assert(OutputSize == 1, "Ackermann's formula requires SISO output (OutputSize == 1)");
90+
StateMatrix O{};
91+
math::Matrix<T, OutputSize, StateSize> CAk = C;
92+
for (std::size_t k = 0; k < StateSize; ++k)
93+
{
94+
for (std::size_t col = 0; col < StateSize; ++col)
95+
O.at(k, col) = CAk.at(0, col);
96+
CAk = CAk * A;
97+
}
98+
return O;
99+
}
100+
101+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
102+
typename LuenbergerObserver<T, StateSize, InputSize, OutputSize>::StateMatrix
103+
LuenbergerObserver<T, StateSize, InputSize, OutputSize>::EvaluateCharacteristicPoly(
104+
const StateMatrix& A, const std::array<T, StateSize>& poles)
105+
{
106+
StateMatrix result = StateMatrix::Identity();
107+
for (std::size_t i = 0; i < StateSize; ++i)
108+
{
109+
StateMatrix shifted = A;
110+
for (std::size_t r = 0; r < StateSize; ++r)
111+
shifted.at(r, r) = shifted.at(r, r) - poles[i];
112+
result = result * shifted;
113+
}
114+
return result;
115+
}
116+
117+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
118+
typename LuenbergerObserver<T, StateSize, InputSize, OutputSize>::GainMatrix
119+
LuenbergerObserver<T, StateSize, InputSize, OutputSize>::AckermannGain(
120+
const Plant& plant, const std::array<T, StateSize>& desiredPoles)
121+
{
122+
static_assert(OutputSize == 1, "Ackermann's formula requires SISO output (OutputSize == 1)");
123+
124+
StateMatrix O = BuildObservabilityMatrix(plant.A, plant.C);
125+
StateMatrix phi = EvaluateCharacteristicPoly(plant.A, desiredPoles);
126+
127+
StateVector eLast{};
128+
eLast.at(StateSize - 1, 0) = T(1);
129+
130+
StateVector OinvEn = solvers::SolveSystem<T, StateSize, 1>(O, eLast);
131+
132+
StateVector Lgain = phi * OinvEn;
133+
134+
GainMatrix result{};
135+
for (std::size_t i = 0; i < StateSize; ++i)
136+
result.at(i, 0) = Lgain.at(i, 0);
137+
return result;
138+
}
139+
140+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
141+
extern template class LuenbergerObserver<float, 2, 1, 1>;
142+
extern template class LuenbergerObserver<float, 3, 1, 1>;
143+
extern template class LuenbergerObserver<float, 2, 2, 1>;
144+
#endif
145+
}

numerical/controllers/implementations/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ target_sources(numerical.controllers_test PRIVATE
1414
TestGainScheduledController.cpp
1515
TestLqg.cpp
1616
TestLqr.cpp
17+
TestLuenbergerObserver.cpp
1718
TestMpc.cpp
1819
TestPidIncremental.cpp
1920
TestSaturationRateLimiter.cpp

0 commit comments

Comments
 (0)