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