LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
eig.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_EIG_H
6#define LINE_UTIL_EIG_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Eigenvalues and singular values, backed by LAPACK.
12 *
13 * LAPACK is BSD-3, so it is the one external numerical dependency that costs
14 * the port nothing in licensing terms: it links cleanly into a BSD-licensed
15 * binary and, unlike GMP or MPFR, imposes no relinking obligation. It is
16 * requested through the Fortran symbols directly (`dgeev_`, `dgesvd_`) rather
17 * than through LAPACKE, because the reference distribution ships the library
18 * without the C headers.
19 *
20 * DOUBLE ONLY, and deliberately so. Eigenvalues of a rational matrix are
21 * algebraic numbers, not rationals: there is no exact instantiation to offer,
22 * and a high-precision one would need a multiprecision QR iteration that
23 * LAPACK cannot provide. Both entry points therefore take Matrix<double>,
24 * and callers templated on T must convert and document the precision loss at
25 * that point rather than pretending otherwise.
26 *
27 * When the build is configured without LAPACK the entry points throw
28 * UnsupportedError naming the missing dependency, which is the same
29 * refuse-by-name policy the CLI uses for unported features: a caller learns
30 * what is missing instead of receiving a silently substituted answer.
31 */
32
33#include <complex>
34#include <cstddef>
35#include <vector>
36
37#include "line/util/error.h"
38#include "line/util/matrix.h"
39
40#ifdef LINE_MP_HAVE_LAPACK
41extern "C" {
42void dgeev_(const char* jobvl, const char* jobvr, const int* n, double* a, const int* lda,
43 double* wr, double* wi, double* vl, const int* ldvl, double* vr, const int* ldvr,
44 double* work, const int* lwork, int* info);
45void dgesvd_(const char* jobu, const char* jobvt, const int* m, const int* n, double* a,
46 const int* lda, double* s, double* u, const int* ldu, double* vt, const int* ldvt,
47 double* work, const int* lwork, int* info);
48void dgees_(const char* jobvs, const char* sort, int (*select)(const double*, const double*),
49 const int* n, double* a, const int* lda, int* sdim, double* wr, double* wi, double* vs,
50 const int* ldvs, double* work, const int* lwork, int* bwork, int* info);
51void dtrexc_(const char* compq, const int* n, double* t, const int* ldt, double* q, const int* ldq,
52 int* ifst, int* ilst, double* work, int* info);
53}
54#endif
55
56namespace line {
57
58/** Eigenvalues of a general real square matrix, in LAPACK's order. */
59inline std::vector<std::complex<double>> eig_values(const Matrix<double>& A) {
60#ifndef LINE_MP_HAVE_LAPACK
61 (void)A;
62 throw UnsupportedError(
63 "eig_values requires LAPACK: reconfigure with -DLINE_MP_USE_LAPACK=ON and liblapack "
64 "available");
65#else
66 const std::size_t n = A.rows();
67 if (A.cols() != n) throw InputError("eig_values: matrix is not square");
68 if (n == 0) return {};
69
70 // LAPACK is column-major; the input is row-major, so transpose on the way in.
71 std::vector<double> a(n * n);
72 for (std::size_t i = 0; i < n; ++i)
73 for (std::size_t j = 0; j < n; ++j) a[j * n + i] = A(i, j);
74
75 const int ni = static_cast<int>(n);
76 std::vector<double> wr(n), wi(n);
77 double vdummy = 0.0;
78 int info = 0, lwork = -1;
79 double wopt = 0.0;
80 const int one = 1;
81 dgeev_("N", "N", &ni, a.data(), &ni, wr.data(), wi.data(), &vdummy, &one, &vdummy, &one, &wopt,
82 &lwork, &info);
83 if (info != 0) throw NumericError("eig_values: LAPACK workspace query failed");
84 lwork = static_cast<int>(wopt);
85 std::vector<double> work(static_cast<std::size_t>(lwork));
86 dgeev_("N", "N", &ni, a.data(), &ni, wr.data(), wi.data(), &vdummy, &one, &vdummy, &one,
87 work.data(), &lwork, &info);
88 if (info != 0) throw NumericError("eig_values: LAPACK dgeev failed to converge");
89
90 std::vector<std::complex<double>> out(n);
91 for (std::size_t i = 0; i < n; ++i) out[i] = std::complex<double>(wr[i], wi[i]);
92 return out;
93#endif
94}
95
96/** Largest modulus over the spectrum, i.e. the spectral radius. */
97inline double spectral_radius(const Matrix<double>& A) {
98 double r = 0.0;
99 for (const std::complex<double>& z : eig_values(A)) {
100 const double m = std::abs(z);
101 if (m > r) r = m;
102 }
103 return r;
104}
105
106/**
107 * Second largest modulus over the spectrum. This is the quantity the NCD
108 * machinery needs (ctmc_courtois's epsMAX is built from the subdominant
109 * eigenvalue of each diagonal block); returns 0 when the matrix is 1 x 1.
110 */
111inline double subdominant_modulus(const Matrix<double>& A) {
112 std::vector<std::complex<double>> e = eig_values(A);
113 if (e.size() < 2) return 0.0;
114 double first = 0.0, second = 0.0;
115 for (const std::complex<double>& z : e) {
116 const double m = std::abs(z);
117 if (m > first) {
118 second = first;
119 first = m;
120 } else if (m > second) {
121 second = m;
122 }
123 }
124 return second;
125}
126
127/** Singular values in descending order. */
128inline std::vector<double> svd_values(const Matrix<double>& A) {
129#ifndef LINE_MP_HAVE_LAPACK
130 (void)A;
131 throw UnsupportedError(
132 "svd_values requires LAPACK: reconfigure with -DLINE_MP_USE_LAPACK=ON and liblapack "
133 "available");
134#else
135 const std::size_t m = A.rows(), n = A.cols();
136 if (m == 0 || n == 0) return {};
137 std::vector<double> a(m * n);
138 for (std::size_t i = 0; i < m; ++i)
139 for (std::size_t j = 0; j < n; ++j) a[j * m + i] = A(i, j);
140
141 const int mi = static_cast<int>(m), nj = static_cast<int>(n);
142 const std::size_t k = m < n ? m : n;
143 std::vector<double> s(k);
144 double udummy = 0.0, vtdummy = 0.0;
145 int info = 0, lwork = -1;
146 double wopt = 0.0;
147 const int one = 1;
148 dgesvd_("N", "N", &mi, &nj, a.data(), &mi, s.data(), &udummy, &one, &vtdummy, &one, &wopt,
149 &lwork, &info);
150 if (info != 0) throw NumericError("svd_values: LAPACK workspace query failed");
151 lwork = static_cast<int>(wopt);
152 std::vector<double> work(static_cast<std::size_t>(lwork));
153 dgesvd_("N", "N", &mi, &nj, a.data(), &mi, s.data(), &udummy, &one, &vtdummy, &one, work.data(),
154 &lwork, &info);
155 if (info != 0) throw NumericError("svd_values: LAPACK dgesvd failed to converge");
156 return s; // dgesvd returns them descending
157#endif
158}
159
160// ---------------------------------------------------------------------------
161// Real Schur form, and reordering its diagonal blocks
162// ---------------------------------------------------------------------------
163
164/**
165 * Real Schur factorization A = Z T Z^T, with Z orthogonal and T upper
166 * quasi-triangular: 1 x 1 diagonal blocks for real eigenvalues and 2 x 2 blocks
167 * for complex conjugate pairs.
168 */
169struct RealSchur {
170 Matrix<double> Z; ///< orthogonal Schur vectors
171 Matrix<double> T; ///< upper quasi-triangular factor
172};
173
174/**
175 * Real Schur factorization of a general square matrix (LAPACK dgees, unsorted).
176 *
177 * The Schur form is the right basis for splitting a spectrum into invariant
178 * subspaces, which is what a multi-regime fluid queue needs: the eigenvector
179 * basis exists only for a diagonalizable matrix and is complex whenever the
180 * spectrum is, while Z is orthogonal and real for every real A.
181 */
183#ifndef LINE_MP_HAVE_LAPACK
184 (void)A;
185 throw UnsupportedError(
186 "schur_decomposition requires LAPACK: reconfigure with -DLINE_MP_USE_LAPACK=ON and "
187 "liblapack available");
188#else
189 const std::size_t n = A.rows();
190 if (A.cols() != n) throw InputError("schur_decomposition: matrix is not square");
191 RealSchur out;
192 out.Z = Matrix<double>(n, n, 0.0);
193 out.T = Matrix<double>(n, n, 0.0);
194 if (n == 0) return out;
195
196 std::vector<double> a(n * n);
197 for (std::size_t i = 0; i < n; ++i)
198 for (std::size_t j = 0; j < n; ++j) a[j * n + i] = A(i, j);
199
200 const int ni = static_cast<int>(n);
201 std::vector<double> wr(n), wi(n), vs(n * n);
202 int sdim = 0, info = 0, lwork = -1;
203 double wopt = 0.0;
204 dgees_("V", "N", nullptr, &ni, a.data(), &ni, &sdim, wr.data(), wi.data(), vs.data(), &ni,
205 &wopt, &lwork, nullptr, &info);
206 if (info != 0) throw NumericError("schur_decomposition: LAPACK workspace query failed");
207 lwork = static_cast<int>(wopt);
208 std::vector<double> work(static_cast<std::size_t>(lwork));
209 dgees_("V", "N", nullptr, &ni, a.data(), &ni, &sdim, wr.data(), wi.data(), vs.data(), &ni,
210 work.data(), &lwork, nullptr, &info);
211 if (info != 0) throw NumericError("schur_decomposition: LAPACK dgees failed to converge");
212
213 for (std::size_t i = 0; i < n; ++i)
214 for (std::size_t j = 0; j < n; ++j) {
215 out.T(i, j) = a[j * n + i];
216 out.Z(i, j) = vs[j * n + i];
217 }
218 return out;
219#endif
220}
221
222/**
223 * Reorder the diagonal blocks of a real Schur form into DESCENDING key order,
224 * stably, updating Z so that A = Z T Z^T still holds.
225 *
226 * This is MATLAB's ordschur with a CLUSTER-NUMBER select vector, whose
227 * documented behaviour is that clusters appear in descending order of the
228 * cluster number. The key is given per diagonal ENTRY; for a 2 x 2 block the
229 * key of its first row is used and the second is ignored, which is the same
230 * requirement MATLAB imposes (a select vector must be constant on a block, and
231 * a caller that splits one is asking for a factorization that does not exist
232 * over the reals).
233 *
234 * WHY dtrexc AND NOT dtrsen. dtrsen splits a spectrum into TWO clusters, the
235 * selected one and the rest, so an ordering into three or more classes needs it
236 * applied recursively to trailing submatrices, with the accumulated Q and the
237 * shifting block boundaries tracked by hand at every level. dtrexc moves ONE
238 * diagonal block from position ifst to position ilst and updates Q itself, so
239 * an arbitrary key ordering is a stable selection sort over blocks with no
240 * submatrix bookkeeping at all. It also refuses to split a 2 x 2 block rather
241 * than silently producing a complex pair astride a boundary, which is the
242 * failure that would otherwise be discovered downstream as a complex "real"
243 * subspace.
244 *
245 * @param s a real Schur factorization, typically from schur_decomposition
246 * @param key one value per diagonal entry; blocks are sorted descending by it
247 */
248inline RealSchur schur_reorder(const RealSchur& s, const std::vector<double>& key) {
249#ifndef LINE_MP_HAVE_LAPACK
250 (void)s;
251 (void)key;
252 throw UnsupportedError(
253 "schur_reorder requires LAPACK: reconfigure with -DLINE_MP_USE_LAPACK=ON and liblapack "
254 "available");
255#else
256 const std::size_t n = s.T.rows();
257 if (s.T.cols() != n || s.Z.rows() != n || s.Z.cols() != n)
258 throw InputError("schur_reorder: the factors are not square or not conformable");
259 if (key.size() != n) throw InputError("schur_reorder: one key per diagonal entry is required");
260 RealSchur out;
261 out.T = s.T;
262 out.Z = s.Z;
263 if (n <= 1) return out;
264
265 // Column-major working copies for LAPACK.
266 std::vector<double> t(n * n), q(n * n);
267 for (std::size_t i = 0; i < n; ++i)
268 for (std::size_t j = 0; j < n; ++j) {
269 t[j * n + i] = s.T(i, j);
270 q[j * n + i] = s.Z(i, j);
271 }
272 // The key travels with its block, so it is permuted alongside T.
273 std::vector<double> k = key;
274
275 const int ni = static_cast<int>(n);
276 std::vector<double> work(n);
277 const double tiny = 0.0; // dgees leaves an exact zero below a 1 x 1 block
278
279 // Stable selection sort over blocks. pos is the first row of the region
280 // still to be ordered.
281 std::size_t pos = 0;
282 while (pos < n) {
283 // Enumerate the blocks of the remaining region and find the first one
284 // carrying the largest key; "first" keeps the sort stable.
285 std::size_t best = pos;
286 double bestkey = k[pos];
287 std::size_t i = pos;
288 while (i < n) {
289 const std::size_t sz = (i + 1 < n && t[i * n + (i + 1)] != tiny) ? 2u : 1u;
290 if (k[i] > bestkey) {
291 bestkey = k[i];
292 best = i;
293 }
294 i += sz;
295 }
296 const std::size_t bsz =
297 (best + 1 < n && t[best * n + (best + 1)] != tiny) ? 2u : 1u;
298 if (best != pos) {
299 int ifst = static_cast<int>(best) + 1; // LAPACK is 1-based
300 int ilst = static_cast<int>(pos) + 1;
301 int info = 0;
302 dtrexc_("V", &ni, t.data(), &ni, q.data(), &ni, &ifst, &ilst, work.data(), &info);
303 if (info == 1)
304 throw NumericError(
305 "schur_reorder: dtrexc could not separate two eigenvalues that are too close; "
306 "the requested ordering splits a 2 x 2 block");
307 if (info != 0) throw NumericError("schur_reorder: LAPACK dtrexc failed");
308 // Move the key with the block: erase it from its old position and
309 // reinsert it at the new one, exactly as dtrexc permuted the rows.
310 const std::vector<double> moved(k.begin() + static_cast<long>(best),
311 k.begin() + static_cast<long>(best + bsz));
312 k.erase(k.begin() + static_cast<long>(best),
313 k.begin() + static_cast<long>(best + bsz));
314 k.insert(k.begin() + static_cast<long>(pos), moved.begin(), moved.end());
315 }
316 pos += bsz;
317 }
318
319 for (std::size_t i = 0; i < n; ++i)
320 for (std::size_t j = 0; j < n; ++j) {
321 out.T(i, j) = t[j * n + i];
322 out.Z(i, j) = q[j * n + i];
323 }
324 return out;
325#endif
326}
327
328/** Numerical rank at the standard max(m,n) eps sigma_1 threshold. */
329inline std::size_t matrix_rank(const Matrix<double>& A) {
330 const std::vector<double> s = svd_values(A);
331 if (s.empty()) return 0;
332 const std::size_t dim = A.rows() > A.cols() ? A.rows() : A.cols();
333 const double tol = static_cast<double>(dim) * 2.220446049250313e-16 * s[0];
334 std::size_t r = 0;
335 for (double v : s)
336 if (v > tol) ++r;
337 return r;
338}
339
340} // namespace line
341
342#endif // LINE_UTIL_EIG_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
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Dense matrix and non-owning view.
double subdominant_modulus(const Matrix< double > &A)
Second largest modulus over the spectrum.
Definition eig.h:111
double spectral_radius(const Matrix< double > &A)
Largest modulus over the spectrum, i.e.
Definition eig.h:97
RealSchur schur_reorder(const RealSchur &s, const std::vector< double > &key)
Reorder the diagonal blocks of a real Schur form into DESCENDING key order, stably,...
Definition eig.h:248
std::vector< std::complex< double > > eig_values(const Matrix< double > &A)
Eigenvalues of a general real square matrix, in LAPACK's order.
Definition eig.h:59
std::size_t matrix_rank(const Matrix< double > &A)
Numerical rank at the standard max(m,n) eps sigma_1 threshold.
Definition eig.h:329
std::vector< double > svd_values(const Matrix< double > &A)
Singular values in descending order.
Definition eig.h:128
RealSchur schur_decomposition(const Matrix< double > &A)
Real Schur factorization of a general square matrix (LAPACK dgees, unsorted).
Definition eig.h:182
Real Schur factorization A = Z T Z^T, with Z orthogonal and T upper quasi-triangular: 1 x 1 diagonal ...
Definition eig.h:169
Matrix< double > Z
orthogonal Schur vectors
Definition eig.h:170
Matrix< double > T
upper quasi-triangular factor
Definition eig.h:171