Linear Algebra  arduino
Accessible implementations of linear algebra algorithms
NoPivotLU.cpp
Go to the documentation of this file.
1 #ifndef ARDUINO
2 #include <linalg/NoPivotLU.hpp>
3 #else
5 #endif
6 
22  // For the intermediate calculations, we'll be working with LU.
23  // It is initialized to the square n×n matrix to be factored.
24 
25  assert(LU.rows() == LU.cols());
26 
27  // The goal of the LU factorization algorithm is to repeatedly apply
28  // transformations Lₖ to the matrix A to eventually end up with an upper-
29  // triangular matrix U:
30  //
31  // Lₙ⋯L₂L₁A = U
32  //
33  // The first transformation L₁ will introduce zeros below the diagonal in
34  // the first column of A, L₂ will introduce zeros below the diagonal in the
35  // second column of L₁A (while preserving the zeros introduced by L₁), and
36  // so on, until all elements below the diagonal are zero.
37 
38  // Loop over all columns of A:
39  for (size_t k = 0; k < LU.cols(); ++k) {
40  // In the following comments, k = [1, n], because this is more intuitive
41  // and it follows the usual mathematical convention.
42  // In the code, however, array indices start at zero, so k = [0, n-1].
43 
44  // In order to introduce a zero in the i-th row and the k-th column of
45  // the matrix, subtract a multiple of the k-th row from the i-th row,
46  // using a scaling factor lᵢₖ such that
47  //
48  // A(i,k) - lᵢₖ·A(k,k) = 0
49  // lᵢₖ = A(i,k) / A(k,k)
50  //
51  // This is the typical Gaussian elimination algorithm.
52  // The element A(k,k) is often called the pivot.
53 
54  // The first update step (k=1) subtracts multiples of the first row from
55  // the rows below it.
56  // It can be represented as a lower-triangular matrix L₁:
57  // ┌ ┐
58  // │ 1 │
59  // │ -l₁₁ 1 │
60  // │ -l₂₁ 0 1 │
61  // │ -l₃₁ 0 0 1 │
62  // └ ┘
63  // Compute the product L₁A to verify this!
64  // You can see that the diagonal just preserves all rows, and the
65  // factors in the first columns of L₁ subtract a multiple of the first
66  // row from the other rows.
67  //
68  // The next step (k=2) follows exactly the same principle, but now
69  // subtracts multiples of the second row, resulting in a matrix L₂:
70  // ┌ ┐
71  // │ 1 │
72  // │ 0 1 │
73  // │ 0 -l₁₂ 1 │
74  // │ 0 -l₂₂ 0 1 │
75  // └ ┘
76  // This can be continued for all n columns of A.
77  //
78  // After applying matrices L₁ through Lₙ to A, it has been transformed
79  // into an upper-triangular matrix U:
80  //
81  // Lₙ⋯L₂L₁A = U
82  // A = L₁⁻¹L₂⁻¹⋯Lₙ⁻¹U
83  // A = LU
84  //
85  // Where
86  // L = L₁⁻¹L₂⁻¹⋯Lₙ⁻¹
87  //
88  // Luckily, inverting the matrices Lₖ is trivial, since they are sparse
89  // lower-triangular matrices with a determinant of 1, so their inverses
90  // will be equal to their adjugates.
91  // As an example, and without loss of generality, computing the adjugate
92  // of L₂ results in:
93  // ┌ ┐
94  // │ 1 │
95  // │ 0 1 │
96  // │ 0 l₁₂ 1 │
97  // │ 0 l₂₂ 0 1 │
98  // └ ┘
99  // This result is not immediately obvious, you have to write out some
100  // 3×3 determinants, but it's clear that many of them will be 0 or 1.
101  // Recall that the adjugate of a matrix is the transpose of the cofactor
102  // matrix.
103  //
104  // Finally, computing the product of all Lₖ⁻¹ factors is trivial as
105  // well, because of the structure of the factors. For example,
106  // L₁⁻¹L₂⁻¹ =
107  // ┌ ┐
108  // │ 1 │
109  // │ l₁₁ 1 │
110  // │ l₂₁ l₁₂ 1 │
111  // │ l₃₁ l₂₂ 0 1 │
112  // └ ┘
113  // In conclusion, we can just combine all factors lᵢₖ into the matrix L,
114  // without explicitly having to invert or multiply any matrices. Even
115  // the minus signs cancel out!
116 
117  // Note that by applying this transformation Lₖ to A, you'll always end
118  // up with zeros below the diagonal in the current column k, so there's
119  // no point in saving these zeros explicitly. Instead, this space is
120  // used to store the scaling factors lᵢₖ, i.e. the strict lower-
121  // triangular elements of L. The diagonal elements of L don't have to be
122  // stored either, because they're always 1.
123 
124  // Use the diagonal element as the pivot:
125  double pivot = LU(k, k);
126 
127  // Compute the k-th column of L, the coefficients lᵢₖ:
128  for (size_t i = k + 1; i < LU.rows(); ++i)
129  LU(i, k) /= pivot;
130 
131  // Now update the rest of the matrix, we already (implicitly) introduced
132  // zeros below the diagonal in the k-th column, and we also stored the
133  // scaling factors for each row that determine Lₖ, but we haven't
134  // actually subtracted the multiples of the pivot row from the rest of
135  // the matrix yet, or in other words, we haven't multiplied the bottom
136  // right block of the matrix by Lₖ yet.
137  //
138  // Again, Lₖ has already been implicitly applied to the k-th column by
139  // setting all values below the diagonal to zero. Also the k-th row is
140  // unaffected by Lₖ because the k-th row of Lₖ is always equal to
141  // ̅eₖ = (0 ... 0 1 0 ... 0).
142  // This means only the rows and columns k+1 through n have to be
143  // updated.
144 
145  // Update the trailing submatrix A'(k+1:n,k+1:n) = LₖA(k+1:n,k+1:n):
146  for (size_t c = k + 1; c < LU.cols(); ++c)
147  // Subtract lᵢₖ times the current pivot row A(k,:):
148  for (size_t i = k + 1; i < LU.rows(); ++i)
149  // A'(i,c) = 1·A(i,c) - lᵢₖ·A(k,c)
150  LU(i, c) -= LU(i, k) * LU(k, c);
151 
152  // We won't handle this here explicitly, but notice how the algorithm
153  // fails when the value of the pivot is zero (or very small), as this
154  // will cause a division by zero. When using IEEE 754 floating point
155  // numbers, this means that the factors lᵢₖ will overflow to ±∞,
156  // and during later calculations, infinities might be subtracted from
157  // eachother, resulting in many elements becoming NaN (Not a Number).
158  //
159  // Even ignoring the numerical issues with LU factorization, this is a
160  // huge dealbreaker: if a zero pivot is encountered anywhere during the
161  // factorization, it fails.
162  // Zero pivots occur even when the matrix is non-singular.
163  }
164  state = Factored;
165 }
167 
173 void NoPivotLU::back_subs(const Matrix &B, Matrix &X) const {
174  // Solve upper triangular system UX = B by solving each column of B as a
175  // vector system Uxᵢ = bᵢ
176  //
177  // ┌ ┐┌ ┐ ┌ ┐
178  // │ u₁₁ u₁₂ u₁₃ u₁₄ ││ x₁ᵢ │ │ b₁ᵢ │
179  // │ u₂₂ u₂₃ u₂₄ ││ x₂ᵢ │ = │ b₂ᵢ │
180  // │ u₃₃ u₃₄ ││ x₃ᵢ │ │ b₃ᵢ │
181  // │ u₄₄ ││ x₄ᵢ │ │ b₄ᵢ │
182  // └ ┘└ ┘ └ ┘
183  //
184  // b₄ᵢ = u₄₄·x₄ᵢ ⟺ x₄ᵢ = b₄ᵢ/u₄₄
185  // b₃ᵢ = u₃₃·x₃ᵢ + u₃₄·x₄ᵢ ⟺ x₃ᵢ = (b₃ᵢ - u₃₄·x₄ᵢ)/u₃₃
186  // b₂ᵢ = u₂₂·x₂ᵢ + u₂₃·x₃ᵢ + u₂₄·x₄ᵢ ⟺ x₂ᵢ = (b₂ᵢ - u₂₃·x₃ᵢ + u₂₄·x₄ᵢ)/u₂₂
187  // ...
188 
189  for (size_t i = 0; i < B.cols(); ++i) {
190  for (size_t r = LU.rows(); r-- > 0;) {
191  X(r, i) = B(r, i);
192  for (size_t c = r + 1; c < LU.cols(); ++c)
193  X(r, i) -= LU(r, c) * X(c, i);
194  X(r, i) /= LU(r, r);
195  }
196  }
197 }
199 
205 void NoPivotLU::forward_subs(const Matrix &B, Matrix &X) const {
206  // Solve lower triangular system LX = B by solving each column of B as a
207  // vector system Lxᵢ = bᵢ.
208  // The diagonal is always 1, due to the construction of the L matrix in the
209  // LU algorithm.
210  //
211  // ┌ ┐┌ ┐ ┌ ┐
212  // │ 1 ││ x₁ᵢ │ │ b₁ᵢ │
213  // │ l₂₁ 1 ││ x₂ᵢ │ = │ b₂ᵢ │
214  // │ l₃₁ l₃₂ 1 ││ x₃ᵢ │ │ b₃ᵢ │
215  // │ l₄₁ l₄₂ l₄₃ 1 ││ x₄ᵢ │ │ b₄ᵢ │
216  // └ ┘└ ┘ └ ┘
217  //
218  // b₁ᵢ = 1·x₁ᵢ ⟺ x₁ᵢ = b₁ᵢ
219  // b₂ᵢ = l₂₁·x₁ᵢ + 1·x₂ᵢ ⟺ x₂ᵢ = b₂ᵢ - l₂₁·x₁ᵢ
220  // b₃ᵢ = l₃₁·x₁ᵢ + l₃₂·x₂ᵢ + 1·x₃ᵢ ⟺ x₃ᵢ = b₃ᵢ - l₃₂·x₂ᵢ - l₃₁·x₁ᵢ
221  // ...
222 
223  for (size_t i = 0; i < B.cols(); ++i) {
224  for (size_t r = 0; r < LU.rows(); ++r) {
225  X(r, i) = B(r, i);
226  for (size_t c = 0; c < r; ++c)
227  X(r, i) -= LU(r, c) * X(c, i);
228  }
229  }
230 }
232 
239  // Solve the system AX = B, or LUX = B.
240  //
241  // Let UX = Z, and first solve LZ = B, which is a simple lower-triangular
242  // system of equations.
243  // Now that Z is known, solve UX = Z, which is a simple upper-triangular
244  // system of equations.
245  assert(is_factored());
246 
247  forward_subs(B, B); // overwrite B with Z
248  back_subs(B, B); // overwrite B (Z) with X
249 }
251 
252 // All implementations of the less interesting functions can be found here:
253 #include "boilerplate/NoPivotLU.ipp"
General matrix class.
Definition: Matrix.hpp:34
size_t rows() const
Get the number of rows of the matrix.
Definition: Matrix.hpp:81
size_t cols() const
Get the number of columns of the matrix.
Definition: Matrix.hpp:83
SquareMatrix LU
Result of a LU factorization: stores the upper-triangular matrix U and the strict lower-triangular pa...
Definition: NoPivotLU.hpp:137
void back_subs(const Matrix &B, Matrix &X) const
Back substitution algorithm for solving upper-triangular systems UX = B.
Definition: NoPivotLU.cpp:173
void solve_inplace(Matrix &B) const
Solve the system AX = B or LUX = B.
Definition: NoPivotLU.cpp:238
bool is_factored() const
Check if this object contains a factorization.
Definition: NoPivotLU.hpp:104
void forward_subs(const Matrix &B, Matrix &X) const
Forward substitution algorithm for solving lower-triangular systems LX = B.
Definition: NoPivotLU.cpp:205
void compute_factorization()
The actual LU factorization algorithm.
Definition: NoPivotLU.cpp:21
enum NoPivotLU::State state