Skip to content

Commit bc51f7c

Browse files
chore: add matrix norms (#196)
* add matrix norms * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix sonar findings --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent b61f468 commit bc51f7c

14 files changed

Lines changed: 390 additions & 13 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ Refer to the documentation to quickly integrate and utilize the library's signal
2626
| [Neural Network](doc/neural_network/README.md) | Layers, activations, losses, model |
2727
| [Optimization](doc/optimization/README.md) | Gradient Descent |
2828
| [Regularization](doc/regularization/README.md) | L1 (Lasso), L2 (Ridge) |
29-
| [Math](doc/math/README.md) | CORDIC, Quaternion, Step Response Metrics |
30-
| [Solvers](doc/solvers/README.md) | Gaussian Elimination, Levinson-Durbin, Durand-Kerner, Cholesky, DARE, Runge-Kutta ODE Integrators (RK4 + Dormand-Prince) |
29+
| [Math](doc/math/README.md) | CORDIC, Quaternion, MatrixNorms, Step Response Metrics |
30+
| [Solvers](doc/solvers/README.md) | Gaussian Elimination, Levinson-Durbin, Durand-Kerner, Cholesky, DARE, Runge-Kutta ODE Integrators (RK4 + Dormand-Prince), Condition Number |
3131
| [Performance Optimization](doc/performance-optimization/README.md) | Compiler optimizations, SIMD |
3232

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

ROADMAP.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ Difficulty legend:
4848
| 45 | IIR filter design (Butterworth/Chebyshev + bilinear) | `filters/passive` | ★★★★★ |
4949
| 46 | H∞ state-feedback control | `robust_control` (new) | ★★★★★ |
5050
| 47 | Model Reference Adaptive Control (MRAC) | `nonlinear_control` (new) | ★★★★★ |
51-
| 50 | Matrix norms & condition number | `math` | ★★★☆☆ |
5251
| 51 | Spectral radius / discrete stability margin | `math` | ★★★☆☆ |
5352
| 52 | Estimator consistency metrics (NEES / NIS) | `estimators` | ★★★☆☆ |
5453

@@ -559,16 +558,6 @@ and [`control_analysis/FrequencyResponse`](numerical/control_analysis/FrequencyR
559558
magnitude/phase. The items below are the missing pieces. All are **float-only**, no-heap, and operate
560559
on bounded `math::Vector`/`math::Matrix` inputs; tests are `TEST_F` on `float`.
561560

562-
### 50. Matrix norms & condition number ★★★☆☆ — `math`
563-
- **What:** `FrobeniusNorm`, `OneNorm`, `InfinityNorm` on `Matrix`; `Vector` `Norm`/`Normalize`;
564-
`ConditionNumber` estimate.
565-
- **Metric value:** M9 (conditioning) — quantifies ill-conditioning for `solvers/`, regression, and
566-
Kalman covariance sanity; foundational gap ([`Matrix`](numerical/math/Matrix.hpp) currently exposes
567-
only `Transpose`/`Trace`).
568-
- **Algorithm:** direct norm sums; condition number from norm ratio (apply the inverse via the
569-
existing `GaussianElimination` rather than forming it explicitly, embedded-style).
570-
- **Reuses:** `math::Matrix`, `solvers::GaussianElimination`.
571-
572561
### 51. Spectral radius / discrete stability margin ★★★☆☆ — `math`
573562
- **What:** Dominant `|eigenvalue|` of a square (state/companion) matrix; `IsSchurStable` (all
574563
`|λ| < 1`) and the stability margin `1 − ρ(A)`.

doc/math/MatrixNorms.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Matrix & Vector Norms
2+
3+
## Overview & Motivation
4+
5+
Linear algebra computations — solvers, Kalman filter covariance updates, regression — depend on the numerical health of the matrices involved. Norms formalise the notion of "size" for matrices and vectors and are the building blocks of every conditioning, stability, and error-bound estimate in the library. They are also the cheapest such quantities to compute: a single pass over the entries with no allocation, suitable for real-time embedded paths.
6+
7+
Vector normalisation, closely related, produces the unit-length direction of a vector and is a recurring primitive in geometry, attitude estimation, and gradient methods.
8+
9+
## Mathematical Theory
10+
11+
### Vector Norm
12+
13+
For a vector $\mathbf{v} \in \mathbb{R}^n$, the Euclidean (L2) norm is
14+
15+
$$\|\mathbf{v}\|_2 = \sqrt{\sum_{i=1}^{n} v_i^2}$$
16+
17+
The unit vector $\hat{\mathbf{v}} = \mathbf{v} / \|\mathbf{v}\|_2$ satisfies $\|\hat{\mathbf{v}}\|_2 = 1$. Normalisation is undefined when $\|\mathbf{v}\|_2 = 0$ and must be guarded.
18+
19+
### Matrix Norms
20+
21+
**Frobenius norm** — treats the matrix as a flattened vector:
22+
23+
$$\|A\|_F = \sqrt{\sum_{i=1}^{m}\sum_{j=1}^{n} a_{ij}^2}$$
24+
25+
It is rotationally invariant under unitary transformations and cheap to compute.
26+
27+
**1-norm (maximum absolute column sum)**:
28+
29+
$$\|A\|_1 = \max_{1 \le j \le n} \sum_{i=1}^{m} |a_{ij}|$$
30+
31+
**Infinity norm (maximum absolute row sum)**:
32+
33+
$$\|A\|_\infty = \max_{1 \le i \le m} \sum_{j=1}^{n} |a_{ij}|$$
34+
35+
The 1-norm and infinity-norm are dual: $\|A\|_\infty = \|A^\top\|_1$.
36+
37+
## Complexity Analysis
38+
39+
| Operation | Time | Space | Notes |
40+
|---------------|---------|--------|------------------------------|
41+
| FrobeniusNorm | $O(mn)$ | $O(1)$ | Single pass, no allocation |
42+
| OneNorm | $O(mn)$ | $O(1)$ | Column-wise sum, running max |
43+
| InfinityNorm | $O(mn)$ | $O(1)$ | Row-wise sum, running max |
44+
| VectorNorm | $O(n)$ | $O(1)$ | Single pass |
45+
| Normalize | $O(n)$ | $O(n)$ | Output vector on stack |
46+
47+
## Step-by-Step Walkthrough
48+
49+
Matrix $A = \begin{bmatrix}3 & 1 \\ 1 & 2\end{bmatrix}$:
50+
51+
1. **FrobeniusNorm**: $\sqrt{9 + 1 + 1 + 4} = \sqrt{15} \approx 3.873$
52+
2. **OneNorm**: column 0 sum $= |3| + |1| = 4$; column 1 sum $= |1| + |2| = 3$; max $= 4$
53+
3. **InfinityNorm**: row 0 sum $= |3| + |1| = 4$; row 1 sum $= |1| + |2| = 3$; max $= 4$
54+
55+
Vector $\mathbf{v} = [3,\, 4]^\top$: $\|\mathbf{v}\|_2 = 5$, and $\hat{\mathbf{v}} = [0.6,\, 0.8]^\top$.
56+
57+
## Pitfalls & Edge Cases
58+
59+
**Zero vector normalisation** — dividing by $\|\mathbf{v}\|_2 = 0$ is undefined. The implementation returns an empty optional for near-zero norms.
60+
61+
**Fast-math semantics**`#pragma GCC optimize("fast-math")` may reorder floating-point operations. The norms are sums of non-negative values, so reordering does not change the sign of the result, but catastrophic cancellation can still occur for near-zero off-diagonal entries.
62+
63+
**Non-square matrices** — FrobeniusNorm, OneNorm, and InfinityNorm apply to any $m \times n$ matrix.
64+
65+
## Variants & Generalizations
66+
67+
The **spectral norm** (largest singular value, $\|A\|_2$) is the tightest but requires an SVD — $O(N^3)$ with a large constant, unsuitable for embedded real-time paths. The 1-norm and infinity-norm are cheap upper bounds used throughout the library instead.
68+
69+
General **p-norms** and weighted norms generalise the vector case; the L2 norm is the only one currently exposed because it is the natural quantity for geometric and least-squares work.
70+
71+
## Applications
72+
73+
- **Conditioning estimates**: the 1-norm feeds the condition number (see `solvers::ConditionNumber`).
74+
- **Convergence tests**: iterative solvers and optimisers stop when a residual norm falls below tolerance.
75+
- **Attitude / geometry**: vector normalisation produces unit direction and rotation axes.
76+
- **Covariance sanity**: the Frobenius norm of a covariance matrix bounds its total variance.
77+
78+
## Connections to Other Algorithms
79+
80+
The 1-norm is the norm used by `solvers::ConditionNumber` for its $\|A\|\cdot\|A^{-1}\|$ estimate. The `math::Matrix` type provides the storage and transpose the norms operate on. Norm-based residual tests appear in `solvers` and `optimization`.
81+
82+
## References & Further Reading
83+
84+
- Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., Chapter 2 (matrix norms)
85+
- Trefethen, L. N. & Bau, D., "Numerical Linear Algebra", Lecture 3 (norms)
86+
- Higham, N. J., "Accuracy and Stability of Numerical Algorithms", 2nd ed.

doc/math/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ Core mathematical primitives for numerical computation.
88
|-----------------------------|-----------------------------------------------------------------------------------------------|
99
| [CORDIC](Cordic.md) | Iterative shift-add engine for sin/cos, atan2, magnitude, and vector rotation — no multiplier |
1010
| [Quaternion](Quaternion.md) | Unit-quaternion rotation type: Hamilton product, SLERP, rotation-matrix and Euler conversions |
11+
| [MatrixNorms](MatrixNorms.md) | Frobenius, 1-norm, infinity-norm on matrices; vector L2 norm/normalize |
1112
| [Step Response Metrics](StepResponseMetrics.md) | Rise time, settling time, percent overshoot, peak time, and steady-state error from a bounded step-response vector |

doc/solvers/ConditionNumber.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Condition Number
2+
3+
## Overview & Motivation
4+
5+
A matrix can be mathematically invertible yet catastrophically sensitive to perturbations in its entries or in the right-hand side of a linear system. This sensitivity, called ill-conditioning, is the root cause of accumulated floating-point error in solvers, regression, and Kalman-filter covariance updates. Measuring it before committing to a solve reveals whether the result can be trusted or whether regularisation, pivoting, or reformulation is warranted.
6+
7+
The condition number is the canonical scalar summary of ill-conditioning. Because it requires the norm of the inverse, it depends on a linear solver — which is why it lives in the `solvers` layer, on top of the `math` norms, rather than in `math` itself.
8+
9+
## Mathematical Theory
10+
11+
For an invertible square matrix $A \in \mathbb{R}^{N \times N}$ and a chosen matrix norm $\|\cdot\|$:
12+
13+
$$\kappa(A) = \|A\| \cdot \|A^{-1}\|$$
14+
15+
The condition number bounds the relative error amplification in the solution $\mathbf{x}$ of $A\mathbf{x} = \mathbf{b}$ due to a perturbation $\delta\mathbf{b}$:
16+
17+
$$\frac{\|\delta\mathbf{x}\|}{\|\mathbf{x}\|} \le \kappa(A) \cdot \frac{\|\delta\mathbf{b}\|}{\|\mathbf{b}\|}$$
18+
19+
$\kappa(A) \ge 1$ always; $\kappa(A) = 1$ only for scalar multiples of orthogonal matrices. A singular matrix has $\kappa(A) = \infty$. The estimate here uses the 1-norm.
20+
21+
### Computing the Inverse via Column-wise Solve
22+
23+
The norm of $A^{-1}$ is obtained without forming an explicit inverse as a first-class object: solving $AX = I$ column by column with partial-pivoting Gaussian elimination yields the inverse columns, from which the 1-norm is accumulated. This mirrors the embedded convention of reusing the existing solver rather than adding a dedicated inversion routine.
24+
25+
## Complexity Analysis
26+
27+
| Operation | Time | Space | Notes |
28+
|-----------------|----------|----------|-----------------------------------------------|
29+
| ConditionNumber | $O(N^3)$ | $O(N^2)$ | Dominated by the $N$-column solve of $AX = I$ |
30+
31+
## Step-by-Step Walkthrough
32+
33+
Matrix $A = \begin{bmatrix}3 & 1 \\ 1 & 2\end{bmatrix}$, 1-norm condition number:
34+
35+
1. $\|A\|_1 = \max(4, 3) = 4$
36+
2. Solve $A\mathbf{x} = \mathbf{e}_0$: $\mathbf{x}_0 = [0.4,\, -0.2]^\top$
37+
3. Solve $A\mathbf{x} = \mathbf{e}_1$: $\mathbf{x}_1 = [-0.2,\, 0.6]^\top$
38+
4. $A^{-1} = \begin{bmatrix}0.4 & -0.2 \\ -0.2 & 0.6\end{bmatrix}$, so $\|A^{-1}\|_1 = \max(0.6, 0.8) = 0.8$
39+
5. $\kappa_1(A) = 4 \times 0.8 = 3.2$
40+
41+
## Pitfalls & Edge Cases
42+
43+
**Singular matrices** — a row of all zeros makes the system unsolvable. The singularity check fires before entering Gaussian elimination, returning an empty result rather than dividing by zero.
44+
45+
**Near-singular matrices** — the condition number can be astronomically large without any row being identically zero. Partial-pivoting elimination still completes, and the returned value reflects the ill-conditioning faithfully.
46+
47+
**Norm choice** — the 1-norm condition number is a practical upper bound on the spectral condition number $\kappa_2$; it is cheaper by orders of magnitude because it avoids the SVD.
48+
49+
## Variants & Generalizations
50+
51+
The **spectral condition number** ($\kappa_2$, ratio of largest to smallest singular value) is the tightest but requires an SVD. The **reciprocal condition number** ($\text{RCOND} = 1/\kappa$) avoids overflow when $\kappa$ is very large; LAPACK-style solvers return this form and compare against machine epsilon. For **positive-definite** systems, Cholesky factorisation supplies the same column-wise solves at half the cost.
52+
53+
## Applications
54+
55+
- **Solver validation**: if $\kappa(A)\cdot\epsilon_{\text{mach}} \gtrsim 1$, the solution has no reliable digits.
56+
- **Kalman filter covariance**: monitoring $\kappa(P)$ detects numerical collapse of the covariance.
57+
- **Regression**: the design-matrix condition number governs least-squares sensitivity to noise.
58+
- **Control design**: ill-conditioned system matrices signal near-uncontrollability or near-unobservability.
59+
60+
## Connections to Other Algorithms
61+
62+
Gaussian elimination with partial pivoting (`solvers::GaussianElimination`) supplies the column-wise solves of $AX = I$. The matrix 1-norm (`math::OneNorm`) supplies both factors of the product. Cholesky decomposition (`solvers::CholeskyDecomposition`) is the positive-definite alternative for the inverse solve.
63+
64+
## References & Further Reading
65+
66+
- Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., Chapter 3 (Gaussian elimination)
67+
- Higham, N. J., "Accuracy and Stability of Numerical Algorithms", 2nd ed., Chapter 6 (condition numbers)
68+
- Trefethen, L. N. & Bau, D., "Numerical Linear Algebra", Lecture 12 (conditioning)

doc/solvers/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ Numerical solvers for linear systems, polynomial roots, and matrix equations.
1212
| [Durand-Kerner](DurandKerner.md) | Simultaneous iterative root-finder for polynomials |
1313
| [Discrete Algebraic Riccati Equation](DiscreteAlgebraicRiccatiEquation.md) | Iterative solver for the DARE arising in LQR and Kalman filter design |
1414
| [Runge-Kutta ODE Integrators](RungeKuttaIntegrators.md) | Fixed-step RK4 and adaptive Dormand-Prince RK45 for ODE integration |
15+
| [Condition Number](ConditionNumber.md) | 1-norm condition number estimate via column-wise inverse solve |

numerical/math/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ target_sources(numerical.math PRIVATE
1717
HyperbolicFunctions.hpp
1818
LinearTimeInvariant.hpp
1919
Matrix.hpp
20+
MatrixNorms.hpp
2021
QNumber.hpp
2122
Quaternion.hpp
2223
RecursiveBuffer.hpp

numerical/math/MatrixNorms.hpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#pragma once
2+
#if defined(__GNUC__) || defined(__clang__)
3+
#pragma GCC optimize("O3", "fast-math")
4+
#endif
5+
#include "numerical/math/CompilerOptimizations.hpp"
6+
#include "numerical/math/Matrix.hpp"
7+
#include <cmath>
8+
#include <cstddef>
9+
#include <optional>
10+
#include <type_traits>
11+
12+
namespace math
13+
{
14+
template<typename T, std::size_t Rows, std::size_t Cols>
15+
[[nodiscard]] OPTIMIZE_FOR_SPEED T FrobeniusNorm(const Matrix<T, Rows, Cols>& a)
16+
{
17+
static_assert(std::is_floating_point_v<T>, "MatrixNorms supports floating-point types");
18+
T sum{};
19+
for (std::size_t i = 0; i < Rows; ++i)
20+
for (std::size_t j = 0; j < Cols; ++j)
21+
sum += a.at(i, j) * a.at(i, j);
22+
return std::sqrt(sum);
23+
}
24+
25+
template<typename T, std::size_t Rows, std::size_t Cols>
26+
[[nodiscard]] OPTIMIZE_FOR_SPEED T OneNorm(const Matrix<T, Rows, Cols>& a)
27+
{
28+
static_assert(std::is_floating_point_v<T>, "MatrixNorms supports floating-point types");
29+
T maxColSum{};
30+
for (std::size_t j = 0; j < Cols; ++j)
31+
{
32+
T colSum{};
33+
for (std::size_t i = 0; i < Rows; ++i)
34+
colSum += std::abs(a.at(i, j));
35+
if (colSum > maxColSum)
36+
maxColSum = colSum;
37+
}
38+
return maxColSum;
39+
}
40+
41+
template<typename T, std::size_t Rows, std::size_t Cols>
42+
[[nodiscard]] OPTIMIZE_FOR_SPEED T InfinityNorm(const Matrix<T, Rows, Cols>& a)
43+
{
44+
static_assert(std::is_floating_point_v<T>, "MatrixNorms supports floating-point types");
45+
T maxRowSum{};
46+
for (std::size_t i = 0; i < Rows; ++i)
47+
{
48+
T rowSum{};
49+
for (std::size_t j = 0; j < Cols; ++j)
50+
rowSum += std::abs(a.at(i, j));
51+
if (rowSum > maxRowSum)
52+
maxRowSum = rowSum;
53+
}
54+
return maxRowSum;
55+
}
56+
57+
template<typename T, std::size_t Size>
58+
[[nodiscard]] OPTIMIZE_FOR_SPEED T VectorNorm(const Vector<T, Size>& v)
59+
{
60+
static_assert(std::is_floating_point_v<T>, "MatrixNorms supports floating-point types");
61+
T sum{};
62+
for (std::size_t i = 0; i < Size; ++i)
63+
sum += v.at(i, 0) * v.at(i, 0);
64+
return std::sqrt(sum);
65+
}
66+
67+
template<typename T, std::size_t Size>
68+
[[nodiscard]] OPTIMIZE_FOR_SPEED std::optional<Vector<T, Size>> Normalize(const Vector<T, Size>& v)
69+
{
70+
static_assert(std::is_floating_point_v<T>, "MatrixNorms supports floating-point types");
71+
T n = VectorNorm(v);
72+
if (n < static_cast<T>(1e-12))
73+
return std::nullopt;
74+
Vector<T, Size> result;
75+
for (std::size_t i = 0; i < Size; ++i)
76+
result.at(i, 0) = v.at(i, 0) / n;
77+
return result;
78+
}
79+
}

numerical/math/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ target_sources(numerical.math_test PRIVATE
1111
TestComplexNumber.cpp
1212
TestCordic.cpp
1313
TestLinearTimeInvariant.cpp
14+
TestMatrixNorms.cpp
1415
TestQNumber.cpp
1516
TestMatrix.cpp
1617
TestQuaternion.cpp
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#include "numerical/math/MatrixNorms.hpp"
2+
#include "numerical/math/Tolerance.hpp"
3+
#include <gtest/gtest.h>
4+
5+
namespace
6+
{
7+
class MatrixNormsTest : public ::testing::Test
8+
{
9+
protected:
10+
math::Matrix<float, 2, 2> a{
11+
{ 3.0f, 1.0f },
12+
{ 1.0f, 2.0f }
13+
};
14+
math::Vector<float, 2> v{ { 3.0f }, { 4.0f } };
15+
};
16+
}
17+
18+
TEST_F(MatrixNormsTest, FrobeniusNorm)
19+
{
20+
float result = math::FrobeniusNorm(a);
21+
EXPECT_NEAR(result, 3.87298f, math::Tolerance<float>());
22+
}
23+
24+
TEST_F(MatrixNormsTest, OneNorm)
25+
{
26+
float result = math::OneNorm(a);
27+
EXPECT_NEAR(result, 4.0f, math::Tolerance<float>());
28+
}
29+
30+
TEST_F(MatrixNormsTest, InfinityNorm)
31+
{
32+
float result = math::InfinityNorm(a);
33+
EXPECT_NEAR(result, 4.0f, math::Tolerance<float>());
34+
}
35+
36+
TEST_F(MatrixNormsTest, VectorNorm)
37+
{
38+
float result = math::VectorNorm(v);
39+
EXPECT_NEAR(result, 5.0f, math::Tolerance<float>());
40+
}
41+
42+
TEST_F(MatrixNormsTest, NormalizeUnit)
43+
{
44+
auto result = math::Normalize(v);
45+
ASSERT_TRUE(result.has_value());
46+
EXPECT_NEAR(result->at(0, 0), 0.6f, math::Tolerance<float>());
47+
EXPECT_NEAR(result->at(1, 0), 0.8f, math::Tolerance<float>());
48+
}
49+
50+
TEST_F(MatrixNormsTest, NormalizeZeroVectorReturnsNullopt)
51+
{
52+
math::Vector<float, 2> zero{ { 0.0f }, { 0.0f } };
53+
auto result = math::Normalize(zero);
54+
EXPECT_FALSE(result.has_value());
55+
}

0 commit comments

Comments
 (0)