LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lstsq.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_LSTSQ_H
6#define LINE_UTIL_LSTSQ_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Least squares for a rectangular system, exact-capable.
12 *
13 * MATLAB reaches for `qr(A,0)` when A has full column rank and for a truncated
14 * SVD when it does not. Neither has an exact counterpart: a QR factor contains
15 * square roots and a singular value is algebraic, not rational. The two
16 * routines here give the SAME vectors by rational means.
17 *
18 * - Full column rank: the normal equations A'A x = A'b. Their solution IS the
19 * least-squares solution, identically, not an approximation of it; the usual
20 * objection to them is the squared condition number, which is a
21 * floating-point concern and disappears at T = Rational.
22 * - Rank deficient: the minimum-norm least-squares solution x = A^+ b, built
23 * from a full-rank factorization A = C F obtained by exact elimination,
24 *
25 * x = F' (F F')^{-1} (C'C)^{-1} C' b.
26 *
27 * For a matrix of rank k this equals the truncated-SVD solution at the same
28 * k, so it reproduces MATLAB's fallback rather than approximating it.
29 *
30 * RANK DETECTION is where exact and inexact arithmetic genuinely part company.
31 * At T = Rational a pivot is either zero or it is not, so the rank is exact and
32 * `tol` is ignored. In floating point a threshold is unavoidable; the default
33 * is max(m,n) * 1e-14 * max|A|, in the spirit of MATLAB's
34 * max(size(A)) * eps(max(sv)) but computed from the entries rather than from
35 * singular values that are not available here.
36 *
37 * NOT PORTED, deliberately: MATLAB's callers follow a rank-deficient solve with
38 * a RANDOM perturbation retry (`rng(23000,'twister')`, demands nudged by
39 * 1e-10..1e-4 times their scale) and keep whichever perturbed model looks
40 * better behaved. That is a floating-point remedy for a floating-point rank
41 * test. It has no exact counterpart, it changes the answer, and reproducing it
42 * would require MATLAB's Mersenne-Twister double stream bit for bit. The exact
43 * pseudoinverse above is what the perturbation is trying to approximate.
44 */
45
46#include <cstddef>
47#include <vector>
48
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/lu.h"
52#include "line/util/matrix.h"
53
54namespace line {
55
56/** Outcome of lstsq: the solution and whether the system was rank deficient. */
57template <class T>
59 std::vector<T> x;
60 std::size_t rank;
61 bool rankdef;
62};
63
64namespace detail {
65
66/** Pivot threshold: exactly zero in an exact field, relative otherwise. */
67template <class T>
68T lstsq_tolerance(const Matrix<T>& A) {
69 const T zero = num_traits<T>::from_int(0);
70 if (num_traits<T>::is_exact) return zero;
71 T mx = zero;
72 for (std::size_t i = 0; i < A.rows(); ++i)
73 for (std::size_t j = 0; j < A.cols(); ++j) {
74 const T a = num_abs(T(A(i, j)));
75 if (a > mx) mx = a;
76 }
77 const std::size_t d = A.rows() > A.cols() ? A.rows() : A.cols();
78 return T(mx * num_traits<T>::from_double(1e-14) *
79 num_traits<T>::from_int(static_cast<long>(d)));
80}
81
82/** A'A and A'b, the normal-equation pair. */
83template <class T>
84void normal_equations(const Matrix<T>& A, const std::vector<T>& b, Matrix<T>& G,
85 std::vector<T>& c) {
86 const std::size_t m = A.rows(), n = A.cols();
87 const T zero = num_traits<T>::from_int(0);
88 G = Matrix<T>(n, n, zero);
89 c.assign(n, zero);
90 for (std::size_t i = 0; i < n; ++i) {
91 for (std::size_t j = i; j < n; ++j) {
92 T s = zero;
93 for (std::size_t k = 0; k < m; ++k) s += A(k, i) * A(k, j);
94 G(i, j) = s;
95 G(j, i) = s;
96 }
97 T s = zero;
98 for (std::size_t k = 0; k < m; ++k) s += A(k, i) * b[k];
99 c[i] = s;
100 }
101}
102
103} // namespace detail
104
105/**
106 * Reduced row echelon form of A, in place, returning the pivot columns.
107 * Rows beyond the returned rank are identically zero.
108 */
109template <class T>
110std::vector<std::size_t> rref(Matrix<T>& A, const T& tol) {
111 const std::size_t m = A.rows(), n = A.cols();
112 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
113 std::vector<std::size_t> piv;
114 std::size_t r = 0;
115 for (std::size_t c = 0; c < n && r < m; ++c) {
116 std::size_t p = r;
117 T best = num_abs(T(A(r, c)));
118 for (std::size_t i = r + 1; i < m; ++i) {
119 const T a = num_abs(T(A(i, c)));
120 if (a > best) {
121 best = a;
122 p = i;
123 }
124 }
125 if (!(best > tol)) continue;
126 if (p != r)
127 for (std::size_t j = 0; j < n; ++j) std::swap(A(r, j), A(p, j));
128 const T d = A(r, c);
129 for (std::size_t j = 0; j < n; ++j) A(r, j) /= d;
130 A(r, c) = one;
131 for (std::size_t i = 0; i < m; ++i) {
132 if (i == r) continue;
133 const T f = A(i, c);
134 if (f == zero) continue;
135 for (std::size_t j = 0; j < n; ++j) A(i, j) -= f * A(r, j);
136 A(i, c) = zero;
137 }
138 piv.push_back(c);
139 ++r;
140 }
141 return piv;
142}
143
144/**
145 * Least-squares solution of A x = b, minimum-norm when A is rank deficient.
146 *
147 * @param A (m x n), any shape
148 * @param b (m)
149 * @param tol pivot threshold; pass 0 for the exact rank
150 */
151template <class T>
152LstsqResult<T> lstsq(const Matrix<T>& A, const std::vector<T>& b, const T& tol) {
153 const std::size_t m = A.rows(), n = A.cols();
154 if (b.size() != m) throw InputError("lstsq: rhs length mismatch");
155 if (n == 0) throw InputError("lstsq: empty system");
156 const T zero = num_traits<T>::from_int(0);
157
158 Matrix<T> Rf = A;
159 const std::vector<std::size_t> pivcols = rref(Rf, tol);
160 const std::size_t k = pivcols.size();
161
162 LstsqResult<T> res;
163 res.rank = k;
164 res.rankdef = (k < n);
165
166 if (!res.rankdef) {
167 Matrix<T> G;
168 std::vector<T> c;
169 detail::normal_equations(A, b, G, c);
170 res.x = solve(G, c);
171 return res;
172 }
173 if (k == 0) {
174 res.x.assign(n, zero);
175 return res;
176 }
177
178 // Full-rank factorization A = C F: C the pivot columns of A, F the k
179 // nonzero rows of the reduced row echelon form.
180 Matrix<T> C(m, k, zero), F(k, n, zero);
181 for (std::size_t j = 0; j < k; ++j)
182 for (std::size_t i = 0; i < m; ++i) C(i, j) = A(i, pivcols[j]);
183 for (std::size_t i = 0; i < k; ++i)
184 for (std::size_t j = 0; j < n; ++j) F(i, j) = Rf(i, j);
185
186 // y = (C'C)^{-1} C' b
187 Matrix<T> CtC;
188 std::vector<T> Ctb;
189 detail::normal_equations(C, b, CtC, Ctb);
190 const std::vector<T> y = solve(CtC, Ctb);
191
192 // x = F' (F F')^{-1} y
193 Matrix<T> FFt(k, k, zero);
194 for (std::size_t i = 0; i < k; ++i)
195 for (std::size_t j = i; j < k; ++j) {
196 T s = zero;
197 for (std::size_t t = 0; t < n; ++t) s += F(i, t) * F(j, t);
198 FFt(i, j) = s;
199 FFt(j, i) = s;
200 }
201 const std::vector<T> w = solve(FFt, y);
202 res.x.assign(n, zero);
203 for (std::size_t t = 0; t < n; ++t) {
204 T s = zero;
205 for (std::size_t i = 0; i < k; ++i) s += F(i, t) * w[i];
206 res.x[t] = s;
207 }
208 return res;
209}
210
211/** Overload picking the default pivot threshold for the arithmetic in use. */
212template <class T>
213LstsqResult<T> lstsq(const Matrix<T>& A, const std::vector<T>& b) {
214 return lstsq(A, b, detail::lstsq_tolerance(A));
215}
216
217} // namespace line
218
219#endif // LINE_UTIL_LSTSQ_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
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
std::vector< std::size_t > rref(Matrix< T > &A, const T &tol)
Reduced row echelon form of A, in place, returning the pivot columns.
Definition lstsq.h:110
LstsqResult< T > lstsq(const Matrix< T > &A, const std::vector< T > &b, const T &tol)
Least-squares solution of A x = b, minimum-norm when A is rank deficient.
Definition lstsq.h:152
T num_abs(const T &v)
Definition number.h:172
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
Number-type abstraction for the templated API port.
Outcome of lstsq: the solution and whether the system was rank deficient.
Definition lstsq.h:58
std::vector< T > x
Definition lstsq.h:59
std::size_t rank
Definition lstsq.h:60