LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sylvester.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_SYLVESTER_H
6#define LINE_UTIL_SYLVESTER_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * The Sylvester equation A X + X B = C, and MATLAB's `lyap(A,B,C)`.
12 *
13 * Solved in the Kronecker form (I_m kron A + B^T kron I_n) vec(X) = vec(C) with
14 * `vec` column-major, so the whole thing is one LU solve of order n*m in `T`.
15 * That is the ONLY formulation available at every arithmetic the port offers:
16 * Bartels-Stewart needs a real Schur factorization, `util/eig.h` is
17 * deliberately double-only (eigenvalues of a rational matrix are algebraic, not
18 * rational), so a Schur-based solver would silently pin every caller to double.
19 * The cost is O((n m)^3) against Bartels-Stewart's O(n^3 + m^3); the callers in
20 * this port pass phase-space matrices whose order is the product of an arrival
21 * MMAP order and a total service-PH order, both small, and the whole point of
22 * the header is that `Rational` and `Real<D>` get an answer at all.
23 *
24 * SO A DOUBLE-ONLY CALLER TAKES `sylvester_schur`/`lyap_schur` BELOW, not these.
25 * The Kronecker arm exists for the arithmetics Schur cannot reach, and the two
26 * linear-noise covariances -- `fluid_lyapunov` and `cache_rmf_lna` -- are double
27 * by construction, since each decides stability with `eig_values` first. Both
28 * once took this arm, and both grew a d^2-by-d^2 LU out of a d-by-d covariance:
29 * an Erlang(64) service made the fluid one 4096-square and it stopped finishing.
30 *
31 * `mfq_multiregime.h` carries the identical Kronecker construction for
32 * `Matrix<double>`; it predates this header and stays where it is because it is
33 * used only from double-only code and moving it would churn two large files
34 * that no test covers at another arithmetic.
35 */
36
37#include <cstddef>
38#include <vector>
39
40#include "line/num/number.h"
41#include "line/util/eig.h"
42#include "line/util/error.h"
43#include "line/util/linalg.h"
44#include "line/util/lu.h"
45#include "line/util/matrix.h"
46
47#ifdef LINE_MP_HAVE_LAPACK
48extern "C" {
49/** Quasi-triangular Sylvester solve, the core of Bartels-Stewart. */
50void dtrsyl_(const char* trana, const char* tranb, const int* isgn, const int* m, const int* n,
51 const double* a, const int* lda, const double* b, const int* ldb, double* c,
52 const int* ldc, double* scale, int* info);
53}
54#endif
55
56namespace line {
57
58/**
59 * The Kronecker operator of a FIXED (A,B) pair, factorized once.
60 *
61 * The MMAP[K]/PH[K]/1 queue-length recursion solves a chain of Sylvester
62 * equations that share A and B and differ only in the right-hand side, one per
63 * queue-length level. Refactorizing per level would make the recursion cubic in
64 * the level count for no reason.
65 */
66template <class T>
68public:
69 SylvesterFactor(const Matrix<T>& A, const Matrix<T>& B) : n_(A.rows()), m_(B.rows()) {
70 const T zero = num_traits<T>::from_int(0);
71 if (A.cols() != n_ || B.cols() != m_)
72 throw InputError("SylvesterFactor: A and B must be square");
73 if (n_ == 0 || m_ == 0) return;
74 LU_ = Matrix<T>(n_ * m_, n_ * m_, zero);
75 for (std::size_t j = 0; j < m_; ++j)
76 for (std::size_t i = 0; i < n_; ++i) {
77 const std::size_t row = j * n_ + i;
78 for (std::size_t k = 0; k < n_; ++k) LU_(row, j * n_ + k) += A(i, k);
79 for (std::size_t k = 0; k < m_; ++k) LU_(row, k * n_ + i) += B(k, j);
80 }
81 piv_ = lu_factor(LU_);
82 }
83
84 /** Solve A X + X B = C. */
86 const T zero = num_traits<T>::from_int(0);
87 Matrix<T> X(n_, m_, zero);
88 if (n_ == 0 || m_ == 0) return X;
89 if (C.rows() != n_ || C.cols() != m_)
90 throw InputError("SylvesterFactor: the right-hand side is not conformable");
91 std::vector<T> rhs(n_ * m_, zero);
92 for (std::size_t j = 0; j < m_; ++j)
93 for (std::size_t i = 0; i < n_; ++i) rhs[j * n_ + i] = C(i, j);
94 lu_solve(LU_, piv_, rhs);
95 for (std::size_t j = 0; j < m_; ++j)
96 for (std::size_t i = 0; i < n_; ++i) X(i, j) = rhs[j * n_ + i];
97 return X;
98 }
99
100 /** Solve A X + X B + C = 0, MATLAB's lyap(A,B,C). */
102 Matrix<T> negC(C.rows(), C.cols());
103 for (std::size_t i = 0; i < C.rows(); ++i)
104 for (std::size_t j = 0; j < C.cols(); ++j) negC(i, j) = -C(i, j);
105 return solve_sylvester(negC);
106 }
107
108private:
109 std::size_t n_, m_;
110 Matrix<T> LU_;
111 std::vector<std::size_t> piv_;
112};
113
114/**
115 * Solve A X + X B = C for X.
116 *
117 * @param A n x n
118 * @param B m x m
119 * @param C n x m
120 */
121template <class T>
123 if (C.rows() != A.rows() || C.cols() != B.rows())
124 throw InputError("sylvester_solve: the blocks are not conformable");
125 return SylvesterFactor<T>(A, B).solve_sylvester(C);
126}
127
128/** MATLAB `lyap(A,B,C)` solves A X + X B + C = 0, i.e. sylvester_solve(A, B, -C). */
129template <class T>
130Matrix<T> lyap_solve(const Matrix<T>& A, const Matrix<T>& B, const Matrix<T>& C) {
131 if (C.rows() != A.rows() || C.cols() != B.rows())
132 throw InputError("lyap_solve: the blocks are not conformable");
133 return SylvesterFactor<T>(A, B).solve_lyap(C);
134}
135
136/**
137 * A X + X B = C by Bartels-Stewart, at double.
138 *
139 * WHY THIS EXISTS BESIDE THE KRONECKER SOLVER ABOVE. The Kronecker form is the
140 * only one available in an arbitrary field, and its O((n m)^3) cost is
141 * acceptable for the phase-space matrices the MMAP queue recursion passes. The
142 * FJ_codes fork-join engine passes matrices of order (C + 1) * m^2 * ma with C
143 * defaulting to 100, i.e. n and m in the hundreds to low thousands: the
144 * Kronecker operator of an 800 x 800 pair is 640000 square, which is 3 TB of
145 * LU. Bartels-Stewart is O(n^3 + m^3) and factorizes the same problem in
146 * fractions of a second, at the price of a real Schur factorization -- LAPACK,
147 * hence double only, exactly as `util/eig.h` is.
148 *
149 * A X + X B = C is solved as Ta Y + Y Tb = Za' C Zb with A = Za Ta Za' and
150 * B = Zb Tb Zb' real Schur, the quasi-triangular core done by dtrsyl, and
151 * X = Za Y Zb'. dtrsyl's `scale` guards against overflow when the two spectra
152 * nearly collide; it is divided out here, and a scale of zero means the
153 * equation has no solution, which is reported by name rather than returned as
154 * an infinity.
155 */
157 const Matrix<double>& C) {
158#ifndef LINE_MP_HAVE_LAPACK
159 (void)A;
160 (void)B;
161 (void)C;
162 throw UnsupportedError(
163 "sylvester_schur requires LAPACK: reconfigure with -DLINE_MP_USE_LAPACK=ON and liblapack "
164 "available");
165#else
166 const std::size_t n = A.rows(), m = B.rows();
167 if (A.cols() != n || B.cols() != m)
168 throw InputError("sylvester_schur: A and B must be square");
169 if (C.rows() != n || C.cols() != m)
170 throw InputError("sylvester_schur: the right-hand side is not conformable");
171 Matrix<double> X(n, m, 0.0);
172 if (n == 0 || m == 0) return X;
173
174 const RealSchur sa = schur_decomposition(A);
175 const RealSchur sb = schur_decomposition(B);
176 // Ct = Za' C Zb, column-major for LAPACK.
177 const Matrix<double> Ct = matmul(matmul(sa.Z.transpose(), C), sb.Z);
178 std::vector<double> ta(n * n), tb(m * m), c(n * m);
179 for (std::size_t i = 0; i < n; ++i)
180 for (std::size_t j = 0; j < n; ++j) ta[j * n + i] = sa.T(i, j);
181 for (std::size_t i = 0; i < m; ++i)
182 for (std::size_t j = 0; j < m; ++j) tb[j * m + i] = sb.T(i, j);
183 for (std::size_t i = 0; i < n; ++i)
184 for (std::size_t j = 0; j < m; ++j) c[j * n + i] = Ct(i, j);
185
186 const int ni = static_cast<int>(n), mi = static_cast<int>(m), isgn = 1;
187 double scale = 0.0;
188 int info = 0;
189 dtrsyl_("N", "N", &isgn, &ni, &mi, ta.data(), &ni, tb.data(), &mi, c.data(), &ni, &scale,
190 &info);
191 if (info < 0) throw InputError("sylvester_schur: LAPACK dtrsyl rejected an argument");
192 if (info == 1)
193 throw NumericError(
194 "sylvester_schur: A and -B have common or very close eigenvalues, so the Sylvester "
195 "equation is singular or nearly so and its solution is not determined");
196 if (scale == 0.0)
197 throw NumericError("sylvester_schur: LAPACK dtrsyl returned a zero scale factor");
198
199 Matrix<double> Y(n, m, 0.0);
200 for (std::size_t i = 0; i < n; ++i)
201 for (std::size_t j = 0; j < m; ++j) Y(i, j) = c[j * n + i] / scale;
202 return matmul(matmul(sa.Z, Y), sb.Z.transpose());
203#endif
204}
205
206/** MATLAB `lyap(A,B,C)` at double via Bartels-Stewart: A X + X B + C = 0. */
208 const Matrix<double>& C) {
209 Matrix<double> negC(C.rows(), C.cols());
210 for (std::size_t i = 0; i < C.rows(); ++i)
211 for (std::size_t j = 0; j < C.cols(); ++j) negC(i, j) = -C(i, j);
212 return sylvester_schur(A, B, negC);
213}
214
215} // namespace line
216
217#endif // LINE_UTIL_SYLVESTER_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
Matrix transpose() const
Definition matrix.h:110
NumericError(const std::string &what)
Definition error.h:45
SylvesterFactor(const Matrix< T > &A, const Matrix< T > &B)
Definition sylvester.h:69
Matrix< T > solve_lyap(const Matrix< T > &C) const
Solve A X + X B + C = 0, MATLAB's lyap(A,B,C).
Definition sylvester.h:101
Matrix< T > solve_sylvester(const Matrix< T > &C) const
Solve A X + X B = C.
Definition sylvester.h:85
UnsupportedError(const std::string &what)
Definition error.h:51
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
Matrix< T > lyap_solve(const Matrix< T > &A, const Matrix< T > &B, const Matrix< T > &C)
MATLAB lyap(A,B,C) solves A X + X B + C = 0, i.e.
Definition sylvester.h:130
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
Matrix< double > sylvester_schur(const Matrix< double > &A, const Matrix< double > &B, const Matrix< double > &C)
A X + X B = C by Bartels-Stewart, at double.
Definition sylvester.h:156
Matrix< T > sylvester_solve(const Matrix< T > &A, const Matrix< T > &B, const Matrix< T > &C)
Solve A X + X B = C for X.
Definition sylvester.h:122
RealSchur schur_decomposition(const Matrix< double > &A)
Real Schur factorization of a general square matrix (LAPACK dgees, unsorted).
Definition eig.h:182
Matrix< double > lyap_schur(const Matrix< double > &A, const Matrix< double > &B, const Matrix< double > &C)
MATLAB lyap(A,B,C) at double via Bartels-Stewart: A X + X B + C = 0.
Definition sylvester.h:207
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