LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_fj.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_SOLVERS_MAM_SOLVER_MAM_FJ_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_FJ_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_fj.m`, the fork-join route of SolverMAM.
12 *
13 * The reference analyzer is a THIN WRAPPER around one third-party engine:
14 * `mainFJ` of `matlab/lib/thirdparty/FJ_codes`, the response-time-tail
15 * approximation of Z. Qiu, J. F. Perez and P. Harrison, "Beyond the Mean in
16 * Fork-Join Queues: Efficient Approximation for Response-Time Tails" (IFIP
17 * Performance 2015). Everything `solver_mam_fj.m` computes on its own is either
18 * a topology check or an M/M/1 closed form; the one quantity that makes it a
19 * FORK-JOIN analyzer -- the synchronisation delay charged at the Join -- is
20 * `mainFJ`'s mean response time minus the branch response time.
21 *
22 * WHAT IS PORTED HERE: the gate, the parameter extraction and the wrapper,
23 * i.e. `fj_is_homogeneous.m`, `fj_extract_params.m` (on top of the already-
24 * ported `api/fj/fj_dist2fj.h`) and `solver_mam_fj.m` itself.
25 * `mam_fj_is_homogeneous` is the 2a predicate of `solver_mam_analyzer.m` and is
26 * what separates the fork-join models this analyzer claims from the ones that
27 * go to `solver_mam_basic_mmap`.
28 *
29 * THE ENGINE IS `api/fj/fj_codes.h` and `api/fj/fj_codes_matrices.h`: `mainFJ`
30 * and the chain it drives -- `returnRT1`, `returnRT2`, `computeT`,
31 * `computeT_NARE`, `constructSRK`, `build_SA`, `computePi`, `returnWait`,
32 * `returnPer`, `generateService`. It is double only, because the T matrix is
33 * the stable invariant subspace of a Riccati pencil (ordered real Schur) and
34 * the two Sylvester equations are of order (C + 1) m^2 ma; this file refuses by
35 * name at any other arithmetic.
36 *
37 * WHY THE MMT TRANSFORM IS NOT SUBSTITUTED FOR IT. `mva/fj_mmt.h` plus
38 * `mva/fj_driver.h` do solve fork-join models, and driving them with
39 * `solver_mam_basic` as the inner solve would produce a full metric tuple. It
40 * would not be this analyzer's tuple, and on this analyzer's models it would
41 * not be sound either: `fj_mmt` REUSES the model's own Source and Sink when it
42 * has them and adds a pair only for a closed layer, so on an open model the
43 * auxiliary stream is injected at the SAME Source that carries the real open
44 * chain -- the transform then reports one merged arrival stream where this
45 * analyzer's decomposition keeps the branch streams apart. The transform is
46 * exercised by SolverMVA and
47 * SolverNC on CLOSED layers, which is the case it was written for. Reporting
48 * its output as `solver_mam_fj` would be a different algorithm on a corrupted
49 * struct, under the reference's method name.
50 *
51 * WHAT THE `api/fj/` FAMILY IS NOT. This codebase does carry ported fork-join
52 * approximations -- `fj_synch_delay`, `fj_respt_2way`, `fj_respt_nt`,
53 * `fj_respt_varki`, `fj_respt_vm`, `fj_rmax`, `fj_order_stat`. They are
54 * DIFFERENT published approximations. Substituting one of them for `mainFJ`
55 * would answer the model under a method name that promises the Qiu-Perez-
56 * Harrison tail estimate, which is precisely the substitution
57 * `solver_mam_runner.h` refuses to make for non-Markovian service.
58 *
59 * REFERENCE DEFECTS in solver_mam_fj.m. 1, 2 and 4 are reproduced -- they are
60 * what the reference reports and changing them would answer differently under
61 * its method name; 3 is not, see `solver_mam_fj` for why that is not a change
62 * of result:
63 *
64 * 1. THE BRANCH METRICS IGNORE THE SERVICE REPRESENTATION ENTIRELY. Every
65 * parallel queue is reported at U = lambda/mu, Q = rho/(1 - rho) and
66 * R = 1/(mu - lambda), the M/M/1 closed forms, even though the gate has
67 * just admitted Erlang(2), HyperExp(2) and MAP(2) service and the arrival
68 * may be a MAP(2). For anything but Exp/Exp those are the numbers of a
69 * different queue, and they are not flagged as approximate.
70 * 2. NO STABILITY GUARD ON THE REPORTED MEANS. When lambda >= mu the same
71 * formulas return a negative queue length and a negative response time.
72 * `mainFJ` itself errors on load >= 1 and `fj_extract_params` warns, so the
73 * model is stopped -- but only by the engine, one call later, and the two
74 * disagree about whether it is an error.
75 * 3. `mainFJ` IS CALLED TWICE PER CLASS, once on a 21-point percentile grid
76 * for the mean and once on the four stored percentiles, and the second call
77 * recomputes the entire T matrix to read four points off a curve the first
78 * call already produced.
79 * 4. THE MEAN IS A TRAPEZOID OVER THE INVERSE CDF with an invented tail: the
80 * grid stops at the 99.9th percentile and the integration closes at p = 1
81 * with `RTp(end) * 1.1`. The 1.1 has no derivation, and for a heavy tail it
82 * is the dominant term of the mean.
83 *
84 * ARITHMETIC. `fj_dist2fj` needs one linear solve per process and stays in the
85 * field, so the GATE and the EXTRACTION hold at every instantiation and an
86 * unsupported topology or distribution is still named exactly at Rational. The
87 * ENGINE is double only and `solver_mam_fj` refuses past that point, which is
88 * why the two checks run before the arithmetic gate rather than after it.
89 */
90
91#include <cmath>
92#include <cstddef>
93#include <string>
94#include <type_traits>
95#include <vector>
96
104#include "line/util/error.h"
105
106namespace line {
107namespace mam {
108
109/** What `fj_is_homogeneous.m` returns: the fork-join pair, or why there is none. */
110struct MamFjInfo {
111 bool ok = false;
112 std::size_t forkNode = 0; ///< 1-based node index
113 std::size_t joinNode = 0; ///< 1-based node index
114 std::vector<std::size_t> queueNodes; ///< the K parallel branches, 1-based node indices
115 std::size_t K = 0;
116 std::string why; ///< the reference's `errorMsg`, empty when ok
117};
118
119/** What `fj_extract_params.m` returns: one arrival and one service per class. */
120template <class T>
122 std::vector<fj::FjDist<T> > arrival;
123 std::vector<fj::FjDist<T> > service;
124 std::size_t K = 0;
125};
126
127namespace fj_detail {
128
129/**
130 * The class-aggregated node routing the reference reads as `sn.rtnodes(i, j)`:
131 * is there ANY class pair routed from i to j? A branch is identified by the
132 * topology, not by which class traverses it.
133 */
134template <class T>
135bool routes_between(const qn::NetworkStruct<T>& L, std::size_t i, std::size_t j) {
136 const T zero = num_traits<T>::from_int(0);
137 for (std::size_t r = 1; r <= L.nclasses; ++r)
138 for (std::size_t s = 1; s <= L.nclasses; ++s)
139 if (L.get_route(r, s, i, j) > zero) return true;
140 return false;
141}
142
143/** The reference's entrywise (D0, D1) comparison at GlobalConstants::FineTol. */
144template <class T>
145bool same_map(const Map<T>& a, const Map<T>& b) {
146 if (a.order() != b.order()) return false;
147 const double tol = lang::GlobalConstants::FineTol;
148 for (std::size_t i = 0; i < a.order(); ++i)
149 for (std::size_t j = 0; j < a.order(); ++j) {
150 if (std::fabs(num_traits<T>::to_double(a.D0(i, j)) -
151 num_traits<T>::to_double(b.D0(i, j))) > tol)
152 return false;
153 if (std::fabs(num_traits<T>::to_double(a.D1(i, j)) -
154 num_traits<T>::to_double(b.D1(i, j))) > tol)
155 return false;
156 }
157 return true;
158}
159
160/**
161 * `sn.procid` as the distribution code FJ_codes switches on. The two enums
162 * share MATLAB's numbering, so this is a check that the type is in the accepted
163 * set rather than a translation.
164 */
165inline bool to_fj_proc_type(lang::ProcessType p, fj::FjProcType& out) {
166 switch (p) {
167 case lang::ProcessType::EXP: out = fj::FjProcType::Exp; return true;
168 case lang::ProcessType::ERLANG: out = fj::FjProcType::Erlang; return true;
170 case lang::ProcessType::MAP: out = fj::FjProcType::Map; return true;
171 default: return false;
172 }
173}
174
175} // namespace fj_detail
176
177/**
178 * Port of `fj_is_homogeneous.m`.
179 *
180 * This is NOT a test for the presence of a Fork: most fork-join models fail it.
181 * It tests membership in the homogeneous class the FJ_codes approximation is
182 * defined on -- one fork-join pair, K parallel queues between them carrying
183 * identical service, open classes, FCFS or PS -- and it is the 2a/2b branch
184 * predicate of the analyzer dispatch. It returns rather than throws for that
185 * reason, with the rejection reason in `why`.
186 */
187template <class T>
190 MamFjInfo info;
191
192 if (!L.is_open_model()) {
193 info.why = "FJ_codes supports open queueing models only";
194 return info;
195 }
196 if (L.fj.empty()) {
197 info.why = "the network contains no Fork-Join structure";
198 return info;
199 }
200
201 std::vector<std::size_t> forks, joins;
202 for (std::size_t i = 1; i <= L.nodes.size(); ++i) {
203 if (L.nodes[i - 1].nodetype == qn::NodeType::Fork) forks.push_back(i);
204 if (L.nodes[i - 1].nodetype == qn::NodeType::Join) joins.push_back(i);
205 }
206 if (forks.empty() || joins.empty()) {
207 info.why = "the network must contain both a Fork and a Join node";
208 return info;
209 }
210 if (forks.size() > 1 || joins.size() > 1) {
211 info.why = "FJ_codes supports a single Fork-Join pair; this model has " +
212 std::to_string(forks.size()) + " forks and " + std::to_string(joins.size()) +
213 " joins";
214 return info;
215 }
216 // FJ_codes computes the response-time tail of an AND-join: `mainFJ`
217 // synchronises on the LAST branch and has no parameter for a quorum, so a
218 // k-of-n model routed here would come back with the all-join tail under a
219 // quorum's name -- the same number for every k. Refuse it by name;
220 // `fj_tail_ordstat` covers the quorum.
221 {
222 const std::size_t nsib = L.join_siblings(joins[0]);
223 if (nsib > 0 && sn::sn_join_quorum(L, joins[0], nsib) < nsib) {
224 info.why = "the Join fires on a quorum; FJ_codes synchronises on every branch and "
225 "has no quorum, so use the order-statistic tail fj_tail_ordstat instead";
226 return info;
227 }
228 }
229 info.forkNode = forks[0];
230 info.joinNode = joins[0];
231
232 bool paired = false;
233 for (std::size_t p = 0; p < L.fj.size(); ++p)
234 if (L.fj[p].first == info.forkNode && L.fj[p].second == info.joinNode) paired = true;
235 if (!paired) {
236 info.why = "the Fork and the Join of this model are not paired with each other";
237 return info;
238 }
239
240 // A branch is a Queue the fork routes INTO and that routes into the join.
241 for (std::size_t i = 1; i <= L.nodes.size(); ++i) {
242 if (L.nodes[i - 1].nodetype != qn::NodeType::Queue) continue;
243 if (fj_detail::routes_between(L, info.forkNode, i) &&
244 fj_detail::routes_between(L, i, info.joinNode))
245 info.queueNodes.push_back(i);
246 }
247 if (info.queueNodes.empty()) {
248 info.why = "no Queue node lies between the Fork and the Join";
249 return info;
250 }
251 info.K = info.queueNodes.size();
252
253 // Homogeneous branches: the approximation solves ONE branch queue and
254 // raises its response time to the K-branch maximum, so branches that differ
255 // have no single queue to solve.
256 const std::size_t st0 = L.nodes[info.queueNodes[0] - 1].station;
257 for (std::size_t r = 1; r <= L.nclasses; ++r) {
258 if (st0 == 0 || L.disabled[st0 - 1][r - 1]) {
259 info.why = "branch queue '" + L.nodes[info.queueNodes[0] - 1].name +
260 "' has no service distribution for class '" + L.classes[r - 1].name + "'";
261 return info;
262 }
263 const Map<T> first = lang::dist_to_map(L.service[st0 - 1][r - 1]);
264 for (std::size_t k = 1; k < info.K; ++k) {
265 const std::size_t st = L.nodes[info.queueNodes[k] - 1].station;
266 if (st == 0 || L.disabled[st - 1][r - 1]) {
267 info.why = "branch queue '" + L.nodes[info.queueNodes[k] - 1].name +
268 "' has no service distribution for class '" + L.classes[r - 1].name +
269 "'";
270 return info;
271 }
272 if (!fj_detail::same_map(first, lang::dist_to_map(L.service[st - 1][r - 1]))) {
273 info.why = "the parallel queues have heterogeneous service distributions for "
274 "class '" + L.classes[r - 1].name + "'; FJ_codes requires homogeneous "
275 "servers";
276 return info;
277 }
278 }
279 }
280
281 for (std::size_t k = 0; k < info.K; ++k) {
282 const std::size_t st = L.nodes[info.queueNodes[k] - 1].station;
283 const SchedStrategy sc = L.stations[st - 1].sched;
284 if (sc != SchedStrategy::FCFS && sc != SchedStrategy::PS) {
285 info.why = "branch queue '" + L.nodes[info.queueNodes[k] - 1].name +
286 "' uses a scheduling strategy FJ_codes does not support; it supports FCFS "
287 "and PS only";
288 return info;
289 }
290 }
291
292 info.ok = true;
293 return info;
294}
295
296/**
297 * Port of `fj_extract_params.m`: the arrival descriptor from the Source and the
298 * service descriptor from the first branch, per class.
299 *
300 * The reference WARNS on an unstable class and calls `mainFJ` anyway, which
301 * then errors with "System not stable"; the two disagree about the severity of
302 * the same condition. This refuses at the earlier point, which is where the
303 * model can still be named.
304 */
305template <class T>
307 if (!info.ok) throw InputError("mam_fj_extract_params: the topology gate has not passed");
308
309 std::size_t src = 0;
310 for (std::size_t i = 1; i <= L.nstations; ++i)
311 if (L.stations[i - 1].nodetype == qn::NodeType::Source) {
312 src = i;
313 break;
314 }
315 if (src == 0) throw InputError("SolverMAM: the fork-join model has no Source node");
316 const std::size_t q0 = L.nodes[info.queueNodes[0] - 1].station;
317
318 MamFjParams<T> par;
319 par.K = info.K;
320 par.arrival.reserve(L.nclasses);
321 par.service.reserve(L.nclasses);
322 for (std::size_t r = 1; r <= L.nclasses; ++r) {
323 fj::FjProcType at, st;
324 if (!fj_detail::to_fj_proc_type(L.service[src - 1][r - 1].type, at))
325 throw UnsupportedError(
326 "SolverMAM: the arrival process of class '" + L.classes[r - 1].name +
327 "' is " + lang::process_to_text(L.service[src - 1][r - 1].type) +
328 ", and the FJ_codes approximation is defined for Exp, HyperExp(2), Erlang(2) and "
329 "MAP(2) arrivals only");
330 if (!fj_detail::to_fj_proc_type(L.service[q0 - 1][r - 1].type, st))
331 throw UnsupportedError(
332 "SolverMAM: the branch service process of class '" + L.classes[r - 1].name +
333 "' is " + lang::process_to_text(L.service[q0 - 1][r - 1].type) +
334 ", and the FJ_codes approximation is defined for Exp, HyperExp(2) and Erlang(2) "
335 "service only");
336 const fj::FjDist<T> a =
338 at);
339 const fj::FjDist<T> s = fj::fj_dist2fj(lang::dist_to_map(L.service[q0 - 1][r - 1]),
341 // mainFJ's own first act: load = lambda / mu, and load >= 1 is an error
342 // there. Every branch sees the full arrival stream, so this is the
343 // per-branch load and not an aggregate.
345 throw InputError(
346 "SolverMAM: class '" + L.classes[r - 1].name +
347 "' offers each fork-join branch a load of at least one (arrival rate " +
348 std::to_string(num_traits<T>::to_double(a.lambda)) + " against service rate " +
349 std::to_string(num_traits<T>::to_double(s.mu)) +
350 "); the response-time tail of an unstable branch does not exist");
351 par.arrival.push_back(a);
352 par.service.push_back(s);
353 }
354 return par;
355}
356
357/** The four percentiles `solver_mam_fj.m` stores for `getPerctRespT`. */
358inline const std::vector<double>& mam_fj_stored_percentiles() {
359 static const std::vector<double> p = {0.50, 0.90, 0.95, 0.99};
360 return p;
361}
362
363/**
364 * The 21-point grid `solver_mam_fj.m` inverts for the MEAN, `[0.01:0.05:0.95,
365 * 0.99, 0.999]`. It is a percentile grid and not a quadrature rule; see defect
366 * 4 in the header for what closing it at p = 1 costs.
367 */
368inline const std::vector<double>& mam_fj_dense_percentiles() {
369 static const std::vector<double> p = [] {
370 std::vector<double> v;
371 for (int k = 0; k < 19; ++k) v.push_back(0.01 + 0.05 * static_cast<double>(k));
372 v.push_back(0.99);
373 v.push_back(0.999);
374 return v;
375 }();
376 return p;
377}
378
379namespace fj_detail {
380
381/** `options.config.fj_accuracy`, the reference's `Cs`, default 100. */
382inline std::size_t fj_accuracy(const MamOptions& opt) {
383 if (opt.fj_accuracy < 1)
384 throw InputError("SolverMAM: config.fj_accuracy is the FJ_codes truncation C of the "
385 "queue-length difference and must be at least 1");
386 return opt.fj_accuracy;
387}
388
389/**
390 * The trapezoid over the inverse CDF that `solver_mam_fj.m` calls the mean.
391 *
392 * Reproduced including the invented tail point at p = 1: the grid stops at the
393 * 99.9th percentile and the integration closes with `RTp(end) * 1.1`, a factor
394 * with no derivation behind it. For a heavy tail that last panel is the
395 * dominant term, so the number is the reference's and not a mean.
396 */
397inline double fj_mean_from_percentiles(const std::vector<double>& pers,
398 const std::vector<double>& rtp) {
399 std::vector<double> p, v;
400 p.push_back(0.0);
401 v.push_back(0.0);
402 for (std::size_t i = 0; i < pers.size(); ++i) {
403 p.push_back(pers[i]);
404 v.push_back(rtp[i]);
405 }
406 p.push_back(1.0);
407 v.push_back(rtp.empty() ? 0.0 : rtp.back() * 1.1);
408 double acc = 0.0;
409 for (std::size_t i = 0; i + 1 < p.size(); ++i)
410 acc += 0.5 * (p[i + 1] - p[i]) * (v[i + 1] + v[i]);
411 return acc;
412}
413
414} // namespace fj_detail
415
416/**
417 * Port of `solver_mam_fj.m`.
418 *
419 * The analyzer is a thin wrapper: `mainFJ` returns the response-time
420 * percentiles of the whole fork-join subnetwork, the mean is read off that
421 * curve by trapezoid, and every station metric the reference reports is an
422 * M/M/1 closed form in (lambda, mu) EXCEPT the Join, which carries the
423 * synchronisation delay, i.e. the fork-join mean minus one branch response
424 * time. Defects 1, 2 and 4 of the header are reproduced deliberately: they are
425 * what the reference reports, and the alternative would be a different set of
426 * numbers under its method name.
427 *
428 * DEFECT 3 IS NOT REPRODUCED. The reference calls `mainFJ` TWICE per class, on
429 * the dense grid for the mean and again on the four stored percentiles, and the
430 * second call rebuilds the entire T matrix to read four points off a curve the
431 * first call already produced. Here the two grids are CONCATENATED into one
432 * call, which is the same inversion of the same phase-type law on a union of
433 * grids: `returnPer` treats every requested percentile independently, so a
434 * point's value does not depend on which other points were asked for.
435 */
436template <class T>
438 std::vector<std::vector<T> >* percentiles_out) {
439 const MamFjInfo info = mam_fj_is_homogeneous(L);
440 if (!info.ok)
441 throw UnsupportedError(
442 "SolverMAM: model '" + L.name +
443 "' is not a homogeneous fork-join network (" + info.why +
444 "). The reference routes such a model to solver_mam_basic_mmap, the MMAP fork-join "
445 "decomposition; select it with method 'dec.source.mmap'");
446 if constexpr (!std::is_same<T, double>::value) {
447 (void)opt;
448 (void)percentiles_out;
449 throw UnsupportedError(
450 "SolverMAM: the FJ_codes fork-join engine solves a non-symmetric algebraic Riccati "
451 "equation by an ORDERED REAL SCHUR factorization and two Sylvester equations of order "
452 "(C + 1) m^2 ma by Bartels-Stewart, all of which are LAPACK and therefore double only. "
453 "Re-run this model at double; there is no exact or extended-precision route to the "
454 "Qiu-Perez-Harrison approximation");
455 } else {
456 const MamFjParams<T> par = mam_fj_extract_params(L, info);
457 const std::size_t C = fj_detail::fj_accuracy(opt);
458 const fj::FjTMode mode = fj::fj_parse_tmode(opt.fj_tmode);
459
460 const std::vector<double>& dense = mam_fj_dense_percentiles();
461 const std::vector<double>& stored = mam_fj_stored_percentiles();
462 std::vector<double> grid = dense;
463 grid.insert(grid.end(), stored.begin(), stored.end());
464
465 const std::size_t M = L.nstations, K = L.nclasses;
467 sol.Q = Matrix<T>(M, K, num_traits<T>::from_int(0));
468 sol.U = Matrix<T>(M, K, num_traits<T>::from_int(0));
469 sol.R = Matrix<T>(M, K, num_traits<T>::from_int(0));
470 sol.Tp = Matrix<T>(M, K, num_traits<T>::from_int(0));
471 // CN and XN are zero in the reference too: solver_mam_fj.m allocates
472 // them and fills neither.
473 sol.C.assign(K, num_traits<T>::from_int(0));
474 sol.X.assign(K, num_traits<T>::from_int(0));
475 sol.iter = 0; // FJ_codes is direct, as the reference's totiter = 0 records
476 if (percentiles_out != nullptr)
477 percentiles_out->assign(K, std::vector<T>(stored.size(),
479
480 std::size_t src = 0, snk = 0;
481 for (std::size_t i = 1; i <= M; ++i)
482 if (L.stations[i - 1].nodetype == qn::NodeType::Source) src = i;
483 for (std::size_t i = 1; i <= L.nodes.size(); ++i)
484 if (L.nodes[i - 1].nodetype == qn::NodeType::Sink) snk = i;
485
486 for (std::size_t r = 1; r <= K; ++r) {
487 const std::vector<std::size_t> Kv(1, info.K);
488 const std::vector<std::size_t> Cv(1, C);
489 const std::vector<fj::FjCodesPercentiles> res =
490 fj::fj_main(par.arrival[r - 1], par.service[r - 1], grid, Kv, Cv, mode);
491
492 std::vector<double> dense_rt(dense.size()), stored_rt(stored.size());
493 for (std::size_t i = 0; i < dense.size(); ++i) dense_rt[i] = res[0].RTp[i];
494 for (std::size_t i = 0; i < stored.size(); ++i)
495 stored_rt[i] = res[0].RTp[dense.size() + i];
496 if (percentiles_out != nullptr)
497 for (std::size_t i = 0; i < stored.size(); ++i)
498 (*percentiles_out)[r - 1][i] = stored_rt[i];
499
500 const double mean_fj_rt = fj_detail::fj_mean_from_percentiles(dense, dense_rt);
501 const double lambda = par.arrival[r - 1].lambda;
502 const double mu = par.service[r - 1].mu;
503 const double rho = lambda / mu;
504 const double branch_rt = 1.0 / (mu - lambda);
505
506 for (std::size_t k = 0; k < info.K; ++k) {
507 const std::size_t st = L.nodes[info.queueNodes[k] - 1].station;
508 sol.U(st - 1, r - 1) = rho;
509 sol.Tp(st - 1, r - 1) = lambda;
510 sol.Q(st - 1, r - 1) = rho / (1.0 - rho);
511 sol.R(st - 1, r - 1) = branch_rt;
512 }
513 const std::size_t fst = L.nodes[info.forkNode - 1].station;
514 if (fst != 0) sol.Tp(fst - 1, r - 1) = lambda;
515 const std::size_t jst = L.nodes[info.joinNode - 1].station;
516 if (jst != 0) {
517 sol.Tp(jst - 1, r - 1) = lambda;
518 const double sync = mean_fj_rt - branch_rt;
519 sol.R(jst - 1, r - 1) = sync;
520 sol.Q(jst - 1, r - 1) = lambda * sync;
521 }
522 if (src != 0) sol.Tp(src - 1, r - 1) = lambda;
523 if (snk != 0) {
524 const std::size_t sst = L.nodes[snk - 1].station;
525 if (sst != 0) sol.Tp(sst - 1, r - 1) = lambda;
526 }
527 }
528 return sol;
529 }
530}
531
532/** `solver_mam_fj.m` without the percentile side channel. */
533template <class T>
537
538/**
539 * `@@SolverMAM/getPerctRespT.m`'s fork-join path: the percentiles
540 * `solver_mam_fj.m` stores in `percResults.RT`, one row per class, at the four
541 * levels `mam_fj_stored_percentiles()` names.
542 *
543 * A REQUESTED LEVEL IS INTERPOLATED, NOT RE-SOLVED, and outside [0.50, 0.99] it
544 * is EXTRAPOLATED off the end segment: the reference reads its stored table
545 * with `interp1(..., 'linear', 'extrap')`. That is reproduced, defect and all.
546 * Asking for the 0.999 quantile of a heavy tail therefore continues the
547 * 0.95-to-0.99 chord rather than inverting the law again, and the further out
548 * the level the worse the estimate -- `returnPer` would answer it exactly, for
549 * the price of another solve. The reference's choice is kept because a caller
550 * comparing MATLAB against this port must see the same number.
551 */
552template <class T>
553std::vector<std::vector<T> > solver_mam_fj_percentiles(const qn::NetworkStruct<T>& L,
554 const MamOptions& opt,
555 const std::vector<double>& percentiles) {
556 const std::vector<double>& stored = mam_fj_stored_percentiles();
557 std::vector<std::vector<T> > all;
558 solver_mam_fj<T>(L, opt, &all);
559
560 std::vector<std::vector<T> > out(all.size(), std::vector<T>(percentiles.size(),
562 for (std::size_t r = 0; r < all.size(); ++r)
563 for (std::size_t i = 0; i < percentiles.size(); ++i) {
564 const double p = percentiles[i];
565 // interp1 linear with extrapolation: the bracketing segment, or the
566 // first / last one continued.
567 std::size_t s = 0;
568 while (s + 2 < stored.size() && p > stored[s + 1]) ++s;
569 const double p0 = stored[s], p1 = stored[s + 1];
570 const T v0 = all[r][s], v1 = all[r][s + 1];
571 out[r][i] = T(v0 + (v1 - v0) * num_traits<T>::from_double((p - p0) / (p1 - p0)));
572 }
573 return out;
574}
575
576} // namespace mam
577} // namespace line
578
579#endif // LINE_SOLVERS_MAM_SOLVER_MAM_FJ_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
T get_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const
P{r,s}(i,j), AS THE USER SET IT.
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::vector< std::vector< bool > > disabled
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< NodeDef > nodes
every node, in creation order
bool is_open_model() const
sn_is_open_model: EVERY class is open, which is not has_open_classes.
std::vector< std::pair< std::size_t, std::size_t > > fj
fj(f,j): the Join node j that closes the Fork node f, 1-based.
std::size_t join_siblings(std::size_t joinNode, std::size_t r=0) const
Port of MNetwork.refreshCapacity.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
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...
The option and result types SolverMAM shares with its analyzers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
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
FjDist< T > fj_dist2fj(const mam::Map< T > &m, FjDistKind kind, FjProcType procType)
Build the fork-join descriptor of a MAP.
Definition fj_dist2fj.h:99
FjTMode
Which route computeT.m takes to the T matrix.
Definition fj_codes.h:102
FjProcType
The subset of ProcessType that the fork-join algorithm accepts.
Definition fj_dist2fj.h:65
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:560
mva::MvaSolution< T > solver_mam_fj(const qn::NetworkStruct< T > &L, const MamOptions &opt, std::vector< std::vector< T > > *percentiles_out)
Port of solver_mam_fj.m.
const std::vector< double > & mam_fj_stored_percentiles()
The four percentiles solver_mam_fj.m stores for getPerctRespT.
MamFjParams< T > mam_fj_extract_params(const qn::NetworkStruct< T > &L, const MamFjInfo &info)
Port of fj_extract_params.m: the arrival descriptor from the Source and the service descriptor from t...
MamFjInfo mam_fj_is_homogeneous(const qn::NetworkStruct< T > &L)
Port of fj_is_homogeneous.m.
const std::vector< double > & mam_fj_dense_percentiles()
The 21-point grid solver_mam_fj.m inverts for the MEAN, [0.01:0.05:0.95, 0.99, 0.999].
std::vector< std::vector< T > > solver_mam_fj_percentiles(const qn::NetworkStruct< T > &L, const MamOptions &opt, const std::vector< double > &percentiles)
@@SolverMAM/getPerctRespT.m's fork-join path: the percentiles solver_mam_fj.m stores in percResults....
std::size_t sn_join_quorum(const qn::NetworkStruct< T > &sn, std::size_t joinNode, std::size_t nbranches)
The number of siblings the Join node joinNode (1-based) fires on, out of nbranches forked.
A queueing network and its refreshed NetworkStruct.
Quorum joins: how many siblings a Join fires on, and what that drops.
Descriptor of an arrival or a service process.
Definition fj_dist2fj.h:76
static constexpr double FineTol
Definition lang_types.h:668
What fj_is_homogeneous.m returns: the fork-join pair, or why there is none.
std::size_t joinNode
1-based node index
std::size_t forkNode
1-based node index
std::string why
the reference's errorMsg, empty when ok
std::vector< std::size_t > queueNodes
the K parallel branches, 1-based node indices
What fj_extract_params.m returns: one arrival and one service per class.
std::vector< fj::FjDist< T > > arrival
std::vector< fj::FjDist< T > > service
The options SolverMAM reads.
Definition mam_types.h:29
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
std::vector< T > C
Definition mva_types.h:98