LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Toggle main menu visibility
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
41
extern
"C"
{
42
void
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);
45
void
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);
48
void
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);
51
void
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
56
namespace
line
{
57
58
/** Eigenvalues of a general real square matrix, in LAPACK's order. */
59
inline
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. */
97
inline
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
*/
111
inline
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. */
128
inline
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
*/
169
struct
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
*/
182
inline
RealSchur
schur_decomposition
(
const
Matrix<double>
& A) {
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
*/
248
inline
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. */
329
inline
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
line::InputError::InputError
InputError(const std::string &what)
Definition
error.h:39
line::Matrix
Definition
matrix.h:56
line::Matrix::cols
std::size_t cols() const
Definition
matrix.h:90
line::Matrix::rows
std::size_t rows() const
Definition
matrix.h:89
line::NumericError::NumericError
NumericError(const std::string &what)
Definition
error.h:45
line::UnsupportedError::UnsupportedError
UnsupportedError(const std::string &what)
Definition
error.h:51
error.h
The exception types the port throws.
matrix.h
Dense matrix and non-owning view.
line
Definition
aoi_dist2ph.h:52
line::subdominant_modulus
double subdominant_modulus(const Matrix< double > &A)
Second largest modulus over the spectrum.
Definition
eig.h:111
line::spectral_radius
double spectral_radius(const Matrix< double > &A)
Largest modulus over the spectrum, i.e.
Definition
eig.h:97
line::schur_reorder
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
line::eig_values
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
line::matrix_rank
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
line::svd_values
std::vector< double > svd_values(const Matrix< double > &A)
Singular values in descending order.
Definition
eig.h:128
line::schur_decomposition
RealSchur schur_decomposition(const Matrix< double > &A)
Real Schur factorization of a general square matrix (LAPACK dgees, unsorted).
Definition
eig.h:182
line::RealSchur
Real Schur factorization A = Z T Z^T, with Z orthogonal and T upper quasi-triangular: 1 x 1 diagonal ...
Definition
eig.h:169
line::RealSchur::Z
Matrix< double > Z
orthogonal Schur vectors
Definition
eig.h:170
line::RealSchur::T
Matrix< double > T
upper quasi-triangular factor
Definition
eig.h:171
include
line
util
eig.h
Generated by
1.18.0