LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mg1.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_LIB_SMC_MG1_H
6#define LINE_LIB_SMC_MG1_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * The M/G/1-type and GI/M/1-type fundamental-matrix solvers of MAMSolver /
12 * SMCSolver, ported from `matlab/lib/thirdparty/MG1files`:
13 * `stat.m`, `MG1_EG.m`, `MG1_Decay.m`, `GIM1_Caudal.m`, `MG1_Shifts.m`,
14 * `MG1_CR.m`, `MG1_FI.m`, `MG1_NI.m` (with `solveSylvPowersDirectSum.m` and
15 * `solveSylvPowersRealSchur_FW.m`), `MG1_RR.m` (with `MG1_RR_Btemp.m` and `MG1_RR_tempB.m`),
16 * `MG1_IS.m` and `GIM1_R.m`.
17 *
18 * These are the third-party numerics the ETAQA aggregation sits on: `MG1_CR`
19 * returns the minimal nonnegative G of an M/G/1-type chain and `GIM1_R` the
20 * minimal nonnegative R of a GI/M/1-type one, and without them
21 * `solver_mam_bmap_map_1` and `solver_mam_map_bmap_1` have nothing to
22 * aggregate. The port follows the MATLAB line by line, including the block
23 * index arithmetic, the stopping tests and the constants, so that a divergence
24 * against the reference is a bug here and not a design difference.
25 *
26 * BLOCK LAYOUT. The reference passes a block sequence as one wide matrix
27 * `A = [A0 A1 A2 ... Amax]`, m rows by m*(max+1) columns. This port carries the
28 * same sequence as a `std::vector<Matrix<double>>` of m x m blocks, which is
29 * the identical object with the index arithmetic done once, in `blocks_of` /
30 * `hcat`, instead of at every use. The GI/M/1 side stacks its blocks
31 * VERTICALLY in the reference; `GIM1_R_ETAQA` is what transposes that stack
32 * into the horizontal one, so everything below is horizontal.
33 *
34 * DOUBLE ONLY, and not by preference. `MG1_Decay` and `GIM1_Caudal` bisect on
35 * the Perron-Frobenius eigenvalue of A(z), which needs LAPACK; `MG1_CR`
36 * evaluates its polynomials at complex roots of unity through an FFT, whose
37 * twiddle factors are cos/sin; `MG1_pi_ETAQA` drops a column chosen by a
38 * numerical rank test, which needs an SVD. None of the three has a
39 * multiprecision or exact counterpart in this tree, so the whole family is
40 * declared on `Matrix<double>` and the solvers that call it refuse at any
41 * other arithmetic rather than down-converting behind the caller's back.
42 *
43 * `MG1_Shifts` IS PORTED IN FULL, all three ShiftTypes at either drift. Its
44 * reference writes the last block of a row shift as
45 * rowhatA(1,maxd*i:end) = uT*A(:,maxd*i:end)
46 * with `i` the value 1 left over from the `beta` loop, so the line addresses
47 * column `maxd`, not block `maxd`. It is nevertheless CORRECT: both sides take
48 * the same columns, so columns `maxd*m+1:end` receive exactly `uT*A_maxd`, and
49 * the stray columns before them are overwritten by the loop that follows. An
50 * earlier version of this header read that line as defective and refused the
51 * branches; the transient-chain Newton iteration under GIM1_R's Ramaswami dual
52 * is what reaches them. With maxd = 1 the `beta` loop never runs, `i` is the
53 * imaginary unit and the reference errors; this port computes the last block
54 * the line intends.
55 */
56
57#include <algorithm>
58#include <cmath>
59#include <complex>
60#include <cstddef>
61#include <limits>
62#include <string>
63#include <vector>
64
66#include "line/num/number.h"
67#include "line/util/eig.h"
68#include "line/util/error.h"
69#include "line/util/fft.h"
70#include "line/util/linalg.h"
71#include "line/util/lstsq.h"
72#include "line/util/lu.h"
73#include "line/util/matrix.h"
74#include "line/util/svd.h"
75
76namespace line {
77namespace smc {
78
79using Blocks = std::vector<Matrix<double>>;
80
81// ---------------------------------------------------------------------------
82// Small matrix helpers, named after the MATLAB they stand for
83// ---------------------------------------------------------------------------
84
85/** A + B. Templated so the point-wise CR step can add complex blocks. */
86template <class T>
87Matrix<T> madd(const Matrix<T>& A, const Matrix<T>& B) {
88 if (A.rows() != B.rows() || A.cols() != B.cols())
89 throw InputError("smc: matrix addition with mismatched shapes");
91 for (std::size_t i = 0; i < A.rows(); ++i)
92 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = A(i, j) + B(i, j);
93 return C;
94}
95
96/** A - B. */
97template <class T>
98Matrix<T> msub(const Matrix<T>& A, const Matrix<T>& B) {
99 if (A.rows() != B.rows() || A.cols() != B.cols())
100 throw InputError("smc: matrix subtraction with mismatched shapes");
102 for (std::size_t i = 0; i < A.rows(); ++i)
103 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = A(i, j) - B(i, j);
104 return C;
105}
106
107/** c * A. */
108inline Matrix<double> mscale(const Matrix<double>& A, double c) {
109 Matrix<double> C(A.rows(), A.cols(), 0.0);
110 for (std::size_t i = 0; i < A.rows(); ++i)
111 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = c * A(i, j);
112 return C;
113}
114
115/** sum(A,2), the row sums, as a column held in a vector. */
116inline std::vector<double> rowsums(const Matrix<double>& A) {
117 std::vector<double> s(A.rows(), 0.0);
118 for (std::size_t i = 0; i < A.rows(); ++i)
119 for (std::size_t j = 0; j < A.cols(); ++j) s[i] += A(i, j);
120 return s;
121}
122
123/** norm(A,inf), the largest absolute row sum. */
124inline double inf_norm(const Matrix<double>& A) {
125 double best = 0.0;
126 for (std::size_t i = 0; i < A.rows(); ++i) {
127 double s = 0.0;
128 for (std::size_t j = 0; j < A.cols(); ++j) s += std::fabs(A(i, j));
129 if (s > best) best = s;
130 }
131 return best;
132}
133
134/** norm(A,inf) over a whole block sequence stacked vertically. */
135inline double inf_norm(const Blocks& blk, std::size_t from) {
136 double best = 0.0;
137 for (std::size_t i = from; i < blk.size(); ++i) best = std::max(best, inf_norm(blk[i]));
138 return best;
139}
140
141/** max(max(abs(A-B))). */
142inline double max_abs_diff(const Matrix<double>& A, const Matrix<double>& B) {
143 double best = 0.0;
144 for (std::size_t i = 0; i < A.rows(); ++i)
145 for (std::size_t j = 0; j < A.cols(); ++j)
146 best = std::max(best, std::fabs(A(i, j) - B(i, j)));
147 return best;
148}
149
150/** max(sum(A)), the largest column sum WITHOUT absolute values, as in MATLAB. */
151inline double max_col_sum(const Matrix<double>& A) {
152 double best = -std::numeric_limits<double>::infinity();
153 for (std::size_t j = 0; j < A.cols(); ++j) {
154 double s = 0.0;
155 for (std::size_t i = 0; i < A.rows(); ++i) s += A(i, j);
156 if (s > best) best = s;
157 }
158 return best;
159}
160
161/** Splits the wide `[A0 A1 ... Amax]` into its m x m blocks. */
162inline Blocks blocks_of(const Matrix<double>& A, std::size_t m) {
163 if (m == 0 || A.cols() % m != 0)
164 throw InputError("smc: the block sequence has an incorrect number of columns");
165 const std::size_t nb = A.cols() / m;
166 Blocks out(nb, Matrix<double>(A.rows(), m, 0.0));
167 for (std::size_t b = 0; b < nb; ++b)
168 for (std::size_t i = 0; i < A.rows(); ++i)
169 for (std::size_t j = 0; j < m; ++j) out[b](i, j) = A(i, b * m + j);
170 return out;
171}
172
173/** Re-assembles a block sequence into the wide `[A0 A1 ... Amax]`. */
174inline Matrix<double> hcat(const Blocks& blk) {
175 if (blk.empty()) return Matrix<double>();
176 const std::size_t r = blk[0].rows(), c = blk[0].cols();
177 Matrix<double> A(r, c * blk.size(), 0.0);
178 for (std::size_t b = 0; b < blk.size(); ++b)
179 for (std::size_t i = 0; i < r; ++i)
180 for (std::size_t j = 0; j < c; ++j) A(i, b * c + j) = blk[b](i, j);
181 return A;
182}
183
184/** Stacks a block sequence vertically, `[A0; A1; ...; Amax]`. */
185inline Matrix<double> vcat(const Blocks& blk) {
186 if (blk.empty()) return Matrix<double>();
187 const std::size_t r = blk[0].rows(), c = blk[0].cols();
188 Matrix<double> A(r * blk.size(), c, 0.0);
189 for (std::size_t b = 0; b < blk.size(); ++b)
190 for (std::size_t i = 0; i < r; ++i)
191 for (std::size_t j = 0; j < c; ++j) A(b * r + i, j) = blk[b](i, j);
192 return A;
193}
194
195/** Splits a vertical stack into its blocks of `r` rows. */
196inline Blocks vblocks_of(const Matrix<double>& A, std::size_t r) {
197 if (r == 0 || A.rows() % r != 0)
198 throw InputError("smc: the stacked block sequence has an incorrect number of rows");
199 const std::size_t nb = A.rows() / r;
200 Blocks out(nb, Matrix<double>(r, A.cols(), 0.0));
201 for (std::size_t b = 0; b < nb; ++b)
202 for (std::size_t i = 0; i < r; ++i)
203 for (std::size_t j = 0; j < A.cols(); ++j) out[b](i, j) = A(b * r + i, j);
204 return out;
205}
206
207// ---------------------------------------------------------------------------
208// stat.m
209// ---------------------------------------------------------------------------
210
211/**
212 * Stationary distribution of a stochastic matrix: the left eigenvector for
213 * eigenvalue 1, nonnegative and summing to one.
214 *
215 * Port of `stat.m`, including its shape: `[A - I, e]` is S x (S+1), so the
216 * reference's `y / B` is a least-squares solve of a consistent overdetermined
217 * system and not a square solve. The normalization is IN the system (the
218 * appended column of ones against the appended 1 on the right), which is why
219 * the result needs no rescaling afterwards.
220 */
221inline std::vector<double> stat(const Matrix<double>& A) {
222 const std::size_t S = A.rows();
223 if (A.cols() != S) throw InputError("stat: matrix is not square");
224 // x [A - I, e] = [0 ... 0 1] <=> [A - I, e]^T x^T = [0 ... 0 1]^T
225 Matrix<double> Bt(S + 1, S, 0.0);
226 for (std::size_t i = 0; i < S; ++i)
227 for (std::size_t j = 0; j < S; ++j) Bt(j, i) = A(i, j) - (i == j ? 1.0 : 0.0);
228 for (std::size_t i = 0; i < S; ++i) Bt(S, i) = 1.0;
229 std::vector<double> y(S + 1, 0.0);
230 y[S] = 1.0;
231 return lstsq(Bt, y, detail::lstsq_tolerance(Bt)).x;
232}
233
234/** theta A, the row vector times matrix product used throughout. */
235inline std::vector<double> rowvec_times(const std::vector<double>& v, const Matrix<double>& A) {
236 return vecmul(v, A);
237}
238
239/** The inner product of a row vector with a column held as a vector. */
240inline double dot(const std::vector<double>& a, const std::vector<double>& b) {
241 if (a.size() != b.size()) throw InputError("smc: inner product with mismatched lengths");
242 double s = 0.0;
243 for (std::size_t i = 0; i < a.size(); ++i) s += a[i] * b[i];
244 return s;
245}
246
247/** The drift of an M/G/1-type sequence, and the invariant vector it uses. */
248struct Drift {
249 double value = 0.0;
250 std::vector<double> theta; ///< stat(A0 + A1 + ... + Amax)
251};
252
253/**
254 * `drift = theta * beta` with `beta = (Amax)e + (Amax+Amax-1)e + ...`, the
255 * expected level increment per transition of the phase process. Repeated
256 * verbatim in MG1_EG, MG1_Shifts, MG1_pi_ETAQA and GIM1_R, so it lives here.
257 */
258inline Drift mg1_drift(const Blocks& A) {
259 const std::size_t dega = A.size() - 1;
260 Matrix<double> sumA = A[dega];
261 std::vector<double> beta = rowsums(sumA);
262 for (std::size_t i = dega; i-- > 1;) {
263 sumA = madd(sumA, A[i]);
264 const std::vector<double> rs = rowsums(sumA);
265 for (std::size_t k = 0; k < beta.size(); ++k) beta[k] += rs[k];
266 }
267 sumA = madd(sumA, A[0]);
268 Drift d;
269 d.theta = stat(sumA);
270 d.value = dot(d.theta, beta);
271 return d;
272}
273
274// ---------------------------------------------------------------------------
275// MG1_Decay.m and GIM1_Caudal.m
276// ---------------------------------------------------------------------------
277
278/**
279 * `max(eig(M))` with MATLAB's semantics on a complex spectrum: the element of
280 * largest modulus, ties broken by the larger phase angle. For the nonnegative
281 * A(z) both callers evaluate, this is the Perron-Frobenius eigenvalue and is
282 * real; the comparisons below then take its real part, which is what MATLAB's
283 * relational operators do on a complex value.
284 */
285inline std::complex<double> max_eig(const Matrix<double>& M) {
286 const std::vector<std::complex<double>> ev = eig_values(M);
287 if (ev.empty()) throw NumericError("smc: empty spectrum");
288 std::complex<double> best = ev[0];
289 for (const std::complex<double>& z : ev) {
290 const double mz = std::abs(z), mb = std::abs(best);
291 if (mz > mb || (mz == mb && std::arg(z) > std::arg(best))) best = z;
292 }
293 return best;
294}
295
296/** A(z) = A0 + A1 z + ... + Amax z^max, by Horner as the reference writes it. */
297inline Matrix<double> poly_at(const Blocks& A, double z) {
298 Matrix<double> temp = A.back();
299 for (std::size_t i = A.size() - 1; i-- > 0;) temp = madd(mscale(temp, z), A[i]);
300 return temp;
301}
302
303/**
304 * The Perron-Frobenius eigenvector of M, right (`left == false`) or left, scaled to unit
305 * sum: the null vector of M - PF(M) I (or its transpose), read off the SVD. The reference
306 * picks the column of `eig` at the largest eigenvalue; both callers normalize it by its sum,
307 * so the scale and sign `eig` happens to return do not matter.
308 */
309inline std::vector<double> pf_vector(const Matrix<double>& M, bool left) {
310 const std::size_t m = M.rows();
311 const double lambda = max_eig(M).real();
312 Matrix<double> K(m, m, 0.0);
313 for (std::size_t i = 0; i < m; ++i)
314 for (std::size_t j = 0; j < m; ++j) K(i, j) = left ? M(j, i) : M(i, j);
315 for (std::size_t i = 0; i < m; ++i) K(i, i) -= lambda;
316 const SvdFactors sv = svd_full(K);
317 std::vector<double> v(m);
318 double s = 0.0;
319 for (std::size_t i = 0; i < m; ++i) {
320 v[i] = sv.Vt(m - 1, i);
321 s += v[i];
322 }
323 for (double& x : v) x /= s;
324 return v;
325}
326
327/**
328 * Decay rate of a recurrent M/G/1-type chain: the unique z > 1 with
329 * PF(A(z)) = z. Port of `MG1_Decay.m`. When `uT` is given it receives the
330 * left PF eigenvector of A(z) at the last bisection point, as the reference's
331 * second output, scaled to unit sum.
332 */
333inline double mg1_decay(const Blocks& A, std::vector<double>* uT = nullptr) {
334 double eta = 1.0, new_eta = 0.0;
335 Matrix<double> temp;
336 while (new_eta - eta < 0.0) {
337 eta += 1.0;
338 temp = poly_at(A, eta);
339 new_eta = max_eig(temp).real();
340 }
341 double eta_min = eta - 1.0, eta_max = eta;
342 eta = eta_min + 0.5;
343 while (eta_max - eta_min > 1e-15) {
344 temp = poly_at(A, eta);
345 new_eta = max_eig(temp).real();
346 if (new_eta < eta) {
347 eta_min = eta;
348 } else {
349 eta_max = eta;
350 }
351 eta = (eta_min + eta_max) / 2.0;
352 }
353 if (uT) *uT = pf_vector(temp, true);
354 return eta;
355}
356
357/**
358 * Caudal characteristic of a GI/M/1-type chain: the spectral radius of R, the
359 * unique z in (0,1) with PF(A(z)) = z. Port of `GIM1_Caudal.m`. When `v` is
360 * given it receives the right PF eigenvector of A(z) at the last bisection
361 * point, scaled to unit sum.
362 */
363inline double gim1_caudal(const Blocks& A, std::vector<double>* v = nullptr) {
364 double eta_min = 0.0, eta_max = 1.0, eta = 0.5;
365 Matrix<double> temp;
366 while (eta_max - eta_min > 1e-15) {
367 temp = poly_at(A, eta);
368 const double new_eta = max_eig(temp).real();
369 if (new_eta > eta) {
370 eta_min = eta;
371 } else {
372 eta_max = eta;
373 }
374 eta = (eta_min + eta_max) / 2.0;
375 }
376 if (v) *v = pf_vector(temp, false);
377 return eta;
378}
379
380// ---------------------------------------------------------------------------
381// MG1_Shifts.m
382// ---------------------------------------------------------------------------
383
384/** What `MG1_Shifts` returns: the shifted sequence and the drift it measured. */
387 double drift = 0.0;
388 double tau = 1.0;
389 std::vector<double> v;
390};
391
392/**
393 * Shift technique for the M/G/1-type sequence. Port of `MG1_Shifts.m`, ShiftType
394 * 'one' (the default), 'tau' and 'dbl'.
395 *
396 * For a positive recurrent chain (drift < 1) 'one' shifts the eigenvalue 1 of A(z) to zero
397 * by subtracting `(A0+...+Ai)e u^T` from block i with `u^T = e^T/m`, and 'tau' shifts the
398 * decay rate tau to infinity by subtracting `e rowhatA_i`; for a transient one (drift >= 1)
399 * 'one' shifts 1 to infinity through the row `theta(Amax+...+Ai)` and 'tau' shifts the
400 * caudal value to zero through the column built on its right eigenvector v. 'dbl' applies
401 * both, in the reference's order. The solver converges in the next root instead of stalling
402 * on the removed one and puts the removed rank-one term back on G (`mg1_unshift`).
403 */
404inline ShiftResult mg1_shifts(const Blocks& Ain, const std::string& shift_type) {
405 if (shift_type != "one" && shift_type != "tau" && shift_type != "dbl")
406 throw InputError("MG1_Shifts: ShiftType '" + shift_type + "' is not one of one, tau, dbl");
407 Blocks A = Ain;
408 const std::size_t m = A[0].rows();
409 const std::size_t maxd = A.size() - 1;
410 const Drift d = mg1_drift(A);
411 ShiftResult out;
412 out.drift = d.value;
413 out.tau = 1.0;
414 out.v.assign(m, 0.0);
415 Blocks hatA(A.size(), Matrix<double>(m, m, 0.0));
416 auto minus_I1 = [&](Blocks& X, double s) {
417 for (std::size_t i = 0; i < m; ++i) X[1](i, i) += s;
418 };
419 // hatA_i = A_i - e rowhat_i, rowhat_maxd = u A_maxd, rowhat_i = f*rowhat_{i+1} + u A_i
420 auto row_shift = [&](const Blocks& X, const std::vector<double>& u, double f) {
421 Blocks R(X.size());
422 std::vector<std::vector<double>> row(X.size());
423 row[maxd] = vecmul(u, X[maxd]);
424 for (std::size_t i = maxd; i-- > 0;) {
425 row[i] = vecmul(u, X[i]);
426 for (std::size_t j = 0; j < m; ++j) row[i][j] += f * row[i + 1][j];
427 }
428 for (std::size_t b = 0; b < X.size(); ++b) {
429 R[b] = X[b];
430 for (std::size_t i = 0; i < m; ++i)
431 for (std::size_t j = 0; j < m; ++j) R[b](i, j) -= row[b][j];
432 }
433 return R;
434 };
435
436 if (d.value < 1.0) {
437 if (shift_type == "tau" || shift_type == "dbl") { // shift tau to infinity
438 std::vector<double> uT;
439 out.tau = mg1_decay(A, &uT);
440 minus_I1(A, -1.0);
441 hatA = row_shift(A, uT, out.tau);
442 }
443 if (shift_type == "dbl") A = hatA;
444 if (shift_type == "one") minus_I1(A, -1.0);
445 if (shift_type == "one" || shift_type == "dbl") { // shift one to zero
446 std::vector<double> col(m, 0.0);
447 for (std::size_t b = 0; b < A.size(); ++b) {
448 const std::vector<double> rs = rowsums(A[b]);
449 for (std::size_t i = 0; i < m; ++i) col[i] += rs[i];
450 hatA[b] = A[b];
451 for (std::size_t i = 0; i < m; ++i)
452 for (std::size_t j = 0; j < m; ++j)
453 hatA[b](i, j) -= col[i] / static_cast<double>(m);
454 }
455 }
456 } else {
457 if (shift_type == "one" || shift_type == "dbl") { // shift one to infinity
458 minus_I1(A, -1.0);
459 hatA = row_shift(A, d.theta, 1.0);
460 }
461 if (shift_type == "dbl") {
462 A = hatA;
463 minus_I1(A, 1.0);
464 }
465 if (shift_type == "tau" || shift_type == "dbl") { // shift tau to zero
466 std::vector<double> v;
467 out.tau = gim1_caudal(A, &v);
468 minus_I1(A, -1.0);
469 out.v = v;
470 std::vector<double> col = mulvec(A[0], v);
471 for (std::size_t b = 0; b < A.size(); ++b) {
472 if (b > 0) {
473 const std::vector<double> Av = mulvec(A[b], v);
474 for (std::size_t i = 0; i < m; ++i) col[i] = col[i] / out.tau + Av[i];
475 }
476 hatA[b] = A[b];
477 for (std::size_t i = 0; i < m; ++i)
478 for (std::size_t j = 0; j < m; ++j) hatA[b](i, j) -= col[i];
479 }
480 }
481 }
482 for (std::size_t i = 0; i < m; ++i) hatA[1](i, i) += 1.0;
483 out.hatA = hatA;
484 return out;
485}
486
487/**
488 * Put back on G the rank-one term a shift removed: `ones/m` for a 'one' shift at
489 * drift < 1, `tau v e^T` for a 'tau' shift at drift > 1, both for 'dbl'. The
490 * reference repeats this switch verbatim in MG1_CR, MG1_FI and MG1_NI.
491 */
492inline void mg1_unshift(Matrix<double>& G, const ShiftResult& sh, const std::string& shift_type) {
493 const std::size_t m = G.rows();
494 const bool one = shift_type == "one" || shift_type == "dbl";
495 const bool tau = shift_type == "tau" || shift_type == "dbl";
496 if (one && sh.drift < 1.0)
497 for (std::size_t i = 0; i < m; ++i)
498 for (std::size_t j = 0; j < m; ++j) G(i, j) += 1.0 / static_cast<double>(m);
499 if (tau && sh.drift > 1.0)
500 for (std::size_t i = 0; i < m; ++i)
501 for (std::size_t j = 0; j < m; ++j) G(i, j) += sh.tau * sh.v[i];
502}
503
504// ---------------------------------------------------------------------------
505// MG1_EG.m
506// ---------------------------------------------------------------------------
507
508/**
509 * G in closed form when A0 has rank one. Port of `MG1_EG.m`.
510 *
511 * `found` is false when the shortcut does not apply, which is the reference's
512 * empty return. A rank-one A0 means every down-transition forgets the phase it
513 * came from, so G is the same rank-one matrix `e beta` in the recurrent case;
514 * this is not an approximation and it is why an M/M/1-shaped input never
515 * enters cyclic reduction at all.
516 */
517inline Matrix<double> mg1_eg(const Blocks& Ain, bool& found) {
518 found = false;
519 Blocks A = Ain;
520 const std::size_t m = A[0].rows();
521 const std::size_t dega = A.size() - 1;
522 const Drift d = mg1_drift(A);
523
524 if (matrix_rank(A[0]) != 1) return Matrix<double>();
525
526 if (d.value < 1.0) {
527 // A0 = alpha beta: G = e beta with beta the normalized first nonzero row.
528 const std::vector<double> rs = rowsums(A[0]);
529 std::size_t first = m;
530 for (std::size_t i = 0; i < m; ++i)
531 if (rs[i] > 0.0) {
532 first = i;
533 break;
534 }
535 if (first == m) return Matrix<double>();
536 Matrix<double> G(m, m, 0.0);
537 for (std::size_t i = 0; i < m; ++i)
538 for (std::size_t j = 0; j < m; ++j) G(i, j) = A[0](first, j) / rs[first];
539 found = true;
540 return G;
541 }
542 if (d.value > 1.0) {
543 // Transient chain: G through the Ramaswami dual and its caudal value.
544 Blocks At(A.size(), Matrix<double>(m, m, 0.0));
545 for (std::size_t b = 0; b < A.size(); ++b)
546 for (std::size_t i = 0; i < m; ++i)
547 for (std::size_t j = 0; j < m; ++j)
548 At[b](i, j) = A[b](j, i) * d.theta[j] / d.theta[i];
549 const double etahat = gim1_caudal(At);
550 Matrix<double> temp = At[dega];
551 for (std::size_t i = dega; i-- > 1;) temp = madd(mscale(temp, etahat), At[i]);
552 Matrix<double> M = matmul(At[0], inverse(msub(eye<double>(m), temp)));
553 Matrix<double> G(m, m, 0.0);
554 for (std::size_t i = 0; i < m; ++i)
555 for (std::size_t j = 0; j < m; ++j) G(i, j) = M(j, i) * d.theta[j] / d.theta[i];
556 found = true;
557 return G;
558 }
559 return Matrix<double>();
560}
561
562// ---------------------------------------------------------------------------
563// MG1_CR.m
564// ---------------------------------------------------------------------------
565
566/** Options of `MG1_CR`, with the reference's defaults. */
568 std::string mode = "ShiftPWCR"; ///< 'ShiftPWCR' or 'PWCR'
569 std::string shift_type = "one";
570 std::size_t max_num_it = 50;
571 std::size_t max_num_root = 2048;
572 double epsilon = 1e-16;
573};
574
575namespace cr_detail {
576
577/** Blockwise DFT along the block index, MATLAB's fft over the block sequence. */
578inline std::vector<Matrix<std::complex<double>>> block_dft(const Blocks& blk, std::size_t n,
579 std::size_t use, bool inverse) {
580 const std::size_t m = blk[0].rows();
581 std::vector<Matrix<std::complex<double>>> out(
582 n, Matrix<std::complex<double>>(m, m, std::complex<double>(0.0, 0.0)));
583 std::vector<std::complex<double>> buf(n);
584 for (std::size_t i = 0; i < m; ++i)
585 for (std::size_t j = 0; j < m; ++j) {
586 for (std::size_t k = 0; k < n; ++k)
587 buf[k] = (k < use && k < blk.size()) ? std::complex<double>(blk[k](i, j), 0.0)
588 : std::complex<double>(0.0, 0.0);
589 dft(buf, inverse);
590 for (std::size_t k = 0; k < n; ++k) out[k](i, j) = buf[k];
591 }
592 return out;
593}
594
595/** The inverse of the above, keeping the real part as `real(ifft(...))` does. */
596inline Blocks block_idft_real(const std::vector<Matrix<std::complex<double>>>& F) {
597 const std::size_t n = F.size(), m = F[0].rows();
598 Blocks out(n, Matrix<double>(m, m, 0.0));
599 std::vector<std::complex<double>> buf(n);
600 for (std::size_t i = 0; i < m; ++i)
601 for (std::size_t j = 0; j < m; ++j) {
602 for (std::size_t k = 0; k < n; ++k) buf[k] = F[k](i, j);
603 dft(buf, true);
604 for (std::size_t k = 0; k < n; ++k) out[k](i, j) = buf[k].real();
605 }
606 return out;
607}
608
609inline Matrix<std::complex<double>> cmul(const Matrix<std::complex<double>>& A,
610 const Matrix<std::complex<double>>& B) {
611 return matmul(A, B);
612}
613
614inline Matrix<std::complex<double>> cinv_i_minus(const Matrix<std::complex<double>>& A) {
615 const std::size_t m = A.rows();
616 Matrix<std::complex<double>> M(m, m, std::complex<double>(0.0, 0.0));
617 for (std::size_t i = 0; i < m; ++i)
618 for (std::size_t j = 0; j < m; ++j)
619 M(i, j) = (i == j ? std::complex<double>(1.0, 0.0) : std::complex<double>(0.0, 0.0)) -
620 A(i, j);
621 return inverse(M);
622}
623
624/** The tail norm the reference measures, over blocks deg/2 .. deg-1. */
625inline double tail_norm(const Blocks& blk) {
626 const std::size_t deg = blk.size();
627 // MATLAB's `for i=deg/2:deg-1` starts at a HALF-INTEGER when deg is odd,
628 // and a half-integer index never matches, so the loop body runs from
629 // ceil(deg/2). Reproduced, or a degree-1 sequence would be measured here
630 // where the reference measures nothing.
631 const std::size_t start = (deg + 1) / 2;
632 double best = 0.0;
633 for (std::size_t i = start; i + 1 <= deg && i < deg; ++i) best = std::max(best, inf_norm(blk[i]));
634 return best;
635}
636
637/** The even-subscript blocks A0, A2, A4, ... of a sequence. */
638inline Blocks even_blocks(const Blocks& b) {
639 Blocks out;
640 for (std::size_t i = 0; i < b.size(); i += 2) out.push_back(b[i]);
641 return out;
642}
643
644/** The odd-subscript blocks A1, A3, A5, ... of a sequence. */
645inline Blocks odd_blocks(const Blocks& b) {
646 Blocks out;
647 for (std::size_t i = 1; i < b.size(); i += 2) out.push_back(b[i]);
648 return out;
649}
650
651} // namespace cr_detail
652
653/**
654 * Cyclic reduction for M/G/1-type Markov chains [Bini, Meini]. Port of
655 * `MG1_CR.m`, default mode 'ShiftPWCR' with ShiftType 'one'.
656 *
657 * WHAT THE ALGORITHM DOES, since the transcription is otherwise opaque. One
658 * step of cyclic reduction eliminates every odd level of the chain and leaves
659 * a chain of the same M/G/1-type shape on the even ones, so the level
660 * distance halves per iteration and the iteration converges quadratically.
661 * Doing that on the block sequences directly is a polynomial composition; the
662 * reference instead evaluates the four sequences at the (nj+1)-th roots of
663 * unity, does the composition POINT-WISE (a small dense inverse per root), and
664 * interpolates back with an inverse transform -- the "point-wise" in PWCR. The
665 * number of roots doubles until the interpolated tail is below (nj+1) eps,
666 * which is the reference's own accuracy control and the reason MaxNumRoot
667 * exists.
668 *
669 * Everything runs on the TRANSPOSED blocks, as the reference does after
670 * `D=D'`, and the final G is transposed back.
671 */
672inline Matrix<double> mg1_cr(const Blocks& Ain, const Mg1CrOptions& opts = Mg1CrOptions()) {
673 if (Ain.empty()) throw InputError("MG1_CR: empty block sequence");
674 const std::size_t m = Ain[0].rows();
675 if (opts.mode != "ShiftPWCR" && opts.mode != "PWCR")
676 throw UnsupportedError("MG1_CR: Mode '" + opts.mode +
677 "' is not supported; the reference offers 'PWCR' and 'ShiftPWCR'");
678
679 bool eg_found = false;
680 const Matrix<double> Geg = mg1_eg(Ain, eg_found);
681 if (eg_found) return Geg;
682
683 Blocks A = Ain;
684 ShiftResult sh;
685 if (opts.mode == "ShiftPWCR") {
686 sh = mg1_shifts(A, opts.shift_type);
687 A = sh.hatA;
688 }
689
690 // D = A', padded with zero blocks to 2^(1+floor(log2(maxd)))+1 of them.
691 const std::size_t maxd = A.size() - 1;
692 if (maxd == 0) throw InputError("MG1_CR: the sequence needs at least two blocks");
693 std::size_t target = 1;
694 while (target < maxd) target <<= 1; // 2^ceil(log2(maxd))
695 if (target == maxd) target <<= 1; // 2^(1+floor(log2(maxd))) when maxd is a power of two
696 target += 1;
697 Blocks D(target, Matrix<double>(m, m, 0.0));
698 for (std::size_t b = 0; b <= maxd; ++b) D[b] = A[b].transpose();
699
700 Blocks Aeven = cr_detail::even_blocks(D);
701 Blocks Aodd = cr_detail::odd_blocks(D);
702 Blocks Ahatodd(Aeven.begin() + 1, Aeven.end());
703 Ahatodd.push_back(D.back());
704 Blocks Ahateven = Aodd;
705
706 Matrix<double> Rj = D[1];
707 for (std::size_t i = 2; i < D.size(); ++i) Rj = madd(Rj, D[i]);
708 Rj = matmul(D[0], inverse(msub(eye<double>(m), Rj)));
709
710 Matrix<double> G(m, m, 0.0);
711 Blocks Anew, Ahatnew;
712 std::size_t numit = 0;
713 while (numit < opts.max_num_it) {
714 ++numit;
715 std::size_t nj = Aodd.size() - 1;
716 double nAnew = 0.0, nAhatnew = 0.0;
717
718 if (nj > 0) {
719 const std::size_t n = nj + 1;
720 const std::vector<Matrix<std::complex<double>>> T1 =
721 cr_detail::block_dft(Aodd, n, n, false);
722 const std::vector<Matrix<std::complex<double>>> T2 =
723 cr_detail::block_dft(Aeven, n, n, false);
724 const std::vector<Matrix<std::complex<double>>> T3 =
725 cr_detail::block_dft(Ahatodd, n, n, false);
726 const std::vector<Matrix<std::complex<double>>> T4 =
727 cr_detail::block_dft(Ahateven, n, n, false);
728 std::vector<Matrix<std::complex<double>>> Ah(n), An(n);
729 const double pi = 3.14159265358979323846;
730 for (std::size_t c = 0; c < n; ++c) {
731 const Matrix<std::complex<double>> W = cr_detail::cinv_i_minus(T1[c]);
732 Ah[c] = madd(T4[c], cr_detail::cmul(cr_detail::cmul(T2[c], W), T3[c]));
733 const double ang = -2.0 * pi * static_cast<double>(c) / static_cast<double>(n);
734 const std::complex<double> w(std::cos(ang), std::sin(ang));
735 Matrix<std::complex<double>> first(m, m, std::complex<double>(0.0, 0.0));
736 for (std::size_t i = 0; i < m; ++i)
737 for (std::size_t j = 0; j < m; ++j) first(i, j) = w * T1[c](i, j);
738 An[c] = madd(first, cr_detail::cmul(cr_detail::cmul(T2[c], W), T2[c]));
739 }
740 Ahatnew = cr_detail::block_idft_real(Ah);
741 Anew = cr_detail::block_idft_real(An);
742 } else {
743 const Matrix<double> temp =
744 matmul(Aeven[0], inverse(msub(eye<double>(m), Aodd[0])));
745 Ahatnew.assign(1, madd(Ahateven[0], matmul(temp, Ahatodd[0])));
746 Anew.clear();
747 Anew.push_back(matmul(temp, Aeven[0]));
748 Anew.push_back(Aodd[0]);
749 }
750
751 nAnew = cr_detail::tail_norm(Anew);
752 nAhatnew = cr_detail::tail_norm(Ahatnew);
753
754 // Double the number of roots until the interpolated tail is negligible.
755 while ((nAnew > static_cast<double>(nj + 1) * opts.epsilon ||
756 nAhatnew > static_cast<double>(nj + 1) * opts.epsilon) &&
757 nj + 1 < opts.max_num_root) {
758 nj = 2 * (nj + 1) - 1;
759 const std::size_t n = nj + 1;
760 const std::size_t stopv = std::min(n, Aodd.size());
761 const std::vector<Matrix<std::complex<double>>> T1 =
762 cr_detail::block_dft(Aodd, n, stopv, false);
763 const std::vector<Matrix<std::complex<double>>> T2 =
764 cr_detail::block_dft(Aeven, n, stopv, false);
765 const std::vector<Matrix<std::complex<double>>> T3 =
766 cr_detail::block_dft(Ahatodd, n, stopv, false);
767 const std::vector<Matrix<std::complex<double>>> T4 =
768 cr_detail::block_dft(Ahateven, n, stopv, false);
769 std::vector<Matrix<std::complex<double>>> Ah(n), An(n);
770 const double pi = 3.14159265358979323846;
771 for (std::size_t c = 0; c < n; ++c) {
772 const Matrix<std::complex<double>> W = cr_detail::cinv_i_minus(T1[c]);
773 Ah[c] = madd(T4[c], cr_detail::cmul(cr_detail::cmul(T2[c], W), T3[c]));
774 const double ang = -2.0 * pi * static_cast<double>(c) / static_cast<double>(n);
775 const std::complex<double> w(std::cos(ang), std::sin(ang));
776 Matrix<std::complex<double>> first(m, m, std::complex<double>(0.0, 0.0));
777 for (std::size_t i = 0; i < m; ++i)
778 for (std::size_t j = 0; j < m; ++j) first(i, j) = w * T1[c](i, j);
779 An[c] = madd(first, cr_detail::cmul(cr_detail::cmul(T2[c], W), T2[c]));
780 }
781 Ahatnew = cr_detail::block_idft_real(Ah);
782 Anew = cr_detail::block_idft_real(An);
783 nAnew = cr_detail::tail_norm(Anew);
784 nAhatnew = cr_detail::tail_norm(Ahatnew);
785 }
786
787 if (nj > 1) {
788 const std::size_t keep = (nj + 1) / 2;
789 Anew.resize(std::min(keep, Anew.size()));
790 Ahatnew.resize(std::min(keep, Ahatnew.size()));
791 }
792
793 Aeven = cr_detail::even_blocks(Anew);
794 Aodd = cr_detail::odd_blocks(Anew);
795 Ahateven = cr_detail::even_blocks(Ahatnew);
796 Ahatodd = cr_detail::odd_blocks(Ahatnew);
797
798 if (opts.mode == "PWCR") {
799 Matrix<double> Rnewj = Anew.size() > 1 ? Anew[1] : Matrix<double>(m, m, 0.0);
800 for (std::size_t i = 2; i < Anew.size(); ++i) Rnewj = madd(Rnewj, Anew[i]);
801 Rnewj = matmul(Anew[0], inverse(msub(eye<double>(m), Rnewj)));
802 const Matrix<double> U =
803 Anew.size() > 1
804 ? msub(eye<double>(m),
805 matmul(Anew[0], inverse(msub(eye<double>(m), Anew[1]))))
806 : eye<double>(m);
807 if (max_abs_diff(Rj, Rnewj) < opts.epsilon || max_col_sum(U) < opts.epsilon) {
808 G = Ahatnew[0];
809 for (std::size_t i = 1; i < Ahatnew.size(); ++i)
810 G = madd(G, matmul(Rnewj, Ahatnew[i]));
811 G = matmul(D[0], inverse(msub(eye<double>(m), G)));
812 break;
813 }
814 Rj = Rnewj;
815 double tail_sum = 0.0;
816 for (std::size_t i = 1; i < Ahatnew.size(); ++i) tail_sum += Ahatnew[i].sum();
817 const double sv = svd_values(Anew[0]).empty() ? 0.0 : svd_values(Anew[0])[0];
818 const Matrix<double> V =
819 msub(eye<double>(m), matmul(D[0], inverse(msub(eye<double>(m), Ahatnew[0]))));
820 if (sv < opts.epsilon || tail_sum < opts.epsilon || max_col_sum(V) < opts.epsilon) {
821 G = matmul(D[0], inverse(msub(eye<double>(m), Ahatnew[0])));
822 break;
823 }
824 } else {
825 const Matrix<double> Gold = G;
826 G = matmul(D[0], inverse(msub(eye<double>(m), Ahatnew[0])));
827 if (inf_norm(msub(G, Gold)) < opts.epsilon || inf_norm(Ahatnew, 1) < opts.epsilon)
828 break;
829 }
830 }
831 if (numit == opts.max_num_it && !Ahatnew.empty())
832 G = matmul(D[0], inverse(msub(eye<double>(m), Ahatnew[0])));
833
834 G = G.transpose();
835
836 // Undo the shift: put back the rank-one term it removed from G.
837 if (opts.mode == "ShiftPWCR") mg1_unshift(G, sh, opts.shift_type);
838 return G;
839}
840
841// ---------------------------------------------------------------------------
842// MG1_FI.m
843// ---------------------------------------------------------------------------
844
845/** Options of `MG1_FI`, with the reference's defaults. */
847 std::string mode = "U-Based"; ///< 'Natural', 'Traditional', 'U-Based', or 'Shift<Mode>'
848 std::string shift_type = "one";
849 std::size_t max_num_it = 10000;
850 double tol = 1e-14;
851};
852
853/**
854 * Functional iterations for M/G/1-type Markov chains [Neuts]. Port of
855 * `MG1_FI.m` for the three modes and the shift variants; the reference's
856 * `NonZeroBlocks` option is not exposed, because it changes only which
857 * products are skipped when some A_i vanish and converges to the same G.
858 *
859 * 'U-Based' is the default and the one `GIM1_R(...,'FI')` uses: it solves
860 * G = (I - sum_{j>=1} A_j G^{j-1})^{-1} A0, which is the U-based iteration and
861 * converges monotonically from below to the minimal nonnegative solution.
862 */
863inline Matrix<double> mg1_fi(const Blocks& Ain, const Mg1FiOptions& opts = Mg1FiOptions()) {
864 const std::size_t m = Ain[0].rows();
865 const std::size_t maxd = Ain.size() - 1;
866
867 bool eg_found = false;
868 const Matrix<double> Geg = mg1_eg(Ain, eg_found);
869 if (eg_found) return Geg;
870
871 Blocks A = Ain;
872 const bool shifted = opts.mode.find("Shift") != std::string::npos;
873 ShiftResult sh;
874 if (shifted) {
875 sh = mg1_shifts(A, opts.shift_type);
876 A = sh.hatA;
877 }
878
879 const bool natural = opts.mode.find("Natural") != std::string::npos;
880 const bool traditional = opts.mode.find("Traditional") != std::string::npos;
881 const bool ubased = opts.mode.find("U-Based") != std::string::npos;
882 if (!natural && !traditional && !ubased)
883 throw UnsupportedError("MG1_FI: Mode '" + opts.mode + "' is not supported");
884
885 Matrix<double> G(m, m, 0.0);
886 double check = 1.0;
887 std::size_t numit = 0;
888 while (check > opts.tol && numit < opts.max_num_it) {
889 const Matrix<double> Gold = G;
890 if (natural) {
891 G = A[maxd];
892 for (std::size_t j = maxd; j-- > 0;) G = madd(A[j], matmul(G, Gold));
893 } else if (traditional) {
894 G = A[maxd];
895 for (std::size_t j = maxd; j-- > 2;) G = madd(A[j], matmul(G, Gold));
896 G = madd(A[0], matmul(G, matmul(Gold, Gold)));
897 G = matmul(inverse(msub(eye<double>(m), A[1])), G);
898 } else {
899 G = A[maxd];
900 for (std::size_t j = maxd; j-- > 1;) G = madd(A[j], matmul(G, Gold));
901 G = matmul(inverse(msub(eye<double>(m), G)), A[0]);
902 }
903 check = inf_norm(msub(G, Gold));
904 ++numit;
905 }
906
907 if (shifted) mg1_unshift(G, sh, opts.shift_type);
908 return G;
909}
910
911// ---------------------------------------------------------------------------
912// Dense block helpers for MG1_NI / MG1_RR / MG1_IS
913// ---------------------------------------------------------------------------
914
915namespace mg1x_detail {
916
917/** A(r0:r0+nr, c0:c0+nc), half-open, 0-based. */
918inline Matrix<double> sub(const Matrix<double>& A, std::size_t r0, std::size_t nr, std::size_t c0,
919 std::size_t nc) {
920 Matrix<double> S(nr, nc, 0.0);
921 for (std::size_t i = 0; i < nr; ++i)
922 for (std::size_t j = 0; j < nc; ++j) S(i, j) = A(r0 + i, c0 + j);
923 return S;
924}
925
926/** A(r0:, c0:) = S. */
927inline void put(Matrix<double>& A, std::size_t r0, std::size_t c0, const Matrix<double>& S) {
928 for (std::size_t i = 0; i < S.rows(); ++i)
929 for (std::size_t j = 0; j < S.cols(); ++j) A(r0 + i, c0 + j) = S(i, j);
930}
931
933 Matrix<double> T(A.cols(), A.rows(), 0.0);
934 for (std::size_t i = 0; i < A.rows(); ++i)
935 for (std::size_t j = 0; j < A.cols(); ++j) T(j, i) = A(i, j);
936 return T;
937}
938
939/** [A B], horizontally. */
941 if (A.rows() != B.rows()) throw InputError("smc: horizontal join with mismatched rows");
942 Matrix<double> C(A.rows(), A.cols() + B.cols(), 0.0);
943 put(C, 0, 0, A);
944 put(C, 0, A.cols(), B);
945 return C;
946}
947
948/** [A; B], vertically. */
950 if (A.cols() != B.cols()) throw InputError("smc: vertical join with mismatched columns");
951 Matrix<double> C(A.rows() + B.rows(), A.cols(), 0.0);
952 put(C, 0, 0, A);
953 put(C, A.rows(), 0, B);
954 return C;
955}
956
957/** Z \ b for a square Z. */
958inline std::vector<double> lsolve(const Matrix<double>& Z, const std::vector<double>& b) {
959 return solve(Z, b);
960}
961
962/**
963 * Economy QR, A = Q R with Q (r x k) orthonormal columns and R (k x c), k = min(r, c), by
964 * Householder reflections. The sign convention differs from LAPACK's, which MG1_RR cannot see:
965 * its generators enter B only through a product invariant under an orthogonal change of basis.
966 */
968 const std::size_t r = A.rows(), c = A.cols(), k = std::min(r, c);
969 Matrix<double> W = A;
970 std::vector<std::vector<double>> vs;
971 for (std::size_t j = 0; j < k; ++j) {
972 double nrm = 0.0;
973 for (std::size_t i = j; i < r; ++i) nrm += W(i, j) * W(i, j);
974 nrm = std::sqrt(nrm);
975 std::vector<double> v(r, 0.0);
976 if (nrm > 0.0) {
977 const double alpha = W(j, j) > 0.0 ? -nrm : nrm;
978 for (std::size_t i = j; i < r; ++i) v[i] = W(i, j);
979 v[j] -= alpha;
980 double vn = 0.0;
981 for (std::size_t i = j; i < r; ++i) vn += v[i] * v[i];
982 if (vn > 0.0) {
983 for (std::size_t col = j; col < c; ++col) {
984 double s = 0.0;
985 for (std::size_t i = j; i < r; ++i) s += v[i] * W(i, col);
986 s = 2.0 * s / vn;
987 for (std::size_t i = j; i < r; ++i) W(i, col) -= s * v[i];
988 }
989 const double vnr = std::sqrt(vn);
990 for (std::size_t i = j; i < r; ++i) v[i] /= vnr;
991 } else {
992 std::fill(v.begin(), v.end(), 0.0);
993 }
994 }
995 vs.push_back(v);
996 }
997 R = Matrix<double>(k, c, 0.0);
998 for (std::size_t i = 0; i < k; ++i)
999 for (std::size_t j = i; j < c; ++j) R(i, j) = W(i, j);
1000 // Q = H_0 H_1 ... H_{k-1} applied to the first k columns of the identity.
1001 Q = Matrix<double>(r, k, 0.0);
1002 for (std::size_t i = 0; i < k; ++i) Q(i, i) = 1.0;
1003 for (std::size_t j = k; j-- > 0;) {
1004 const std::vector<double>& v = vs[j];
1005 for (std::size_t col = 0; col < k; ++col) {
1006 double s = 0.0;
1007 for (std::size_t i = j; i < r; ++i) s += v[i] * Q(i, col);
1008 s *= 2.0;
1009 for (std::size_t i = j; i < r; ++i) Q(i, col) -= s * v[i];
1010 }
1011 }
1012}
1013
1014} // namespace mg1x_detail
1015
1016// ---------------------------------------------------------------------------
1017// solveSylvPowersDirectSum.m, solveSylvPowersRealSchur_FW.m
1018// ---------------------------------------------------------------------------
1019
1020/**
1021 * Solve sum_{j=1}^N B_j Y A^{j-1} = C directly, through the Kronecker form of vec(Y).
1022 * Port of `solveSylvPowersDirectSum.m`; `B` holds the N blocks B_1..B_N (n x n), A is m x m.
1023 */
1025 const Matrix<double>& C) {
1026 const std::size_t m = A.rows(), n = C.rows(), N = B.size();
1027 const Matrix<double> At = mg1x_detail::trans(A);
1028 Matrix<double> P = eye<double>(m); // (A')^(j-1)
1029 Matrix<double> Z(m * n, m * n, 0.0);
1030 for (std::size_t j = 0; j < N; ++j) {
1031 for (std::size_t a = 0; a < m; ++a)
1032 for (std::size_t b = 0; b < m; ++b) {
1033 const double p = P(a, b);
1034 if (p == 0.0) continue;
1035 for (std::size_t k = 0; k < n; ++k)
1036 for (std::size_t l = 0; l < n; ++l) Z(a * n + k, b * n + l) += p * B[j](k, l);
1037 }
1038 P = matmul(P, At);
1039 }
1040 std::vector<double> c(m * n);
1041 for (std::size_t col = 0; col < m; ++col)
1042 for (std::size_t k = 0; k < n; ++k) c[col * n + k] = C(k, col);
1043 const std::vector<double> y = mg1x_detail::lsolve(Z, c);
1044 Matrix<double> Y(n, m, 0.0);
1045 for (std::size_t col = 0; col < m; ++col)
1046 for (std::size_t k = 0; k < n; ++k) Y(k, col) = y[col * n + k];
1047 return Y;
1048}
1049
1050/**
1051 * Solve sum_{j=1}^N B_j Y A^{j-1} = C by a real Schur form A = U T U': with Y = X U' the
1052 * system becomes sum_j B_j X T^{j-1} = C U, and T quasi-triangular makes it a forward
1053 * substitution over the columns of X, one n x n solve per 1 x 1 block and one 2n x 2n solve
1054 * per 2 x 2 block. Port of `solveSylvPowersRealSchur_FW.m`, including its 1e-13 test on the
1055 * subdiagonal that tells the two block shapes apart.
1056 */
1058 const Matrix<double>& C) {
1059 const std::size_t m = A.rows(), n = C.rows(), N = B.size();
1060 const RealSchur sf = schur_decomposition(A);
1061 const Matrix<double>& T = sf.T;
1062 const Matrix<double> E = matmul(C, sf.Z);
1063 Blocks Tp(N, eye<double>(m));
1064 for (std::size_t j = 1; j < N; ++j) Tp[j] = matmul(Tp[j - 1], T);
1065 // S(a,b) = sum_j B_j (T^{j-1})(a,b)
1066 auto S = [&](std::size_t a, std::size_t b) {
1067 Matrix<double> out(n, n, 0.0);
1068 for (std::size_t j = 0; j < N; ++j) {
1069 const double t = Tp[j](a, b);
1070 if (t == 0.0) continue;
1071 for (std::size_t k = 0; k < n; ++k)
1072 for (std::size_t l = 0; l < n; ++l) out(k, l) += t * B[j](k, l);
1073 }
1074 return out;
1075 };
1076 // sum_{l<k} S(l,col) X(:,l)
1077 Matrix<double> X(n, m, 0.0);
1078 auto W = [&](std::size_t k, std::size_t col) {
1079 std::vector<double> w(n, 0.0);
1080 for (std::size_t l = 0; l < k; ++l) {
1081 const Matrix<double> Sl = S(l, col);
1082 for (std::size_t a = 0; a < n; ++a)
1083 for (std::size_t b = 0; b < n; ++b) w[a] += Sl(a, b) * X(b, l);
1084 }
1085 return w;
1086 };
1087 const double epsilon = 10e-14;
1088 std::size_t k = 0;
1089 while (k < m) {
1090 if (k + 1 == m || std::abs(T(k + 1, k)) < epsilon) {
1091 const Matrix<double> Z = S(k, k);
1092 const std::vector<double> w = W(k, k);
1093 std::vector<double> rhs(n);
1094 for (std::size_t a = 0; a < n; ++a) rhs[a] = E(a, k) - w[a];
1095 const std::vector<double> x = mg1x_detail::lsolve(Z, rhs);
1096 for (std::size_t a = 0; a < n; ++a) X(a, k) = x[a];
1097 k += 1;
1098 } else {
1099 Matrix<double> Z(2 * n, 2 * n, 0.0);
1100 mg1x_detail::put(Z, 0, 0, S(k, k));
1101 mg1x_detail::put(Z, 0, n, S(k + 1, k));
1102 mg1x_detail::put(Z, n, 0, S(k, k + 1));
1103 mg1x_detail::put(Z, n, n, S(k + 1, k + 1));
1104 const std::vector<double> w0 = W(k, k), w1 = W(k, k + 1);
1105 std::vector<double> rhs(2 * n);
1106 for (std::size_t a = 0; a < n; ++a) {
1107 rhs[a] = E(a, k) - w0[a];
1108 rhs[n + a] = E(a, k + 1) - w1[a];
1109 }
1110 const std::vector<double> x = mg1x_detail::lsolve(Z, rhs);
1111 for (std::size_t a = 0; a < n; ++a) {
1112 X(a, k) = x[a];
1113 X(a, k + 1) = x[n + a];
1114 }
1115 k += 2;
1116 }
1117 }
1118 return matmul(X, mg1x_detail::trans(sf.Z));
1119}
1120
1121// ---------------------------------------------------------------------------
1122// MG1_NI.m
1123// ---------------------------------------------------------------------------
1124
1125/** Options of `MG1_NI`, with the reference's defaults. */
1127 /// 'DirectSum', 'RealSchur' or 'ComplexSchur', each optionally with the 'Shift' suffix
1128 std::string mode = "RealSchurShift";
1129 std::size_t max_num_it = 50;
1130 std::string shift_type = "one";
1131 double epsilon = 1e-14;
1132};
1133
1134/**
1135 * Newton iteration for M/G/1-type Markov chains. Port of `MG1_NI.m`.
1136 *
1137 * Each step linearizes G = sum_i A_i G^i at the current G and solves the resulting
1138 * Sylvester-power equation sum_j B_j Y G^{j-1} = G - B_0 for the correction Y, so the
1139 * iteration converges quadratically; the shift variants run it on the sequence with the
1140 * unit root moved to zero (`mg1_shifts`) and put the removed rank-one term back at the end.
1141 *
1142 * 'ComplexSchur' asks the reference for a complex Schur form of G. It solves the SAME linear
1143 * system as 'RealSchur', whose real quasi-triangular form this port uses for both, so the two
1144 * modes differ only in rounding: there is no complex Schur factorization in this tree.
1145 */
1146inline Matrix<double> mg1_ni(const Blocks& Ain, const Mg1NiOptions& opts = Mg1NiOptions()) {
1147 if (Ain.size() < 2) throw InputError("MG1_NI: the sequence needs at least two blocks");
1148 std::string base = opts.mode;
1149 const bool shifted = base.size() > 5 && base.compare(base.size() - 5, 5, "Shift") == 0;
1150 if (shifted) base = base.substr(0, base.size() - 5);
1151 if (base != "DirectSum" && base != "RealSchur" && base != "ComplexSchur")
1152 throw InputError("MG1_NI: Mode '" + opts.mode + "' is not one of DirectSum, RealSchur, "
1153 "ComplexSchur, each optionally with the Shift suffix");
1154
1155 bool eg_found = false;
1156 const Matrix<double> Geg = mg1_eg(Ain, eg_found);
1157 if (eg_found) return Geg;
1158
1159 Blocks A = Ain;
1160 ShiftResult sh;
1161 if (shifted) {
1162 sh = mg1_shifts(A, opts.shift_type);
1163 A = sh.hatA;
1164 }
1165
1166 const std::size_t m = A[0].rows();
1167 const std::size_t N = A.size() - 1;
1168 Matrix<double> G(m, m, 0.0);
1169 double check = 1.0;
1170 std::size_t numit = 0;
1171 while (check > opts.epsilon && numit < opts.max_num_it) {
1172 const Matrix<double> Gold = G;
1173 Blocks Bk(N + 1);
1174 Bk[N] = A[N];
1175 for (std::size_t i = N; i-- > 0;) Bk[i] = madd(A[i], matmul(Bk[i + 1], G));
1176 const Matrix<double> C = msub(G, Bk[0]);
1177 Blocks Bs(Bk.begin() + 1, Bk.end());
1178 for (std::size_t i = 0; i < m; ++i) Bs[0](i, i) -= 1.0;
1179 const Matrix<double> Y =
1180 base == "DirectSum" ? sylv_powers_direct(G, Bs, C) : sylv_powers_real_schur(G, Bs, C);
1181 G = madd(G, Y);
1182 check = inf_norm(msub(G, Gold));
1183 ++numit;
1184 }
1185
1186 if (shifted) mg1_unshift(G, sh, opts.shift_type);
1187 return G;
1188}
1189
1190// ---------------------------------------------------------------------------
1191// MG1_RR.m (with MG1_RR_Btemp.m and MG1_RR_tempB.m)
1192// ---------------------------------------------------------------------------
1193
1194/** Options of `MG1_RR`, with the reference's defaults. */
1196 std::string mode = "Direct"; ///< 'Direct', 'DispStruct' or 'DispStructFFT'
1197 std::size_t max_num_it = 50;
1198};
1199
1200namespace rr_detail {
1201
1202/**
1203 * The displacement representation B = L(b) + L(c1) L(Z r1)' + L(c2) L(Z r2)' of the
1204 * Ramaswami-reduction matrix, with L(x) the block lower-triangular Toeplitz matrix whose
1205 * first block column is x (N*m x m) and Z the block down-shift. Only the generators are
1206 * stored; `left` and `right` are `MG1_RR_tempB.m` and `MG1_RR_Btemp.m`.
1207 */
1208struct Disp {
1209 std::size_t N, m;
1211
1212 /** Block k (0-based) of a stacked N*m x m generator. */
1213 Matrix<double> blk(const Matrix<double>& x, std::size_t k) const {
1214 return mg1x_detail::sub(x, k * m, m, 0, m);
1215 }
1216
1217 /** temp * B, `MG1_RR_tempB.m`. */
1219 const std::size_t hd = temp.rows();
1220 Matrix<double> out(hd, N * m, 0.0);
1221 // temp * L(x): block column l is sum_{i>=l} temp_i x_{i-l}
1222 auto times_L = [&](const Matrix<double>& x) {
1223 Matrix<double> r(hd, N * m, 0.0);
1224 for (std::size_t l = 0; l < N; ++l) {
1225 Matrix<double> acc(hd, m, 0.0);
1226 for (std::size_t i = l; i < N; ++i)
1227 acc = madd(acc, matmul(mg1x_detail::sub(temp, 0, hd, i * m, m), blk(x, i - l)));
1228 mg1x_detail::put(r, 0, l * m, acc);
1229 }
1230 return r;
1231 };
1232 // (temp L(c)) L(Z r)': L(Z r)' is block upper triangular with block (i,l) =
1233 // (Zr)_{l-i}' = r_{l-i-1}' for l > i, zero otherwise.
1234 auto times_LZrT = [&](const Matrix<double>& P, const Matrix<double>& r) {
1235 Matrix<double> q(hd, N * m, 0.0);
1236 for (std::size_t l = 1; l < N; ++l) {
1237 Matrix<double> acc(hd, m, 0.0);
1238 for (std::size_t i = 0; i < l; ++i)
1239 acc = madd(acc, matmul(mg1x_detail::sub(P, 0, hd, i * m, m),
1240 mg1x_detail::trans(blk(r, l - i - 1))));
1241 mg1x_detail::put(q, 0, l * m, acc);
1242 }
1243 return q;
1244 };
1245 out = madd(out, times_LZrT(times_L(c1), r1));
1246 out = madd(out, times_LZrT(times_L(c2), r2));
1247 out = madd(out, times_L(b));
1248 return out;
1249 }
1250
1251 /** B * temp, `MG1_RR_Btemp.m`. */
1253 const std::size_t hd = temp.cols();
1254 // L(Z r)' temp: block row i is sum_{l>i} r_{l-i-1}' temp_l
1255 auto LZrT_times = [&](const Matrix<double>& r) {
1256 Matrix<double> q(N * m, hd, 0.0);
1257 for (std::size_t i = 0; i + 1 < N; ++i) {
1258 Matrix<double> acc(m, hd, 0.0);
1259 for (std::size_t l = i + 1; l < N; ++l)
1260 acc = madd(acc, matmul(mg1x_detail::trans(blk(r, l - i - 1)),
1261 mg1x_detail::sub(temp, l * m, m, 0, hd)));
1262 mg1x_detail::put(q, i * m, 0, acc);
1263 }
1264 return q;
1265 };
1266 // L(x) P: block row i is sum_{l<=i} x_{i-l} P_l
1267 auto L_times = [&](const Matrix<double>& x, const Matrix<double>& P) {
1268 Matrix<double> q(N * m, hd, 0.0);
1269 for (std::size_t i = 0; i < N; ++i) {
1270 Matrix<double> acc(m, hd, 0.0);
1271 for (std::size_t l = 0; l <= i; ++l)
1272 acc = madd(acc, matmul(blk(x, i - l), mg1x_detail::sub(P, l * m, m, 0, hd)));
1273 mg1x_detail::put(q, i * m, 0, acc);
1274 }
1275 return q;
1276 };
1277 Matrix<double> out = L_times(c1, LZrT_times(r1));
1278 out = madd(out, L_times(c2, LZrT_times(r2)));
1279 out = madd(out, L_times(b, temp));
1280 return out;
1281 }
1282};
1283
1284inline double sum_all(const Matrix<double>& A) {
1285 double s = 0.0;
1286 for (std::size_t i = 0; i < A.rows(); ++i)
1287 for (std::size_t j = 0; j < A.cols(); ++j) s += A(i, j);
1288 return s;
1289}
1290
1291} // namespace rr_detail
1292
1293/**
1294 * Ramaswami reduction for M/G/1-type Markov chains [Bini, Meini, Ramaswami]. Port of
1295 * `MG1_RR.m`.
1296 *
1297 * The chain is reduced to a QBD whose level is N blocks wide, and cyclic reduction on that
1298 * QBD is run in the Sherman-Morrison-Woodbury form of the reference, carrying only the
1299 * rank-m corrections `uhat` and `vT`; G is then read off by the formula at the end of
1300 * Section 3 of the paper, G = (I - vT uhat)^{-1} A0.
1301 *
1302 * 'Direct' (the default) stores the N*m x N*m matrix B outright. 'DispStruct' stores it
1303 * through its displacement generators (`rr_detail::Disp`), compressed to rank 2m by a QR of
1304 * each side and an SVD of the small core at every step. 'DispStructFFT' is the reference's
1305 * FFT evaluation of the same block-Toeplitz products; this port evaluates those products
1306 * directly, so it returns the 'DispStruct' answer, which the FFT reproduces up to rounding.
1307 */
1308inline Matrix<double> mg1_rr(const Blocks& D, const Mg1RrOptions& opts = Mg1RrOptions()) {
1309 using namespace mg1x_detail;
1310 if (D.size() < 2) throw InputError("MG1_RR: the sequence needs at least two blocks");
1311 if (opts.mode != "Direct" && opts.mode != "DispStruct" && opts.mode != "DispStructFFT")
1312 throw InputError("MG1_RR: Mode '" + opts.mode +
1313 "' is not one of Direct, DispStruct, DispStructFFT");
1314
1315 bool eg_found = false;
1316 const Matrix<double> Geg = mg1_eg(D, eg_found);
1317 if (eg_found) return Geg;
1318
1319 const std::size_t m = D[0].rows();
1320 const std::size_t N = D.size() - 1;
1321 const Matrix<double> I = eye<double>(m);
1322 Matrix<double> D0 = D[0];
1323 Matrix<double> vT(m, N * m, 0.0);
1324 for (std::size_t k = 1; k <= N; ++k) put(vT, 0, (k - 1) * m, D[k]);
1325 const Matrix<double> vT_orig = vT;
1326 Matrix<double> uhat(N * m, m, 0.0);
1327 put(uhat, 0, 0, I);
1328
1329 // [0; uhat(m+1:end,:)]
1330 auto ZZTu = [&](const Matrix<double>& u) {
1331 Matrix<double> z = u;
1332 for (std::size_t i = 0; i < m; ++i)
1333 for (std::size_t j = 0; j < m; ++j) z(i, j) = 0.0;
1334 return z;
1335 };
1336
1337 if (opts.mode == "Direct") {
1338 Matrix<double> B(N * m, N * m, 0.0);
1339 for (std::size_t i = 0; i + m < N * m; ++i) B(m + i, i) = 1.0;
1340 double check = std::min(inf_norm(B), inf_norm(D0));
1341 std::size_t numit = 0;
1342 while (check > 1e-15 && numit < opts.max_num_it) {
1343 ++numit;
1344 const Matrix<double> S = inverse(msub(I, matmul(vT, uhat)));
1345 const Matrix<double> T = matmul(S, matmul(vT, ZZTu(uhat)));
1346 const Matrix<double> mid = madd(madd(matmul(S, sub(vT, 0, m, 0, m)), T), I);
1347 const Matrix<double> newD0 = matmul(matmul(D0, mid), D0);
1348 Matrix<double> newB = matmul(uhat, matmul(S, matmul(vT, B)));
1349 newB = matmul(B, madd(B, newB));
1350 const Matrix<double> newuhat = madd(uhat, matmul(B, matmul(matmul(uhat, mid), D0)));
1351 const Matrix<double> newvT = madd(vT, matmul(matmul(matmul(D0, S), vT), B));
1352 B = newB;
1353 vT = newvT;
1354 uhat = newuhat;
1355 D0 = newD0;
1356 check = std::min(inf_norm(B), inf_norm(D0));
1357 }
1358 } else {
1359 if (N < 2)
1360 throw InputError("MG1_RR: Mode '" + opts.mode +
1361 "' needs at least three blocks A0, A1, A2; use Mode 'Direct'");
1362 rr_detail::Disp B{N, m, Matrix<double>(N * m, m, 0.0), Matrix<double>(N * m, m, 0.0),
1363 Matrix<double>(N * m, m, 0.0), Matrix<double>(N * m, m, 0.0),
1364 Matrix<double>(N * m, m, 0.0)};
1365 if (N >= 2) put(B.b, m, 0, I);
1366 bool first = true;
1367 double check = std::min(rr_detail::sum_all(B.b), inf_norm(D0));
1368 std::size_t numit = 0;
1369 while ((check > 1e-15 || first) && numit < opts.max_num_it) {
1370 ++numit;
1371 first = false;
1372 // step 1
1373 const Matrix<double> S = inverse(msub(I, matmul(vT, uhat)));
1374 const Matrix<double> T = matmul(S, matmul(vT, ZZTu(uhat)));
1375 const Matrix<double> mid = madd(madd(matmul(S, sub(vT, 0, m, 0, m)), T), I);
1376 // step 2
1377 const Matrix<double> newD0 = matmul(matmul(D0, mid), D0);
1378 // step 3
1379 const Matrix<double> newuhat = madd(uhat, B.right(matmul(matmul(uhat, mid), D0)));
1380 const Matrix<double> newvT = madd(vT, B.left(matmul(matmul(D0, S), vT)));
1381 // [S T; S I+T]
1382 Matrix<double> ST(2 * m, 2 * m, 0.0);
1383 put(ST, 0, 0, S);
1384 put(ST, 0, m, T);
1385 put(ST, m, 0, S);
1386 put(ST, m, m, madd(I, T));
1387 // (I - A)^{-1} X through its structure, for X with N*m rows
1388 auto inv_struct = [&](const Matrix<double>& X) {
1389 const std::size_t w = X.cols();
1390 const Matrix<double> t = matmul(ST, vjoin(matmul(vT, X), sub(X, 0, m, 0, w)));
1391 Matrix<double> out(N * m, w, 0.0);
1392 put(out, 0, 0, madd(sub(X, 0, m, 0, w), sub(t, 0, m, 0, w)));
1393 if (N > 1)
1394 put(out, m, 0,
1395 madd(sub(X, m, (N - 1) * m, 0, w),
1396 matmul(sub(uhat, m, (N - 1) * m, 0, m), sub(t, m, m, 0, w))));
1397 return out;
1398 };
1399 // step 4
1400 const Matrix<double> newb = B.right(inv_struct(B.b));
1401
1402 // step 5, part 1: H = [e1pZu, e2pZ2u*S, e2pZ2u*T + Z2u]
1403 Matrix<double> e1pZu(N * m, m, 0.0), Z2u(N * m, m, 0.0), e2pZ2u(N * m, m, 0.0);
1404 put(e1pZu, 0, 0, I);
1405 if (N > 1) put(e1pZu, m, 0, sub(uhat, m, (N - 1) * m, 0, m));
1406 if (N > 1) put(e2pZ2u, m, 0, I);
1407 if (N > 2) {
1408 put(Z2u, 2 * m, 0, sub(uhat, m, (N - 2) * m, 0, m));
1409 put(e2pZ2u, 2 * m, 0, sub(uhat, m, (N - 2) * m, 0, m));
1410 }
1411 const Matrix<double> H =
1412 hjoin(hjoin(e1pZu, matmul(e2pZ2u, S)), madd(matmul(e2pZ2u, T), Z2u));
1413 // KT = [S*[vT(:,m+1:end) 0]; -vT; [-I 0]]
1414 Matrix<double> vshift(m, N * m, 0.0);
1415 if (N > 1) put(vshift, 0, 0, sub(vT, 0, m, m, (N - 1) * m));
1416 Matrix<double> negI(m, N * m, 0.0);
1417 for (std::size_t i = 0; i < m; ++i) negI(i, i) = -1.0;
1418 const Matrix<double> KT =
1419 vjoin(vjoin(matmul(S, vshift), mscale(vT, -1.0)), negI);
1420 // W = [c1 c2 B*H B*inv_struct([c1 c2])]
1421 const Matrix<double> C12 = hjoin(B.c1, B.c2);
1422 const Matrix<double> W =
1423 hjoin(hjoin(C12, B.right(H)), B.right(inv_struct(C12)));
1424 // step 6, part 1
1425 Matrix<double> Q1, R1;
1426 qr_econ(W, Q1, R1);
1427 // YT = [ (row block through Zu) * B ; KT * B ; [r1 r2]' ]
1428 const Matrix<double> R12t = trans(hjoin(B.r1, B.r2)); // 2m x N*m
1429 const Matrix<double> Zu = ZZTu(uhat);
1430 const Matrix<double> tq =
1431 matmul(hjoin(sub(R12t, 0, 2 * m, 0, m), matmul(R12t, Zu)), ST); // 2m x 2m
1432 Matrix<double> temp2 = matmul(sub(tq, 0, 2 * m, 0, m), vT);
1433 put(temp2, 0, 0, madd(sub(temp2, 0, 2 * m, 0, m), sub(tq, 0, 2 * m, m, m)));
1434 const Matrix<double> YT =
1435 vjoin(vjoin(B.left(madd(temp2, R12t)), B.left(KT)), R12t); // 7m x N*m
1436 // step 6, part 2
1437 Matrix<double> Q2, R2;
1438 qr_econ(trans(YT), Q2, R2);
1439 // step 7
1440 const SvdFactors sv = svd_full(matmul(R1, trans(R2)));
1441 // step 8
1442 Matrix<double> US(sv.U.rows(), 2 * m, 0.0);
1443 for (std::size_t i = 0; i < sv.U.rows(); ++i)
1444 for (std::size_t j = 0; j < 2 * m; ++j) US(i, j) = sv.U(i, j) * sv.s[j];
1445 const Matrix<double> cc = matmul(Q1, US);
1446 Matrix<double> V2(sv.Vt.cols(), 2 * m, 0.0);
1447 for (std::size_t i = 0; i < sv.Vt.cols(); ++i)
1448 for (std::size_t j = 0; j < 2 * m; ++j) V2(i, j) = sv.Vt(j, i);
1449 const Matrix<double> rr = matmul(Q2, V2);
1450 B.c1 = sub(cc, 0, N * m, 0, m);
1451 B.c2 = sub(cc, 0, N * m, m, m);
1452 B.r1 = sub(rr, 0, N * m, 0, m);
1453 B.r2 = sub(rr, 0, N * m, m, m);
1454
1455 B.b = newb;
1456 vT = newvT;
1457 uhat = newuhat;
1458 D0 = newD0;
1459 check = std::min(rr_detail::sum_all(B.b), inf_norm(D0));
1460 }
1461 }
1462
1463 // G = (I - [A1 ... AN] uhat)^{-1} A0
1464 return matmul(inverse(msub(I, matmul(vT_orig, uhat))), D[0]);
1465}
1466
1467// ---------------------------------------------------------------------------
1468// MG1_IS.m
1469// ---------------------------------------------------------------------------
1470
1471/** Options of `MG1_IS`, with the reference's defaults. */
1473 std::string mode = "MSignBalzer"; ///< 'MSignStandard', 'MSignBalzer' or 'Schur'
1474 std::size_t max_num_it = 50;
1475};
1476
1477/**
1478 * Invariant subspace method for M/G/1-type Markov chains [Akar, Sohraby]. Port of `MG1_IS.m`.
1479 *
1480 * F(z) = zI - A(z) is mapped by the Moebius transform z = (1+s)/(1-s) onto a matrix
1481 * polynomial H(s) whose companion matrix, after a rank-one correction that moves the unit
1482 * root off the imaginary axis, has exactly m eigenvalues in the open left half plane. A basis
1483 * T of that invariant subspace gives G = (T1 + T2)(T1 - T2)^{-1}, invariant to the choice of
1484 * basis. The matrix-sign modes find the subspace as the range of sign(Z) - I; 'Schur' orders
1485 * a real Schur form with the left half plane first, as the reference's ordschur 'lhp' does.
1486 */
1487inline Matrix<double> mg1_is(const Blocks& D, const Mg1IsOptions& opts = Mg1IsOptions()) {
1488 using namespace mg1x_detail;
1489 if (opts.mode != "MSignStandard" && opts.mode != "MSignBalzer" && opts.mode != "Schur")
1490 throw InputError("MG1_IS: Mode '" + opts.mode +
1491 "' is not one of MSignStandard, MSignBalzer, Schur");
1492 if (D.size() < 3)
1493 throw InputError("MG1_IS: the sequence needs at least three blocks A0, A1, A2; the "
1494 "reference reads rows m+1:2m of an m*max-row subspace basis");
1495
1496 bool eg_found = false;
1497 const Matrix<double> Geg = mg1_eg(D, eg_found);
1498 if (eg_found) return Geg;
1499
1500 const double epsilon = 1e-14;
1501 const std::size_t m = D[0].rows();
1502 const std::size_t f = D.size() - 1;
1503 const double drift = mg1_drift(D).value - 1.0;
1504 const double sgn = drift > 0.0 ? 1.0 : (drift < 0.0 ? -1.0 : 0.0);
1505
1506 // Step 1, F(z) = z - A(z)
1507 Blocks F(f + 1);
1508 for (std::size_t i = 0; i <= f; ++i) F[i] = mscale(D[i], -1.0);
1509 for (std::size_t i = 0; i < m; ++i) F[1](i, i) += 1.0;
1510
1511 // Step 2, H(s) = sum_i F_i (1-s)^(f-i) (1+s)^i = sum_j H_j s^j
1512 Blocks H(f + 1, Matrix<double>(m, m, 0.0));
1513 for (std::size_t i = 0; i <= f; ++i) {
1514 std::vector<double> c{1.0};
1515 auto conv = [](const std::vector<double>& a, double s1) { // a * [1 s1]
1516 std::vector<double> r(a.size() + 1, 0.0);
1517 for (std::size_t k = 0; k < a.size(); ++k) {
1518 r[k] += a[k];
1519 r[k + 1] += s1 * a[k];
1520 }
1521 return r;
1522 };
1523 for (std::size_t j = 0; j < f - i; ++j) c = conv(c, -1.0);
1524 for (std::size_t j = 0; j < i; ++j) c = conv(c, 1.0);
1525 for (std::size_t j = 0; j <= f; ++j) H[j] = madd(H[j], mscale(F[i], c[j]));
1526 }
1527
1528 // Step 3, hatH_i = H_f^{-1} H_i
1529 const Matrix<double> Hfinv = inverse(H[f]);
1530 Blocks hatH(f);
1531 for (std::size_t i = 0; i < f; ++i) hatH[i] = matmul(Hfinv, H[i]);
1532
1533 // Step 4, y and xT; x0T = [0 1] / [hatH0 e], a least-squares solve as in MATLAB
1534 const std::size_t mf = m * f;
1535 std::vector<double> y(mf, 0.0);
1536 for (std::size_t i = 0; i < m; ++i) y[i] = 1.0;
1537 Matrix<double> Mt(m + 1, m, 0.0); // [hatH0 e]'
1538 for (std::size_t i = 0; i < m; ++i) {
1539 for (std::size_t j = 0; j < m; ++j) Mt(j, i) = hatH[0](i, j);
1540 Mt(m, i) = 1.0;
1541 }
1542 std::vector<double> rhs(m + 1, 0.0);
1543 rhs[m] = 1.0;
1544 const std::vector<double> x0 = lstsq(Mt, rhs).x;
1545 std::vector<double> xT(mf, 0.0);
1546 for (std::size_t i = 1; i < f; ++i) {
1547 const std::vector<double> xi = vecmul(x0, hatH[i]);
1548 for (std::size_t j = 0; j < m; ++j) xT[(i - 1) * m + j] = xi[j];
1549 }
1550 for (std::size_t j = 0; j < m; ++j) xT[(f - 1) * m + j] = x0[j];
1551
1552 // Step 5, the companion matrix plus the rank-one correction
1553 Matrix<double> Z(mf, mf, 0.0);
1554 for (std::size_t i = 1; i < f; ++i) put(Z, (i - 1) * m, i * m, eye<double>(m));
1555 for (std::size_t i = 0; i < f; ++i) put(Z, (f - 1) * m, i * m, mscale(hatH[i], -1.0));
1556 double xy = 0.0;
1557 for (std::size_t k = 0; k < mf; ++k) xy += xT[k] * y[k];
1558 for (std::size_t a = 0; a < mf; ++a)
1559 for (std::size_t b = 0; b < mf; ++b) Z(a, b) += sgn * (y[a] / xy) * xT[b];
1560
1562 if (opts.mode != "Schur") {
1563 // Step 6, matrix sign function, optionally with Balzer's determinantal scaling
1564 Matrix<double> Zold = Z;
1565 Matrix<double> Znew = mscale(madd(Zold, inverse(Zold)), 0.5);
1566 std::size_t numit = 0;
1567 double check = 1.0;
1568 while (check > epsilon && numit < opts.max_num_it) {
1569 ++numit;
1570 Zold = Znew;
1571 const double determ =
1572 opts.mode == "MSignStandard"
1573 ? 0.5
1574 : 1.0 / (1.0 + std::pow(std::abs(lu_det(Zold)), 1.0 / static_cast<double>(mf)));
1575 Znew = madd(mscale(Zold, determ), mscale(inverse(Zold), 1.0 - determ));
1576 check = max_col_sum(msub(Znew, Zold)) / max_col_sum(Zold);
1577 }
1578 // Step 7, T = orth(Znew - I)
1579 const Matrix<double> K = msub(Znew, eye<double>(mf));
1580 const SvdFactors sv = svd_full(K);
1581 // MATLAB orth: rank tolerance max(size) * eps(max(s))
1582 const double tol = static_cast<double>(mf) *
1583 (std::nextafter(sv.s[0], std::numeric_limits<double>::infinity()) - sv.s[0]);
1584 std::size_t r = 0;
1585 for (double s : sv.s)
1586 if (s > tol) ++r;
1587 if (r != m)
1588 throw NumericError("MG1_IS: the sign iteration left an invariant subspace of dimension " +
1589 std::to_string(r) + ", not m = " + std::to_string(m));
1590 T = sub(sv.U, 0, mf, 0, m);
1591 } else {
1592 const RealSchur sf = schur_decomposition(Z);
1593 std::vector<double> key(mf, 0.0);
1594 for (std::size_t k = 0; k < mf; ++k) key[k] = sf.T(k, k) < 0.0 ? 1.0 : 0.0;
1595 const RealSchur ord = schur_reorder(sf, key);
1596 T = sub(ord.Z, 0, mf, 0, m);
1597 }
1598
1599 // Step 8
1600 const Matrix<double> T1 = sub(T, 0, m, 0, m), T2 = sub(T, m, m, 0, m);
1601 return matmul(madd(T1, T2), inverse(msub(T1, T2)));
1602}
1603
1604// ---------------------------------------------------------------------------
1605// GIM1_R.m
1606// ---------------------------------------------------------------------------
1607
1608/**
1609 * R of a GI/M/1-type Markov chain, through the G of its dual. Port of
1610 * `GIM1_R.m` for Dual 'A', 'R' and 'B' and every Algor: 'FI', 'CR', 'NI', 'RR', 'IS'.
1611 *
1612 * THE DUAL IS THE WHOLE IDEA. There is no cyclic reduction for R directly, so
1613 * the chain is transposed into an M/G/1-type one whose G carries the same
1614 * information: the Ramaswami dual `diag(theta)^-1 A_i' diag(theta)` for a
1615 * transient chain, and the Bright dual, which additionally rescales block i by
1616 * eta^(i-1) with eta the caudal characteristic, for a positive recurrent one.
1617 * 'A' picks between them by the drift, which is what makes it the fastest
1618 * default. R is then read back off G by the inverse similarity, times eta in
1619 * the Bright case.
1620 */
1621inline Matrix<double> gim1_r(const Blocks& Ain, const std::string& dual,
1622 const std::string& algor) {
1623 const std::size_t m = Ain[0].rows();
1624 const std::size_t dega = Ain.size() - 1;
1625 Blocks A = Ain;
1626
1627 // drift > 1: positive recurrent GI/M/1; drift < 1: transient.
1628 const Drift d = mg1_drift(A);
1629 std::vector<double> theta = d.theta;
1630 const bool ram = (dual == "R") || (dual == "A" && d.value <= 1.0);
1631 double eta = 1.0;
1632
1633 if (ram) {
1634 for (std::size_t b = 0; b <= dega; ++b) {
1635 Matrix<double> Bb(m, m, 0.0);
1636 for (std::size_t i = 0; i < m; ++i)
1637 for (std::size_t j = 0; j < m; ++j) Bb(i, j) = A[b](j, i) * theta[j] / theta[i];
1638 A[b] = Bb;
1639 }
1640 } else if (dual == "B" || dual == "A") {
1641 eta = (d.value > 1.0) ? gim1_caudal(Ain) : mg1_decay(Ain);
1642 // theta of A0 + A1 eta + ... + Amax eta^max, shifted to be stochastic.
1643 Matrix<double> sumAeta = mscale(Ain[dega], std::pow(eta, static_cast<double>(dega)));
1644 for (std::size_t i = dega; i-- > 0;)
1645 sumAeta = madd(sumAeta, mscale(Ain[i], std::pow(eta, static_cast<double>(i))));
1646 Matrix<double> shifted = sumAeta;
1647 for (std::size_t i = 0; i < m; ++i) shifted(i, i) += (1.0 - eta);
1648 theta = stat(shifted);
1649 for (std::size_t b = 0; b <= dega; ++b) {
1650 Matrix<double> Bb(m, m, 0.0);
1651 const double s = std::pow(eta, static_cast<double>(b) - 1.0);
1652 for (std::size_t i = 0; i < m; ++i)
1653 for (std::size_t j = 0; j < m; ++j)
1654 Bb(i, j) = s * A[b](j, i) * theta[j] / theta[i];
1655 A[b] = Bb;
1656 }
1657 } else {
1658 throw InputError("GIM1_R: Dual '" + dual + "' is not one of 'A', 'B', 'R'");
1659 }
1660
1662 if (algor == "FI") {
1663 G = mg1_fi(A);
1664 } else if (algor == "CR") {
1665 G = mg1_cr(A);
1666 } else if (algor == "NI") {
1667 G = mg1_ni(A);
1668 } else if (algor == "RR") {
1669 G = mg1_rr(A);
1670 } else if (algor == "IS") {
1671 G = mg1_is(A);
1672 } else {
1673 throw InputError("GIM1_R: Algorithm '" + algor + "' is not supported");
1674 }
1675
1676 Matrix<double> R(m, m, 0.0);
1677 for (std::size_t i = 0; i < m; ++i)
1678 for (std::size_t j = 0; j < m; ++j) R(i, j) = G(j, i) * theta[j] / theta[i];
1679 if (!ram)
1680 for (std::size_t i = 0; i < m; ++i)
1681 for (std::size_t j = 0; j < m; ++j) R(i, j) *= eta;
1682 return R;
1683}
1684
1685} // namespace smc
1686} // namespace line
1687
1688#endif // LINE_LIB_SMC_MG1_H
InputError(const std::string &what)
Definition error.h:39
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
Matrix transpose() const
Definition matrix.h:110
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
std::complex<double> as a number type for the generic linear algebra.
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Discrete Fourier transform of arbitrary length, in double complex.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Least squares for a rectangular system, exact-capable.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
Matrix< double > trans(const Matrix< double > &A)
Definition mg1.h:932
void put(Matrix< double > &A, std::size_t r0, std::size_t c0, const Matrix< double > &S)
A(r0:, c0:) = S.
Definition mg1.h:927
Matrix< double > sub(const Matrix< double > &A, std::size_t r0, std::size_t nr, std::size_t c0, std::size_t nc)
A(r0:r0+nr, c0:c0+nc), half-open, 0-based.
Definition mg1.h:918
std::vector< double > lsolve(const Matrix< double > &Z, const std::vector< double > &b)
Z \ b for a square Z.
Definition mg1.h:958
void qr_econ(const Matrix< double > &A, Matrix< double > &Q, Matrix< double > &R)
Economy QR, A = Q R with Q (r x k) orthonormal columns and R (k x c), k = min(r, c),...
Definition mg1.h:967
Matrix< double > hjoin(const Matrix< double > &A, const Matrix< double > &B)
[A B], horizontally.
Definition mg1.h:940
Matrix< double > vjoin(const Matrix< double > &A, const Matrix< double > &B)
[A; B], vertically.
Definition mg1.h:949
double sum_all(const Matrix< double > &A)
Definition mg1.h:1284
double max_col_sum(const Matrix< double > &A)
max(sum(A)), the largest column sum WITHOUT absolute values, as in MATLAB.
Definition mg1.h:151
Blocks vblocks_of(const Matrix< double > &A, std::size_t r)
Splits a vertical stack into its blocks of r rows.
Definition mg1.h:196
Matrix< double > hcat(const Blocks &blk)
Re-assembles a block sequence into the wide [A0 A1 ... Amax].
Definition mg1.h:174
ShiftResult mg1_shifts(const Blocks &Ain, const std::string &shift_type)
Shift technique for the M/G/1-type sequence.
Definition mg1.h:404
Blocks blocks_of(const Matrix< double > &A, std::size_t m)
Splits the wide [A0 A1 ... Amax] into its m x m blocks.
Definition mg1.h:162
double gim1_caudal(const Blocks &A, std::vector< double > *v=nullptr)
Caudal characteristic of a GI/M/1-type chain: the spectral radius of R, the unique z in (0,...
Definition mg1.h:363
Matrix< double > poly_at(const Blocks &A, double z)
A(z) = A0 + A1 z + ... + Amax z^max, by Horner as the reference writes it.
Definition mg1.h:297
Matrix< T > msub(const Matrix< T > &A, const Matrix< T > &B)
A - B.
Definition mg1.h:98
double inf_norm(const Matrix< double > &A)
norm(A,inf), the largest absolute row sum.
Definition mg1.h:124
Matrix< double > mg1_eg(const Blocks &Ain, bool &found)
G in closed form when A0 has rank one.
Definition mg1.h:517
Matrix< double > mg1_is(const Blocks &D, const Mg1IsOptions &opts=Mg1IsOptions())
Invariant subspace method for M/G/1-type Markov chains [Akar, Sohraby].
Definition mg1.h:1487
double mg1_decay(const Blocks &A, std::vector< double > *uT=nullptr)
Decay rate of a recurrent M/G/1-type chain: the unique z > 1 with PF(A(z)) = z.
Definition mg1.h:333
Matrix< double > gim1_r(const Blocks &Ain, const std::string &dual, const std::string &algor)
R of a GI/M/1-type Markov chain, through the G of its dual.
Definition mg1.h:1621
std::vector< double > stat(const Matrix< double > &A)
Stationary distribution of a stochastic matrix: the left eigenvector for eigenvalue 1,...
Definition mg1.h:221
Matrix< double > sylv_powers_direct(const Matrix< double > &A, const Blocks &B, const Matrix< double > &C)
Solve sum_{j=1}^N B_j Y A^{j-1} = C directly, through the Kronecker form of vec(Y).
Definition mg1.h:1024
Matrix< double > mg1_cr(const Blocks &Ain, const Mg1CrOptions &opts=Mg1CrOptions())
Cyclic reduction for M/G/1-type Markov chains [Bini, Meini].
Definition mg1.h:672
Matrix< double > mg1_fi(const Blocks &Ain, const Mg1FiOptions &opts=Mg1FiOptions())
Functional iterations for M/G/1-type Markov chains [Neuts].
Definition mg1.h:863
std::vector< double > rowsums(const Matrix< double > &A)
sum(A,2), the row sums, as a column held in a vector.
Definition mg1.h:116
Drift mg1_drift(const Blocks &A)
drift = theta * beta with beta = (Amax)e + (Amax+Amax-1)e + ..., the expected level increment per tra...
Definition mg1.h:258
std::complex< double > max_eig(const Matrix< double > &M)
max(eig(M)) with MATLAB's semantics on a complex spectrum: the element of largest modulus,...
Definition mg1.h:285
double dot(const std::vector< double > &a, const std::vector< double > &b)
The inner product of a row vector with a column held as a vector.
Definition mg1.h:240
std::vector< Matrix< double > > Blocks
Definition mg1.h:79
Matrix< double > mg1_rr(const Blocks &D, const Mg1RrOptions &opts=Mg1RrOptions())
Ramaswami reduction for M/G/1-type Markov chains [Bini, Meini, Ramaswami].
Definition mg1.h:1308
Matrix< T > madd(const Matrix< T > &A, const Matrix< T > &B)
A + B.
Definition mg1.h:87
void mg1_unshift(Matrix< double > &G, const ShiftResult &sh, const std::string &shift_type)
Put back on G the rank-one term a shift removed: ones/m for a 'one' shift at drift < 1,...
Definition mg1.h:492
std::vector< double > rowvec_times(const std::vector< double > &v, const Matrix< double > &A)
theta A, the row vector times matrix product used throughout.
Definition mg1.h:235
std::vector< double > pf_vector(const Matrix< double > &M, bool left)
The Perron-Frobenius eigenvector of M, right (left == false) or left, scaled to unit sum: the null ve...
Definition mg1.h:309
Matrix< double > sylv_powers_real_schur(const Matrix< double > &A, const Blocks &B, const Matrix< double > &C)
Solve sum_{j=1}^N B_j Y A^{j-1} = C by a real Schur form A = U T U': with Y = X U' the system becomes...
Definition mg1.h:1057
double max_abs_diff(const Matrix< double > &A, const Matrix< double > &B)
max(max(abs(A-B))).
Definition mg1.h:142
Matrix< double > mscale(const Matrix< double > &A, double c)
c * A.
Definition mg1.h:108
Matrix< double > vcat(const Blocks &blk)
Stacks a block sequence vertically, [A0; A1; ...; Amax].
Definition mg1.h:185
Matrix< double > mg1_ni(const Blocks &Ain, const Mg1NiOptions &opts=Mg1NiOptions())
Newton iteration for M/G/1-type Markov chains.
Definition mg1.h:1146
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
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
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
std::vector< T > mulvec(const Matrix< T > &A, const std::vector< T > &v)
Matrix times column vector, A v.
Definition linalg.h:62
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
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
T lu_det(const Matrix< T > &A)
Determinant of a square matrix, by the same partial-pivoting elimination.
Definition lu.h:122
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
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
RealSchur schur_decomposition(const Matrix< double > &A)
Real Schur factorization of a general square matrix (LAPACK dgees, unsorted).
Definition eig.h:182
void dft(std::vector< std::complex< double > > &a, bool inverse)
In-place DFT of a.
Definition fft.h:120
SvdFactors svd_full(const Matrix< double > &A)
Full SVD of a real matrix, singular values in descending order.
Definition svd.h:48
Number-type abstraction for the templated API port.
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
A = U diag(s) Vt, with U (m x m), s of length min(m,n) and Vt (n x n).
Definition svd.h:41
Matrix< double > Vt
Definition svd.h:44
Matrix< double > U
Definition svd.h:42
std::vector< double > s
Definition svd.h:43
The drift of an M/G/1-type sequence, and the invariant vector it uses.
Definition mg1.h:248
double value
Definition mg1.h:249
std::vector< double > theta
stat(A0 + A1 + ... + Amax)
Definition mg1.h:250
Options of MG1_CR, with the reference's defaults.
Definition mg1.h:567
std::string shift_type
Definition mg1.h:569
std::string mode
'ShiftPWCR' or 'PWCR'
Definition mg1.h:568
std::size_t max_num_root
Definition mg1.h:571
std::size_t max_num_it
Definition mg1.h:570
Options of MG1_FI, with the reference's defaults.
Definition mg1.h:846
std::string shift_type
Definition mg1.h:848
std::string mode
'Natural', 'Traditional', 'U-Based', or 'Shift<Mode>'
Definition mg1.h:847
std::size_t max_num_it
Definition mg1.h:849
Options of MG1_IS, with the reference's defaults.
Definition mg1.h:1472
std::size_t max_num_it
Definition mg1.h:1474
std::string mode
'MSignStandard', 'MSignBalzer' or 'Schur'
Definition mg1.h:1473
Options of MG1_NI, with the reference's defaults.
Definition mg1.h:1126
std::size_t max_num_it
Definition mg1.h:1129
std::string shift_type
Definition mg1.h:1130
std::string mode
'DirectSum', 'RealSchur' or 'ComplexSchur', each optionally with the 'Shift' suffix
Definition mg1.h:1128
Options of MG1_RR, with the reference's defaults.
Definition mg1.h:1195
std::size_t max_num_it
Definition mg1.h:1197
std::string mode
'Direct', 'DispStruct' or 'DispStructFFT'
Definition mg1.h:1196
What MG1_Shifts returns: the shifted sequence and the drift it measured.
Definition mg1.h:385
std::vector< double > v
Definition mg1.h:389
The displacement representation B = L(b) + L(c1) L(Z r1)' + L(c2) L(Z r2)' of the Ramaswami-reduction...
Definition mg1.h:1208
Matrix< double > blk(const Matrix< double > &x, std::size_t k) const
Block k (0-based) of a stacked N*m x m generator.
Definition mg1.h:1213
Matrix< double > r1
Definition mg1.h:1210
Matrix< double > left(const Matrix< double > &temp) const
temp * B, MG1_RR_tempB.m.
Definition mg1.h:1218
Matrix< double > r2
Definition mg1.h:1210
Matrix< double > b
Definition mg1.h:1210
Matrix< double > right(const Matrix< double > &temp) const
B * temp, MG1_RR_Btemp.m.
Definition mg1.h:1252
Matrix< double > c1
Definition mg1.h:1210
Matrix< double > c2
Definition mg1.h:1210
Singular value decomposition WITH the singular vectors, and the Moore-Penrose pseudo-inverse built fr...