Skip to content

Commit 73b74b1

Browse files
reuse components to reduce duplication
1 parent cb8dc75 commit 73b74b1

19 files changed

Lines changed: 248 additions & 77 deletions

doc/math/MatrixNorms.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ Vector normalisation, closely related, produces the unit-length direction of a v
88

99
## Mathematical Theory
1010

11-
### Vector Norm
11+
### Dot Product & Vector Norm
1212

13-
For a vector $\mathbf{v} \in \mathbb{R}^n$, the Euclidean (L2) norm is
13+
For vectors $\mathbf{a}, \mathbf{b} \in \mathbb{R}^n$, the dot product is $\mathbf{a}\cdot\mathbf{b} = \sum_{i=1}^{n} a_i b_i$. The Euclidean (L2) norm is the square root of the self dot product:
1414

15-
$$\|\mathbf{v}\|_2 = \sqrt{\sum_{i=1}^{n} v_i^2}$$
15+
$$\|\mathbf{v}\|_2 = \sqrt{\mathbf{v}\cdot\mathbf{v}} = \sqrt{\sum_{i=1}^{n} v_i^2}$$
1616

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.
17+
`VectorNorm` is implemented in terms of `DotProduct`. 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.
1818

1919
### Matrix Norms
2020

@@ -38,6 +38,7 @@ The 1-norm and infinity-norm are dual: $\|A\|_\infty = \|A^\top\|_1$.
3838

3939
| Operation | Time | Space | Notes |
4040
|---------------|---------|--------|------------------------------|
41+
| DotProduct | $O(n)$ | $O(1)$ | Single pass; `VectorNorm` builds on it |
4142
| FrobeniusNorm | $O(mn)$ | $O(1)$ | Single pass, no allocation |
4243
| OneNorm | $O(mn)$ | $O(1)$ | Column-wise sum, running max |
4344
| InfinityNorm | $O(mn)$ | $O(1)$ | Row-wise sum, running max |

doc/math/MatrixOperations.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Matrix Operations
2+
3+
## Overview & Motivation
4+
5+
Small structural matrix utilities that are needed across the library but do not belong to any single algorithm:
6+
7+
- **`Symmetrize`** enforces exact symmetry on a matrix that should be symmetric in theory but drifts under floating-point round-off.
8+
- **`CongruenceTransform`** computes the quadratic form `A·M·Aᵀ` — the single most common shape in covariance-propagating code (Kalman predict `F·P·Fᵀ`, innovation covariance `H·P·Hᵀ`, Joseph update `(I−KH)·P·(I−KH)ᵀ`, etc.).
9+
10+
Both are recurring needs in Kalman filters, EM parameter updates, and Riccati/Lyapunov solvers.
11+
12+
## Mathematical Theory
13+
14+
Any square matrix decomposes into a symmetric and a skew-symmetric part:
15+
16+
$$M = \underbrace{\tfrac{1}{2}(M + M^\top)}_{\text{symmetric}} + \underbrace{\tfrac{1}{2}(M - M^\top)}_{\text{skew}}$$
17+
18+
`Symmetrize` returns the symmetric part $\tfrac{1}{2}(M + M^\top)$. It is the orthogonal projection (in the Frobenius inner product) of $M$ onto the subspace of symmetric matrices, so it is the *closest* symmetric matrix to $M$. Applying it to an already-symmetric matrix is a no-op (idempotent).
19+
20+
If $M$ is symmetric, `CongruenceTransform` returns a symmetric result exactly (in exact arithmetic): $(AMA^\top)^\top = A M^\top A^\top = A M A^\top$. This makes it the natural building block for propagating a covariance $P$ through a linear map $A$: $P \mapsto A P A^\top$.
21+
22+
## Complexity Analysis
23+
24+
| Operation | Time | Space | Notes |
25+
|--------------------|----------|----------|----------------------------------------|
26+
| Symmetrize | $O(n^2)$ | $O(n^2)$ | One transpose, add, scale |
27+
| CongruenceTransform| $O(n^2 m + n m^2)$ | $O(nm)$ | For $A \in \mathbb{R}^{n\times m}$, two matrix products |
28+
29+
## Step-by-Step Walkthrough
30+
31+
**Symmetrize** — for $M = \begin{bmatrix}1 & 3 \\ -1 & 2\end{bmatrix}$: $M^\top = \begin{bmatrix}1 & -1 \\ 3 & 2\end{bmatrix}$, so $\tfrac{1}{2}(M + M^\top) = \begin{bmatrix}1 & 1 \\ 1 & 2\end{bmatrix}$ — off-diagonals averaged, diagonal unchanged.
32+
33+
**CongruenceTransform** — with $A \in \mathbb{R}^{n\times m}$ and symmetric $M \in \mathbb{R}^{m\times m}$, the result $A M A^\top \in \mathbb{R}^{n\times n}$ is the covariance of $A x$ when $x$ has covariance $M$.
34+
35+
## Pitfalls & Edge Cases
36+
37+
**Symmetrize is not a fix for indefiniteness** — it removes the skew part but does not make a matrix positive-definite; covariance code typically also adds a small diagonal jitter (`+ εI`) separately.
38+
39+
**CongruenceTransform association** — the implementation evaluates `(A·M)·Aᵀ`, matching the left-associative `operator*`; results are bit-identical to hand-written `A * M * A.Transpose()`. Under `fast-math` the symmetry of the output can still carry tiny round-off asymmetry — follow with `Symmetrize` when exact symmetry is required.
40+
41+
**Float-only** — both are `static_assert(std::is_floating_point_v<T>)`.
42+
43+
## Variants & Generalizations
44+
45+
The skew-symmetric part $\tfrac{1}{2}(M - M^\top)$ is `Symmetrize`'s companion. The transposed congruence $A^\top M A$ (used by `DiscreteAlgebraicRiccatiEquation`) is obtained by passing `A.Transpose()`.
46+
47+
## Applications
48+
49+
- **Kalman predict / update**`F·P·Fᵀ`, `H·P·Hᵀ`, Joseph form `(I−KH)·P·(I−KH)ᵀ + K·R·Kᵀ` across the KF/EKF/UKF/smoother family.
50+
- **EM / covariance updates** — re-symmetrize `Q`, `R`, `P` after asymmetric matrix products (`estimators::ExpectationMaximization`).
51+
- **Riccati / Lyapunov solutions** — propagate and enforce symmetry of the solution matrix.
52+
53+
## Connections to Other Algorithms
54+
55+
Operate on `math::Matrix` / `math::SquareMatrix`. Consumed by the `filters::active` Kalman family and `estimators::ExpectationMaximization`; `CongruenceTransform` pairs naturally with `Symmetrize` for covariance-positivity hygiene.
56+
57+
## References & Further Reading
58+
59+
- Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., §2 (symmetric/skew decomposition)
60+
- Higham, N. J., "Accuracy and Stability of Numerical Algorithms", 2nd ed. (symmetry enforcement in covariance recursions)

doc/math/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ Core mathematical primitives for numerical computation.
1212
| [Householder Transform](HouseholderTransform.md) | Householder reflector for a sub-column — orthogonal, backward-stable factorization primitive |
1313
| [Givens Rotation](GivensRotation.md) | Plane rotation zeroing one entry — streaming/sparse factorization primitive |
1414
| [Triangular Solve](TriangularSolve.md) | Upper-triangular back-substitution shared by Gaussian elimination and QR |
15+
| [Matrix Operations](MatrixOperations.md) | Structural matrix utilities — `Symmetrize` (closest symmetric matrix) |
1516
| [Step Response Metrics](StepResponseMetrics.md) | Rise time, settling time, percent overshoot, peak time, and steady-state error from a bounded step-response vector |

numerical/estimators/ConsistencyMetrics.hpp

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "numerical/math/CompilerOptimizations.hpp"
88
#include "numerical/math/Matrix.hpp"
9+
#include "numerical/math/MatrixNorms.hpp"
910
#include "numerical/solvers/GaussianElimination.hpp"
1011
#include <array>
1112
#include <cmath>
@@ -48,7 +49,6 @@ namespace estimators
4849

4950
private:
5051
[[nodiscard]] static OPTIMIZE_FOR_SPEED std::optional<StateVector> Solve(const CovarianceMatrix& matrix, const StateVector& rhs);
51-
[[nodiscard]] static OPTIMIZE_FOR_SPEED T DotProduct(const StateVector& a, const StateVector& b);
5252
};
5353

5454
template<typename T, std::size_t Dim>
@@ -57,7 +57,7 @@ namespace estimators
5757
auto z = Solve(covariance, error);
5858
if (!z.has_value())
5959
return std::nullopt;
60-
return DotProduct(error, z.value());
60+
return math::DotProduct(error, z.value());
6161
}
6262

6363
template<typename T, std::size_t Dim>
@@ -66,7 +66,7 @@ namespace estimators
6666
auto z = Solve(innovationCovariance, innovation);
6767
if (!z.has_value())
6868
return std::nullopt;
69-
return DotProduct(innovation, z.value());
69+
return math::DotProduct(innovation, z.value());
7070
}
7171

7272
template<typename T, std::size_t Dim>
@@ -106,15 +106,6 @@ namespace estimators
106106
return solver.Solve(matrix, rhs);
107107
}
108108

109-
template<typename T, std::size_t Dim>
110-
OPTIMIZE_FOR_SPEED T ConsistencyMetrics<T, Dim>::DotProduct(const StateVector& a, const StateVector& b)
111-
{
112-
T result{};
113-
for (std::size_t i = 0; i < Dim; ++i)
114-
result += a.at(i, 0) * b.at(i, 0);
115-
return result;
116-
}
117-
118109
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
119110
extern template class ConsistencyMetrics<float, 1>;
120111
extern template class ConsistencyMetrics<float, 2>;

numerical/estimators/offline/ExpectationMaximization.hpp

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "numerical/filters/active/KalmanSmoother.hpp"
88
#include "numerical/math/CompilerOptimizations.hpp"
99
#include "numerical/math/Matrix.hpp"
10+
#include "numerical/math/MatrixOperations.hpp"
1011
#include "numerical/solvers/GaussianElimination.hpp"
1112
#include <array>
1213
#include <cmath>
@@ -78,9 +79,6 @@ namespace estimators
7879
const StateMatrix& fullStateCov,
7980
const MeasurementCovariance& obsOuterSum,
8081
std::size_t numSteps) const;
81-
82-
template<std::size_t N>
83-
static math::SquareMatrix<float, N> Symmetrize(const math::SquareMatrix<float, N>& M);
8482
};
8583

8684
// Implementation //
@@ -191,7 +189,7 @@ namespace estimators
191189
laggedStateCov, transitionCrossCov.Transpose())
192190
.Transpose();
193191
const float invTm1 = 1.0f / static_cast<float>(numSteps - 1);
194-
auto Q_new = Symmetrize<StateSize>((currentStateCov - F_new * transitionCrossCov.Transpose()) * invTm1);
192+
auto Q_new = math::Symmetrize((currentStateCov - F_new * transitionCrossCov.Transpose()) * invTm1);
195193
Q_new += StateMatrix::Identity() * 1e-6f;
196194
return { F_new, Q_new };
197195
}
@@ -210,19 +208,11 @@ namespace estimators
210208
fullStateCov, obsStateCrossCov.Transpose())
211209
.Transpose();
212210
const float invT = 1.0f / static_cast<float>(numSteps);
213-
auto R_new = Symmetrize<MeasurementSize>((obsOuterSum - H_new * obsStateCrossCov.Transpose()) * invT);
211+
auto R_new = math::Symmetrize((obsOuterSum - H_new * obsStateCrossCov.Transpose()) * invT);
214212
R_new += MeasurementCovariance::Identity() * 1e-6f;
215213
return { H_new, R_new };
216214
}
217215

218-
template<std::size_t StateSize, std::size_t MeasurementSize, std::size_t MaxSteps>
219-
template<std::size_t N>
220-
math::SquareMatrix<float, N>
221-
ExpectationMaximization<StateSize, MeasurementSize, MaxSteps>::Symmetrize(const math::SquareMatrix<float, N>& M)
222-
{
223-
return (M + M.Transpose()) * 0.5f;
224-
}
225-
226216
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
227217
extern template class ExpectationMaximization<2, 1, 10>;
228218
extern template class ExpectationMaximization<4, 2, 20>;

numerical/estimators/offline/LinearRegression.hpp

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
#include "numerical/estimators/Estimator.hpp"
1010
#include "numerical/math/CompilerOptimizations.hpp"
11-
#include "numerical/solvers/GaussianElimination.hpp"
11+
#include "numerical/solvers/QrDecomposition.hpp"
1212

1313
namespace estimators
1414
{
@@ -40,17 +40,15 @@ namespace estimators
4040

4141
for (size_t i = 0; i < Samples; ++i)
4242
{
43-
X_design.at(i, 0) = T(0.9999f);
43+
X_design.at(i, 0) = T{ 1 };
4444

4545
for (size_t j = 0; j < Features; ++j)
4646
X_design.at(i, j + 1) = X.at(i, j);
4747
}
4848

49-
auto X_transpose = X_design.Transpose();
50-
auto XtX = X_transpose * X_design;
51-
auto Xty = X_transpose * y;
52-
53-
coefficients = solvers::SolveSystem<T, Features + 1, 1>(XtX, Xty);
49+
solvers::QrDecomposition<T, Samples, Features + 1> qr;
50+
qr.Decompose(X_design);
51+
coefficients = qr.SolveLeastSquares(y);
5452
}
5553

5654
template<typename T, std::size_t Samples, std::size_t Features>

numerical/estimators/offline/PolynomialFitting.hpp

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
#include "numerical/math/CompilerOptimizations.hpp"
88
#include "numerical/math/Matrix.hpp"
9-
#include "numerical/solvers/GaussianElimination.hpp"
9+
#include "numerical/solvers/QrDecomposition.hpp"
1010
#include <type_traits>
1111

1212
namespace estimators
@@ -43,11 +43,9 @@ namespace estimators
4343
v.at(i, j) = v.at(i, j - 1) * x.at(i, 0);
4444
}
4545

46-
auto vt = v.Transpose();
47-
auto normalMatrix = vt * v;
48-
auto rhs = vt * y;
49-
50-
coefficients = solvers::SolveSystem<T, Degree + 1, 1>(normalMatrix, rhs);
46+
solvers::QrDecomposition<T, Samples, Degree + 1> qr;
47+
qr.Decompose(v);
48+
coefficients = qr.SolveLeastSquares(y);
5149
}
5250

5351
template<typename T, std::size_t Samples, std::size_t Degree>

numerical/filters/active/ExtendedKalmanFilter.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "infra/util/Function.hpp"
44
#include "numerical/filters/active/KalmanFilterBase.hpp"
55
#include "numerical/math/CompilerOptimizations.hpp"
6+
#include "numerical/math/MatrixOperations.hpp"
67

78
#if defined(__GNUC__) || defined(__clang__)
89
#pragma GCC optimize("O3", "fast-math")
@@ -94,7 +95,7 @@ namespace filters
9495
{
9596
auto F = stateJacobianFn(this->state());
9697
this->state() = stateTransitionFn(this->state());
97-
this->covariance() = F * this->covariance() * F.Transpose() + this->processNoise();
98+
this->covariance() = math::CongruenceTransform(F, this->covariance()) + this->processNoise();
9899
}
99100

100101
template<typename QNumberType, std::size_t StateSize, std::size_t MeasurementSize, std::size_t ControlSize>
@@ -103,7 +104,7 @@ namespace filters
103104
{
104105
auto F = stateJacobianWithControlFn(this->state(), u);
105106
this->state() = stateTransitionWithControlFn(this->state(), u);
106-
this->covariance() = F * this->covariance() * F.Transpose() + this->processNoise();
107+
this->covariance() = math::CongruenceTransform(F, this->covariance()) + this->processNoise();
107108
}
108109

109110
template<typename QNumberType, std::size_t StateSize, std::size_t MeasurementSize, std::size_t ControlSize>

numerical/filters/active/KalmanFilter.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "numerical/filters/active/KalmanFilterBase.hpp"
44
#include "numerical/math/CompilerOptimizations.hpp"
55
#include "numerical/math/LinearTimeInvariant.hpp"
6+
#include "numerical/math/MatrixOperations.hpp"
67

78
#if defined(__GNUC__) || defined(__clang__)
89
#pragma GCC optimize("O3", "fast-math")
@@ -108,15 +109,15 @@ namespace filters
108109
OPTIMIZE_FOR_SPEED void KalmanFilter<QNumberType, StateSize, MeasurementSize, ControlSize>::Predict()
109110
{
110111
this->state() = stateTransition * this->state();
111-
this->covariance() = stateTransition * this->covariance() * stateTransition.Transpose() + this->processNoise();
112+
this->covariance() = math::CongruenceTransform(stateTransition, this->covariance()) + this->processNoise();
112113
}
113114

114115
template<typename QNumberType, std::size_t StateSize, std::size_t MeasurementSize, std::size_t ControlSize>
115116
OPTIMIZE_FOR_SPEED void KalmanFilter<QNumberType, StateSize, MeasurementSize, ControlSize>::Predict(const ControlVector& u)
116117
requires(ControlSize > 0)
117118
{
118119
this->state() = stateTransition * this->state() + controlInputMatrix * u;
119-
this->covariance() = stateTransition * this->covariance() * stateTransition.Transpose() + this->processNoise();
120+
this->covariance() = math::CongruenceTransform(stateTransition, this->covariance()) + this->processNoise();
120121
}
121122

122123
template<typename QNumberType, std::size_t StateSize, std::size_t MeasurementSize, std::size_t ControlSize>

numerical/filters/active/KalmanFilterBase.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "numerical/math/CompilerOptimizations.hpp"
44
#include "numerical/math/Matrix.hpp"
5+
#include "numerical/math/MatrixOperations.hpp"
56
#include "numerical/solvers/GaussianElimination.hpp"
67

78
#if defined(__GNUC__) || defined(__clang__)
@@ -135,7 +136,7 @@ namespace filters
135136
typename KalmanFilterBase<QNumberType, StateSize, MeasurementSize, ControlSize>::KalmanGain
136137
KalmanFilterBase<QNumberType, StateSize, MeasurementSize, ControlSize>::ComputeKalmanGain(const MeasurementMatrix& H) const
137138
{
138-
auto S = H * covariance_ * H.Transpose() + measurementNoise_;
139+
auto S = math::CongruenceTransform(H, covariance_) + measurementNoise_;
139140

140141
// Solve S Kᵀ = H P instead of explicit K = P Hᵀ S⁻¹ (S is symmetric)
141142
auto KTranspose = solvers::SolveSystem<QNumberType, MeasurementSize, StateSize>(S, H * covariance_);
@@ -150,7 +151,7 @@ namespace filters
150151
state_ = state_ + K * innovation;
151152

152153
auto IminusKH = StateMatrix::Identity() - K * H;
153-
covariance_ = IminusKH * covariance_ * IminusKH.Transpose() + K * measurementNoise_ * K.Transpose();
154+
covariance_ = math::CongruenceTransform(IminusKH, covariance_) + math::CongruenceTransform(K, measurementNoise_);
154155
}
155156

156157
template<typename QNumberType, std::size_t StateSize, std::size_t MeasurementSize, std::size_t ControlSize>

0 commit comments

Comments
 (0)