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 * `mfq_multiregime.h` carries the identical Kronecker construction for
25 * `Matrix<double>`; it predates this header and stays where it is because it is
26 * used only from double-only code and moving it would churn two large files
27 * that no test covers at another arithmetic.
28 */
29
30#include <cstddef>
31#include <vector>
32
33#include "line/num/number.h"
34#include "line/util/eig.h"
35#include "line/util/error.h"
36#include "line/util/linalg.h"
37#include "line/util/lu.h"
38#include "line/util/matrix.h"
39
40#ifdef LINE_MP_HAVE_LAPACK
41extern "C" {
42/** Quasi-triangular Sylvester solve, the core of Bartels-Stewart. */
43void dtrsyl_(const char* trana, const char* tranb, const int* isgn, const int* m, const int* n,
44 const double* a, const int* lda, const double* b, const int* ldb, double* c,
45 const int* ldc, double* scale, int* info);
46}
47#endif
48
49namespace line {
50
51/**
52 * The Kronecker operator of a FIXED (A,B) pair, factorized once.
53 *
54 * The MMAP[K]/PH[K]/1 queue-length recursion solves a chain of Sylvester
55 * equations that share A and B and differ only in the right-hand side, one per
56 * queue-length level. Refactorizing per level would make the recursion cubic in
57 * the level count for no reason.
58 */
59template <class T>
61public:
62 SylvesterFactor(const Matrix<T>& A, const Matrix<T>& B) : n_(A.rows()), m_(B.rows()) {
63 const T zero = num_traits<T>::from_int(0);
64 if (A.cols() != n_ || B.cols() != m_)
65 throw InputError("SylvesterFactor: A and B must be square");
66 if (n_ == 0 || m_ == 0) return;
67 LU_ = Matrix<T>(n_ * m_, n_ * m_, zero);
68 for (std::size_t j = 0; j < m_; ++j)
69 for (std::size_t i = 0; i < n_; ++i) {
70 const std::size_t row = j * n_ + i;
71 for (std::size_t k = 0; k < n_; ++k) LU_(row, j * n_ + k) += A(i, k);
72 for (std::size_t k = 0; k < m_; ++k) LU_(row, k * n_ + i) += B(k, j);
73 }
74 piv_ = lu_factor(LU_);
75 }
76
77 /** Solve A X + X B = C. */
79 const T zero = num_traits<T>::from_int(0);
80 Matrix<T> X(n_, m_, zero);
81 if (n_ == 0 || m_ == 0) return X;
82 if (C.rows() != n_ || C.cols() != m_)
83 throw InputError("SylvesterFactor: the right-hand side is not conformable");
84 std::vector<T> rhs(n_ * m_, zero);
85 for (std::size_t j = 0; j < m_; ++j)
86 for (std::size_t i = 0; i < n_; ++i) rhs[j * n_ + i] = C(i, j);
87 lu_solve(LU_, piv_, rhs);
88 for (std::size_t j = 0; j < m_; ++j)
89 for (std::size_t i = 0; i < n_; ++i) X(i, j) = rhs[j * n_ + i];
90 return X;
91 }
92
93 /** Solve A X + X B + C = 0, MATLAB's lyap(A,B,C). */
94 Matrix<T> solve_lyap(const Matrix<T>& C) const {
95 Matrix<T> negC(C.rows(), C.cols());
96 for (std::size_t i = 0; i < C.rows(); ++i)
97 for (std::size_t j = 0; j < C.cols(); ++j) negC(i, j) = -C(i, j);
98 return solve_sylvester(negC);
99 }
100
101private:
102 std::size_t n_, m_;
103 Matrix<T> LU_;
104 std::vector<std::size_t> piv_;
105};
106
107/**
108 * Solve A X + X B = C for X.
109 *
110 * @param A n x n
111 * @param B m x m
112 * @param C n x m
113 */
114template <class T>
116 if (C.rows() != A.rows() || C.cols() != B.rows())
117 throw InputError("sylvester_solve: the blocks are not conformable");
118 return SylvesterFactor<T>(A, B).solve_sylvester(C);
119}
120
121/** MATLAB `lyap(A,B,C)` solves A X + X B + C = 0, i.e. sylvester_solve(A, B, -C). */
122template <class T>
123Matrix<T> lyap_solve(const Matrix<T>& A, const Matrix<T>& B, const Matrix<T>& C) {
124 if (C.rows() != A.rows() || C.cols() != B.rows())
125 throw InputError("lyap_solve: the blocks are not conformable");
126 return SylvesterFactor<T>(A, B).solve_lyap(C);
127}
128
129/**
130 * A X + X B = C by Bartels-Stewart, at double.
131 *
132 * WHY THIS EXISTS BESIDE THE KRONECKER SOLVER ABOVE. The Kronecker form is the
133 * only one available in an arbitrary field, and its O((n m)^3) cost is
134 * acceptable for the phase-space matrices the MMAP queue recursion passes. The
135 * FJ_codes fork-join engine passes matrices of order (C + 1) * m^2 * ma with C
136 * defaulting to 100, i.e. n and m in the hundreds to low thousands: the
137 * Kronecker operator of an 800 x 800 pair is 640000 square, which is 3 TB of
138 * LU. Bartels-Stewart is O(n^3 + m^3) and factorizes the same problem in
139 * fractions of a second, at the price of a real Schur factorization -- LAPACK,
140 * hence double only, exactly as `util/eig.h` is.
141 *
142 * A X + X B = C is solved as Ta Y + Y Tb = Za' C Zb with A = Za Ta Za' and
143 * B = Zb Tb Zb' real Schur, the quasi-triangular core done by dtrsyl, and
144 * X = Za Y Zb'. dtrsyl's `scale` guards against overflow when the two spectra
145 * nearly collide; it is divided out here, and a scale of zero means the
146 * equation has no solution, which is reported by name rather than returned as
147 * an infinity.
148 */
150 const Matrix<double>& C) {
151#ifndef LINE_MP_HAVE_LAPACK
152 (void)A;
153 (void)B;
154 (void)C;
155 throw UnsupportedError(
156 "sylvester_schur requires LAPACK: reconfigure with -DLINE_MP_USE_LAPACK=ON and liblapack "
157 "available");
158#else
159 const std::size_t n = A.rows(), m = B.rows();
160 if (A.cols() != n || B.cols() != m)
161 throw InputError("sylvester_schur: A and B must be square");
162 if (C.rows() != n || C.cols() != m)
163 throw InputError("sylvester_schur: the right-hand side is not conformable");
164 Matrix<double> X(n, m, 0.0);
165 if (n == 0 || m == 0) return X;
166
167 const RealSchur sa = schur_decomposition(A);
168 const RealSchur sb = schur_decomposition(B);
169 // Ct = Za' C Zb, column-major for LAPACK.
170 const Matrix<double> Ct = matmul(matmul(sa.Z.transpose(), C), sb.Z);
171 std::vector<double> ta(n * n), tb(m * m), c(n * m);
172 for (std::size_t i = 0; i < n; ++i)
173 for (std::size_t j = 0; j < n; ++j) ta[j * n + i] = sa.T(i, j);
174 for (std::size_t i = 0; i < m; ++i)
175 for (std::size_t j = 0; j < m; ++j) tb[j * m + i] = sb.T(i, j);
176 for (std::size_t i = 0; i < n; ++i)
177 for (std::size_t j = 0; j < m; ++j) c[j * n + i] = Ct(i, j);
178
179 const int ni = static_cast<int>(n), mi = static_cast<int>(m), isgn = 1;
180 double scale = 0.0;
181 int info = 0;
182 dtrsyl_("N", "N", &isgn, &ni, &mi, ta.data(), &ni, tb.data(), &mi, c.data(), &ni, &scale,
183 &info);
184 if (info < 0) throw InputError("sylvester_schur: LAPACK dtrsyl rejected an argument");
185 if (info == 1)
186 throw NumericError(
187 "sylvester_schur: A and -B have common or very close eigenvalues, so the Sylvester "
188 "equation is singular or nearly so and its solution is not determined");
189 if (scale == 0.0)
190 throw NumericError("sylvester_schur: LAPACK dtrsyl returned a zero scale factor");
191
192 Matrix<double> Y(n, m, 0.0);
193 for (std::size_t i = 0; i < n; ++i)
194 for (std::size_t j = 0; j < m; ++j) Y(i, j) = c[j * n + i] / scale;
195 return matmul(matmul(sa.Z, Y), sb.Z.transpose());
196#endif
197}
198
199/** MATLAB `lyap(A,B,C)` at double via Bartels-Stewart: A X + X B + C = 0. */
201 const Matrix<double>& C) {
202 Matrix<double> negC(C.rows(), C.cols());
203 for (std::size_t i = 0; i < C.rows(); ++i)
204 for (std::size_t j = 0; j < C.cols(); ++j) negC(i, j) = -C(i, j);
205 return sylvester_schur(A, B, negC);
206}
207
208} // namespace line
209
210#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:62
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:94
Matrix< T > solve_sylvester(const Matrix< T > &C) const
Solve A X + X B = C.
Definition sylvester.h:78
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.
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:123
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:149
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:115
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:200
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