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 `strfind(T_Mode, 'Sylvest') > 0` and defaults to NARE
107 // for everything else, including an empty string and a misspelling.
108 if (s.find("Sylvest") != std::string::npos) return FjTMode::Sylvester;
109 return FjTMode::Nare;
110}
111
112/** What `computeT.m` returns. */
113struct FjCodesT {
114 Matrix<double> T; ///< the all-busy generator, (newdim * ma) square
115 Matrix<double> S; ///< build_SA's S, newdim square
116 Matrix<double> A_jump; ///< build_SA's A_jump, newdim square
117 Matrix<double> S_Arr; ///< kron(S, I_ma)
118 std::vector<double> sum_Ajump; ///< row sums of kron(A_jump, I_ma)
119 double residual = 0.0; ///< inf-norm the reference prints
120 std::size_t iterations = 0; ///< Sylvester mode only
121};
122
123/** What `computePi.m` returns. */
124struct FjCodesPi {
125 std::vector<double> pi0; ///< unnormalized, length newdim * ma
126 double En1 = 0.0; ///< mean number of arrivals in a not-all-busy period
127};
128
129/** What `returnWait.m` returns: the waiting time as a phase-type law. */
131 std::vector<double> wait_alpha; ///< defective, mass prob_wait
133 double prob_wait = 0.0;
134 std::vector<double> alfa; ///< -pi0 T^-1, the all-busy occupancy
135};
136
137/** One line of `mainFJ`'s output cell: the percentiles of a K-node queue. */
139 std::size_t K = 0;
140 std::vector<double> percentiles; ///< in PERCENT, as the reference stores them
141 std::vector<double> RTp;
142};
143
144namespace fjdetail {
145
146/** MATLAB `A / B` for a row vector: x with x B = a. */
147inline std::vector<double> rdivide_row(const std::vector<double>& a, const Matrix<double>& B) {
148 const Matrix<double> iB = inverse(B);
149 return vecmul(a, iB);
150}
151
152/** Elementwise infinity norm of A - B. */
153inline double max_abs_diff(const Matrix<double>& A, const Matrix<double>& B) {
154 double d = 0.0;
155 for (std::size_t i = 0; i < A.rows(); ++i)
156 for (std::size_t j = 0; j < A.cols(); ++j) {
157 const double x = std::fabs(A(i, j) - B(i, j));
158 if (x > d) d = x;
159 }
160 return d;
161}
162
163/** The inf-norm (maximum absolute row sum) the reference reports. */
164inline double inf_norm(const Matrix<double>& A) {
165 double best = 0.0;
166 for (std::size_t i = 0; i < A.rows(); ++i) {
167 double s = 0.0;
168 for (std::size_t j = 0; j < A.cols(); ++j) s += std::fabs(A(i, j));
169 if (s > best) best = s;
170 }
171 return best;
172}
173
174inline Matrix<double> madd(const Matrix<double>& A, const Matrix<double>& B) {
175 Matrix<double> C(A.rows(), A.cols());
176 for (std::size_t i = 0; i < A.rows(); ++i)
177 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = A(i, j) + B(i, j);
178 return C;
179}
180
181inline Matrix<double> msub(const Matrix<double>& A, const Matrix<double>& B) {
182 Matrix<double> C(A.rows(), A.cols());
183 for (std::size_t i = 0; i < A.rows(); ++i)
184 for (std::size_t j = 0; j < A.cols(); ++j) C(i, j) = A(i, j) - B(i, j);
185 return C;
186}
187
188/** A sub-block of A, rows [r0, r1) and columns [c0, c1). */
189inline Matrix<double> block(const Matrix<double>& A, std::size_t r0, std::size_t r1,
190 std::size_t c0, std::size_t c1) {
191 Matrix<double> B(r1 - r0, c1 - c0, 0.0);
192 for (std::size_t i = r0; i < r1; ++i)
193 for (std::size_t j = c0; j < c1; ++j) B(i - r0, j - c0) = A(i, j);
194 return B;
195}
196
197} // namespace fjdetail
198
199/**
200 * Port of `computeT_NARE.m`: the T matrix as the stable invariant subspace of
201 *
202 * H = [ I (x) D0 I (x) D1 ]
203 * [ -A_jump (x) I -S ]
204 *
205 * The m eigenvalues of SMALLEST real part are ordered to the front of the real
206 * Schur form, X is read off the resulting basis as Q1(m+1:2m, 1:m) /
207 * Q1(1:m, 1:m), and T = S + X (I (x) D1).
208 *
209 * A complex-conjugate pair astride the m/2m boundary would ask for an invariant
210 * subspace that does not exist over the reals; LAPACK's dtrexc refuses to split
211 * such a block, and the split is detected here first so that it is named as the
212 * spectrum condition it is rather than as a reordering failure.
213 */
215 const Matrix<double>& S, const Matrix<double>& A_jump,
216 double* residual) {
217 const std::size_t m = S.rows();
218 const std::size_t ma = D0.rows();
219 if (ma == 0 || m % ma != 0)
220 throw InputError("fj_compute_t_nare: the phase space is not a multiple of the arrival "
221 "order");
222 const std::size_t ms = m / ma;
223 const Matrix<double> Ims = eye<double>(ms);
224 const Matrix<double> Ima = eye<double>(ma);
225 const Matrix<double> ImsD0 = mam::kron(Ims, D0);
226 const Matrix<double> ImsD1 = mam::kron(Ims, D1);
227 const Matrix<double> AjIma = mam::kron(A_jump, Ima);
228
229 Matrix<double> H(2 * m, 2 * m, 0.0);
230 for (std::size_t i = 0; i < m; ++i)
231 for (std::size_t j = 0; j < m; ++j) {
232 H(i, j) = ImsD0(i, j);
233 H(i, m + j) = ImsD1(i, j);
234 H(m + i, j) = -AjIma(i, j);
235 H(m + i, m + j) = -S(i, j);
236 }
237
238 const RealSchur sc = schur_decomposition(H);
239 // The real part of each diagonal entry: a 2 x 2 block contributes its own
240 // trace/2 to both of its entries, which is what ordeig reports for the pair.
241 const std::size_t n = 2 * m;
242 std::vector<double> re(n, 0.0);
243 std::vector<int> blk(n, 1); // 1 for a 1 x 1 block, 2 for the first row of a 2 x 2
244 for (std::size_t i = 0; i < n;) {
245 const bool pair = (i + 1 < n) && sc.T(i + 1, i) != 0.0;
246 if (pair) {
247 const double r = 0.5 * (sc.T(i, i) + sc.T(i + 1, i + 1));
248 re[i] = r;
249 re[i + 1] = r;
250 blk[i] = 2;
251 blk[i + 1] = 0;
252 i += 2;
253 } else {
254 re[i] = sc.T(i, i);
255 blk[i] = 1;
256 i += 1;
257 }
258 }
259 std::vector<std::size_t> order(n);
260 for (std::size_t i = 0; i < n; ++i) order[i] = i;
261 std::stable_sort(order.begin(), order.end(),
262 [&re](std::size_t a, std::size_t b) { return re[a] < re[b]; });
263 std::vector<double> key(n, 0.0);
264 for (std::size_t i = 0; i < m; ++i) key[order[i]] = 1.0;
265 for (std::size_t i = 0; i + 1 < n; ++i)
266 if (blk[i] == 2 && key[i] != key[i + 1])
267 throw NumericError(
268 "fj_compute_t_nare: the m eigenvalues of smallest real part split a complex "
269 "conjugate pair, so the stable invariant subspace of the Riccati pencil is not "
270 "real and the T matrix of this model is not defined");
271
272 const RealSchur ord = schur_reorder(sc, key);
273 const Matrix<double> Q11 = fjdetail::block(ord.Z, 0, m, 0, m);
274 const Matrix<double> Q21 = fjdetail::block(ord.Z, m, 2 * m, 0, m);
275 const Matrix<double> X = matmul(Q21, inverse(Q11));
276 const Matrix<double> T = fjdetail::madd(S, matmul(X, ImsD1));
277 if (residual != nullptr)
278 *residual = fjdetail::inf_norm(
279 fjdetail::madd(fjdetail::madd(matmul(T, X), matmul(X, ImsD0)), AjIma));
280 return T;
281}
282
283/**
284 * Port of `computeT.m`.
285 *
286 * The Sylvester route is the paper's Section 5.1 and the NARE route its Section
287 * 5.2; they solve the same equation and the reference defaults to NARE. Both
288 * are offered because `options.config.fj_tmode` selects between them, and
289 * because they fail on different models: the iteration converges linearly and
290 * can stall, while the Schur route is direct but needs the stable subspace to
291 * be real.
292 */
293inline FjCodesT fj_compute_t(const FjDist<double>& arrival, const FjDist<double>& service,
294 const FjCodesServiceH& h, std::size_t C, FjTMode mode) {
295 const FjCodesSA sa = fj_build_sa(service, h, C);
296 const std::size_t d0 = arrival.lambda0.rows();
297 const Matrix<double> Id0 = eye<double>(d0);
298
299 FjCodesT out;
300 out.S = sa.S;
301 out.A_jump = sa.A_jump;
302 out.S_Arr = mam::kron(sa.S, Id0);
303 const Matrix<double> A_jump_Arr = mam::kron(sa.A_jump, Id0);
304
305 if (mode == FjTMode::Sylvester) {
306 const std::size_t ms = sa.S.rows();
307 const std::size_t m = ms * d0;
308 const Matrix<double> ID0 = mam::kron(eye<double>(ms), arrival.lambda0);
309 const Matrix<double> DS =
310 matmul(mam::kron(eye<double>(ms), arrival.lambda1), A_jump_Arr);
311 const Matrix<double> Im = eye<double>(m);
312 Matrix<double> Tnew = out.S_Arr;
313 Matrix<double> Told(m, m, 0.0);
315 // The reference iterates without a cap; 500 is far past the linear
316 // convergence of every model the gate admits, and a stall is reported.
317 const std::size_t max_iter = 500;
318 std::size_t it = 0;
319 while (fjdetail::max_abs_diff(Told, Tnew) > 1e-10) {
320 if (++it > max_iter)
321 throw NumericError(
322 "fj_compute_t: the Sylvester iteration of computeT.m did not reach 1e-10 in " +
323 std::to_string(max_iter) +
324 " steps; solve this model with the NARE route (config.fj_tmode = 'NARE')");
325 Told = Tnew;
326 L = lyap_schur(Tnew, ID0, Im);
327 Tnew = fjdetail::madd(out.S_Arr, matmul(L, DS));
328 }
329 out.T = Tnew;
330 out.iterations = it;
331 if (it > 0)
332 out.residual = fjdetail::inf_norm(
333 fjdetail::madd(fjdetail::madd(matmul(out.T, L), matmul(L, ID0)), Im));
334 } else {
335 out.T = fj_compute_t_nare(arrival.lambda0, arrival.lambda1, out.S_Arr, sa.A_jump,
336 &out.residual);
337 }
338
339 out.sum_Ajump.assign(A_jump_Arr.rows(), 0.0);
340 for (std::size_t i = 0; i < A_jump_Arr.rows(); ++i) {
341 double s = 0.0;
342 for (std::size_t j = 0; j < A_jump_Arr.cols(); ++j) s += A_jump_Arr(i, j);
343 out.sum_Ajump[i] = s;
344 }
345 return out;
346}
347
348/**
349 * The boundary solve both branches of `computePi.m` end with:
350 *
351 * pi0 [ pi0mat - I , T^-1 e ] = [ 0 ... 0 , -1 ]
352 *
353 * an OVERDETERMINED system by one column, which MATLAB's mrdivide answers in
354 * the least-squares sense. It is consistent -- the extra column is the
355 * normalization that fixes the scale of the null vector -- so least squares
356 * returns the exact solution and not an approximation.
357 */
358inline std::vector<double> fj_boundary_solve(const Matrix<double>& pi0mat,
359 const Matrix<double>& T) {
360 const std::size_t nd = pi0mat.rows();
361 if (pi0mat.cols() != nd || T.rows() != nd || T.cols() != nd)
362 throw InputError("fj_boundary_solve: the boundary blocks are not conformable");
363 const Matrix<double> iT = inverse(T);
364 Matrix<double> M(nd, nd + 1, 0.0);
365 for (std::size_t i = 0; i < nd; ++i) {
366 for (std::size_t j = 0; j < nd; ++j) M(i, j) = pi0mat(i, j) - (i == j ? 1.0 : 0.0);
367 double s = 0.0;
368 for (std::size_t j = 0; j < nd; ++j) s += iT(i, j);
369 M(i, nd) = s;
370 }
371 std::vector<double> b(nd + 1, 0.0);
372 b[nd] = -1.0;
373 const LstsqResult<double> r = lstsq(M.transpose(), b);
374 return r.x;
375}
376
377/**
378 * Port of `computePi.m`: the all-busy boundary vector and E[n1].
379 *
380 * The two branches are not two implementations of one formula. For EXPONENTIAL
381 * service the not-all-busy space has exactly as many phases as the all-busy one
382 * (m = 1 makes both (C + 1)-dimensional), so the return to the all-busy period
383 * is a square map and `Q0` can be inverted directly. For phase-type service the
384 * two spaces differ and the reference goes through `constructSRK`, whose Ke and
385 * Kc project between them. Running the second branch on exponential service
386 * would give the same answer; running the first on anything else is a shape
387 * error, which is why the reference switches on `SerChoice`.
388 */
389inline FjCodesPi fj_compute_pi(const Matrix<double>& T, const FjDist<double>& arrival,
390 const FjDist<double>& service, const FjCodesServiceH& h,
391 std::size_t C, const Matrix<double>& S,
392 const Matrix<double>& A_jump) {
393 const std::size_t da = arrival.lambda0.rows();
394 FjCodesPi out;
395
396 if (service.choice == 1) {
397 const std::size_t ms = S.cols();
398 const Matrix<double> S_notallbusy = fj_construct_not_all_busy(C, service, h);
399 const Matrix<double> Q0 = mam::krons(S_notallbusy, arrival.lambda0);
400 if (Q0.rows() != ms * da)
401 throw NumericError(
402 "fj_compute_pi: the not-all-busy space and the all-busy space have different "
403 "dimensions, which the exponential branch of computePi.m assumes they do not");
404 const Matrix<double> Igral =
406 const Matrix<double> pi0mat =
407 matmul(matmul(matmul(Igral, mam::kron(A_jump, eye<double>(da))), inverse(Q0)),
408 mam::kron(eye<double>(ms), arrival.lambda1));
409 out.pi0 = fj_boundary_solve(pi0mat, T);
410 double sp = 0.0;
411 for (double v : out.pi0) sp += v;
412 const std::vector<double> pm = vecmul(out.pi0, pi0mat);
413 double spm = 0.0;
414 for (double v : pm) spm += v;
415 out.En1 = spm / sp;
416 return out;
417 }
418
419 const FjCodesSRK srk = fj_construct_srk(C, service, h, S);
420 const std::size_t dtmat = T.rows() / arrival.lambda0.cols();
421 const std::size_t dsexp = srk.Se.rows();
422 const std::size_t dnb = dsexp - dtmat;
423
424 const Matrix<double> Sedash = fjdetail::block(srk.Se, dtmat, dsexp, dtmat, dsexp);
425 const Matrix<double> Rbusy = fjdetail::block(srk.R0, dtmat, dsexp, 0, dsexp);
426 Matrix<double> Iidle_small(dsexp, dnb, 0.0);
427 for (std::size_t i = 0; i < dnb; ++i) Iidle_small(dtmat + i, i) = 1.0;
428 const Matrix<double> Ida = eye<double>(da);
429 const Matrix<double> Iidle = mam::kron(Iidle_small, Ida);
430
431 const Matrix<double> Qidle = mam::krons(Sedash, arrival.lambda0);
432 const Matrix<double> Qbusy = mam::kron(Rbusy, arrival.lambda1);
433 const Matrix<double> Bmap = [&] {
434 const Matrix<double> M = matmul(matmul(Iidle, inverse(Qidle)), Qbusy);
435 Matrix<double> N(M.rows(), M.cols());
436 for (std::size_t i = 0; i < M.rows(); ++i)
437 for (std::size_t j = 0; j < M.cols(); ++j) N(i, j) = -M(i, j);
438 return N;
439 }();
440 const Matrix<double> Kemap = mam::kron(srk.Ke, Ida);
441 const Matrix<double> Kcmap = mam::kron(srk.Kc, Ida);
442
443 const Matrix<double> BB = mam::kron(eye<double>(srk.Sestar.cols()), arrival.lambda0);
444 const Matrix<double> Igral =
445 lyap_schur(T, BB, matmul(Kemap, mam::kron(srk.Sestar, Ida)));
446 const Matrix<double> pi0mat = matmul(matmul(Igral, Bmap), Kcmap);
447 out.pi0 = fj_boundary_solve(pi0mat, T);
448 double sp = 0.0;
449 for (double v : out.pi0) sp += v;
450
451 // -pi0 Igral Iidle Qidle^-1 (I (x) D1), left to right as MATLAB reads it.
452 std::vector<double> row = vecmul(out.pi0, Igral);
453 for (double& v : row) v = -v;
454 row = vecmul(row, Iidle);
455 row = fjdetail::rdivide_row(row, Qidle);
456 row = vecmul(row, mam::kron(eye<double>(Sedash.cols()), arrival.lambda1));
457 double sr = 0.0;
458 for (double v : row) sr += v;
459 out.En1 = sr / sp;
460 return out;
461}
462
463/** Port of `returnWait.m`: the stationary waiting time as a phase-type law. */
464inline FjCodesWait fj_return_wait(double En1, const std::vector<double>& pi0,
465 const Matrix<double>& T, const std::vector<double>& phi,
466 const std::vector<double>& sum_Ajump) {
467 const std::size_t ds = phi.size();
468 FjCodesWait out;
469 out.alfa = fjdetail::rdivide_row(pi0, T);
470 for (double& v : out.alfa) v = -v;
471
472 double ap = 0.0;
473 for (std::size_t i = 0; i < ds; ++i) ap += out.alfa[i] * phi[i];
474 std::vector<double> rhos(ds, 0.0);
475 for (std::size_t i = 0; i < ds; ++i) rhos[i] = phi[i] * out.alfa[i] / ap;
476
477 double sp = 0.0;
478 for (double v : pi0) sp += v;
479 double asum = 0.0;
480 for (std::size_t i = 0; i < ds; ++i) asum += out.alfa[i] * sum_Ajump[i];
481 const double En0 = asum / sp;
482
483 out.prob_wait = (En0 - 1.0) / (En0 - 1.0 + En1);
484 out.wait_alpha.assign(ds, 0.0);
485 for (std::size_t i = 0; i < ds; ++i) out.wait_alpha[i] = out.prob_wait * rhos[i];
486
487 out.wait_Smat = Matrix<double>(ds, ds, 0.0);
488 for (std::size_t i = 0; i < ds; ++i)
489 for (std::size_t j = 0; j < ds; ++j)
490 out.wait_Smat(i, j) = out.alfa[j] * T(j, i) / out.alfa[i];
491 return out;
492}
493
494/**
495 * Port of `returnPer.m`: the percentiles of a (possibly defective) phase-type
496 * law, by uniformization.
497 *
498 * The law is uniformized at c = max(-diag(A)) into P = A / c + I, the
499 * absorption probability by time t is the Poisson mixture sum_k p_k(ct) a_k
500 * with a_k = alpha P^k e, and the series is truncated where its partial sums
501 * reach the total mass alpha (I - P)^-1 e. The percentile itself is then found
502 * by scanning t downwards on a 0.001 grid from three times the mean, extending
503 * the bracket by half a mean whenever the target is not yet reached. A target
504 * below the defect 1 - sum(alpha) is answered with zero, which is the
505 * reference's convention for a percentile the law never attains.
506 */
507inline std::vector<double> fj_return_per(const std::vector<double>& vec, const Matrix<double>& A,
508 const std::vector<double>& pers) {
509 using std::exp;
510 const std::size_t m = A.cols();
511 if (vec.size() != m || A.rows() != m)
512 throw InputError("fj_return_per: the phase-type pair is not conformable");
513
514 const std::vector<double> negvA = [&] {
515 std::vector<double> v = vecmul(vec, inverse(A));
516 for (double& x : v) x = -x;
517 return v;
518 }();
519 double meanRT = 0.0;
520 for (double v : negvA) meanRT += v;
521 if (!(meanRT > 0.0))
522 throw NumericError("fj_return_per: the phase-type law has a non-positive mean, so its "
523 "percentiles are not defined");
524
525 double c = 0.0;
526 for (std::size_t i = 0; i < m; ++i) c = std::max(c, -A(i, i));
527 if (!(c > 0.0))
528 throw NumericError("fj_return_per: the phase-type generator has no negative diagonal");
529 Matrix<double> P(m, m, 0.0);
530 for (std::size_t i = 0; i < m; ++i)
531 for (std::size_t j = 0; j < m; ++j) P(i, j) = A(i, j) / c + (i == j ? 1.0 : 0.0);
532
533 Matrix<double> ImP(m, m, 0.0);
534 for (std::size_t i = 0; i < m; ++i)
535 for (std::size_t j = 0; j < m; ++j) ImP(i, j) = (i == j ? 1.0 : 0.0) - P(i, j);
536 const std::vector<double> vImP = vecmul(vec, inverse(ImP));
537 double M = 0.0;
538 for (double v : vImP) M += v;
539
540 double a0 = 0.0;
541 for (double v : vec) a0 += v;
542 double sum_a = a0;
543 std::vector<double> ak;
544 std::vector<double> vP(m, 0.0);
545 for (std::size_t i = 0; i < m; ++i) {
546 double s = 0.0;
547 for (std::size_t j = 0; j < m; ++j) s += P(i, j);
548 vP[i] = s;
549 }
550 while (std::fabs(sum_a - M) >= 1e-10) {
551 double t = 0.0;
552 for (std::size_t i = 0; i < m; ++i) t += vec[i] * vP[i];
553 ak.push_back(t);
554 sum_a += t;
555 vP = mulvec(P, vP);
556 if (ak.size() > 2000000)
557 throw NumericError("fj_return_per: the uniformized Poisson series did not reach the "
558 "total absorption mass, so the percentile scan cannot terminate");
559 }
560 const std::size_t K1 = ak.size();
561
562 // The absorption CDF at t, as the reference evaluates it.
563 const auto cdf_at = [&](double t) {
564 double pM = exp(-c * t);
565 double F = pM * a0;
566 for (std::size_t k = 1; k <= K1; ++k) {
567 pM = c * t * pM / static_cast<double>(k);
568 F += pM * ak[k - 1];
569 }
570 return 1.0 - F;
571 };
572
573 std::vector<double> out(pers.size(), 0.0);
574 for (std::size_t p = 0; p < pers.size(); ++p) {
575 if (pers[p] < 1.0 - a0) continue; // the law never attains this percentile
576 double MaxTime = 3.0 * meanRT;
577 for (;;) {
578 if (cdf_at(MaxTime) < pers[p]) {
579 MaxTime += 0.5 * meanRT;
580 continue;
581 }
582 bool found = false;
583 // MATLAB's colon operator indexes the grid, it does not accumulate
584 // a subtraction, so t is formed as MaxTime - k * 0.001.
585 const std::size_t steps = static_cast<std::size_t>(MaxTime / 0.001);
586 for (std::size_t k = 0; k <= steps; ++k) {
587 const double t = MaxTime - static_cast<double>(k) * 0.001;
588 if (cdf_at(t) < pers[p]) {
589 out[p] = t + 0.001;
590 found = true;
591 break;
592 }
593 }
594 if (!found)
595 throw NumericError(
596 "fj_return_per: the downward scan reached t = 0 without the response-time CDF "
597 "falling below " + std::to_string(pers[p]) +
598 ", so this percentile has no bracket; the reference silently reports the "
599 "previous percentile here");
600 break;
601 }
602 }
603 return out;
604}
605
606/**
607 * Port of `returnRT1.m`: the response-time percentiles of the ONE-node queue,
608 * which are exact.
609 */
610inline std::vector<double> fj_return_rt1(const FjDist<double>& arrival,
611 const FjDist<double>& service,
612 const std::vector<double>& pers) {
614 mm.D0 = arrival.lambda0;
615 mm.D1 = arrival.lambda1;
616 mm.Dc.push_back(arrival.lambda1);
617
618 std::vector<mam::PhService<double>> svc(1);
619 svc[0].sigma = service.tau_st;
620 svc[0].S = service.ST;
621
622 const std::vector<mam::StDistrPh<double>> ph = mam::mmapph1fcfs_stdistr_ph(mm, svc);
623 if (ph.empty())
624 throw NumericError("fj_return_rt1: MMAPPH1FCFS returned no sojourn-time law");
625 return fj_return_per(ph[0].alpha, ph[0].A, pers);
626}
627
628/** What `returnRT2.m` produces, plus the waiting-time law it discards. */
630 std::vector<double> RTp; ///< response-time percentiles
631 std::vector<double> wait_alpha; ///< the waiting-time law, defective
633 double prob_wait = 0.0;
634 double residual = 0.0; ///< the T-matrix residual of computeT
635};
636
637/**
638 * Port of `returnRT2.m`: the response-time percentiles of the TWO-node
639 * fork-join queue, under the Section 4 approximation.
640 *
641 * The response time is assembled as one phase-type law over three blocks: the
642 * service process of a job that arrives in a not-all-busy period, the TIME-
643 * REVERSED service process of a job that arrives in an all-busy period, and the
644 * waiting time. The reversal is what lets the job's own service be appended to
645 * the waiting time it accrued: `stat_service_phase` is the stationary phase
646 * occupancy, `tr_ST` is the reversed generator, and `tildeP` is the coupling
647 * that hands the reversed process over to the waiting-time block. Phases of
648 * zero stationary occupancy are dropped -- they are unreachable and the
649 * reversal divides by their occupancy.
650 */
651inline FjCodesRT2 fj_return_rt2(const FjDist<double>& arrival, const FjDist<double>& service,
652 const std::vector<double>& pers, std::size_t C, FjTMode mode) {
653 const FjCodesServiceH h = fj_build_service_h(service);
654 const FjCodesT ct = fj_compute_t(arrival, service, h, C, mode);
655 const std::size_t mWait = ct.A_jump.rows();
656 const std::size_t n = ct.T.rows();
657
658 std::vector<double> phi(n, 0.0);
659 for (std::size_t i = 0; i < n; ++i) {
660 double s = 0.0;
661 for (std::size_t j = 0; j < n; ++j) s += ct.T(i, j) - ct.S_Arr(i, j);
662 phi[i] = s;
663 }
664 const FjCodesPi pi = fj_compute_pi(ct.T, arrival, service, h, C, ct.S, ct.A_jump);
665 const FjCodesWait w = fj_return_wait(pi.En1, pi.pi0, ct.T, phi, ct.sum_Ajump);
666
667 const FjCodesGenService gs = fj_generate_service(service, h, C, ct.S);
668 const Matrix<double> ST = mam::kron(gs.T, arrival.Ia);
669
670 std::vector<double> pi0n = pi.pi0;
671 double sp = 0.0;
672 for (double v : pi0n) sp += v;
673 for (double& v : pi0n) v /= sp;
674
675 const std::size_t dim = arrival.ma * gs.newdim;
676 const std::size_t dim_notbusy = arrival.ma * gs.dim_notbusy;
677 const std::size_t dim_service = dim + dim_notbusy;
678 const std::size_t Tr = ST.rows();
679 const std::size_t Sc = w.wait_Smat.cols();
680 if (Tr != dim_service)
681 throw NumericError("fj_return_rt2: the tagged-job service space and the phase blocks "
682 "disagree in size");
683
684 std::vector<double> notbusy_start(dim_service, 0.0);
685 for (std::size_t i = 0; i < dim; ++i)
686 notbusy_start[i] = (1.0 - w.prob_wait) * pi0n[i];
687
688 // TS = T - S_Arr, the rates at which a new job enters service, then row
689 // normalized into the jump kernel of the all-busy phase.
690 Matrix<double> TS = fjdetail::msub(ct.T, ct.S_Arr);
691 std::vector<double> busy_start(dim_service, 0.0);
692 {
693 const std::vector<double> aTS = vecmul(w.alfa, TS);
694 if (aTS.size() != dim)
695 throw NumericError("fj_return_rt2: the all-busy phase space and the tagged job's "
696 "all-busy service block have different sizes");
697 double s = 0.0;
698 for (double v : aTS) s += v;
699 for (std::size_t i = 0; i < dim; ++i) busy_start[i] = w.prob_wait * aTS[i] / s;
700 }
701 for (std::size_t i = 0; i < TS.rows(); ++i) {
702 double s = 0.0;
703 for (std::size_t j = 0; j < TS.cols(); ++j) s += TS(i, j);
704 for (std::size_t j = 0; j < TS.cols(); ++j) TS(i, j) /= s;
705 }
706
707 const std::vector<double> stat = [&] {
708 Matrix<double> negST(Tr, Tr);
709 for (std::size_t i = 0; i < Tr; ++i)
710 for (std::size_t j = 0; j < Tr; ++j) negST(i, j) = -ST(i, j);
711 return fjdetail::rdivide_row(busy_start, negST);
712 }();
713 std::vector<bool> nz(Tr, false);
714 for (std::size_t i = 0; i < Tr; ++i) nz[i] = stat[i] > 0.0;
715
716 std::vector<double> tr_start(Tr, 0.0);
717 for (std::size_t j = 0; j < Tr; ++j) {
718 double s = 0.0;
719 for (std::size_t k = 0; k < Tr; ++k) s += ST(j, k);
720 tr_start[j] = -s * stat[j];
721 }
722
723 Matrix<double> tr_ST(Tr, Tr, 0.0);
724 for (std::size_t i = 0; i < Tr; ++i) {
725 if (!nz[i]) continue;
726 for (std::size_t j = 0; j < Tr; ++j)
727 if (nz[j]) tr_ST(i, j) = ST(j, i) * stat[j] / stat[i];
728 }
729 std::vector<double> tr_exit(Tr, 0.0);
730 for (std::size_t i = 0; i < Tr; ++i) {
731 double s = 0.0;
732 for (std::size_t j = 0; j < Tr; ++j) s += tr_ST(i, j);
733 tr_exit[i] = -s;
734 }
735
736 // TS2 = TS' diag(alfa), row normalized with the reference's 1e-11 floor.
737 Matrix<double> TS2(Sc, Sc, 0.0);
738 for (std::size_t i = 0; i < Sc; ++i)
739 for (std::size_t j = 0; j < Sc; ++j) TS2(i, j) = TS(j, i) * w.alfa[j];
740 for (std::size_t i = 0; i < Sc; ++i) {
741 double s = 0.0;
742 for (std::size_t j = 0; j < Sc; ++j) s += TS2(i, j);
743 if (std::fabs(s) < 10e-12) s = 1.0;
744 for (std::size_t j = 0; j < Sc; ++j) TS2(i, j) /= s;
745 }
746
747 Matrix<double> tildeP(Tr, Sc, 0.0);
748 for (std::size_t i = 0; i < Sc; ++i)
749 for (std::size_t j = 0; j < Sc; ++j) tildeP(i, j) = tr_exit[i] * TS2(i, j);
750
751 std::vector<std::size_t> keep;
752 for (std::size_t i = 0; i < Tr; ++i)
753 if (nz[i]) keep.push_back(i);
754 const std::size_t m_tr = keep.size();
755
756 const std::size_t total = Tr + m_tr + Sc;
757 std::vector<double> gamma(total, 0.0);
758 for (std::size_t i = 0; i < Tr; ++i) gamma[i] = notbusy_start[i];
759 for (std::size_t i = 0; i < m_tr; ++i) gamma[Tr + i] = tr_start[keep[i]];
760
761 Matrix<double> Cres(total, total, 0.0);
762 for (std::size_t i = 0; i < Tr; ++i)
763 for (std::size_t j = 0; j < Tr; ++j) Cres(i, j) = ST(i, j);
764 for (std::size_t i = 0; i < m_tr; ++i) {
765 for (std::size_t j = 0; j < m_tr; ++j) Cres(Tr + i, Tr + j) = tr_ST(keep[i], keep[j]);
766 for (std::size_t j = 0; j < Sc; ++j) Cres(Tr + i, Tr + m_tr + j) = tildeP(keep[i], j);
767 }
768 for (std::size_t i = 0; i < Sc; ++i)
769 for (std::size_t j = 0; j < Sc; ++j)
770 Cres(Tr + m_tr + i, Tr + m_tr + j) = w.wait_Smat(i, j);
771
772 FjCodesRT2 out;
773 out.RTp = fj_return_per(gamma, Cres, pers);
774 out.wait_alpha = w.wait_alpha;
775 out.wait_Smat = w.wait_Smat;
776 out.prob_wait = w.prob_wait;
777 out.residual = ct.residual;
778 (void)mWait;
779 return out;
780}
781
782/**
783 * Port of `mainFJ.m`: the response-time percentiles of a K-node fork-join
784 * queue, interpolated between the exact one-node and the approximate two-node
785 * results in log K.
786 *
787 * @param pers the target percentiles as PROBABILITIES in (0, 1)
788 * @param K one entry per fork-join width to report
789 * @param Cs the truncation levels; only the LAST one reaches the answer, as
790 * in the reference
791 */
792inline std::vector<FjCodesPercentiles> fj_main(const FjDist<double>& arrival,
793 const FjDist<double>& service,
794 const std::vector<double>& pers,
795 const std::vector<std::size_t>& K,
796 const std::vector<std::size_t>& Cs, FjTMode mode) {
797 using std::log;
798 if (!(arrival.lambda / service.mu < 1.0))
799 throw InputError("mainFJ: the system is not stable, the mean arrival rate " +
800 std::to_string(arrival.lambda) +
801 " is not below the mean service rate " + std::to_string(service.mu));
802 if (Cs.empty()) throw InputError("mainFJ: no truncation level C was given");
803 if (pers.empty()) throw InputError("mainFJ: no percentile was requested");
804
805 std::vector<double> rt1, rt2;
806 for (std::size_t c = 0; c < Cs.size(); ++c) {
807 rt1 = fj_return_rt1(arrival, service, pers);
808 rt2 = fj_return_rt2(arrival, service, pers, Cs[c], mode).RTp;
809 }
810
811 std::vector<FjCodesPercentiles> out(K.size());
812 for (std::size_t k = 0; k < K.size(); ++k) {
813 out[k].K = K[k];
814 out[k].percentiles.resize(pers.size());
815 out[k].RTp.assign(pers.size(), 0.0);
816 for (std::size_t p = 0; p < pers.size(); ++p) {
817 out[k].percentiles[p] = 100.0 * pers[p];
818 out[k].RTp[p] = rt1[p] + (rt2[p] - rt1[p]) * log(static_cast<double>(K[k])) / log(2.0);
819 }
820 }
821 return out;
822}
823
824} // namespace fj
825} // namespace line
826
827#endif // LINE_API_FJ_FJ_CODES_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
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:610
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:507
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:792
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:358
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:214
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:293
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:651
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:389
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:464
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'.
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
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: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
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:169
Matrix< double > Z
orthogonal Schur vectors
Definition eig.h:170
Matrix< double > T
upper quasi-triangular factor
Definition eig.h:171
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:138
std::vector< double > percentiles
in PERCENT, as the reference stores them
Definition fj_codes.h:140
std::vector< double > RTp
Definition fj_codes.h:141
What computePi.m returns.
Definition fj_codes.h:124
std::vector< double > pi0
unnormalized, length newdim * ma
Definition fj_codes.h:125
double En1
mean number of arrivals in a not-all-busy period
Definition fj_codes.h:126
What returnRT2.m produces, plus the waiting-time law it discards.
Definition fj_codes.h:629
std::vector< double > RTp
response-time percentiles
Definition fj_codes.h:630
std::vector< double > wait_alpha
the waiting-time law, defective
Definition fj_codes.h:631
Matrix< double > wait_Smat
Definition fj_codes.h:632
double residual
the T-matrix residual of computeT
Definition fj_codes.h:634
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:113
Matrix< double > T
the all-busy generator, (newdim * ma) square
Definition fj_codes.h:114
Matrix< double > S
build_SA's S, newdim square
Definition fj_codes.h:115
double residual
inf-norm the reference prints
Definition fj_codes.h:119
std::vector< double > sum_Ajump
row sums of kron(A_jump, I_ma)
Definition fj_codes.h:118
Matrix< double > S_Arr
kron(S, I_ma)
Definition fj_codes.h:117
std::size_t iterations
Sylvester mode only.
Definition fj_codes.h:120
Matrix< double > A_jump
build_SA's A_jump, newdim square
Definition fj_codes.h:116
What returnWait.m returns: the waiting time as a phase-type law.
Definition fj_codes.h:130
std::vector< double > wait_alpha
defective, mass prob_wait
Definition fj_codes.h:131
std::vector< double > alfa
-pi0 T^-1, the all-busy occupancy
Definition fj_codes.h:134
Matrix< double > wait_Smat
Definition fj_codes.h:132
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).