Skip to content

Commit 6fdfd61

Browse files
Merge branch 'main' into feature/add-biquad-filter
2 parents 03be881 + 2e933fb commit 6fdfd61

12 files changed

Lines changed: 448 additions & 184 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, Real-Input FFT (RFFT), Power Spectral Density, DCT, Window Functions, Signal Detectors, Convolution & Correlation, Goertzel Algorithm |
2020
| [Control Analysis](doc/control_analysis/README.md) | Frequency Response, Root Locus, Controllability/Observability Matrices & Gramians |
21-
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF, Gain-Scheduled Controller, Lead-Lag Compensator, Luenberger Observer |
21+
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, LQI (Integral/Servo State Feedback), MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF, Gain-Scheduled Controller, Lead-Lag Compensator, 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, LMS / NLMS Adaptive Filter (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, Savitzky-Golay Filter, Biquad/Second-Order-Section Cascade |

ROADMAP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ Difficulty legend:
2727

2828
| # | Component | Target module | Difficulty |
2929
|----|------------------------------------------------------|---------------------------|------------|
30-
| 20 | Integral / servo state feedback (LQI) | `controllers` | ★★★☆☆ |
3130
| 27 | QR decomposition (Householder / Givens) | `solvers` | ★★★★☆ |
3231
| 28 | LU decomposition with partial pivoting | `solvers` | ★★★★☆ |
3332
| 29 | Matrix exponential (scaling & squaring + Padé) | `math` | ★★★★☆ |
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Integral State Feedback (LQI / Servo)
2+
3+
## Overview & Motivation
4+
5+
Plain LQR drives the system state toward the origin but leaves a persistent steady-state offset when a constant reference or disturbance is present. The integral state feedback controller
6+
(LQI, or servo LQR) removes this offset by augmenting the plant with integrators on the
7+
tracking error. A single LQR solve on the augmented system produces two gains: one that
8+
acts on the physical states and one that closes the integral loop, guaranteeing zero
9+
steady-state error to constant references with no manual trim.
10+
11+
## Mathematical Theory
12+
13+
### Plant Model
14+
15+
The discrete-time plant is
16+
17+
$$x_{k+1} = A x_k + B u_k, \quad y_k = C x_k$$
18+
19+
with state $x \in \mathbb{R}^n$, input $u \in \mathbb{R}^m$, and tracked output $y \in \mathbb{R}^p$.
20+
21+
### Augmented Plant
22+
23+
Define the integral-of-error state $x_i \in \mathbb{R}^p$:
24+
25+
$$x_{i,k+1} = x_{i,k} + (r_k - y_k) T_s$$
26+
27+
Stacking $\xi = [x^\top \; x_i^\top]^\top$ gives the augmented system
28+
29+
$$\xi_{k+1} = A_a \xi_k + B_a u_k + E_a r_k$$
30+
31+
$$A_a = \begin{bmatrix} A & 0 \\ -C T_s & I \end{bmatrix}, \quad B_a = \begin{bmatrix} B \\ 0 \end{bmatrix}, \quad E_a = \begin{bmatrix} 0 \\ T_s I \end{bmatrix}$$
32+
33+
### LQR Design on the Augmented Plant
34+
35+
Minimise the infinite-horizon quadratic cost
36+
37+
$$J = \sum_{k=0}^{\infty} \bigl(\xi_k^\top Q \xi_k + u_k^\top R u_k\bigr)$$
38+
39+
by solving the Discrete Algebraic Riccati Equation (DARE) for $P$:
40+
41+
$$P = A_a^\top P A_a - A_a^\top P B_a (R + B_a^\top P B_a)^{-1} B_a^\top P A_a + Q$$
42+
43+
The optimal gain partitions as $K_a = [K_x \mid K_i]$ where $K_x \in \mathbb{R}^{m \times n}$ acts
44+
on the physical states and $K_i \in \mathbb{R}^{m \times p}$ acts on the integral states.
45+
46+
### Control Law
47+
48+
$$u_k = -K_x x_k - K_i x_{i,k}$$
49+
50+
At equilibrium $r - y = 0$, so $x_{i}$ stops changing, and the control law holds $y = r$ exactly.
51+
52+
## Complexity Analysis
53+
54+
| Case | Time | Space | Notes |
55+
|--------|--------------|--------------|------------------------------------|
56+
| Design | $O((n+p)^3)$ | $O((n+p)^2)$ | DARE iteration on augmented system |
57+
| Update | $O(m(n+p))$ | $O(p)$ | Two matrix-vector products |
58+
59+
Design is a one-time offline computation. The per-sample update cost is dominated by the two
60+
gain-state products and the integral accumulation.
61+
62+
## Step-by-Step Walkthrough
63+
64+
Consider a scalar plant ($n=1$, $m=1$, $p=1$, $T_s = 0.01$):
65+
66+
1. Form $A_a$ (2×2), $B_a$ (2×1) from plant matrices and $T_s$.
67+
2. Choose $Q$ (2×2 diagonal) and $R$ (scalar) to weight states and input.
68+
3. Solve DARE → $P$ (2×2) → $K_a = [k_x \; k_i]$ (1×2).
69+
4. Per step: accumulate $x_i \mathrel{+}= (r - y) T_s$, output $u = -k_x x - k_i x_i$.
70+
5. After ~200 steps the output converges to $r$ within numerical tolerance.
71+
72+
## Pitfalls & Edge Cases
73+
74+
- **Integral windup**: when the actuator saturates, the integral keeps growing because the
75+
control cannot reach the demanded value. Apply a clamp on $x_i$ or back-calculate
76+
(anti-windup) to prevent divergence after the saturation clears.
77+
- **Marginally stable plant**: a plant with an open-loop integrator combined with the error
78+
integrator yields a double-integrator augmented system. The DARE still converges if the
79+
augmented pair $(A_a, B_a)$ is stabilisable; verify that condition before deploying.
80+
- **Slow integral weighting**: under-weighting the integral state in $Q$ allows steady-state
81+
error to persist for many steps before correcting; over-weighting causes overshoot.
82+
- **Sample-time mismatch**: the discrete integral $x_i$ accumulates $T_s$-scaled errors.
83+
Using the wrong $T_s$ at run time shifts the effective integral gain and breaks zero-error
84+
convergence.
85+
86+
## Variants & Generalizations
87+
88+
- **Continuous-time LQI**: replace the discrete integrator with $\dot{x}_i = r - y$ and solve
89+
the continuous ARE.
90+
- **Output-feedback LQI (LQGI)**: combine with a Kalman filter when only the output (not the
91+
full state) is measurable — the separation principle still holds.
92+
- **Anti-windup**: add a saturation block on $x_i$ with back-calculation to recover from
93+
actuator limits without integral drift.
94+
- **Multiple outputs**: the design extends directly to $p > 1$ by stacking $p$ integral states.
95+
96+
## Applications
97+
98+
- **Motor position/speed servo**: eliminate gravity or friction offsets without manual trim.
99+
- **Process control**: temperature or pressure regulation with constant load disturbances.
100+
- **Aerospace attitude control**: integral action compensates for persistent aerodynamic moments.
101+
- **Robotics**: joint torque control with payload-induced constant forces.
102+
103+
## Connections to Other Algorithms
104+
105+
- **Lqr**: the base regulator; LQI delegates the DARE solve to `Lqr` on the augmented plant.
106+
- **DiscreteAlgebraicRiccatiEquation**: the inner solver used by `Lqr`.
107+
- **Lqg**: pairs `Lqr` with a Kalman filter; LQI could similarly pair with `Lqg` for
108+
output-feedback servo control.
109+
- **Pid**: the integral channel of a PID is the scalar, single-output analogue of $K_i x_i$.
110+
- **SaturationRateLimiter**: provides output clamping for anti-windup on the LQI integral.
111+
112+
## References & Further Reading
113+
114+
- B. D. O. Anderson, J. B. Moore, *Optimal Control: Linear Quadratic Methods*, Prentice Hall, 1990.
115+
- G. F. Franklin, J. D. Powell, A. Emami-Naeini, *Feedback Control of Dynamic Systems*, 8th ed., Pearson, 2019. Chapter 9.
116+
- K. J. Åström, B. Wittenmark, *Computer-Controlled Systems: Theory and Design*, 3rd ed., Prentice Hall, 1997. Chapter 5.

doc/controllers/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Feedback control algorithms for regulating dynamic systems in real time.
99
| [Bang-Bang / Hysteresis Controller](BangBangHysteresis.md) | Two-state relay controller with Schmitt-trigger dead-band to prevent chatter |
1010
| [PID Controller](Pid.md) | Proportional-Integral-Derivative controller using a discrete recursive formulation |
1111
| [LQR Controller](Lqr.md) | Linear Quadratic Regulator — optimal state-feedback control minimizing a quadratic cost |
12+
| [LQI / Servo Controller](IntegralStateFeedbackLqi.md) | Integral state feedback (LQI) — LQR augmented with integrators on the tracking error for zero steady-state offset |
1213
| [LQG Controller](Lqg.md) | Linear Quadratic Gaussian — output-feedback optimal control via LQR + Kalman Filter |
1314
| [MPC Controller](Mpc.md) | Model Predictive Controller — receding-horizon optimal control with constraint handling |
1415
| [Linear Time-Invariant Model](LinearTimeInvariant.md) | Discrete-time state-space plant model (A, B, C, D) shared across controllers and filters |

numerical/controllers/implementations/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ target_sources(numerical.controllers.implementations PRIVATE
1515
BangBangHysteresis.hpp
1616
Feedforward2Dof.hpp
1717
GainScheduledController.hpp
18+
IntegralStateFeedbackLqi.hpp
1819
LeadLagCompensator.hpp
1920
Lqg.hpp
2021
Lqr.hpp
@@ -28,6 +29,7 @@ numerical_add_coverage_sources(numerical.controllers.implementations
2829
BangBangHysteresis.cpp
2930
Feedforward2Dof.cpp
3031
GainScheduledController.cpp
32+
IntegralStateFeedbackLqi.cpp
3133
LeadLagCompensator.cpp
3234
Lqg.cpp
3335
Lqr.cpp
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include "numerical/controllers/implementations/IntegralStateFeedbackLqi.hpp"
2+
3+
namespace controllers
4+
{
5+
template class IntegralStateFeedbackLqi<float, 2, 1, 1>;
6+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#pragma once
2+
3+
#if defined(__GNUC__) || defined(__clang__)
4+
#pragma GCC optimize("O3", "fast-math")
5+
#endif
6+
7+
#include "numerical/controllers/implementations/Lqr.hpp"
8+
#include "numerical/math/CompilerOptimizations.hpp"
9+
#include "numerical/math/LinearTimeInvariant.hpp"
10+
#include "numerical/math/Matrix.hpp"
11+
12+
namespace controllers
13+
{
14+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
15+
class IntegralStateFeedbackLqi
16+
{
17+
static_assert(std::is_floating_point_v<T>, "IntegralStateFeedbackLqi supports floating-point types");
18+
19+
static constexpr std::size_t AugmentedSize = StateSize + OutputSize;
20+
21+
public:
22+
using StateVector = math::Vector<T, StateSize>;
23+
using InputVector = math::Vector<T, InputSize>;
24+
using OutputVector = math::Vector<T, OutputSize>;
25+
using GainStateMatrix = math::Matrix<T, InputSize, StateSize>;
26+
using GainIntegralMatrix = math::Matrix<T, InputSize, OutputSize>;
27+
using IntegralVector = math::Vector<T, OutputSize>;
28+
29+
IntegralStateFeedbackLqi(const GainStateMatrix& kx, const GainIntegralMatrix& ki, T ts);
30+
31+
IntegralStateFeedbackLqi(
32+
const math::LinearTimeInvariant<T, StateSize, InputSize, OutputSize>& plant,
33+
const math::SquareMatrix<T, AugmentedSize>& Q,
34+
const math::SquareMatrix<T, InputSize>& R,
35+
T ts);
36+
37+
OPTIMIZE_FOR_SPEED InputVector ComputeControl(
38+
const StateVector& x,
39+
const OutputVector& reference,
40+
const OutputVector& measured);
41+
42+
void Reset();
43+
44+
[[nodiscard]] const GainStateMatrix& GetGainState() const;
45+
[[nodiscard]] const GainIntegralMatrix& GetGainIntegral() const;
46+
47+
private:
48+
GainStateMatrix gainState{};
49+
GainIntegralMatrix gainIntegral{};
50+
IntegralVector integral{};
51+
T sampleTime{};
52+
};
53+
54+
// Implementation //
55+
56+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
57+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::IntegralStateFeedbackLqi(
58+
const GainStateMatrix& kx, const GainIntegralMatrix& ki, T ts)
59+
: gainState{ kx }
60+
, gainIntegral{ ki }
61+
, sampleTime{ ts }
62+
{}
63+
64+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
65+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::IntegralStateFeedbackLqi(
66+
const math::LinearTimeInvariant<T, StateSize, InputSize, OutputSize>& plant,
67+
const math::SquareMatrix<T, AugmentedSize>& Q,
68+
const math::SquareMatrix<T, InputSize>& R,
69+
T ts)
70+
: sampleTime{ ts }
71+
{
72+
math::SquareMatrix<T, AugmentedSize> Aa{};
73+
math::Matrix<T, AugmentedSize, InputSize> Ba{};
74+
75+
Aa.SetBlock(plant.A, 0, 0);
76+
Aa.SetBlock(plant.C * T(-ts), StateSize, 0);
77+
for (std::size_t r = 0; r < OutputSize; ++r)
78+
Aa.at(StateSize + r, StateSize + r) = T(1);
79+
Ba.SetBlock(plant.B, 0, 0);
80+
81+
Lqr<T, AugmentedSize, InputSize> lqr{ Aa, Ba, Q, R };
82+
const auto& Ka = lqr.GetGain();
83+
84+
gainState = Ka.template GetBlock<InputSize, StateSize>(0, 0);
85+
gainIntegral = Ka.template GetBlock<InputSize, OutputSize>(0, StateSize);
86+
}
87+
88+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
89+
OPTIMIZE_FOR_SPEED
90+
typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::InputVector
91+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::ComputeControl(
92+
const StateVector& x,
93+
const OutputVector& reference,
94+
const OutputVector& measured)
95+
{
96+
auto error = reference - measured;
97+
integral = integral + error * sampleTime;
98+
return (gainState * x + gainIntegral * integral) * T(-1);
99+
}
100+
101+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
102+
void IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::Reset()
103+
{
104+
integral = IntegralVector{};
105+
}
106+
107+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
108+
const typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GainStateMatrix&
109+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GetGainState() const
110+
{
111+
return gainState;
112+
}
113+
114+
template<typename T, std::size_t StateSize, std::size_t InputSize, std::size_t OutputSize>
115+
const typename IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GainIntegralMatrix&
116+
IntegralStateFeedbackLqi<T, StateSize, InputSize, OutputSize>::GetGainIntegral() const
117+
{
118+
return gainIntegral;
119+
}
120+
121+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
122+
extern template class IntegralStateFeedbackLqi<float, 2, 1, 1>;
123+
#endif
124+
}

numerical/controllers/implementations/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ target_sources(numerical.controllers_test PRIVATE
1212
TestBangBangHysteresis.cpp
1313
TestFeedforward2Dof.cpp
1414
TestGainScheduledController.cpp
15+
TestIntegralStateFeedbackLqi.cpp
1516
TestLeadLagCompensator.cpp
1617
TestLqg.cpp
1718
TestLqr.cpp

0 commit comments

Comments
 (0)