-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add qr decomposition #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
469ba72
add qr decomposition
gabrielfrasantos e0845f0
Apply suggestions from code review
gabrielfrasantos b282186
break down into more libraries
gabrielfrasantos cb8dc75
merge main
gabrielfrasantos 58975ca
Apply suggestions from code review
gabrielfrasantos 73b74b1
reuse components to reduce duplication
gabrielfrasantos 0fab49a
merge main
gabrielfrasantos 554bfdd
Apply suggestions from code review
gabrielfrasantos File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Givens Rotation | ||
|
|
||
| ## Overview & Motivation | ||
|
|
||
| A Givens rotation zeros a single matrix entry by rotating two rows in a plane, leaving all others untouched. Because it touches only two rows, it is the tool of choice for *incremental* linear algebra: streaming a new row into an existing QR factor, sparse triangularization, and the implicit-shift sweeps of QR/SVD eigen-iterations. `ComputeGivens` derives the rotation coefficients; `ApplyGivens` applies them to a scalar pair. | ||
|
|
||
| ## Mathematical Theory | ||
|
|
||
| Given two scalars $a$ (the value to keep) and $b$ (the value to zero), the rotation | ||
|
|
||
| $$G = \begin{bmatrix} c & s \\ -s & c \end{bmatrix}, \quad c = \frac{a}{r},\; s = \frac{b}{r},\; r = \sqrt{a^2 + b^2}$$ | ||
|
|
||
| is orthogonal ($c^2 + s^2 = 1$) and satisfies | ||
|
|
||
| $$G \begin{bmatrix} a \\ b \end{bmatrix} = \begin{bmatrix} r \\ 0 \end{bmatrix}$$ | ||
|
|
||
| Applied across a pair of rows, it zeros the target entry while preserving Euclidean length. | ||
|
|
||
| ## Complexity Analysis | ||
|
|
||
| | Operation | Time | Space | Notes | | ||
| |-----------------|--------|--------|----------------------------------| | ||
| | ComputeGivens | $O(1)$ | $O(1)$ | One `sqrt`, two divides | | ||
| | ApplyGivens | $O(1)$ | $O(1)$ | Rotates one scalar pair | | ||
| | Rotate two rows | $O(n)$ | $O(1)$ | `ApplyGivens` across `n` columns | | ||
|
|
||
| ## Step-by-Step Walkthrough | ||
|
|
||
| `ComputeGivens(3, 4)`: $r = 5$, $c = 0.6$, $s = 0.8$. Applying to $(x, y) = (3, 4)$: $x' = 0.6\cdot 3 + 0.8\cdot 4 = 5$, $y' = -0.8\cdot 3 + 0.6\cdot 4 = 0$ — the second component is annihilated and the norm is preserved. | ||
|
|
||
| ## Pitfalls & Edge Cases | ||
|
|
||
| **Degenerate pair** — when $a = b = 0$ the rotation is undefined; `ComputeGivens` returns the identity $(c, s) = (1, 0)$ so applying it is a safe no-op. | ||
|
|
||
| **Float-only** — `static_assert(std::is_floating_point_v<T>)`. | ||
|
|
||
| ## Variants & Generalizations | ||
|
|
||
| Householder reflectors (`math::HouseholderTransform`) zero an entire sub-column at once and are cheaper for dense factorization; Givens wins when only one entry (or one streamed row) changes. Fast/"square-root-free" Givens variants trade the `sqrt` for extra bookkeeping. | ||
|
|
||
| ## Applications | ||
|
|
||
| - **Streaming QR update** — rotate a new row into an existing `R` (`QrDecomposition::GivensUpdateRow`). | ||
| - **QR / SVD iterations** — implicit-shift bulge chasing. | ||
| - **Sparse triangularization** — zero isolated entries without touching the rest. | ||
|
|
||
| ## Connections to Other Algorithms | ||
|
|
||
| Used by `solvers::QrDecomposition`; complements `math::HouseholderTransform`. | ||
|
|
||
| ## References & Further Reading | ||
|
|
||
| - Givens, W., "Computation of Plane Unitary Rotations Transforming a General Matrix to Triangular Form", SIAM J. Appl. Math. 6(1), 1958 | ||
| - Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., §5.1 (Givens rotations) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Householder Transform | ||
|
|
||
| ## Overview & Motivation | ||
|
|
||
| A Householder reflector is the workhorse of numerically stable dense linear algebra. A single reflector mirrors a vector onto a coordinate axis, zeroing every entry below a chosen pivot in one orthogonal step. Chaining reflectors triangularizes a matrix (QR), bidiagonalizes it (SVD), or tridiagonalizes a symmetric matrix (eigensolvers). `HouseholderVector` computes the reflector for one sub-column; it is the shared primitive those factorizations call. | ||
|
|
||
| ## Mathematical Theory | ||
|
|
||
| For a vector $x$, the Householder reflector is the orthogonal matrix | ||
|
|
||
| $$H = I - \beta\, v v^\top$$ | ||
|
|
||
| chosen so that $Hx$ is zero below the pivot. With $\sigma = \sum_{i>\text{start}} x_i^2$ and $\|x\| = \sqrt{x_{\text{start}}^2 + \sigma}$, the reflector maps $x_{\text{start}} \mapsto \mp\|x\|$. The pivot sign is chosen as $-\operatorname{sign}(x_{\text{start}})\|x\|$ to avoid cancellation: | ||
|
|
||
| $$v_{\text{start}} = 1, \quad v_i = x_i / v_0, \quad \beta = \frac{2 v_0^2}{\sigma + v_0^2}$$ | ||
|
|
||
| $H$ is symmetric and orthogonal ($H = H^\top = H^{-1}$), so applying it is backward stable. | ||
|
|
||
| ## Complexity Analysis | ||
|
|
||
| | Operation | Time | Space | Notes | | ||
| |-----------------------|--------|--------|-----------------------------------------| | ||
| | HouseholderVector | $O(n)$ | $O(1)$ | Builds `v` (stored implicit unit pivot) | | ||
| | Apply $H$ to a vector | $O(n)$ | $O(1)$ | `x - β·v·(vᵀx)` — never form `H` | | ||
|
|
||
| ## Step-by-Step Walkthrough | ||
|
|
||
| For $x = [4, 3, 0, 0]^\top$, pivot `start = 0`: $\sigma = 9$, $\|x\| = 5$. Since $x_0 > 0$, $v_0 = -\sigma/(x_0 + \|x\|) = -1$, giving $\beta = 1$ and $v = [1, -3, 0, 0]^\top$. Reflecting yields $Hx = [-5, 0, 0, 0]^\top$ — the sub-column collapsed onto the axis. | ||
|
|
||
| ## Pitfalls & Edge Cases | ||
|
|
||
| **Already-zero sub-column** — when $\sigma \approx 0$ there is nothing to zero; the routine returns $\beta = 0$ (identity reflector) and callers skip the update. | ||
|
|
||
| **Never form `H` explicitly** — apply it as `x − β·v·(vᵀx)` to keep the cost $O(n)$ per column instead of $O(n^2)$. | ||
|
|
||
| **Float-only** — `static_assert(std::is_floating_point_v<T>)`; the sign-fixing and normalisation assume real floating-point arithmetic. | ||
|
|
||
| ## Variants & Generalizations | ||
|
|
||
| Givens rotations (`math::GivensRotation`) achieve the same zeroing one entry at a time, preferable for sparse or streaming updates. Complex Householder reflectors extend this to unitary triangularization. | ||
|
|
||
| ## Applications | ||
|
|
||
| - **QR decomposition** — one reflector per column triangularizes `A`. | ||
| - **SVD / eigensolvers** — bidiagonalization and tridiagonalization. | ||
| - **Square-root Kalman filtering** — covariance factor updates. | ||
|
|
||
| ## Connections to Other Algorithms | ||
|
|
||
| Used by `solvers::QrDecomposition`; complements `math::GivensRotation` and `math::SolveUpperTriangular`. Operates on `math::Vector`. | ||
|
|
||
| ## References & Further Reading | ||
|
|
||
| - Householder, A. S., "Unitary Triangularization of a Nonsymmetric Matrix", JACM 5(4), 1958 | ||
| - Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., §5.1 (Householder reflections) |
|
gabrielfrasantos marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Matrix Operations | ||
|
|
||
| ## Overview & Motivation | ||
|
|
||
| Small structural matrix utilities that are needed across the library but do not belong to any single algorithm: | ||
|
|
||
| - **`Symmetrize`** enforces exact symmetry on a matrix that should be symmetric in theory but drifts under floating-point round-off. | ||
| - **`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.). | ||
|
|
||
| Both are recurring needs in Kalman filters, EM parameter updates, and Riccati/Lyapunov solvers. | ||
|
|
||
| ## Mathematical Theory | ||
|
|
||
| Any square matrix decomposes into a symmetric and a skew-symmetric part: | ||
|
|
||
| $$M = \underbrace{\tfrac{1}{2}(M + M^\top)}_{\text{symmetric}} + \underbrace{\tfrac{1}{2}(M - M^\top)}_{\text{skew}}$$ | ||
|
|
||
| `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). | ||
|
|
||
| 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$. | ||
|
|
||
| ## Complexity Analysis | ||
|
|
||
| | Operation | Time | Space | Notes | | ||
| |---------------------|--------------------|----------|---------------------------------------------------------| | ||
| | Symmetrize | $O(n^2)$ | $O(n^2)$ | One transpose, add, scale | | ||
| | CongruenceTransform | $O(n^2 m + n m^2)$ | $O(nm)$ | For $A \in \mathbb{R}^{n\times m}$, two matrix products | | ||
|
|
||
| ## Step-by-Step Walkthrough | ||
|
|
||
| **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. | ||
|
|
||
| **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$. | ||
|
|
||
| ## Pitfalls & Edge Cases | ||
|
|
||
| **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. | ||
|
|
||
| **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. | ||
|
|
||
| **Float-only** — both are `static_assert(std::is_floating_point_v<T>)`. | ||
|
|
||
| ## Variants & Generalizations | ||
|
|
||
| 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()`. | ||
|
|
||
| ## Applications | ||
|
|
||
| - **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. | ||
| - **EM / covariance updates** — re-symmetrize `Q`, `R`, `P` after asymmetric matrix products (`estimators::ExpectationMaximization`). | ||
| - **Riccati / Lyapunov solutions** — propagate and enforce symmetry of the solution matrix. | ||
|
|
||
| ## Connections to Other Algorithms | ||
|
|
||
| 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. | ||
|
|
||
| ## References & Further Reading | ||
|
|
||
| - Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., §2 (symmetric/skew decomposition) | ||
| - Higham, N. J., "Accuracy and Stability of Numerical Algorithms", 2nd ed. (symmetry enforcement in covariance recursions) |
|
gabrielfrasantos marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Triangular Solve | ||
|
|
||
| ## Overview & Motivation | ||
|
|
||
| Nearly every dense linear solver ends with a triangular system. Gaussian elimination reduces `Ax = b` to an upper-triangular `Ux = c`; QR factorization solves least squares through `Rx = Qᵀb`; Cholesky solves two triangular systems back-to-back. `SolveUpperTriangular` is the shared back-substitution kernel these routines call so the algorithm lives in exactly one place. | ||
|
|
||
| ## Mathematical Theory | ||
|
|
||
| Given an upper-triangular matrix $R \in \mathbb{R}^{n \times n}$ (entries below the diagonal ignored) and a right-hand side $c$, back-substitution solves $Rx = c$ bottom-up: | ||
|
|
||
| $$x_i = \frac{1}{r_{ii}}\left(c_i - \sum_{j=i+1}^{n} r_{ij}\, x_j\right), \quad i = n, n-1, \dots, 1$$ | ||
|
|
||
| Each unknown depends only on those already computed, so a single reverse sweep suffices. | ||
|
|
||
| ## Complexity Analysis | ||
|
|
||
| | Operation | Time | Space | Notes | | ||
| |----------------------|----------|--------|---------------------------------| | ||
| | SolveUpperTriangular | $O(n^2)$ | $O(n)$ | One reverse sweep, stack output | | ||
|
|
||
| ## Step-by-Step Walkthrough | ||
|
|
||
| For $R = \begin{bmatrix}2 & -1 & 3 \\ 0 & 4 & 1 \\ 0 & 0 & 5\end{bmatrix}$, $c = [10,\, -5,\, 15]^\top$: | ||
|
|
||
| 1. $x_3 = 15 / 5 = 3$ | ||
| 2. $x_2 = (-5 - 1\cdot 3)/4 = -2$ | ||
| 3. $x_1 = (10 - (-1)(-2) - 3\cdot 3)/2 = 1$ ⇒ $x = [1,\, -2,\, 3]^\top$. | ||
|
|
||
| ## Pitfalls & Edge Cases | ||
|
|
||
| **Singular / near-zero pivot** — a zero diagonal entry makes the system unsolvable. The routine asserts `|r_{ii}| > 0` via `really_assert`; callers (`GaussianElimination`, `QrDecomposition`) detect rank deficiency before reaching it. | ||
|
|
||
| **Generic on `T`** — the kernel is templated on any supported numeric type (`float`, `Q15`, `Q31`) because Gaussian elimination is instantiated for all three; it uses `math::ToFloat` only for the pivot assertion. | ||
|
|
||
| ## Variants & Generalizations | ||
|
|
||
| A lower-triangular forward-substitution is the mirror image (top-down sweep) and can be added when Cholesky/LU forward solves need it. Block triangular solves generalise this to matrix right-hand sides. | ||
|
|
||
| ## Applications | ||
|
|
||
| - **Gaussian elimination** — final back-substitution after forward elimination. | ||
| - **QR least squares** — solving `Rx = Qᵀb`. | ||
| - Any factor-then-solve routine producing a triangular factor. | ||
|
|
||
| ## Connections to Other Algorithms | ||
|
|
||
| Shared by `solvers::GaussianElimination` and `solvers::QrDecomposition`. Operates on `math::Matrix` / `math::Vector`. | ||
|
|
||
| ## References & Further Reading | ||
|
|
||
| - Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., §3.1 (triangular systems) | ||
| - Trefethen, L. N. & Bau, D., "Numerical Linear Algebra", Lecture 17 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.