LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lu.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_LU_H
6#define LINE_UTIL_LU_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * LU factorization with partial pivoting, templated on the number type.
12 *
13 * Port of mp_pfqn's gmpla/mpq_ludcmp.c + mpq_lubksb.c, generalized from mpq_t
14 * to any field type and with the 1-based Numerical-Recipes indexing removed.
15 * In exact arithmetic pivoting is not needed for stability, only to avoid a
16 * zero pivot, but the same largest-magnitude rule is kept so that the exact
17 * and double paths take identical elimination orders and can be diffed.
18 */
19
20#include <cstddef>
21#include <vector>
22
23#include "line/num/number.h"
24#include "line/util/error.h"
25#include "line/util/matrix.h"
26
27namespace line {
28
29/**
30 * The ordered type the pivot search compares in.
31 *
32 * It is T itself for every ordered field, so the real instantiations compile to
33 * the same code as before. A complex element type has no order, and its
34 * specialization (in line/num/complex_number.h) reduces to the real modulus --
35 * which is what MATLAB's own pivot rule uses on a complex matrix.
36 */
37template <class T>
38struct pivot_mag {
39 using type = T;
40 static type of(const T& x) { return num_abs(x); }
41};
42
43/**
44 * In-place LU of A (n x n). Returns the row permutation; A holds L (unit
45 * diagonal, implicit) below and U on and above the diagonal.
46 */
47template <class T>
48std::vector<std::size_t> lu_factor(Matrix<T>& A) {
49 using Mag = typename pivot_mag<T>::type;
50 const std::size_t n = A.rows();
51 if (A.cols() != n) throw InputError("lu_factor: matrix is not square");
52 std::vector<std::size_t> piv(n);
53 for (std::size_t k = 0; k < n; ++k) {
54 std::size_t p = k;
55 Mag amax = pivot_mag<T>::of(A(k, k));
56 for (std::size_t i = k + 1; i < n; ++i) {
57 Mag a = pivot_mag<T>::of(A(i, k));
58 if (a > amax) {
59 amax = a;
60 p = i;
61 }
62 }
63 if (amax == num_traits<Mag>::from_int(0)) throw NumericError("lu_factor: singular matrix");
64 piv[k] = p;
65 if (p != k) {
66 for (std::size_t j = 0; j < n; ++j) std::swap(A(k, j), A(p, j));
67 }
68 const T d = A(k, k);
69 for (std::size_t i = k + 1; i < n; ++i) {
70 if (A(i, k) == num_traits<T>::from_int(0)) continue;
71 T f = A(i, k) / d;
72 A(i, k) = f; // store the multiplier in place, as in Crout
73 for (std::size_t j = k + 1; j < n; ++j) A(i, j) -= f * A(k, j);
74 }
75 }
76 return piv;
77}
78
79/**
80 * Solve LUx = Pb in place on b, using the factors from lu_factor.
81 *
82 * The whole permutation must be applied to b BEFORE any elimination.
83 * lu_factor swaps entire rows, multiplier columns included, so LU(i,k) is the
84 * multiplier of the row that ends up at position i. Interleaving the swaps
85 * with the forward updates -- swap b[k], then update b[i] with LU(i,k) --
86 * pairs the row identity at step k with a final-order multiplier whenever a
87 * later step moves that row, and silently returns a wrong solution. It does
88 * so without any warning sign: the factorization still satisfies LU = PA and
89 * the pivots are all healthy. The failure needs a pivot sequence that moves an
90 * already-eliminated row, which diagonally dominant matrices rarely produce,
91 * so it hides until a generator with a small leading diagonal is solved.
92 */
93template <class T>
94void lu_solve(const Matrix<T>& LU, const std::vector<std::size_t>& piv, std::vector<T>& b) {
95 const std::size_t n = LU.rows();
96 if (b.size() != n) throw InputError("lu_solve: rhs length mismatch");
97 for (std::size_t k = 0; k < n; ++k)
98 if (piv[k] != k) std::swap(b[k], b[piv[k]]);
99 for (std::size_t i = 0; i < n; ++i)
100 for (std::size_t k = 0; k < i; ++k) b[i] -= LU(i, k) * b[k];
101 for (std::size_t i = n; i-- > 0;) {
102 T s = b[i];
103 for (std::size_t j = i + 1; j < n; ++j) s -= LU(i, j) * b[j];
104 b[i] = s / LU(i, i);
105 }
106}
107
108/**
109 * Determinant of a square matrix, by the same partial-pivoting elimination.
110 *
111 * SINGULAR IS A VALUE HERE, NOT AN ERROR, which is why this does not go through
112 * `lu_factor`: that one throws on a zero pivot because every caller of it is
113 * solving a system, where a zero pivot means the question has no answer. A
114 * determinant of a singular matrix is zero, a perfectly good answer, and
115 * Cramer's rule needs it -- `MarkovProcess.getProbState` forms a numerator
116 * matrix that IS singular whenever the state has probability zero.
117 *
118 * The sign comes from the parity of the row swaps, so it is exact even where
119 * the product of the pivots is not.
120 */
121template <class T>
122T lu_det(const Matrix<T>& A) {
123 using Mag = typename pivot_mag<T>::type;
124 const std::size_t n = A.rows();
125 if (A.cols() != n) throw InputError("lu_det: matrix is not square");
126 if (n == 0) return num_traits<T>::from_int(1);
127 Matrix<T> U = A;
128 const T zero = num_traits<T>::from_int(0);
129 T det = num_traits<T>::from_int(1);
130 for (std::size_t k = 0; k < n; ++k) {
131 std::size_t p = k;
132 Mag amax = pivot_mag<T>::of(U(k, k));
133 for (std::size_t i = k + 1; i < n; ++i) {
134 Mag a = pivot_mag<T>::of(U(i, k));
135 if (a > amax) {
136 amax = a;
137 p = i;
138 }
139 }
140 if (amax == num_traits<Mag>::from_int(0)) return zero;
141 if (p != k) {
142 for (std::size_t j = 0; j < n; ++j) std::swap(U(k, j), U(p, j));
143 det = T(zero - det);
144 }
145 const T d = U(k, k);
146 det = T(det * d);
147 for (std::size_t i = k + 1; i < n; ++i) {
148 if (U(i, k) == zero) continue;
149 const T f = U(i, k) / d;
150 for (std::size_t j = k + 1; j < n; ++j) U(i, j) -= f * U(k, j);
151 }
152 }
153 return det;
154}
155
156/** Convenience: solve Ax = b, leaving A and b untouched. */
157template <class T>
158std::vector<T> solve(const Matrix<T>& A, const std::vector<T>& b) {
159 Matrix<T> LU = A;
160 std::vector<T> x = b;
161 std::vector<std::size_t> piv = lu_factor(LU);
162 lu_solve(LU, piv, x);
163 return x;
164}
165
166} // namespace line
167
168#endif // LINE_UTIL_LU_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
T num_abs(const T &v)
Definition number.h:172
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
T lu_det(const Matrix< T > &A)
Determinant of a square matrix, by the same partial-pivoting elimination.
Definition lu.h:122
Number-type abstraction for the templated API port.
The ordered type the pivot search compares in.
Definition lu.h:38
static type of(const T &x)
Definition lu.h:40