Skip to content

Commit 7ca1c09

Browse files
feat: add feedback linearization (#216)
* add feedback linearization * add test for a real plant * Update TestFeedbackLinearization.cpp * 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 45d3be2 commit 7ca1c09

13 files changed

Lines changed: 515 additions & 196 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
2626
| [Regularization](doc/regularization/README.md) | L1 (Lasso), L2 (Ridge) |
2727
| [Math](doc/math/README.md) | CORDIC, Quaternion, MatrixNorms, Step Response Metrics, MatrixExponential |
2828
| [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 |
29+
| [Nonlinear Control](doc/nonlinear_control/README.md) | Feedback Linearization |
2930
| [Robust Control](doc/robust_control/README.md) | Active Disturbance Rejection Control (ADRC + ESO), Sliding Mode Control (SMC), Disturbance Observer (DOB) |
3031
| [Performance Optimization](doc/performance-optimization/README.md) | Compiler optimizations, SIMD |
3132

ROADMAP.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,6 @@ the library does not yet expose. Detailed below under
227227
- **Algorithm / paper:** P. Kaminski, A. Bryson, S. Schmidt, "Discrete Square Root Filtering: A Survey of Current Techniques," *IEEE Trans. AC*, 16(6), 1971.
228228
- **Reuses:** [KalmanFilterBase.hpp](numerical/filters/active/KalmanFilterBase.hpp), [Cholesky](numerical/solvers/CholeskyDecomposition.hpp), item 27.
229229

230-
### 40. Feedback linearization *(float-first)*
231-
- **What:** Cancel a control-affine system's known nonlinear dynamics via a coordinate transform + inner control law, leaving an equivalent linear system that an outer loop (PD/LQR) can drive.
232-
- **Embedded value:** One linear gain set works across the whole operating envelope of any structurally-known nonlinear plant (robot arms, quadrotors, electromechanical drives) — no gain scheduling, no lookup tables.
233-
- **Algorithm / paper:** A. Isidori, *Nonlinear Control Systems* (1995); Slotine & Li, *Applied Nonlinear Control*.
234-
- **Reuses:** an injected control-affine plant model, `math::Matrix`, new `nonlinear_control/` module. (The manipulator computed-torque instance lives in robotics-toolbox-cpp.)
235-
236230
### 41. Backstepping controller *(float-first)*
237231
- **What:** Recursive Lyapunov-based design for strict-feedback systems, stabilizing one integrator stage at a time.
238232
- **Embedded value:** Systematic, provably-stable control for cascaded nonlinear plants (electromechanical, flight).
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Feedback Linearization
2+
3+
## Overview & Motivation
4+
5+
Nonlinear plants such as robot arms, quadrotors, and electromechanical drives are only well-controlled by a fixed linear gain over a narrow operating range. Feedback linearization resolves this by exploiting a known model of the plant's nonlinearity to cancel it exactly in the closed loop, leaving an equivalent linear system — decoupled integrator chains — that a single outer-loop gain set can drive correctly across the full operating envelope. No gain scheduling, no lookup tables, no re-tuning when the operating point changes.
6+
7+
## Mathematical Theory
8+
9+
### Control-Affine Plant
10+
11+
The technique applies to plants whose output $y \in \mathbb{R}^m$ satisfies, after $r$ differentiations,
12+
13+
$$y^{(r)} = a(x) + B(x)\, u$$
14+
15+
where $x \in \mathbb{R}^n$ is the state, $u \in \mathbb{R}^m$ is the input, $a(x) \in \mathbb{R}^m$ is the **drift term** (known nonlinear dynamics), and $B(x) \in \mathbb{R}^{m \times m}$ is the **decoupling matrix** (state-dependent input gain). The integer $r$ is the relative degree. For mechanical systems ($r = 2$), $B(x) = M(q)$ is the inertia matrix and $a(x) = C(q, \dot{q})\dot{q} + g(q)$ is the Coriolis-plus-gravity term.
16+
17+
### Inner Control Law (Cancellation)
18+
19+
The inner law selects $u$ so that the term $a(x)$ is cancelled and the decoupling matrix is factored out:
20+
21+
$$u = B(x)\, v + a(x)$$
22+
23+
Substituting into the plant equation yields
24+
25+
$$y^{(r)} = a(x) + B(x)\bigl(B(x)\,v + a(x)\bigr) - a(x) = v$$
26+
27+
leaving pure integrator chains $y^{(r)} = v$, provided $B(x)$ is nonsingular.
28+
29+
### Outer Control Law (Linear Outer Loop)
30+
31+
With the plant reduced to integrators, a PD outer loop commands the virtual input:
32+
33+
$$v = y_d^{(r)} + K_d\,\dot{e} + K_p\, e, \quad e = y_d - y, \quad \dot{e} = \dot{y}_d - \dot{y}$$
34+
35+
The closed-loop error satisfies the linear ODE
36+
37+
$$e^{(r)} + K_d\,\dot{e} + K_p\, e = 0$$
38+
39+
whose eigenvalues are set by choosing $K_p, K_d$. Critical damping per channel requires $K_d = 2\sqrt{K_p}$.
40+
41+
### Combined Law
42+
43+
Expanding yields the single expression evaluated on the hot path:
44+
45+
$$u = B(x)\bigl(y_d^{(r)} + K_d\,\dot{e} + K_p\, e\bigr) + a(x)$$
46+
47+
No matrix inversion appears on the hot path: the law multiplies by $B(x)$, not by $B(x)^{-1}$.
48+
49+
## Complexity Analysis
50+
51+
| Operation | Time | Space | Notes |
52+
|--------------|--------------------|--------------|----------------------------------------|
53+
| Construction | $O(m^2)$ | $O(m^2)$ | Copy two gain matrices |
54+
| ComputeInput | $O(m^2)$ | $O(m)$ extra | Two matrix-vector products dominate |
55+
| Model query | $O(m^2)$–$O(nm^2)$ | $O(m^2)$ | Implementation-defined; injected model |
56+
57+
All storage is in fixed-size stack arrays; the law itself performs no heap allocation.
58+
59+
## Step-by-Step Walkthrough
60+
61+
Consider a 2-DOF planar arm with $m = 2$, $K_p = 100 I$, $K_d = 20 I$, and at one instant:
62+
63+
- State $x = [0.1, 0.2]^\top$, $\dot{x} = [0, 0]^\top$.
64+
- Reference $y_d = [0.5, 0.5]^\top$, $\dot{y}_d = [0, 0]^\top$, $\ddot{y}_d = [0, 0]^\top$.
65+
- Model returns $B(x) = I$ and $a(x) = [0.3, 0.1]^\top$.
66+
67+
1. Compute error: $e = [0.4, 0.3]^\top$, $\dot{e} = [0, 0]^\top$.
68+
2. Compute virtual input: $v = 0 + 20 \cdot 0 + 100 \cdot [0.4, 0.3]^\top = [40, 30]^\top$.
69+
3. Inner law: $u = I \cdot [40, 30]^\top + [0.3, 0.1]^\top = [40.3, 30.1]^\top$.
70+
71+
The gravity-like drift $a(x)$ is added directly; the outer PD term drives position error to zero.
72+
73+
## Pitfalls & Edge Cases
74+
75+
- **Singular decoupling matrix**: if $B(x)$ is rank-deficient the inner law is undefined. The condition $\det B(x) \neq 0$ must hold throughout the operating region.
76+
- **Model mismatch**: cancellation is only as exact as the model. Unmodelled dynamics or parameter error leaves a residual nonlinearity; pair with a robust or adaptive outer term to bound the error.
77+
- **Zero dynamics**: exact linearisation of the output may leave internal states unobservable. These zero dynamics can be unstable even when the output tracks perfectly. Verify stability of the internal dynamics before deployment.
78+
- **Actuator limits**: the inner law can command arbitrarily large $u$ near the start of a transient. Saturation on $u$ breaks the exact cancellation argument; scale $K_p$, $K_d$ or add a reference pre-filter to keep the command within actuator bounds.
79+
- **Float precision**: for large $m$, matrix products accumulate rounding error proportional to $m \cdot \epsilon_\text{float}$. Verify the gain matrices are well-conditioned.
80+
81+
## Variants & Generalizations
82+
83+
- **Input-output linearization (SISO)**: for scalar output with relative degree $r > 1$, the cancellation uses Lie derivatives $L_f^r h(x)$ and $L_g L_f^{r-1} h(x)$, and the input is $u = (v - L_f^r h(x)) / L_g L_f^{r-1} h(x)$. The singularity condition $L_g L_f^{r-1} h \neq 0$ replaces $\det B \neq 0$.
84+
- **Computed-torque control**: the mechanical specialisation with $B = M(q)$ and $a = C(q,\dot{q})\dot{q} + g(q)$. The canonical instantiation lives in robotics-toolbox-cpp.
85+
- **Partial feedback linearization**: linearizes only the input-output channels, leaving the rest of the state dynamics (zero dynamics) uncontrolled by the outer loop.
86+
- **Adaptive feedback linearization / MRAC**: replaces the fixed model with an online-adapted estimate, enabling cancellation under parametric uncertainty.
87+
88+
## Applications
89+
90+
- Robot manipulators: decoupled Cartesian impedance or position control across the full joint-space workspace.
91+
- Quadrotor UAVs: attitude and altitude decoupling for independent channel control.
92+
- Electromechanical drives: cancellation of back-EMF and friction in torque-controlled axes.
93+
- Chemical process control: inversion of Hammerstein-type nonlinear input maps.
94+
95+
## Connections to Other Algorithms
96+
97+
- **Backstepping**: recursive alternative for strict-feedback systems; tolerates drift terms that cannot be directly cancelled.
98+
- **Model Reference Adaptive Control (MRAC)**: adapts the model online; complements feedback linearization when the plant parameters are unknown.
99+
- **LQR**: natural choice for the outer linear loop once the plant has been linearized.
100+
- **Sliding Mode Control**: robustifies the outer loop against residual model mismatch by adding a discontinuous reaching term.
101+
102+
## References & Further Reading
103+
104+
- A. Isidori, *Nonlinear Control Systems*, 3rd ed., Springer, 1995.
105+
- J.-J. Slotine, W. Li, *Applied Nonlinear Control*, Prentice-Hall, 1991, Chapter 6.
106+
- H. K. Khalil, *Nonlinear Systems*, 3rd ed., Prentice-Hall, 2002, Chapter 13.

doc/nonlinear_control/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Nonlinear Control
2+
3+
Algorithms for nonlinear control design: controllers that exploit a known plant model to cancel or structurally transform nonlinear dynamics.
4+
5+
## Algorithms
6+
7+
| Algorithm | Description |
8+
|----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
9+
| [Feedback Linearization](FeedbackLinearization.md) | Cancels a control-affine plant's known nonlinear dynamics via an inner control law, leaving decoupled integrator chains that a simple outer PD/LQR loop drives |

numerical/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ add_subdirectory(math)
77
add_subdirectory(neural_network)
88
add_subdirectory(optimization)
99
add_subdirectory(regularization)
10+
add_subdirectory(nonlinear_control)
1011
add_subdirectory(robust_control)
1112
add_subdirectory(solvers)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
numerical_add_header_library(numerical.nonlinear_control STATIC)
2+
3+
target_include_directories(numerical.nonlinear_control ${NUMERICAL_VISIBILITY}
4+
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/../../>"
5+
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
6+
)
7+
8+
target_link_libraries(numerical.nonlinear_control ${NUMERICAL_VISIBILITY}
9+
numerical.math
10+
infra.util
11+
)
12+
13+
target_sources(numerical.nonlinear_control PRIVATE
14+
FeedbackLinearization.hpp
15+
)
16+
17+
numerical_add_coverage_sources(numerical.nonlinear_control
18+
FeedbackLinearization.cpp
19+
)
20+
21+
add_subdirectory(test)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) 2024 Numerical Toolbox Contributors
2+
// SPDX-License-Identifier: MIT
3+
4+
#include "numerical/nonlinear_control/FeedbackLinearization.hpp"
5+
6+
namespace nonlinear_control
7+
{
8+
template class FeedbackLinearization<float, 2>;
9+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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/Matrix.hpp"
9+
#include <cstddef>
10+
#include <type_traits>
11+
12+
namespace nonlinear_control
13+
{
14+
template<typename T, std::size_t Dim>
15+
class ControlAffineModel
16+
{
17+
static_assert(std::is_floating_point_v<T>, "ControlAffineModel supports floating-point types");
18+
static_assert(Dim > 0, "ControlAffineModel requires Dim > 0");
19+
20+
public:
21+
using StateVector = math::Vector<T, Dim>;
22+
using DecouplingMatrix = math::SquareMatrix<T, Dim>;
23+
24+
virtual ~ControlAffineModel() = default;
25+
26+
[[nodiscard]] virtual DecouplingMatrix DecouplingMatrixAt(const StateVector& x) const = 0;
27+
[[nodiscard]] virtual StateVector DriftTerm(const StateVector& x) const = 0;
28+
};
29+
30+
template<typename T, std::size_t Dim>
31+
class FeedbackLinearization
32+
{
33+
static_assert(std::is_floating_point_v<T>, "FeedbackLinearization supports floating-point types");
34+
static_assert(Dim > 0, "FeedbackLinearization requires Dim > 0");
35+
36+
public:
37+
using StateVector = math::Vector<T, Dim>;
38+
using GainMatrix = math::SquareMatrix<T, Dim>;
39+
40+
FeedbackLinearization(const ControlAffineModel<T, Dim>& model, const GainMatrix& kp, const GainMatrix& kd);
41+
42+
OPTIMIZE_FOR_SPEED StateVector ComputeInput(const StateVector& x, const StateVector& xDot,
43+
const StateVector& yd, const StateVector& ydDot, const StateVector& ydDdot);
44+
45+
private:
46+
const ControlAffineModel<T, Dim>& model;
47+
GainMatrix kp;
48+
GainMatrix kd;
49+
};
50+
51+
template<typename T, std::size_t Dim>
52+
FeedbackLinearization<T, Dim>::FeedbackLinearization(
53+
const ControlAffineModel<T, Dim>& model, const GainMatrix& kp, const GainMatrix& kd)
54+
: model{ model }
55+
, kp{ kp }
56+
, kd{ kd }
57+
{}
58+
59+
template<typename T, std::size_t Dim>
60+
OPTIMIZE_FOR_SPEED typename FeedbackLinearization<T, Dim>::StateVector
61+
FeedbackLinearization<T, Dim>::ComputeInput(const StateVector& x, const StateVector& xDot,
62+
const StateVector& yd, const StateVector& ydDot, const StateVector& ydDdot)
63+
{
64+
const StateVector e{ yd - x };
65+
const StateVector eDot{ ydDot - xDot };
66+
const StateVector v{ ydDdot + kd * eDot + kp * e };
67+
const auto B{ model.DecouplingMatrixAt(x) };
68+
const StateVector a{ model.DriftTerm(x) };
69+
return B * v + a;
70+
}
71+
72+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
73+
extern template class FeedbackLinearization<float, 2>;
74+
#endif
75+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
add_executable(numerical.nonlinear_control_test)
2+
emil_build_for(numerical.nonlinear_control_test BOOL NUMERICAL_TOOLBOX_BUILD_TESTS)
3+
emil_add_test(numerical.nonlinear_control_test)
4+
5+
target_link_libraries(numerical.nonlinear_control_test PUBLIC
6+
gmock_main
7+
numerical.nonlinear_control
8+
)
9+
10+
target_sources(numerical.nonlinear_control_test PRIVATE
11+
TestFeedbackLinearization.cpp
12+
)

0 commit comments

Comments
 (0)