Skip to content

Commit c12de89

Browse files
feat: add alpha beta filter (#171)
* add alpha beta filter * fix unit test * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent e31a58b commit c12de89

11 files changed

Lines changed: 389 additions & 178 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
2121
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF |
2222
| [Dynamics](doc/dynamics/README.md) | Euler-Lagrange, Newton-Euler, Recursive Newton-Euler, ABA |
2323
| [Estimators](doc/estimators/README.md) | Linear Regression, Yule-Walker (offline), Recursive Least Squares (online) |
24-
| [Filters](doc/filters/README.md) | Kalman, Extended Kalman, Unscented Kalman, Complementary, FIR, IIR, Exponential Moving Average, Moving Average, Median Filter |
24+
| [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 |
2525
| [Kinematics](doc/kinematics/README.md) | Forward Kinematics |
2626
| [Neural Network](doc/neural_network/README.md) | Layers, activations, losses, model |
2727
| [Optimization](doc/optimization/README.md) | Gradient Descent |

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-
| 8 | Alpha-beta / alpha-beta-gamma filter | `filters/active` | ★★☆☆☆ |
3130
| 10 | Gain-scheduled controller | `controllers` | ★★☆☆☆ |
3231
| 11 | Convolution & correlation utilities | `analysis` | ★★☆☆☆ |
3332
| 12 | Polynomial least-squares curve fitting | `estimators/offline` | ★★☆☆☆ |
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Alpha-Beta / Alpha-Beta-Gamma Filter
2+
3+
## Overview & Motivation
4+
5+
In embedded control and tracking applications, a sensor delivers a position measurement every sample period, but that measurement is corrupted by noise. A simple lowpass filter smooths the noise but cannot estimate velocity, which is needed for prediction and control. A full Kalman filter computes optimal gains but requires covariance propagation — a matrix inverse every step — which is too expensive for a fast ISR.
6+
7+
The alpha-beta (and its extension, alpha-beta-gamma) filter resolves this tension. It maintains a position and velocity estimate (and optionally acceleration) using only a few multiply-adds per sample. The gains are fixed constants, computed once at design time from a single scalar parameter. The result is a deterministic, constant-time predictor-corrector that delivers most of the benefit of a steady-state Kalman filter at a fraction of the cost.
8+
9+
## Mathematical Theory
10+
11+
### State Model
12+
13+
The filter assumes constant-velocity (order 2) or constant-acceleration (order 3) kinematics. For order 2, the state is $\mathbf{x} = [p, \dot{p}]^\top$; for order 3, $\mathbf{x} = [p, \dot{p}, \ddot{p}]^\top$.
14+
15+
### Predict Step
16+
17+
$$\hat{p}^- = \hat{p} + T_s \hat{v} + \tfrac{1}{2} T_s^2 \hat{a} \quad (\hat{a} \text{ omitted for order 2})$$
18+
$$\hat{v}^- = \hat{v} + T_s \hat{a} \quad (\hat{a} \text{ omitted for order 2})$$
19+
$$\hat{a}^- = \hat{a}$$
20+
21+
### Correct Step
22+
23+
Let the innovation (residual) be $r = z - \hat{p}^-$, where $z$ is the measured position. Then:
24+
25+
$$\hat{p} = \hat{p}^- + \alpha r$$
26+
$$\hat{v} = \hat{v}^- + \frac{\beta}{T_s} r$$
27+
$$\hat{a} = \hat{a}^- + \frac{2\gamma}{T_s^2} r \quad (\text{order 3 only})$$
28+
29+
The denominators $T_s$ and $T_s^2$ convert the dimensionless residual into velocity and acceleration corrections.
30+
31+
### Kalata Steady-State Design (Tracking Index)
32+
33+
For the order-2 case, Kalata (1984) defines the tracking index $\lambda = \frac{\sigma_w T_s^2}{\sigma_v}$, where $\sigma_w$ is process noise intensity and $\sigma_v$ is measurement noise standard deviation. The critically-damped gains are:
34+
35+
$$r = \frac{4 + \lambda - \sqrt{8\lambda + \lambda^2}}{4}$$
36+
$$\alpha = 1 - r^2$$
37+
$$\beta = 2(2 - \alpha) - 4\sqrt{1 - \alpha}$$
38+
39+
A single scalar $\lambda$ thus controls the smoothing/lag trade-off.
40+
41+
### Stability Conditions
42+
43+
For the order-2 filter, Simpson's triangle requires:
44+
45+
$$0 < \alpha < 1, \qquad 0 < \beta < 4 - 2\alpha$$
46+
47+
Violation of the second bound causes oscillatory divergence.
48+
49+
## Complexity Analysis
50+
51+
| Case | Time | Space | Notes |
52+
|---------|--------|--------|-------------------------------------------------|
53+
| Best | $O(1)$ | $O(N)$ | $N \in \{2, 3\}$ state words plus fixed scalars |
54+
| Average | $O(1)$ | $O(N)$ | same |
55+
| Worst | $O(1)$ | $O(N)$ | gains are precomputed; no covariance update |
56+
57+
The hot path is a handful of fused multiply-add operations: predict costs 2–4 MACs, correct costs 2–3 MACs.
58+
59+
## Step-by-Step Walkthrough
60+
61+
Consider an order-2 filter with $\alpha = 0.5$, $\beta = 0.1$, $T_s = 1.0\,\text{s}$, measuring a ramp $z[n] = 0.2n$.
62+
63+
| Step | $z$ | $\hat{p}^-$ | $\hat{v}^-$ | $r$ | $\hat{p}$ | $\hat{v}$ |
64+
|----------|-----|-------------|-------------|------|-----------|-----------|
65+
| 0 (seed) | 0.0 |||| 0.0 | 0.0 |
66+
| 1 | 0.2 | 0.0 | 0.0 | 0.2 | 0.10 | 0.020 |
67+
| 2 | 0.4 | 0.12 | 0.020 | 0.28 | 0.26 | 0.048 |
68+
||||||||
69+
70+
After several hundred steps, $\hat{v} \to 0.2$ and lag $\to 0$.
71+
72+
## Pitfalls & Edge Cases
73+
74+
- **Small $T_s$**: the corrections $\beta/T_s$ and $2\gamma/T_s^2$ grow large. Precomputing these as constants (done at construction) avoids repeated division on the hot path and flags numerical range issues early.
75+
- **Stability boundary**: gains near $\beta = 4 - 2\alpha$ produce marginally stable responses. In practice, keep $\beta < 3 - 2\alpha$ for a margin of safety.
76+
- **Initialization**: the first sample seeds the position; velocity and acceleration are zero. Transient overshoot on a step input decays at a rate governed by the gains.
77+
- **Order-3 on a ramp**: the acceleration state will correctly settle near zero rather than accumulating a phantom bias, provided gains are stable.
78+
79+
## Variants & Generalizations
80+
81+
- **Order 2 ($\alpha$-$\beta$)**: tracks position and velocity; optimal for constant-velocity targets.
82+
- **Order 3 ($\alpha$-$\beta$-$\gamma$)**: adds acceleration; suitable for maneuvering targets but requires additional tuning of $\gamma$.
83+
- **Adaptive gains**: switching $\alpha$ between large (maneuver) and small (coast) values gives an interactive multiple-model (IMM) flavor without full Kalman complexity.
84+
- **Steady-state Kalman**: the $\alpha$-$\beta$ filter is exactly a scalar Kalman filter whose Riccati equation has converged, making $\lambda$ the natural design parameter.
85+
86+
## Applications
87+
88+
- **Radar / ranging**: smoothing noisy range or angle measurements while estimating radial velocity.
89+
- **Motor control**: fusing encoder position to estimate shaft velocity for a feedback loop.
90+
- **IMU pre-filtering**: attenuating high-frequency vibration before integrating acceleration.
91+
- **Any tight ISR**: when covariance propagation is too expensive but a plain IIR gives no velocity.
92+
93+
## Connections to Other Algorithms
94+
95+
- **KalmanFilter**: the $\alpha$-$\beta$ filter is its steady-state specialization; the full filter adapts gains to non-stationary noise.
96+
- **ExponentialMovingAverage**: position-only smoothing — no velocity estimate, equivalent to $\beta = 0$.
97+
- **ComplementaryFilter**: fuses two sensors in the frequency domain; similar predict/correct intuition but requires two measurement streams.
98+
99+
## References & Further Reading
100+
101+
- P. Kalata, "The tracking index: A generalized parameter for alpha-beta and alpha-beta-gamma target trackers," *IEEE Transactions on Aerospace and Electronic Systems*, 20(2), pp. 174–182, 1984.
102+
- S. Blackman and R. Popoli, *Design and Analysis of Modern Tracking Systems*, Artech House, 1999.
103+
- R. G. Brown and P. Y. C. Hwang, *Introduction to Random Signals and Applied Kalman Filtering*, 4th ed., Wiley, 2012.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include "numerical/filters/active/AlphaBetaFilter.hpp"
2+
3+
namespace filters
4+
{
5+
template class AlphaBetaFilter<float, 2>;
6+
template class AlphaBetaFilter<float, 3>;
7+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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 <array>
9+
#include <cmath>
10+
#include <cstddef>
11+
#include <type_traits>
12+
13+
namespace filters
14+
{
15+
template<typename T, std::size_t Order>
16+
class AlphaBetaFilter
17+
{
18+
static_assert(std::is_floating_point_v<T>, "AlphaBetaFilter supports floating-point types");
19+
static_assert(Order == 2 || Order == 3, "AlphaBetaFilter Order must be 2 or 3");
20+
21+
public:
22+
struct Gains
23+
{
24+
T alpha;
25+
T beta;
26+
};
27+
28+
AlphaBetaFilter(T alpha, T beta, T Ts)
29+
requires(Order == 2);
30+
31+
AlphaBetaFilter(T alpha, T beta, T gamma, T Ts)
32+
requires(Order == 3);
33+
34+
OPTIMIZE_FOR_SPEED T Filter(T measuredPosition);
35+
36+
std::array<T, Order> State() const;
37+
38+
void Reset(T position = T{});
39+
40+
static Gains GainsFromTrackingIndex(T lambda);
41+
42+
private:
43+
T samplePeriod{};
44+
T gainAlpha{};
45+
T gainBeta{};
46+
T gainGamma{};
47+
T betaOverTs{};
48+
T twoGammaOverTs2{};
49+
std::array<T, Order> state{};
50+
bool initialized{ false };
51+
};
52+
53+
// Implementation //
54+
55+
template<typename T, std::size_t Order>
56+
AlphaBetaFilter<T, Order>::AlphaBetaFilter(T alpha, T beta, T Ts)
57+
requires(Order == 2)
58+
: samplePeriod{ Ts }
59+
, gainAlpha{ alpha }
60+
, gainBeta{ beta }
61+
, gainGamma{ T{} }
62+
, betaOverTs{ beta / Ts }
63+
, twoGammaOverTs2{ T{} }
64+
{}
65+
66+
template<typename T, std::size_t Order>
67+
AlphaBetaFilter<T, Order>::AlphaBetaFilter(T alpha, T beta, T gamma, T Ts)
68+
requires(Order == 3)
69+
: samplePeriod{ Ts }
70+
, gainAlpha{ alpha }
71+
, gainBeta{ beta }
72+
, gainGamma{ gamma }
73+
, betaOverTs{ beta / Ts }
74+
, twoGammaOverTs2{ T{ 2 } * gamma / (Ts * Ts) }
75+
{}
76+
77+
template<typename T, std::size_t Order>
78+
OPTIMIZE_FOR_SPEED T AlphaBetaFilter<T, Order>::Filter(T measuredPosition)
79+
{
80+
if (!initialized)
81+
{
82+
state[0] = measuredPosition;
83+
initialized = true;
84+
return measuredPosition;
85+
}
86+
87+
T predicted0{};
88+
T predicted1{};
89+
T predicted2{};
90+
91+
if constexpr (Order == 3)
92+
{
93+
predicted0 = state[0] + samplePeriod * state[1] + T{ 0.5 } * samplePeriod * samplePeriod * state[2];
94+
predicted1 = state[1] + samplePeriod * state[2];
95+
predicted2 = state[2];
96+
}
97+
else
98+
{
99+
predicted0 = state[0] + samplePeriod * state[1];
100+
predicted1 = state[1];
101+
}
102+
103+
T residual{ measuredPosition - predicted0 };
104+
105+
state[0] = predicted0 + gainAlpha * residual;
106+
state[1] = predicted1 + betaOverTs * residual;
107+
108+
if constexpr (Order == 3)
109+
{
110+
state[2] = predicted2 + twoGammaOverTs2 * residual;
111+
}
112+
113+
return state[0];
114+
}
115+
116+
template<typename T, std::size_t Order>
117+
std::array<T, Order> AlphaBetaFilter<T, Order>::State() const
118+
{
119+
return state;
120+
}
121+
122+
template<typename T, std::size_t Order>
123+
void AlphaBetaFilter<T, Order>::Reset(T position)
124+
{
125+
state = {};
126+
state[0] = position;
127+
initialized = false;
128+
}
129+
130+
template<typename T, std::size_t Order>
131+
typename AlphaBetaFilter<T, Order>::Gains AlphaBetaFilter<T, Order>::GainsFromTrackingIndex(T lambda)
132+
{
133+
T r{ (T{ 4 } + lambda - std::sqrt(T{ 8 } * lambda + lambda * lambda)) / T{ 4 } };
134+
T alpha{ T{ 1 } - r * r };
135+
T beta{ T{ 2 } * (T{ 2 } - alpha) - T{ 4 } * std::sqrt(T{ 1 } - alpha) };
136+
return Gains{ alpha, beta };
137+
}
138+
139+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
140+
extern template class AlphaBetaFilter<float, 2>;
141+
extern template class AlphaBetaFilter<float, 3>;
142+
#endif
143+
}

numerical/filters/active/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ target_link_libraries(numerical.filters.active ${NUMERICAL_VISIBILITY}
1212
)
1313

1414
target_sources(numerical.filters.active PRIVATE
15+
AlphaBetaFilter.hpp
1516
ComplementaryFilter.hpp
1617
ExtendedKalmanFilter.hpp
1718
KalmanFilter.hpp
@@ -21,6 +22,7 @@ target_sources(numerical.filters.active PRIVATE
2122
)
2223

2324
numerical_add_coverage_sources(numerical.filters.active
25+
AlphaBetaFilter.cpp
2426
ComplementaryFilter.cpp
2527
ExtendedKalmanFilter.cpp
2628
KalmanFilter.cpp

numerical/filters/active/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ target_link_libraries(numerical.filters.active_test PUBLIC
88
)
99

1010
target_sources(numerical.filters.active_test PRIVATE
11+
TestAlphaBetaFilter.cpp
1112
TestComplementaryFilter.cpp
1213
TestExtendedKalmanFilter.cpp
1314
TestKalmanFilter.cpp

0 commit comments

Comments
 (0)