LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fj_codes.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_API_FJ_FJ_CODES_H
6#define LINE_API_FJ_FJ_CODES_H
7
8/**
9 * @file
10 * @ingroup api_fj
11 * FJ_codes, the fork-join response-time-tail approximation of Z. Qiu, J. F.
12 * Perez and P. Harrison, "Beyond the Mean in Fork-Join Queues: Efficient
13 * Approximation for Response-Time Tails" (IFIP Performance 2015). Third-party,
14 * BSD-3-Clause, Copyright 2015 Imperial College London; see
15 * THIRD-PARTY-NOTICES.md and `python/line_solver/lib/thirdparty/fj/LICENSE.txt`.
16 *
17 * Port of the solve layer of `matlab/lib/thirdparty/FJ_codes`: `computeT.m`,
18 * `computeT_NARE.m`, `computePi.m`, `returnWait.m`, `returnPer.m`,
19 * `returnRT1.m`, `returnRT2.m` and `mainFJ.m`. The state-space construction is
20 * `fj_codes_matrices.h`.
21 *
22 * THE METHOD IN ONE PARAGRAPH. The response-time percentiles of the ONE-node
23 * queue are exact (it is a MAP/PH/1 queue and its sojourn time is a phase-type
24 * law). The TWO-node fork-join queue is solved by the approximation of Section
25 * 4: the queue-length DIFFERENCE between the two branches is truncated at C, so
26 * the all-busy period becomes a finite-phase Markov-modulated fluid whose
27 * generator T solves a Riccati equation, and the waiting and response times
28 * come out as phase-type laws over that phase space. A K-node queue is then
29 * INTERPOLATED (Section 6) as RT_1 + (RT_2 - RT_1) * log(K) / log(2), one
30 * percentile at a time. Nothing about K enters the matrices: K appears only in
31 * that last line.
32 *
33 * WHAT `returnRT1` USES HERE. The reference calls `Q_CT_MAP_MAP_1` of the QMAM
34 * toolbox for the one-node sojourn time. This port calls
35 * `mmapph1fcfs_stdistr_ph`, which returns the same object -- the sojourn time
36 * of the MAP/PH/1 queue as a phase-type pair (alpha, A) -- through BUTools'
37 * MMAPPH1FCFS, the route the Java port also takes. Both are exact for this
38 * queue, so this is a change of implementation and not of method. The Java port
39 * additionally CATCHES a failure of that solve and substitutes the raw SERVICE
40 * time PH; that is not reproduced, because a service time reported as a
41 * response time is a wrong number rather than a degraded one.
42 *
43 * ARITHMETIC. `double` only. `computeT_NARE` needs an ORDERED real Schur
44 * factorization (the stable invariant subspace of a 2m x 2m Hamiltonian-like
45 * pencil), and both Sylvester equations in `computePi` are of order (C + 1) m^2
46 * ma, which is in the hundreds to low thousands at the default C = 100 -- far
47 * past what the field-generic Kronecker Sylvester solver can carry. Both are
48 * LAPACK, exactly as `util/eig.h` is. Callers at another arithmetic must refuse
49 * by name; `solver_mam_fj.h` does.
50 *
51 * REFERENCE DEFECTS, reproduced unless stated:
52 *
53 * 1. `mainFJ` LOOPS OVER `Cs` AND KEEPS ONLY THE LAST. `percentileRT_1` and
54 * `percentileRT_2` are overwritten each iteration and only the final C
55 * survives, so a vector of accuracies costs the full solve per entry and
56 * answers for one of them. Reproduced; LINE passes a scalar.
57 * 2. `returnRT1` IS INSIDE THAT LOOP although it does not depend on C.
58 * Reproduced, and it is why a two-entry `Cs` doubles the MAP/PH/1 solve.
59 * 3. `returnRT2` COMPUTES `percentileWait` AND DISCARDS IT. The waiting-time
60 * percentiles are a complete result of the same phase-type pair the
61 * response time is then built from. NOT reproduced: computing an unused
62 * percentile inversion is pure cost, and the inversion is the expensive
63 * step. `fj_return_rt2` returns the waiting-time PH pair instead, so a
64 * caller that wants those percentiles can invert it without a second solve.
65 * 4. `returnPer` SCANS THE INVERSE CDF ON A FIXED 0.001 GRID downwards from
66 * 3 * mean, which quantizes every percentile to a millisecond of model time
67 * regardless of the time scale of the model. Reproduced: the grid is the
68 * method's resolution and changing it would move every reported number.
69 * 5. `returnPer` CAN FALL OFF ITS OWN SCAN. If the scan reaches t = 0 without
70 * the CDF dropping below the target, `temp_percentileRT` keeps the value
71 * from the PREVIOUS percentile, or is undefined on the first. NOT
72 * reproduced: this port reports it by name.
73 * 6. `computeT`'s Sylvester iteration HAS NO ITERATION CAP. NOT reproduced:
74 * the loop is capped and a failure to converge is reported by name rather
75 * than hanging.
76 * 7. THE RESIDUAL NORMS ARE PRINTED, from `computeT` and `computeT_NARE`, on
77 * every call. NOT reproduced: they are returned in `FjCodesT::residual`
78 * instead, because a solver that writes to stdout corrupts the CLI's JSON.
79 */
80
81#include <algorithm>
82#include <cmath>
83#include <cstddef>
84#include <string>
85#include <vector>
86
91#include "line/util/eig.h"
92#include "line/util/error.h"
93#include "line/util/linalg.h"
94#include "line/util/lstsq.h"
95#include "line/util/matrix.h"
96#include "line/util/sylvester.h"
97
98namespace line {
99namespace fj {
100
101/** Which route `computeT.m` takes to the T matrix. */
102enum class FjTMode { Nare, Sylvester };
103
104/** Parse the reference's `T_Mode` string, whose default is 'NARE'. */
105inline FjTMode fj_parse_tmode(const std::string& s) {
106 // The reference tests `contains(T_Mode, 'Sylvest', 'IgnoreCase', true)` and defaults
107 // to NARE for everything else, including an empty string.
108 std::string lower(s);
109 for (char& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
110 if (lower.find("sylvest") != std::string::npos) return FjTMode::Sylvester;
111 return FjTMode::Nare;
112}
113
114/** What `computeT.m` returns. */
115struct FjCodesT {
116 Matrix<double> T; ///< the all-busy generator, (newdim * ma) square
117 Matrix<double> S; ///< build_SA's S, newdim square
118 Matrix<double> A_jump; ///< build_SA's A_jump, newdim square
119 Matrix<double> S_Arr; ///< kron(S, I_ma)
120 std::vector<double> sum_Ajump; ///< row sums of kron(A_jump, I_ma)
121 double residual = 0.0; ///< inf-norm the reference prints
122 std::size_t iterations = 0; ///< Sylvester mode only
123};
124
125/** What `computePi.m` returns. */
126struct FjCodesPi {
127 std::vector<double> pi0; ///< unnormalized, length newdim * ma
128 double En1 = 0.0; ///< mean number of arrivals in a not-all-busy period
129};
130
131/** What `returnWait.m` returns: the waiting time as a phase-type law. */
133 std::vector<double> wait_alpha; ///< defective, mass prob_wait
135 double prob_wait = 0.0;
136 std::vector<double> alfa; ///< -pi0 T^-1, the all-busy occupancy
137};
138
139/** One line of `mainFJ`'s output cell: the percentiles of a K-node queue. */
141 std::size_t K = 0;
142 std::vector<double> percentiles; ///< in PERCENT, as the reference stores them
143 std::vector<double> RTp;
144};
145
146namespace fjdetail {
147
148/** MATLAB `A / B` for a row vector: x with x B = a. */
149inline std::vector<double> rdivide_row(const std::vector<double>& a, const Matrix<double>& B) {
150 const Matrix<double> iB = inverse(B);
151 return vecmul(a, iB);
152}
153
154/** Elementwise infinity norm of A - B. */
155inline double max_abs_diff(const Matrix<double>& A, const Matrix<double>& B) {
156 double d = 0.0;
157 for (std::size_t i = 0; i < A.rows(); ++i)
158 for (std::size_t j = 0; j < A.cols(); ++j) {
159 const double x = std::fabs(A(i, j) - B(i, j));
160 if (x > d) d = x;
161 }
162 return d;
163}
164
165/** The inf-norm (maximum absolute row sum) the reference reports. */
166inline double inf_norm(const Matrix<double>& A) {
167 double best = 0.0;
168 for (std::size_t i = 0; i < A.rows(); ++i) {
169 double s = 0.0;
170 for (std::size_t j = 0; j < A.cols(); ++j) s += std::fabs(A(i, j));
171 if (s > best) best = s;
172 }
173 return best;
174}
175
176inline Matrix<double> madd(const Matrix<double>& A, const Matrix<double>& B) {
177 Matrix<double> C(A.rows(), A.cols());
178 for (std::size_t i = 0; i < A.rows(); ++i)
179 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = A(i, j) + B(i, j);
180 return C;
181}
182
183inline Matrix<double> msub(const Matrix<double>& A, const Matrix<double>& B) {
184 Matrix<double> C(A.rows(), A.cols());
185 for (std::size_t i = 0; i < A.rows(); ++i)
186 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = A(i, j) - B(i, j);
187 return C;
188}
189
190/** A sub-block of A, rows [r0, r1) and columns [c0, c1). */
191inline Matrix<double> block(const Matrix<double>& A, std::size_t r0, std::size_t r1,
192 std::size_t c0, std::size_t c1) {
193 Matrix<double> B(r1 - r0, c1 - c0, 0.0);
194 for (std::size_t i = r0; i < r1; ++i)
195 for (std::size_t j = c0; j < c1; ++j) B(i - r0, j - c0) = A(i, j);
196 return B;
197}
198
199} // namespace fjdetail
200
201/**
202 * Port of `computeT_NARE.m`: the T matrix as the stable invariant subspace of
203 *
204 * H = [ I (x) D0 I (x) D1 ]
205 * [ -A_jump (x) I -S ]
206 *
207 * The m eigenvalues of SMALLEST real part are ordered to the front of the real
208 * Schur form, X is read off the resulting basis as Q1(m+1:2m, 1:m) /
209 * Q1(1:m, 1:m), and T = S + X (I (x) D1).
210 *
211 * A complex-conjugate pair astride the m/2m boundary would ask for an invariant
212 * subspace that does not exist over the reals; LAPACK's dtrexc refuses to split
213 * such a block, and the split is detected here first so that it is named as the
214 * spectrum condition it is rather than as a reordering failure.
215 */
217 const Matrix<double>& S, const Matrix<double>& A_jump,
218 double* residual) {
219 const std::size_t m = S.rows();
220 const std::size_t ma = D0.rows();
221 if (ma == 0 || m % ma != 0)
222 throw InputError("fj_compute_t_nare: the phase space is not a multiple of the arrival "
223 "order");
224 const std::size_t ms = m / ma;
225 const Matrix<double> Ims = eye<double>(ms);
226 const Matrix<double> Ima = eye<double>(ma);
227 const Matrix<double> ImsD0 = mam::kron(Ims, D0);
228 const Matrix<double> ImsD1 = mam::kron(Ims, D1);
229 const Matrix<double> AjIma = mam::kron(A_jump, Ima);
230
231 Matrix<double> H(2 * m, 2 * m, 0.0);
232 for (std::size_t i = 0; i < m; ++i)
233 for (std::size_t j = 0; j < m; ++j) {
234 H(i, j) = ImsD0(i, j);
235 H(i, m + j) = ImsD1(i, j);
236 H(m + i, j) = -AjIma(i, j);
237 H(m + i, m + j) = -S(i, j);
238 }
239
240 const RealSchur sc = schur_decomposition(H);
241 // The real part of each diagonal entry: a 2 x 2 block contributes its own
242 // trace/2 to both of its entries, which is what ordeig reports for the pair.
243 const std::size_t n = 2 * m;
244 std::vector<double> re(n, 0.0);
245 std::vector<int> blk(n, 1); // 1 for a 1 x 1 block, 2 for the first row of a 2 x 2
246 for (std::size_t i = 0; i < n;) {
247 const bool pair = (i + 1 < n) && sc.T(i + 1, i) != 0.0;
248 if (pair) {
249 const double r = 0.5 * (sc.T(i, i) + sc.T(i + 1, i + 1));
250 re[i] = r;
251 re[i + 1] = r;
252 blk[i] = 2;
253 blk[i + 1] = 0;
254 i += 2;
255 } else {
256 re[i] = sc.T(i, i);
257 blk[i] = 1;
258 i += 1;
259 }
260 }
261 std::vector<std::size_t> order(n);
262 for (std::size_t i = 0; i < n; ++i) order[i] = i;
263 std::stable_sort(order.begin(), order.end(),
264 [&re](std::size_t a, std::size_t b) { return re[a] < re[b]; });
265 std::vector<double> key(n, 0.0);
266 for (std::size_t i = 0; i < m; ++i) key[order[i]] = 1.0;
267 for (std::size_t i = 0; i + 1 < n; ++i)
268 if (blk[i] == 2 && key[i] != key[i + 1])
269 throw NumericError(ErrorKind{"Degenerate"},
270 "fj_compute_t_nare: the m eigenvalues of smallest real part split a complex "
271 "conjugate pair, so the stable invariant subspace of the Riccati pencil is not "
272 "real and the T matrix of this model is not defined");
273
274 const RealSchur ord = schur_reorder(sc, key);
275 const Matrix<double> Q11 = fjdetail::block(ord.Z, 0, m, 0, m);
276 const Matrix<double> Q21 = fjdetail::block(ord.Z, m, 2 * m, 0, m);
277 const Matrix<double> X = matmul(Q21, inverse(Q11));
278 const Matrix<double> T = fjdetail::madd(S, matmul(X, ImsD1));
279 if (residual != nullptr)
280 *residual = fjdetail::inf_norm(
281 fjdetail::madd(fjdetail::madd(matmul(T, X), matmul(X, ImsD0)), AjIma));
282 return T;
283}
284
285/**
286 * Port of `computeT.m`.
287 *
288 * The Sylvester route is the paper's Section 5.1 and the NARE route its Section
289 * 5.2; they solve the same equation and the reference defaults to NARE. Both
290 * are offered because `options.config.fj_tmode` selects between them, and
291 * because they fail on different models: the iteration converges linearly and
292 * can stall, while the Schur route is direct but needs the stable subspace to
293 * be real.
294 */
295inline FjCodesT fj_compute_t(const FjDist<double>& arrival, const FjDist<double>& service,
296 const FjCodesServiceH& h, std::size_t C, FjTMode mode) {
297 const FjCodesSA sa = fj_build_sa(service, h, C);
298 const std::size_t d0 = arrival.lambda0.rows();
299 const Matrix<double> Id0 = eye<double>(d0);
300
301 FjCodesT out;
302 out.S = sa.S;
303 out.A_jump = sa.A_jump;
304 out.S_Arr = mam::kron(sa.S, Id0);
305 const Matrix<double> A_jump_Arr = mam::kron(sa.A_jump, Id0);
306
307 if (mode == FjTMode::Sylvester) {
308 const std::size_t ms = sa.S.rows();
309 const std::size_t m = ms * d0;
310 const Matrix<double> ID0 = mam::kron(eye<double>(ms), arrival.lambda0);
311 const Matrix<double> DS =
312 matmul(mam::kron(eye<double>(ms), arrival.lambda1), A_jump_Arr);
313 const Matrix<double> Im = eye<double>(m);
314 Matrix<double> Tnew = out.S_Arr;
315 Matrix<double> Told(m, m, 0.0);
317 // The reference iterates without a cap; 500 is far past the linear
318 // convergence of every model the gate admits, and a stall is reported.
319 const std::size_t max_iter = 500;
320 std::size_t it = 0;
321 while (fjdetail::max_abs_diff(Told, Tnew) > 1e-10) {
322 if (++it > max_iter)
323 throw NumericError(ErrorKind{"NonConvergence"},
324 "fj_compute_t: the Sylvester iteration of computeT.m did not reach 1e-10 in " +
325 std::to_string(max_iter) +
326 " steps; solve this model with the NARE route (config.fj_tmode = 'NARE')");
327 Told = Tnew;
328 L = lyap_schur(Tnew, ID0, Im);
329 Tnew = fjdetail::madd(out.S_Arr, matmul(L, DS));
330 }
331 out.T = Tnew;
332 out.iterations = it;
333 if (it > 0)
334 out.residual = fjdetail::inf_norm(
335 fjdetail::madd(fjdetail::madd(matmul(out.T, L), matmul(L, ID0)), Im));
336 } else {
337 out.T = fj_compute_t_nare(arrival.lambda0, arrival.lambda1, out.S_Arr, sa.A_jump,
338 &out.residual);
339 }
340
341 out.sum_Ajump.assign(A_jump_Arr.rows(), 0.0);
342 for (std::size_t i = 0; i < A_jump_Arr.rows(); ++i) {
343 double s = 0.0;
344 for (std::size_t j = 0; j < A_jump_Arr.cols(); ++j) s += A_jump_Arr(i, j);
345 out.sum_Ajump[i] = s;
346 }
347 return out;
348}
349
350/**
351 * The boundary solve both branches of `computePi.m` end with:
352 *
353 * pi0 [ pi0mat - I , T^-1 e ] = [ 0 ... 0 , -1 ]
354 *
355 * an OVERDETERMINED system by one column, which MATLAB's mrdivide answers in
356 * the least-squares sense. It is consistent -- the extra column is the
357 * normalization that fixes the scale of the null vector -- so least squares
358 * returns the exact solution and not an approximation.
359 */
360inline std::vector<double> fj_boundary_solve(const Matrix<double>& pi0mat,
361 const Matrix<double>& T) {
362 const std::size_t nd = pi0mat.rows();
363 if (pi0mat.cols() != nd || T.rows() != nd || T.cols() != nd)
364 throw InputError("fj_boundary_solve: the boundary blocks are not conformable");
365 const Matrix<double> iT = inverse(T);
366 Matrix<double> M(nd, nd + 1, 0.0);
367 for (std::size_t i = 0; i < nd; ++i) {
368 for (std::size_t j = 0; j < nd; ++j) M(i, j) = pi0mat(i, j) - (i == j ? 1.0 : 0.0);
369 double s = 0.0;
370 for (std::size_t j = 0; j < nd; ++j) s += iT(i, j);
371 M(i, nd) = s;
372 }
373 std::vector<double> b(nd + 1, 0.0);
374 b[nd] = -1.0;
375 const LstsqResult<double> r = lstsq(M.transpose(), b);
376 return r.x;
377}
378
379/**
380 * Port of `computePi.m`: the all-busy boundary vector and E[n1].
381 *
382 * The two branches are not two implementations of one formula. For EXPONENTIAL
383 * service the not-all-busy space has exactly as many phases as the all-busy one
384 * (m = 1 makes both (C + 1)-dimensional), so the return to the all-busy period
385 * is a square map and `Q0` can be inverted directly. For phase-type service the
386 * two spaces differ and the reference goes through `constructSRK`, whose Ke and
387 * Kc project between them. Running the second branch on exponential service
388 * would give the same answer; running the first on anything else is a shape
389 * error, which is why the reference switches on `SerChoice`.
390 */
391inline FjCodesPi fj_compute_pi(const Matrix<double>& T, const FjDist<double>& arrival,
392 const FjDist<double>& service, const FjCodesServiceH& h,
393 std::size_t C, const Matrix<double>& S,
394 const Matrix<double>& A_jump) {
395 const std::size_t da = arrival.lambda0.rows();
396 FjCodesPi out;
397
398 if (service.choice == 1) {
399 const std::size_t ms = S.cols();
400 const Matrix<double> S_notallbusy = fj_construct_not_all_busy(C, service, h);
401 const Matrix<double> Q0 = mam::krons(S_notallbusy, arrival.lambda0);
402 if (Q0.rows() != ms * da)
403 throw NumericError(ErrorKind{"Degenerate"},
404 "fj_compute_pi: the not-all-busy space and the all-busy space have different "
405 "dimensions, which the exponential branch of computePi.m assumes they do not");
406 const Matrix<double> Igral =
408 const Matrix<double> pi0mat =
409 matmul(matmul(matmul(Igral, mam::kron(A_jump, eye<double>(da))), inverse(Q0)),
410 mam::kron(eye<double>(ms), arrival.lambda1));
411 out.pi0 = fj_boundary_solve(pi0mat, T);
412 double sp = 0.0;
413 for (double v : out.pi0) sp += v;
414 const std::vector<double> pm = vecmul(out.pi0, pi0mat);
415 double spm = 0.0;
416 for (double v : pm) spm += v;
417 out.En1 = spm / sp;
418 return out;
419 }
420
421 const FjCodesSRK srk = fj_construct_srk(C, service, h, S);
422 const std::size_t dtmat = T.rows() / arrival.lambda0.cols();
423 const std::size_t dsexp = srk.Se.rows();
424 const std::size_t dnb = dsexp - dtmat;
425
426 const Matrix<double> Sedash = fjdetail::block(srk.Se, dtmat, dsexp, dtmat, dsexp);
427 const Matrix<double> Rbusy = fjdetail::block(srk.R0, dtmat, dsexp, 0, dsexp);
428 Matrix<double> Iidle_small(dsexp, dnb, 0.0);
429 for (std::size_t i = 0; i < dnb; ++i) Iidle_small(dtmat + i, i) = 1.0;
430 const Matrix<double> Ida = eye<double>(da);
431 const Matrix<double> Iidle = mam::kron(Iidle_small, Ida);
432
433 const Matrix<double> Qidle = mam::krons(Sedash, arrival.lambda0);
434 const Matrix<double> Qbusy = mam::kron(Rbusy, arrival.lambda1);
435 const Matrix<double> Bmap = [&] {
436 const Matrix<double> M = matmul(matmul(Iidle, inverse(Qidle)), Qbusy);
437 Matrix<double> N(M.rows(), M.cols());
438 for (std::size_t i = 0; i < M.rows(); ++i)
439 for (std::size_t j = 0; j < M.cols(); ++j) N(i, j) = -M(i, j);
440 return N;
441 }();
442 const Matrix<double> Kemap = mam::kron(srk.Ke, Ida);
443 const Matrix<double> Kcmap = mam::kron(srk.Kc, Ida);
444
445 const Matrix<double> BB = mam::kron(eye<double>(srk.Sestar.cols()), arrival.lambda0);
446 const Matrix<double> Igral =
447 lyap_schur(T, BB, matmul(Kemap, mam::kron(srk.Sestar, Ida)));
448 const Matrix<double> pi0mat = matmul(matmul(Igral, Bmap), Kcmap);
449 out.pi0 = fj_boundary_solve(pi0mat, T);
450 double sp = 0.0;
451 for (double v : out.pi0) sp += v;
452
453 // -pi0 Igral Iidle Qidle^-1 (I (x) D1), left to right as MATLAB reads it.
454 std::vector<double> row = vecmul(out.pi0, Igral);
455 for (double& v : row) v = -v;
456 row = vecmul(row, Iidle);
457 row = fjdetail::rdivide_row(row, Qidle);
458 row = vecmul(row, mam::kron(eye<double>(Sedash.cols()), arrival.lambda1));
459 double sr = 0.0;
460 for (double v : row) sr += v;
461 out.En1 = sr / sp;
462 return out;
463}
464
465/** Port of `returnWait.m`: the stationary waiting time as a phase-type law. */
466inline FjCodesWait fj_return_wait(double En1, const std::vector<double>& pi0,
467 const Matrix<double>& T, const std::vector<double>& phi,
468 const std::vector<double>& sum_Ajump) {
469 const std::size_t ds = phi.size();
470 FjCodesWait out;
471 out.alfa = fjdetail::rdivide_row(pi0, T);
472 for (double& v : out.alfa) v = -v;
473
474 double ap = 0.0;
475 for (std::size_t i = 0; i < ds; ++i) ap += out.alfa[i] * phi[i];
476 std::vector<double> rhos(ds, 0.0);
477 for (std::size_t i = 0; i < ds; ++i) rhos[i] = phi[i] * out.alfa[i] / ap;
478
479 double sp = 0.0;
480 for (double v : pi0) sp += v;
481 double asum = 0.0;
482 for (std::size_t i = 0; i < ds; ++i) asum += out.alfa[i] * sum_Ajump[i];
483 const double En0 = asum / sp;
484
485 out.prob_wait = (En0 - 1.0) / (En0 - 1.0 + En1);
486 out.wait_alpha.assign(ds, 0.0);
487 for (std::size_t i = 0; i < ds; ++i) out.wait_alpha[i] = out.prob_wait * rhos[i];
488
489 out.wait_Smat = Matrix<double>(ds, ds, 0.0);
490 for (std::size_t i = 0; i < ds; ++i)
491 for (std::size_t j = 0; j < ds; ++j)
492 out.wait_Smat(i, j) = out.alfa[j] * T(j, i) / out.alfa[i];
493 return out;
494}
495
496/**
497 * Port of `returnPer.m`: the percentiles of a (possibly defective) phase-type
498 * law, by uniformization.
499 *
500 * The law is uniformized at c = max(-diag(A)) into P = A / c + I, the
501 * absorption probability by time t is the Poisson mixture sum_k p_k(ct) a_k
502 * with a_k = alpha P^k e, and the series is truncated where its partial sums
503 * reach the total mass alpha (I - P)^-1 e. The percentile itself is then found
504 * by scanning t downwards on a 0.001 grid from three times the mean, extending
505 * the bracket by half a mean whenever the target is not yet reached. A target
506 * below the defect 1 - sum(alpha) is answered with zero, which is the
507 * reference's convention for a percentile the law never attains.
508 */
509inline std::vector<double> fj_return_per(const std::vector<double>& vec, const Matrix<double>& A,
510 const std::vector<double>& pers) {
511 using std::exp;
512 const std::size_t m = A.cols();
513 if (vec.size() != m || A.rows() != m)
514 throw InputError("fj_return_per: the phase-type pair is not conformable");
515
516 const std::vector<double> negvA = [&] {
517 std::vector<double> v = vecmul(vec, inverse(A));
518 for (double& x : v) x = -x;
519 return v;
520 }();
521 double meanRT = 0.0;
522 for (double v : negvA) meanRT += v;
523 if (!(meanRT > 0.0))
524 throw NumericError(ErrorKind{"Degenerate"}, "fj_return_per: the phase-type law has a non-positive mean, so its "
525 "percentiles are not defined");
526
527 double c = 0.0;
528 for (std::size_t i = 0; i < m; ++i) c = std::max(c, -A(i, i));
529 if (!(c > 0.0))
530 throw NumericError(ErrorKind{"Degenerate"}, "fj_return_per: the phase-type generator has no negative diagonal");
531 Matrix<double> P(m, m, 0.0);
532 for (std::size_t i = 0; i < m; ++i)
533 for (std::size_t j = 0; j < m; ++j) P(i, j) = A(i, j) / c + (i == j ? 1.0 : 0.0);
534
535 Matrix<double> ImP(m, m, 0.0);
536 for (std::size_t i = 0; i < m; ++i)
537 for (std::size_t j = 0; j < m; ++j) ImP(i, j) = (i == j ? 1.0 : 0.0) - P(i, j);
538 const std::vector<double> vImP = vecmul(vec, inverse(ImP));
539 double M = 0.0;
540 for (double v : vImP) M += v;
541
542 double a0 = 0.0;
543 for (double v : vec) a0 += v;
544 double sum_a = a0;
545 std::vector<double> ak;
546 std::vector<double> vP(m, 0.0);
547 for (std::size_t i = 0; i < m; ++i) {
548 double s = 0.0;
549 for (std::size_t j = 0; j < m; ++j) s += P(i, j);
550 vP[i] = s;
551 }
552 while (std::fabs(sum_a - M) >= 1e-10) {
553 double t = 0.0;
554 for (std::size_t i = 0; i < m; ++i) t += vec[i] * vP[i];
555 ak.push_back(t);
556 sum_a += t;
557 vP = mulvec(P, vP);
558 if (ak.size() > 2000000)
559 throw NumericError(ErrorKind{"NonConvergence"}, "fj_return_per: the uniformized Poisson series did not reach the "
560 "total absorption mass, so the percentile scan cannot terminate");
561 }
562 const std::size_t K1 = ak.size();
563
564 // The absorption CDF at t, as the reference evaluates it.
565 const auto cdf_at = [&](double t) {
566 double pM = exp(-c * t);
567 double F = pM * a0;
568 for (std::size_t k = 1; k <= K1; ++k) {
569 pM = c * t * pM / static_cast<double>(k);
570 F += pM * ak[k - 1];
571 }
572 return 1.0 - F;
573 };
574
575 std::vector<double> out(pers.size(), 0.0);
576 for (std::size_t p = 0; p < pers.size(); ++p) {
577 if (pers[p] < 1.0 - a0) continue; // the law never attains this percentile
578 double MaxTime = 3.0 * meanRT;
579 for (;;) {
580 if (cdf_at(MaxTime) < pers[p]) {
581 MaxTime += 0.5 * meanRT;
582 continue;
583 }
584 bool found = false;
585 // MATLAB's colon operator indexes the grid, it does not accumulate
586 // a subtraction, so t is formed as MaxTime - k * 0.001.
587 const std::size_t steps = static_cast<std::size_t>(MaxTime / 0.001);
588 for (std::size_t k = 0; k <= steps; ++k) {
589 const double t = MaxTime - static_cast<double>(k) * 0.001;
590 if (cdf_at(t) < pers[p]) {
591 out[p] = t + 0.001;
592 found = true;
593 break;
594 }
595 }
596 if (!found)
597 throw NumericError(ErrorKind{"Degenerate"},
598 "fj_return_per: the downward scan reached t = 0 without the response-time CDF "
599 "falling below " + std::to_string(pers[p]) +
600 ", so this percentile has no bracket; the reference silently reports the "
601 "previous percentile here");
602 break;
603 }
604 }
605 return out;
606}
607
608/**
609 * Port of `returnRT1.m`: the response-time percentiles of the ONE-node queue,
610 * which are exact.
611 */
612inline std::vector<double> fj_return_rt1(const FjDist<double>& arrival,
613 const FjDist<double>& service,
614 const std::vector<double>& pers) {
616 mm.D0 = arrival.lambda0;
617 mm.D1 = arrival.lambda1;
618 mm.Dc.push_back(arrival.lambda1);
619
620 std::vector<mam::PhService<double>> svc(1);
621 svc[0].sigma = service.tau_st;
622 svc[0].S = service.ST;
623
624 const std::vector<mam::StDistrPh<double>> ph = mam::mmapph1fcfs_stdistr_ph(mm, svc);
625 if (ph.empty())
626 throw NumericError(ErrorKind{"Degenerate"}, "fj_return_rt1: MMAPPH1FCFS returned no sojourn-time law");
627 return fj_return_per(ph[0].alpha, ph[0].A, pers);
628}
629
630/** What `returnRT2.m` produces, plus the waiting-time law it discards. */
632 std::vector<double> RTp; ///< response-time percentiles
633 std::vector<double> wait_alpha; ///< the waiting-time law, defective
635 double prob_wait = 0.0;
636 double residual = 0.0; ///< the T-matrix residual of computeT
637};
638
639/**
640 * Port of `returnRT2.m`: the response-time percentiles of the TWO-node
641 * fork-join queue, under the Section 4 approximation.
642 *
643 * The response time is assembled as one phase-type law over three blocks: the
644 * service process of a job that arrives in a not-all-busy period, the TIME-
645 * REVERSED service process of a job that arrives in an all-busy period, and the
646 * waiting time. The reversal is what lets the job's own service be appended to
647 * the waiting time it accrued: `stat_service_phase` is the stationary phase
648 * occupancy, `tr_ST` is the reversed generator, and `tildeP` is the coupling
649 * that hands the reversed process over to the waiting-time block. Phases of
650 * zero stationary occupancy are dropped -- they are unreachable and the
651 * reversal divides by their occupancy.
652 */
653inline FjCodesRT2 fj_return_rt2(const FjDist<double>& arrival, const FjDist<double>& service,
654 const std::vector<double>& pers, std::size_t C, FjTMode mode) {
655 const FjCodesServiceH h = fj_build_service_h(service);
656 const FjCodesT ct = fj_compute_t(arrival, service, h, C, mode);
657 const std::size_t mWait = ct.A_jump.rows();
658 const std::size_t n = ct.T.rows();
659
660 std::vector<double> phi(n, 0.0);
661 for (std::size_t i = 0; i < n; ++i) {
662 double s = 0.0;
663 for (std::size_t j = 0; j < n; ++j) s += ct.T(i, j) - ct.S_Arr(i, j);
664 phi[i] = s;
665 }
666 const FjCodesPi pi = fj_compute_pi(ct.T, arrival, service, h, C, ct.S, ct.A_jump);
667 const FjCodesWait w = fj_return_wait(pi.En1, pi.pi0, ct.T, phi, ct.sum_Ajump);
668
669 const FjCodesGenService gs = fj_generate_service(service, h, C, ct.S);
670 const Matrix<double> ST = mam::kron(gs.T, arrival.Ia);
671
672 std::vector<double> pi0n = pi.pi0;
673 double sp = 0.0;
674 for (double v : pi0n) sp += v;
675 for (double& v : pi0n) v /= sp;
676
677 const std::size_t dim = arrival.ma * gs.newdim;
678 const std::size_t dim_notbusy = arrival.ma * gs.dim_notbusy;
679 const std::size_t dim_service = dim + dim_notbusy;
680 const std::size_t Tr = ST.rows();
681 const std::size_t Sc = w.wait_Smat.cols();
682 if (Tr != dim_service)
683 throw NumericError(ErrorKind{"Degenerate"}, "fj_return_rt2: the tagged-job service space and the phase blocks "
684 "disagree in size");
685
686 std::vector<double> notbusy_start(dim_service, 0.0);
687 for (std::size_t i = 0; i < dim; ++i)
688 notbusy_start[i] = (1.0 - w.prob_wait) * pi0n[i];
689
690 // TS = T - S_Arr, the rates at which a new job enters service, then row
691 // normalized into the jump kernel of the all-busy phase.
692 Matrix<double> TS = fjdetail::msub(ct.T, ct.S_Arr);
693 std::vector<double> busy_start(dim_service, 0.0);
694 {
695 const std::vector<double> aTS = vecmul(w.alfa, TS);
696 if (aTS.size() != dim)
697 throw NumericError(ErrorKind{"Degenerate"}, "fj_return_rt2: the all-busy phase space and the tagged job's "
698 "all-busy service block have different sizes");
699 double s = 0.0;
700 for (double v : aTS) s += v;
701 for (std::size_t i = 0; i < dim; ++i) busy_start[i] = w.prob_wait * aTS[i] / s;
702 }
703 for (std::size_t i = 0; i < TS.rows(); ++i) {
704 double s = 0.0;
705 for (std::size_t j = 0; j < TS.cols(); ++j) s += TS(i, j);
706 for (std::size_t j = 0; j < TS.cols(); ++j) TS(i, j) /= s;
707 }
708
709 const std::vector<double> stat = [&] {
710 Matrix<double> negST(Tr, Tr);
711 for (std::size_t i = 0; i < Tr; ++i)
712 for (std::size_t j = 0; j < Tr; ++j) negST(i, j) = -ST(i, j);
713 return fjdetail::rdivide_row(busy_start, negST);
714 }();
715 std::vector<bool> nz(Tr, false);
716 for (std::size_t i = 0; i < Tr; ++i) nz[i] = stat[i] > 0.0;
717
718 std::vector<double> tr_start(Tr, 0.0);
719 for (std::size_t j = 0; j < Tr; ++j) {
720 double s = 0.0;
721 for (std::size_t k = 0; k < Tr; ++k) s += ST(j, k);
722 tr_start[j] = -s * stat[j];
723 }
724
725 Matrix<double> tr_ST(Tr, Tr, 0.0);
726 for (std::size_t i = 0; i < Tr; ++i) {
727 if (!nz[i]) continue;
728 for (std::size_t j = 0; j < Tr; ++j)
729 if (nz[j]) tr_ST(i, j) = ST(j, i) * stat[j] / stat[i];
730 }
731 std::vector<double> tr_exit(Tr, 0.0);
732 for (std::size_t i = 0; i < Tr; ++i) {
733 double s = 0.0;
734 for (std::size_t j = 0; j < Tr; ++j) s += tr_ST(i, j);
735 tr_exit[i] = -s;
736 }
737
738 // TS2 = TS' diag(alfa), row normalized with the reference's 1e-11 floor.
739 Matrix<double> TS2(Sc, Sc, 0.0);
740 for (std::size_t i = 0; i < Sc; ++i)
741 for (std::size_t j = 0; j < Sc; ++j) TS2(i, j) = TS(j, i) * w.alfa[j];
742 for (std::size_t i = 0; i < Sc; ++i) {
743 double s = 0.0;
744 for (std::size_t j = 0; j < Sc; ++j) s += TS2(i, j);
745 if (std::fabs(s) < 10e-12) s = 1.0;
746 for (std::size_t j = 0; j < Sc; ++j) TS2(i, j) /= s;
747 }
748
749 Matrix<double> tildeP(Tr, Sc, 0.0);
750 for (std::size_t i = 0; i < Sc; ++i)
751 for (std::size_t j = 0; j < Sc; ++j) tildeP(i, j) = tr_exit[i] * TS2(i, j);
752
753 std::vector<std::size_t> keep;
754 for (std::size_t i = 0; i < Tr; ++i)
755 if (nz[i]) keep.push_back(i);
756 const std::size_t m_tr = keep.size();
757
758 const std::size_t total = Tr + m_tr + Sc;
759 std::vector<double> gamma(total, 0.0);
760 for (std::size_t i = 0; i < Tr; ++i) gamma[i] = notbusy_start[i];
761 for (std::size_t i = 0; i < m_tr; ++i) gamma[Tr + i] = tr_start[keep[i]];
762
763 Matrix<double> Cres(total, total, 0.0);
764 for (std::size_t i = 0; i < Tr; ++i)
765 for (std::size_t j = 0; j < Tr; ++j) Cres(i, j) = ST(i, j);
766 for (std::size_t i = 0; i < m_tr; ++i) {
767 for (std::size_t j = 0; j < m_tr; ++j) Cres(Tr + i, Tr + j) = tr_ST(keep[i], keep[j]);
768 for (std::size_t j = 0; j < Sc; ++j) Cres(Tr + i, Tr + m_tr + j) = tildeP(keep[i], j);
769 }
770 for (std::size_t i = 0; i < Sc; ++i)
771 for (std::size_t j = 0; j < Sc; ++j)
772 Cres(Tr + m_tr + i, Tr + m_tr + j) = w.wait_Smat(i, j);
773
774 FjCodesRT2 out;
775 out.RTp = fj_return_per(gamma, Cres, pers);
776 out.wait_alpha = w.wait_alpha;
777 out.wait_Smat = w.wait_Smat;
778 out.prob_wait = w.prob_wait;
779 out.residual = ct.residual;
780 (void)mWait;
781 return out;
782}
783
784/**
785 * Port of `mainFJ.m`: the response-time percentiles of a K-node fork-join
786 * queue, interpolated between the exact one-node and the approximate two-node
787 * results in log K.
788 *
789 * @param pers the target percentiles as PROBABILITIES in (0, 1)
790 * @param K one entry per fork-join width to report
791 * @param Cs the truncation levels; only the LAST one reaches the answer, as
792 * in the reference
793 */
794inline std::vector<FjCodesPercentiles> fj_main(const FjDist<double>& arrival,
795 const FjDist<double>& service,
796 const std::vector<double>& pers,
797 const std::vector<std::size_t>& K,
798 const std::vector<std::size_t>& Cs, FjTMode mode) {
799 using std::log;
800 if (!(arrival.lambda / service.mu < 1.0))
801 throw InputError("mainFJ: the system is not stable, the mean arrival rate " +
802 std::to_string(arrival.lambda) +
803 " is not below the mean service rate " + std::to_string(service.mu));
804 if (Cs.empty()) throw InputError("mainFJ: no truncation level C was given");
805 if (pers.empty()) throw InputError("mainFJ: no percentile was requested");
806
807 std::vector<double> rt1, rt2;
808 for (std::size_t c = 0; c < Cs.size(); ++c) {
809 rt1 = fj_return_rt1(arrival, service, pers);
810 rt2 = fj_return_rt2(arrival, service, pers, Cs[c], mode).RTp;
811 }
812
813 std::vector<FjCodesPercentiles> out(K.size());
814 for (std::size_t k = 0; k < K.size(); ++k) {
815 out[k].K = K[k];
816 out[k].percentiles.resize(pers.size());
817 out[k].RTp.assign(pers.size(), 0.0);
818 for (std::size_t p = 0; p < pers.size(); ++p) {
819 out[k].percentiles[p] = 100.0 * pers[p];
820 out[k].RTp[p] = rt1[p] + (rt2[p] - rt1[p]) * log(static_cast<double>(K[k])) / log(2.0);
821 }
822 }
823 return out;
824}
825
826} // namespace fj
827} // namespace line
828
829#endif // LINE_API_FJ_FJ_CODES_H
InputError(const std::string &what)
Definition error.h:105
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:112
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
The state-space construction of FJ_codes, the fork-join response-time-tail approximation of Z.
Conversion of a LINE MAP into the arrival or service descriptor of the fork-join response-time-tail a...
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Least squares for a rectangular system, exact-capable.
Dense matrix and non-owning view.
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
The MMAP[K]/PH[K]/1 FCFS queue: per-class mean number in system and per-class queue-length distributi...
std::vector< double > fj_return_rt1(const FjDist< double > &arrival, const FjDist< double > &service, const std::vector< double > &pers)
Port of returnRT1.m: the response-time percentiles of the ONE-node queue, which are exact.
Definition fj_codes.h:612
std::vector< double > fj_return_per(const std::vector< double > &vec, const Matrix< double > &A, const std::vector< double > &pers)
Port of returnPer.m: the percentiles of a (possibly defective) phase-type law, by uniformization.
Definition fj_codes.h:509
std::vector< FjCodesPercentiles > fj_main(const FjDist< double > &arrival, const FjDist< double > &service, const std::vector< double > &pers, const std::vector< std::size_t > &K, const std::vector< std::size_t > &Cs, FjTMode mode)
Port of mainFJ.m: the response-time percentiles of a K-node fork-join queue, interpolated between the...
Definition fj_codes.h:794
FjTMode fj_parse_tmode(const std::string &s)
Parse the reference's T_Mode string, whose default is 'NARE'.
Definition fj_codes.h:105
std::vector< double > fj_boundary_solve(const Matrix< double > &pi0mat, const Matrix< double > &T)
The boundary solve both branches of computePi.m end with:
Definition fj_codes.h:360
FjCodesSRK fj_construct_srk(std::size_t C, const FjDist< double > &service, const FjCodesServiceH &h, const Matrix< double > &S)
Port of constructSRK.m.
Matrix< double > fj_compute_t_nare(const Matrix< double > &D0, const Matrix< double > &D1, const Matrix< double > &S, const Matrix< double > &A_jump, double *residual)
Port of computeT_NARE.m: the T matrix as the stable invariant subspace of.
Definition fj_codes.h:216
FjCodesServiceH fj_build_service_h(const FjDist< double > &service)
Port of build_Service_h.m.
FjCodesGenService fj_generate_service(const FjDist< double > &service, const FjCodesServiceH &h, std::size_t C, const Matrix< double > &S)
Port of generateService.m.
FjCodesT fj_compute_t(const FjDist< double > &arrival, const FjDist< double > &service, const FjCodesServiceH &h, std::size_t C, FjTMode mode)
Port of computeT.m.
Definition fj_codes.h:295
FjCodesRT2 fj_return_rt2(const FjDist< double > &arrival, const FjDist< double > &service, const std::vector< double > &pers, std::size_t C, FjTMode mode)
Port of returnRT2.m: the response-time percentiles of the TWO-node fork-join queue,...
Definition fj_codes.h:653
FjTMode
Which route computeT.m takes to the T matrix.
Definition fj_codes.h:102
Matrix< double > fj_construct_not_all_busy(std::size_t C, const FjDist< double > &service, const FjCodesServiceH &h)
Port of constructNotAllBusy.m.
FjCodesSA fj_build_sa(const FjDist< double > &service, const FjCodesServiceH &h, std::size_t C)
Port of build_SA.m.
FjCodesPi fj_compute_pi(const Matrix< double > &T, const FjDist< double > &arrival, const FjDist< double > &service, const FjCodesServiceH &h, std::size_t C, const Matrix< double > &S, const Matrix< double > &A_jump)
Port of computePi.m: the all-busy boundary vector and E[n1].
Definition fj_codes.h:391
FjCodesWait fj_return_wait(double En1, const std::vector< double > &pi0, const Matrix< double > &T, const std::vector< double > &phi, const std::vector< double > &sum_Ajump)
Port of returnWait.m: the stationary waiting time as a phase-type law.
Definition fj_codes.h:466
Matrix< T > krons(const Matrix< T > &A, const Matrix< T > &B)
Kronecker sum, MATLAB's krons: kron(A, I_nb) + kron(I_na, B).
Definition mmap_lambda.h:71
Matrix< T > kron(const Matrix< T > &A, const Matrix< T > &B)
Kronecker product.
Definition mmap_lambda.h:57
std::vector< StDistrPh< T > > mmapph1fcfs_stdistr_ph(const Mmap< T > &arrival, const std::vector< PhService< T > > &svc, double precision=1e-14)
Per-class SOJOURN TIME as a continuous phase-type law, BUTools' 'stDistrPH'.
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:235
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
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 > 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:169
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
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Outcome of lstsq: the solution and whether the system was rank deficient.
Definition lstsq.h:58
std::vector< T > x
Definition lstsq.h:59
Real Schur factorization A = Z T Z^T, with Z orthogonal and T upper quasi-triangular: 1 x 1 diagonal ...
Definition eig.h:156
Matrix< double > T
upper quasi-triangular factor
Definition eig.h:158
generateService.m: the service process seen by a tagged job.
Matrix< double > T
(newdim + dim_notbusy) square
std::size_t newdim
the all-busy part
std::size_t dim_notbusy
the not-all-busy part
One line of mainFJ's output cell: the percentiles of a K-node queue.
Definition fj_codes.h:140
std::vector< double > percentiles
in PERCENT, as the reference stores them
Definition fj_codes.h:142
std::vector< double > RTp
Definition fj_codes.h:143
What computePi.m returns.
Definition fj_codes.h:126
std::vector< double > pi0
unnormalized, length newdim * ma
Definition fj_codes.h:127
double En1
mean number of arrivals in a not-all-busy period
Definition fj_codes.h:128
What returnRT2.m produces, plus the waiting-time law it discards.
Definition fj_codes.h:631
std::vector< double > RTp
response-time percentiles
Definition fj_codes.h:632
std::vector< double > wait_alpha
the waiting-time law, defective
Definition fj_codes.h:633
Matrix< double > wait_Smat
Definition fj_codes.h:634
double residual
the T-matrix residual of computeT
Definition fj_codes.h:636
build_SA.m: the level-constant generator and the head-of-line jump.
Matrix< double > A_jump
a job completes and the next enters service
Matrix< double > S
no job completes, ((C+1) m^2) square
constructSRK.m: the extended generator and the busy/idle projectors.
Matrix< double > R0
not-busy to busy, on an arrival
Matrix< double > Se
busy and not-busy phases together
Matrix< double > Sestar
the busy-to-not-busy block of Se, in place
Matrix< double > Ke
newdim x (newdim + dim_notbusy), the busy rows
Matrix< double > Kc
(newdim + dim_notbusy) x newdim, the busy columns
build_Service_h.m: the two-subtask phase process of one fork-join job.
What computeT.m returns.
Definition fj_codes.h:115
Matrix< double > T
the all-busy generator, (newdim * ma) square
Definition fj_codes.h:116
Matrix< double > S
build_SA's S, newdim square
Definition fj_codes.h:117
double residual
inf-norm the reference prints
Definition fj_codes.h:121
std::vector< double > sum_Ajump
row sums of kron(A_jump, I_ma)
Definition fj_codes.h:120
Matrix< double > S_Arr
kron(S, I_ma)
Definition fj_codes.h:119
std::size_t iterations
Sylvester mode only.
Definition fj_codes.h:122
Matrix< double > A_jump
build_SA's A_jump, newdim square
Definition fj_codes.h:118
What returnWait.m returns: the waiting time as a phase-type law.
Definition fj_codes.h:132
std::vector< double > wait_alpha
defective, mass prob_wait
Definition fj_codes.h:133
std::vector< double > alfa
-pi0 T^-1, the all-busy occupancy
Definition fj_codes.h:136
Matrix< double > wait_Smat
Definition fj_codes.h:134
Descriptor of an arrival or a service process.
Definition fj_dist2fj.h:76
Matrix< T > lambda1
Definition fj_dist2fj.h:80
std::vector< T > tau_st
Definition fj_dist2fj.h:87
Matrix< T > lambda0
Definition fj_dist2fj.h:79
Matrix< T > Ia
Definition fj_dist2fj.h:82
std::size_t ma
Definition fj_dist2fj.h:81
Matrix< T > ST
Definition fj_dist2fj.h:85
An MMAP: the underlying MAP plus the per-class arrival matrices.
Definition mmap_lambda.h:45
Matrix< T > D0
Definition mmap_lambda.h:46
Matrix< T > D1
Definition mmap_lambda.h:47
std::vector< Matrix< T > > Dc
per-class matrices, sum_c Dc = D1
Definition mmap_lambda.h:48
The Sylvester equation A X + X B = C, and MATLAB's lyap(A,B,C).