Skip to content

Commit 4256f8d

Browse files
feat: add smc robust control (#207)
* add smc robust control * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * increase coverage --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 3992b55 commit 4256f8d

13 files changed

Lines changed: 489 additions & 186 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
2828
| [Regularization](doc/regularization/README.md) | L1 (Lasso), L2 (Ridge) |
2929
| [Math](doc/math/README.md) | CORDIC, Quaternion, MatrixNorms, Step Response Metrics, MatrixExponential |
3030
| [Solvers](doc/solvers/README.md) | Gaussian Elimination, Levinson-Durbin, Durand-Kerner, Cholesky, DARE, Runge-Kutta ODE Integrators (RK4 + Dormand-Prince), Spectral Radius & Discrete Stability Margin, QR Decomposition (Householder / Givens), LU Decomposition with Partial Pivoting |
31+
| [Robust Control](doc/robust_control/README.md) | Sliding Mode Control (SMC) |
3132
| [Performance Optimization](doc/performance-optimization/README.md) | Compiler optimizations, SIMD |
3233

3334
Each category page lists its algorithms with a brief description and links to the detailed documentation.

ROADMAP.md

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

2828
| # | Component | Target module | Difficulty |
2929
|----|------------------------------------------------------|---------------------------|------------|
30-
| 30 | Continuous → discrete conversion (`c2d`) | `math` | ★★★★☆ |
31-
| 34 | Sliding Mode Control (SMC) | `robust_control` (new) | ★★★★☆ |
3230
| 35 | Disturbance Observer (DOB) | `robust_control` (new) | ★★★★☆ |
3331
| 36 | Active Disturbance Rejection Control (ADRC + ESO) | `robust_control` (new) | ★★★★☆ |
3432
| 37 | Hilbert transform / analytic signal / envelope | `analysis` | ★★★★☆ |
@@ -208,11 +206,6 @@ the library does not yet expose. Detailed below under
208206
- **Algorithm / paper:** S. Madgwick, "An efficient orientation filter for inertial and inertial/magnetic sensor arrays," 2010; R. Mahony, T. Hamel, J.-M. Pflimlin, "Nonlinear Complementary Filters on the Special Orthogonal Group," *IEEE Trans. AC*, 53(5), 2008.
209207
- **Reuses:** **Item 18 (Quaternion)**, `math::Geometry3D`.
210208

211-
### 34. Sliding Mode Control (SMC) *(float-first)*
212-
- **What:** Variable-structure controller driving the state onto a sliding surface, with a boundary layer to tame chattering.
213-
- **Embedded value:** Robust to matched disturbances and parameter uncertainty — popular in motor drives and power electronics.
214-
- **Algorithm / paper:** V. Utkin, "Variable Structure Systems with Sliding Modes," *IEEE Trans. AC*, 22(2), 1977; Slotine & Li, *Applied Nonlinear Control* (1991).
215-
- **Reuses:** `math::LinearTimeInvariant`, item 3 (saturation), new `robust_control/` module.
216209

217210
### 35. Disturbance Observer (DOB) *(float-first)*
218211
- **What:** Estimates and cancels lumped disturbance/model mismatch using the plant inverse and a Q-filter.

doc/robust_control/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Robust Control
2+
3+
Algorithms for robust control design: controllers that explicitly account for disturbances, uncertainty, and model mismatch.
4+
5+
## Algorithms
6+
7+
| Algorithm | Description |
8+
|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
9+
| [Sliding Mode Control](SlidingModeControl.md) | Variable-structure controller driving the state onto a sliding surface with a boundary layer to suppress chattering — robust to matched disturbances and parameter uncertainty |
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Sliding Mode Control
2+
3+
## Overview & Motivation
4+
5+
Variable-structure control systems change their structure depending on the current system state. Sliding Mode Control (SMC) is the most widely used instantiation: the controller switches between two or more structures to drive the state onto a designer-specified manifold — the **sliding surface** — and keep it there. Once constrained to the surface, the closed-loop dynamics are governed entirely by the surface geometry, independent of the plant model or matched disturbances. This structural robustness makes SMC a preferred choice for motor drives, DC-DC power converters, and any embedded plant whose parameters drift or are poorly known.
6+
7+
## Mathematical Theory
8+
9+
### State-Space Plant
10+
11+
The controller operates on a discrete-time linear plant
12+
13+
$$x_{k+1} = A x_k + B u_k, \quad x \in \mathbb{R}^n,\; u \in \mathbb{R}^m$$
14+
15+
where $A \in \mathbb{R}^{n \times n}$ and $B \in \mathbb{R}^{n \times m}$.
16+
17+
### Sliding Surface
18+
19+
A linear sliding surface is defined by
20+
21+
$$s(x) = S x, \quad S \in \mathbb{R}^{m \times n}$$
22+
23+
The sliding manifold $\{x : s(x) = 0\}$ is an $(n-m)$-dimensional subspace. The matrix $S$ is designed so the reduced-order dynamics on the manifold are stable and meet the desired closed-loop poles.
24+
25+
### Equivalent Control
26+
27+
The **equivalent control** $u_{eq}$ is the unique input that holds the state on $s = 0$ (i.e., $\dot{s} = 0$) for the nominal plant:
28+
29+
$$u_{eq} = -(S B)^{-1} S A x$$
30+
31+
This exists if and only if $S B$ is nonsingular, which is the relative-degree-one condition: each input channel must directly influence its corresponding sliding variable.
32+
33+
### Switching (Reaching) Term
34+
35+
The **switching term** adds a robust push toward the surface:
36+
37+
$$u_{sw} = (S B)^{-1} K \operatorname{sat}(s/\phi)$$
38+
39+
where $K \in \mathbb{R}^{m}$ is the per-channel switching gain and $\phi > 0$ is the **boundary-layer thickness**. The saturation function
40+
41+
$$\operatorname{sat}(\sigma) = \begin{cases} \sigma & |\sigma| \le 1 \\ \operatorname{sign}(\sigma) & |\sigma| > 1 \end{cases}$$
42+
43+
replaces the discontinuous $\operatorname{sign}(s)$ of ideal SMC with a continuous ramp inside $|s| \le \phi$, eliminating infinite-bandwidth chattering while maintaining the reaching property.
44+
45+
### Total Control Law
46+
47+
$$u = u_{eq} - u_{sw} = -(S B)^{-1}\bigl[S A x + K\,\operatorname{sat}(s/\phi)\bigr]$$
48+
49+
### Reaching Condition
50+
51+
The Lyapunov function $V = \tfrac{1}{2} s^\top s$ satisfies $\dot{V} < 0$ outside the boundary layer when $K_i > |d_i|$ for each matched-disturbance channel $d_i$. This guarantees finite-time arrival at $|s| \le \phi$.
52+
53+
### Sliding-Mode Dynamics
54+
55+
On the surface $s = 0$, the state evolves according to the $(n-m)$-dimensional reduced-order system. For a single-input system with $S = [c_1, \ldots, c_{n-1}, 1]$, the sliding pole is determined by the characteristic polynomial of the first $(n-1)$ rows under the surface constraint.
56+
57+
## Complexity Analysis
58+
59+
| Operation | Time | Space | Notes |
60+
|----------------|----------------|---------------|---------------------------------|
61+
| Construction | $O(m^3 + n m)$ | $O(nm + m^2)$ | $(SB)^{-1}$ inversion once |
62+
| ComputeControl | $O(n^2 + m^2)$ | $O(1)$ extra | $S A x$ dominates for large $n$ |
63+
| Surface | $O(nm)$ | $O(1)$ extra | monitoring only |
64+
65+
All data is stored in fixed-size arrays; no heap allocation at any point.
66+
67+
## Step-by-Step Walkthrough
68+
69+
Consider a double-integrator plant ($n=2$, $m=1$):
70+
71+
$$A = \begin{bmatrix}0 & 1\\0 & 0\end{bmatrix}, \quad B = \begin{bmatrix}0\\1\end{bmatrix}$$
72+
73+
with surface $S = [1\; 1]$, gain $K = 2$, boundary layer $\phi = 0.05$.
74+
75+
1. **Precompute** $SB = 1$, so $(SB)^{-1} = 1$.
76+
2. **At state** $x = [1, 0]^\top$: $s = 1 > \phi$, $\operatorname{sat}(s/\phi) = 1$.
77+
3. **Equivalent control**: $u_{eq} = -(SA)x = -[0\;1][1\;0]^\top = 0$.
78+
4. **Switching term**: $u_{sw} = 1 \cdot 2 \cdot 1 = 2$.
79+
5. **Total**: $u = 0 - 2 = -2$. The large negative input decelerates the state toward the surface.
80+
6. Once $|s| \le 0.05$, the saturation ramps linearly and the control becomes smooth.
81+
82+
## Pitfalls & Edge Cases
83+
84+
- **Singular $SB$**: the equivalent control is undefined. This occurs when the surface does not satisfy the relative-degree-one condition. Assert non-singularity at construction.
85+
- **Chattering**: ideal SMC ($\phi \to 0$) switches at infinite frequency, exciting unmodeled dynamics and wearing actuators. The boundary layer is not optional for real hardware.
86+
- **Gain too small**: if $K_i \le |d_i|$, the switching term cannot overcome the disturbance and the surface is never reached. The state remains bounded but does not converge.
87+
- **Boundary-layer error**: the steady-state tracking error is $O(\phi)$. Reducing $\phi$ improves accuracy at the cost of higher-frequency control activity.
88+
- **Discrete-time reaching**: the Zeno-like finite-time result holds in continuous time; in discrete time the state enters an $O(\phi + \Delta t)$ neighborhood of the surface.
89+
90+
## Variants & Generalizations
91+
92+
- **Higher-order SMC** (super-twisting): drives both $s$ and $\dot{s}$ to zero simultaneously, eliminating chattering without a boundary layer at the cost of requiring $\dot{s}$ estimates.
93+
- **Terminal SMC**: uses a nonlinear surface $s = \dot{e} + \beta e^{p/q}$ to achieve finite-time convergence to the origin (not just to the surface).
94+
- **Integral SMC**: augments the surface with an integral of the state to achieve zero steady-state error in the presence of constant disturbances.
95+
- **Adaptive switching gain**: adjusts $K$ online to match the unknown disturbance bound, avoiding over-gain chattering.
96+
97+
## Applications
98+
99+
- Brushless motor current and speed control: fast switching, large disturbance rejection.
100+
- DC-DC converters: inherent switching structure matches SMC's variable-structure nature.
101+
- Pneumatic and hydraulic actuators: strong friction/backlash rejection via matched-disturbance cancellation.
102+
- Satellite attitude control: robustness to inertia uncertainty and external torques.
103+
104+
## Connections to Other Algorithms
105+
106+
- **LQR**: smooth optimal alternative; minimises quadratic cost but has no guaranteed robustness to matched disturbances.
107+
- **Disturbance Observer (DOB)**: estimates and cancels the disturbance algebraically; complementary to SMC for unmatched disturbances.
108+
- **SaturationRateLimiter**: the boundary-layer `sat` function is semantically identical to the saturation block already in the library.
109+
- **LuenbergerObserver**: may be combined with SMC when the full state is not measured (output feedback SMC).
110+
111+
## References & Further Reading
112+
113+
- V. Utkin, "Variable Structure Systems with Sliding Modes," *IEEE Transactions on Automatic Control*, 22(2), pp. 212–222, 1977.
114+
- J.-J. Slotine, W. Li, *Applied Nonlinear Control*, Prentice-Hall, 1991, Chapter 7.
115+
- H. K. Khalil, *Nonlinear Systems*, 3rd ed., Prentice-Hall, 2002, Chapter 14.

numerical/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ add_subdirectory(math)
99
add_subdirectory(neural_network)
1010
add_subdirectory(optimization)
1111
add_subdirectory(regularization)
12+
add_subdirectory(robust_control)
1213
add_subdirectory(solvers)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
numerical_add_header_library(numerical.robust_control STATIC)
2+
3+
target_include_directories(numerical.robust_control ${NUMERICAL_VISIBILITY}
4+
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/../../>"
5+
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
6+
)
7+
8+
target_link_libraries(numerical.robust_control ${NUMERICAL_VISIBILITY}
9+
numerical.math
10+
numerical.solver
11+
infra.util
12+
)
13+
14+
target_sources(numerical.robust_control PRIVATE
15+
SlidingModeControl.hpp
16+
)
17+
18+
numerical_add_coverage_sources(numerical.robust_control
19+
SlidingModeControl.cpp
20+
)
21+
22+
add_subdirectory(test)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include "numerical/robust_control/SlidingModeControl.hpp"
2+
3+
namespace robust_control
4+
{
5+
template class SlidingModeControl<float, 2, 1>;
6+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#pragma once
2+
3+
#if defined(__GNUC__) || defined(__clang__)
4+
#pragma GCC optimize("O3", "fast-math")
5+
#endif
6+
7+
#include "infra/util/ReallyAssert.hpp"
8+
#include "numerical/math/CompilerOptimizations.hpp"
9+
#include "numerical/math/LinearTimeInvariant.hpp"
10+
#include "numerical/math/Matrix.hpp"
11+
#include "numerical/solvers/GaussianElimination.hpp"
12+
#include <cmath>
13+
#include <cstddef>
14+
#include <type_traits>
15+
16+
namespace robust_control
17+
{
18+
template<typename T, std::size_t StateSize, std::size_t InputSize>
19+
class SlidingModeControl
20+
{
21+
static_assert(std::is_floating_point_v<T>, "SlidingModeControl supports floating-point types");
22+
static_assert(StateSize > 0 && InputSize > 0, "SlidingModeControl requires positive dimensions");
23+
24+
public:
25+
using StateVector = math::Vector<T, StateSize>;
26+
using InputVector = math::Vector<T, InputSize>;
27+
using SurfaceMatrix = math::Matrix<T, InputSize, StateSize>;
28+
using SBMatrix = math::SquareMatrix<T, InputSize>;
29+
using PlantType = math::LinearTimeInvariant<T, StateSize, InputSize, StateSize>;
30+
31+
SlidingModeControl(const PlantType& plant,
32+
const SurfaceMatrix& surface,
33+
const InputVector& switchGain,
34+
T phi);
35+
36+
OPTIMIZE_FOR_SPEED InputVector ComputeControl(const StateVector& x);
37+
OPTIMIZE_FOR_SPEED InputVector ComputeControl(const StateVector& x, const StateVector& reference);
38+
39+
[[nodiscard]] InputVector Surface(const StateVector& x) const;
40+
void SetBoundaryLayer(T phi);
41+
42+
[[nodiscard]] static T Sat(T s, T phi);
43+
44+
private:
45+
OPTIMIZE_FOR_SPEED InputVector ScaledSat(const InputVector& s) const;
46+
47+
PlantType plant;
48+
SurfaceMatrix surface;
49+
SBMatrix sbInv;
50+
InputVector switchGain;
51+
T boundaryLayer;
52+
};
53+
54+
template<typename T, std::size_t StateSize, std::size_t InputSize>
55+
SlidingModeControl<T, StateSize, InputSize>::SlidingModeControl(
56+
const PlantType& plant,
57+
const SurfaceMatrix& surface,
58+
const InputVector& switchGain,
59+
T phi)
60+
: plant{ plant }
61+
, surface{ surface }
62+
, switchGain{ switchGain }
63+
, boundaryLayer{ phi }
64+
{
65+
really_assert(phi > T{ 0 });
66+
67+
const auto sb = surface * plant.B;
68+
const auto identity = SBMatrix::Identity();
69+
sbInv = solvers::SolveSystem<T, InputSize, InputSize>(sb, identity);
70+
}
71+
72+
template<typename T, std::size_t StateSize, std::size_t InputSize>
73+
OPTIMIZE_FOR_SPEED typename SlidingModeControl<T, StateSize, InputSize>::InputVector
74+
SlidingModeControl<T, StateSize, InputSize>::ComputeControl(const StateVector& x)
75+
{
76+
const auto s = surface * x;
77+
const auto sax = surface * (plant.A * x);
78+
const auto uEq = sbInv * sax * T{ -1 };
79+
const auto uSw = sbInv * ScaledSat(s);
80+
return uEq - uSw;
81+
}
82+
83+
template<typename T, std::size_t StateSize, std::size_t InputSize>
84+
OPTIMIZE_FOR_SPEED typename SlidingModeControl<T, StateSize, InputSize>::InputVector
85+
SlidingModeControl<T, StateSize, InputSize>::ComputeControl(const StateVector& x, const StateVector& reference)
86+
{
87+
const auto error = x - reference;
88+
return ComputeControl(error);
89+
}
90+
91+
template<typename T, std::size_t StateSize, std::size_t InputSize>
92+
typename SlidingModeControl<T, StateSize, InputSize>::InputVector
93+
SlidingModeControl<T, StateSize, InputSize>::Surface(const StateVector& x) const
94+
{
95+
return surface * x;
96+
}
97+
98+
template<typename T, std::size_t StateSize, std::size_t InputSize>
99+
void SlidingModeControl<T, StateSize, InputSize>::SetBoundaryLayer(T phi)
100+
{
101+
really_assert(phi > T{ 0 });
102+
boundaryLayer = phi;
103+
}
104+
105+
template<typename T, std::size_t StateSize, std::size_t InputSize>
106+
T SlidingModeControl<T, StateSize, InputSize>::Sat(T s, T phi)
107+
{
108+
const T ratio = s / phi;
109+
if (ratio >= T{ 1 })
110+
return T{ 1 };
111+
if (ratio <= T{ -1 })
112+
return T{ -1 };
113+
return ratio;
114+
}
115+
116+
template<typename T, std::size_t StateSize, std::size_t InputSize>
117+
OPTIMIZE_FOR_SPEED typename SlidingModeControl<T, StateSize, InputSize>::InputVector
118+
SlidingModeControl<T, StateSize, InputSize>::ScaledSat(const InputVector& s) const
119+
{
120+
InputVector result{};
121+
for (std::size_t i = 0; i < InputSize; ++i)
122+
result.at(i, 0) = switchGain.at(i, 0) * Sat(s.at(i, 0), boundaryLayer);
123+
return result;
124+
}
125+
126+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
127+
extern template class SlidingModeControl<float, 2, 1>;
128+
#endif
129+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
add_executable(numerical.robust_control_test)
2+
emil_build_for(numerical.robust_control_test BOOL NUMERICAL_TOOLBOX_BUILD_TESTS)
3+
emil_add_test(numerical.robust_control_test)
4+
5+
target_link_libraries(numerical.robust_control_test PUBLIC
6+
gmock_main
7+
numerical.robust_control
8+
)
9+
10+
target_sources(numerical.robust_control_test PRIVATE
11+
TestSlidingModeControl.cpp
12+
)

0 commit comments

Comments
 (0)