LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fj_driver.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_MVA_FJ_DRIVER_H
6#define LINE_SOLVERS_MVA_FJ_DRIVER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The fork-join fixed point that drives one inner MVA solve.
12 *
13 * Port of @@NetworkSolver/fjFixedPoint.m. The transform (fj_mmt.h) turns a model
14 * with a Fork into a plain mixed queueing network carrying auxiliary open
15 * classes; this driver solves that network repeatedly, each pass re-setting the
16 * auxiliary arrival rates from the current forkLambda, recomputing the
17 * synchronisation delay at the join from the branch response times the solve
18 * reports, moving forkLambda halfway towards the join throughput, and merging
19 * the auxiliary classes back into the ones they stand for.
20 *
21 * The inner solve is a callback so the same driver serves both entry points that
22 * need it: SolverLN, which solves a layer with solver_mva_analyzer, and
23 * SolverMVA over a general Network, which solves with mva_dispatch. The callback
24 * takes the transformed model by reference (the driver mutates its services and
25 * chains between passes) and returns an MvaSolution over the auxiliary-expanded
26 * class set; everything else here -- the merge-back, the auxiliary-column drop
27 * and the non-finite guard -- is codebase-independent and lives here once.
28 */
29
31#include <algorithm>
32#include <cmath>
33#include <string>
34#include <utility>
35#include <vector>
36
40#include "line/util/error.h"
41
42namespace line {
43namespace mva {
44
45using lang::Distrib;
47
48/**
49 * Port of ModelAdapter.findPathsCS' per-node charge: the queue length of the
50 * merge set at this station divided by its throughput, which is the time a job
51 * of that set spends at the node. See the reference comment carried in
52 * solver_ln.h: a non-station node, and a station the merge set never completes
53 * at, contribute nothing.
54 */
55template <class T>
56T fj_node_time(const qn::NetworkStruct<T>& V, std::size_t nd,
57 const std::vector<std::size_t>& merge, const Matrix<T>& QN,
58 const Matrix<T>& TN) {
59 const T zero = num_traits<T>::from_int(0);
60 const std::size_t st = V.nodes[nd - 1].station;
61 if (st == 0) return zero;
62 T q = zero, t = zero;
63 for (std::size_t k : merge) {
64 q += QN(st - 1, k - 1);
65 t += TN(st - 1, k - 1);
66 }
67 if (!(t > zero)) return zero;
68 return T(q / t);
69}
70
71/**
72 * Port of ModelAdapter.findPathsCS: the response time along every path from a
73 * fork to ITS join, in the given class. `merge`'s first entry is the class being
74 * traversed and its remaining entries the auxiliary classes of the forks whose
75 * branches are being walked; the end node's own time is subtracted back off,
76 * since the synchronisation delay is what is being computed and must not be
77 * counted into the branch it delays.
78 *
79 * NESTED FORKS ARE COLLAPSED IN PLACE. Meeting a fork part-way along a branch,
80 * the walk recurses to THAT fork's join, forms its `E[max]` there and then
81 * continues from its join with `t0 + E[max]` -- so an inner fork contributes the
82 * time its own branches take in parallel, not the time they would take in series.
83 * It also WRITES the inner join's synchronisation delay for the merge set as a
84 * side effect, which is why `V` is taken by non-const reference here: the
85 * reference does the same, and it is the only place an inner join's delay is set
86 * (the driver's own loop sets it only for outer forks).
87 *
88 * The `visited` set of (node, class) pairs is the reference's own cycle guard: a
89 * path returning to a pair it already holds is a routing loop, such as the .Aux
90 * self-loop of a call whose mean exceeds one, not a new branch. It replaces the
91 * depth cap this port used while it handled a single fork, which could not tell a
92 * legitimate deep nest from a cycle.
93 */
94template <class T>
95void fj_find_paths(FjMmt<T>& tr, std::size_t curNode, std::size_t endNode, std::size_t curClass,
96 const std::vector<std::size_t>& merge, const Matrix<T>& QN, const Matrix<T>& TN,
97 const T& t0, std::vector<T>& out,
98 std::vector<std::pair<std::size_t, std::size_t>> visited) {
100 const T zero = num_traits<T>::from_int(0);
101 if (curNode == endNode) {
102 out.push_back(T(t0 - fj_node_time(V, curNode, merge, QN, TN)));
103 return;
104 }
105 const std::pair<std::size_t, std::size_t> here(curNode, curClass);
106 if (std::find(visited.begin(), visited.end(), here) != visited.end()) return;
107 visited.push_back(here);
108
109 for (std::size_t s = 1; s <= V.classes.size(); ++s)
110 for (std::size_t nd = 1; nd <= V.nodes.size(); ++nd) {
111 // CS-aware: a synthesized ClassSwitch on a branch carries its switch
112 // in `csmatrix`, not in `P`, so a walk over `P` alone would end the
113 // path there and lose the rest of the branch's response time.
114 if (!(detail::fj_route_cs(V, curClass, s, curNode, nd) > zero)) continue;
115 std::vector<std::size_t> m2 = merge;
116 m2[0] = s;
117
118 // Is `nd` another fork of this transform? Its node type is Router by
119 // now, so the fork records are the only way to tell.
120 std::size_t inner = tr.forks.size();
121 for (std::size_t b = 0; b < tr.forks.size(); ++b)
122 if (tr.forks[b].node == nd && tr.forks[b].joinNode != 0) inner = b;
123
124 if (inner < tr.forks.size()) {
125 const std::size_t ijoin = tr.forks[inner].joinNode;
126 const std::size_t istat = tr.forks[inner].joinStation;
127 // the inner fork's own auxiliary class for the class arriving at it
128 std::size_t iaux = 0;
129 for (std::size_t x : tr.auxclasses)
130 if (tr.fjforkmap[x] == inner && tr.fjclassmap[x] == s) iaux = x;
131 std::vector<std::size_t> m3 = m2;
132 if (iaux != 0) m3.push_back(iaux);
133
134 std::vector<T> paths;
135 fj_find_paths(tr, nd, ijoin, s, m3, QN, TN, zero, paths, visited);
136 T d0 = zero;
137 if (!paths.empty()) {
138 T mean = zero;
139 for (const T& x : paths) mean += x;
140 mean = T(mean / num_traits<T>::from_int(static_cast<long>(paths.size())));
141 // The join fires on the k-th branch completion, k = the branch
142 // count on a standard join and the declared quorum on a PARTIAL one.
143 d0 = fj_expected_ordstat(paths, fj_join_quorum(V, ijoin, paths.size()));
144 // The inner join's delay, for every class of the merge set.
145 // Note the reference charges E[max] - mean here WITHOUT the
146 // fanOut factor it applies at an outer fork. Under a quorum the
147 // k-th completion can precede a branch's own, and then the join
148 // adds no delay: the parent left on a sibling.
149 const Distrib<T> d =
150 fj_exp_fit_mean(d0 > mean ? T(d0 - mean) : num_traits<T>::from_int(0));
151 if (istat != 0)
152 for (std::size_t cls : m3) V.set_service(istat, cls, d);
153 }
154 fj_find_paths(tr, ijoin, endNode, s, m2, QN, TN, T(t0 + d0), out, visited);
155 } else {
156 fj_find_paths(tr, nd, endNode, s, m2, QN, TN,
157 T(t0 + fj_node_time(V, nd, m2, QN, TN)), out, visited);
158 }
159 }
160}
161
162/** The leading `n` columns of a class-indexed metric. */
163template <class T>
164Matrix<T> fj_leading_cols(const Matrix<T>& A, std::size_t n) {
165 Matrix<T> out(A.rows(), n, num_traits<T>::from_int(0));
166 for (std::size_t i = 0; i < A.rows(); ++i)
167 for (std::size_t j = 0; j < n && j < A.cols(); ++j) out(i, j) = A(i, j);
168 return out;
169}
170
171/** The mixed absolute/relative stopping test of the fork-join loop. */
172template <class T>
173bool fj_converged(const Matrix<T>& A, const Matrix<T>& B, double iter_tol) {
174 if (A.rows() != B.rows() || A.cols() != B.cols()) return false;
175 for (std::size_t i = 0; i < A.rows(); ++i)
176 for (std::size_t j = 0; j < A.cols(); ++j) {
177 const double d = std::fabs(num_traits<T>::to_double(A(i, j)) -
178 num_traits<T>::to_double(B(i, j)));
179 if (d > GlobalConstants::Zero +
180 iter_tol * std::fabs(num_traits<T>::to_double(B(i, j))))
181 return false;
182 }
183 return true;
184}
185
186/**
187 * Port of `fjFixedPoint.m:130-136`: the firing throughput of fork `fa`, per
188 * class of the BASE model.
189 *
190 * A fork is not a station and has no throughput of its own, so the reference
191 * derives one from the visit ratios: the fork's node visits, divided by the
192 * class's total visits to its reference station, times the throughput observed
193 * there. The divisor is the CHAIN's visit total at the reference station and
194 * the multiplier the CHAIN's throughput there, because visits are normalised
195 * per chain and a class-only ratio would not be scale-free under class
196 * switching.
197 *
198 * TWO INDEX SPACES MEET HERE, exactly as in the reference. `visits` is indexed
199 * by STATEFUL node and `refstat` is a STATION, so the reference station must be
200 * mapped through `stateful_of_station` before it indexes `visits`; `nodevisits`
201 * is indexed by NODE, which is what `parent` names. The throughput `TN` comes
202 * from the solve of the TRANSFORMED model and is indexed by its station rows,
203 * which the transform leaves aligned with the base model's -- the same
204 * assumption `fjFixedPoint.m` makes when it writes `TN(nodeToStation(joinIdx))`.
205 *
206 * `parent` is this fork's own index when it is not nested: an inner fork fires
207 * as often as the outer one it sits behind, so the visits that measure it are
208 * the OUTER fork's, which is what `sortForks` returns in `parent_forks`.
209 */
210template <class T>
211std::vector<T> fj_fork_tput(const qn::NetworkStruct<T>& L, const FjMmt<T>& tr, const Matrix<T>& TN,
212 std::size_t fa) {
213 const T zero = num_traits<T>::from_int(0);
214 std::vector<T> tnfork(L.nclasses, zero);
215 const std::size_t pnode = tr.forks[tr.forks[fa].parent].node;
216 for (std::size_t c = 0; c < L.nchains; ++c) {
217 const std::vector<std::size_t>& ic = L.inchain[c];
218 for (std::size_t r : ic) {
219 const std::size_t rs = L.classes[r - 1].refstat;
220 const std::size_t isf = L.stateful_of_station(rs);
221 T den = zero, tsum = zero;
222 for (std::size_t k : ic) {
223 den += L.visits[c](isf - 1, k - 1);
224 tsum += TN(rs - 1, k - 1);
225 }
226 if (!(den > zero)) continue;
227 tnfork[r - 1] = T(T(L.nodevisits[c](pnode - 1, r - 1) / den) * tsum);
228 }
229 }
230 return tnfork;
231}
232
233/** A metric read out of a solve, with a non-finite entry read as zero. */
234template <class T>
235T fj_finite_or_zero(const T& x) {
236 if (!std::isfinite(num_traits<T>::to_double(x))) return num_traits<T>::from_int(0);
237 return x;
238}
239
240/**
241 * Port of the `heidelberger-trivedi` arm of `fjFixedPoint.m:212-255`: the
242 * synchronisation delays of one pass, written onto the transformed model.
243 *
244 * The branch response time of an auxiliary class is its TOTAL response time over
245 * the model, less what it spends at the join (the residual synchronisation delay
246 * this function is about to overwrite) and at the auxiliary delay (the original
247 * class's time outside the span). That leaves exactly the time the branch takes.
248 * From those,
249 *
250 * d0 = E[X_(k)] fires the join. k is the branch count on a standard join and
251 * the declared quorum on a PARTIAL one.
252 * di = d0 - ri is what branch i still waits at the join once it has finished.
253 * r0 is the original class's cycle response time less its own time
254 * at the join, i.e. everything outside the span, which is what
255 * the auxiliary delay must hold so that the auxiliary token
256 * cycles at the original's rate.
257 *
258 * The join charges `d0 * fanOut` to the ORIGINAL class, which is the whole span:
259 * `fj_ht` routed it straight past the branches. `fanOut` is 1 on every model that
260 * reaches here -- `fj_ht` refuses tasksPerLink > 1 by name -- and is kept as a
261 * named factor because the reference multiplies by it.
262 *
263 * UNDER A QUORUM `d0` can precede a branch's own completion, and then that branch
264 * waits no further; `di` floors at zero, which is a boundary of the transform and
265 * not a choice. The reference raises a line_warning there; this port has no
266 * warning channel out of the fixed point (see mva_dispatch.h).
267 */
268template <class T>
270 const T zero = num_traits<T>::from_int(0);
272 for (std::size_t fa = 0; fa < tr.forks.size(); ++fa) {
273 const std::size_t fnode = tr.forks[fa].node;
274 const std::size_t jnode = tr.forks[fa].joinNode;
275 if (jnode == 0) continue;
276 const std::size_t jstat = tr.forks[fa].joinStation;
277 const std::size_t adstat = tr.auxDelayStation[jnode];
278 for (std::size_t c = 0; c < L.nchains; ++c) {
279 const std::vector<std::size_t>& ic = L.inchain[c];
280 for (std::size_t xi = 0; xi < ic.size(); ++xi) {
281 const std::size_t r = ic[xi];
282 if (num_traits<T>::to_double(L.nodevisits[c](fnode - 1, r - 1)) == 0.0) continue;
283 // THIS FORK's auxiliary classes for r. The reference selects them
284 // by `fjclassmap == r` alone, which on a model with a SECOND fork
285 // over the same class would mix the two forks' branches into one
286 // order statistic; the fork is added here because H-T charges each
287 // fork at its own join.
288 std::vector<std::size_t> br;
289 for (std::size_t s : tr.auxclasses)
290 if (tr.fjforkmap[s] == fa && tr.fjclassmap[s] == r) br.push_back(s);
291 if (br.empty()) continue;
292
293 std::vector<T> ri(br.size(), zero);
294 for (std::size_t b = 0; b < br.size(); ++b) {
295 T acc = zero;
296 for (std::size_t i = 0; i < out.R.rows(); ++i)
297 acc += fj_finite_or_zero(out.R(i, br[b] - 1));
298 // the two subtracted terms are read RAW, as in the reference:
299 // its NaN sweep applies to the summed matrix only
300 acc = T(acc - out.R(adstat - 1, br[b] - 1));
301 ri[b] = T(acc - out.R(jstat - 1, br[b] - 1));
302 }
303 const T d0 = fj_expected_ordstat(ri, fj_join_quorum(L, jnode, br.size()));
304
305 T r0 = zero;
306 for (std::size_t i = 0; i < out.R.rows(); ++i) {
307 T row = zero;
308 for (std::size_t yi = 0; yi < ic.size(); ++yi) row += out.R(i, ic[yi] - 1);
309 r0 += fj_finite_or_zero(row);
310 }
311 r0 = T(r0 - out.R(jstat - 1, r - 1));
312
313 V.set_service(jstat, r,
315 tr.forks[fa].fanOut))));
316 const Distrib<T> outside = fj_exp_fit_mean(r0);
317 for (std::size_t b = 0; b < br.size(); ++b) {
318 T di = T(d0 - ri[b]);
319 if (di < zero) di = zero;
320 V.set_service(jstat, br[b], fj_exp_fit_mean(di));
321 V.set_service(adstat, br[b], outside);
322 }
323 }
324 }
325 }
326}
327
328/**
329 * Port of the `heidelberger-trivedi` arm of `fjFixedPoint.m:263-291`: fold the
330 * auxiliary columns back into the classes they stand for.
331 *
332 * EVERY JOIN LOSES ITS ORIGINAL-CLASS METRICS FIRST. The original class was
333 * routed straight past the branches and charged the whole span at the join, so
334 * what the solve reports for it there is the transform's own bookkeeping and not
335 * a queue the model has; the branches' figures, which the auxiliary classes
336 * carry, are what belongs there. Throughput is the exception: the join's
337 * original-class rate is how often the fork-join completes, so it is saved and
338 * put back after the merge, where the reference puts it back too.
339 *
340 * The auxiliary DELAY rows are left in place rather than deleted. The reference
341 * deletes them; they are appended after every base station, so the leading block
342 * the caller reads is the same either way, and deleting them would only move the
343 * join rows the restore step above indexes by their pre-deletion position.
344 */
345template <class T>
347 const T zero = num_traits<T>::from_int(0);
348 std::vector<bool> orig(tr.V.classes.size() + 1, false);
349 for (std::size_t s : tr.auxclasses) orig[tr.fjclassmap[s]] = true;
350
351 std::vector<std::vector<T>> tnJoin(tr.joinStations.size(),
352 std::vector<T>(L.nclasses + 1, zero));
353 for (std::size_t a = 0; a < tr.joinStations.size(); ++a) {
354 const std::size_t js = tr.joinStations[a];
355 for (std::size_t r = 1; r <= L.nclasses; ++r) {
356 if (!orig[r]) continue;
357 tnJoin[a][r] = out.Tp(js - 1, r - 1);
358 out.Q(js - 1, r - 1) = zero;
359 out.R(js - 1, r - 1) = zero;
360 out.Tp(js - 1, r - 1) = zero;
361 out.U(js - 1, r - 1) = zero;
362 }
363 }
364 const std::size_t nrows = out.Tp.rows();
365 for (std::size_t s : tr.auxclasses) {
366 const std::size_t r = tr.fjclassmap[s];
367 for (std::size_t i = 0; i < nrows; ++i) {
368 out.Q(i, r - 1) = T(out.Q(i, r - 1) + out.Q(i, s - 1));
369 out.U(i, r - 1) = T(out.U(i, r - 1) + out.U(i, s - 1));
370 out.Tp(i, r - 1) = T(out.Tp(i, r - 1) + out.Tp(i, s - 1));
371 out.R(i, r - 1) =
372 out.Tp(i, r - 1) != zero ? T(out.Q(i, r - 1) / out.Tp(i, r - 1)) : zero;
373 }
374 }
375 for (std::size_t a = 0; a < tr.joinStations.size(); ++a) {
376 const std::size_t js = tr.joinStations[a];
377 for (std::size_t r = 1; r <= L.nclasses; ++r)
378 if (orig[r]) out.Tp(js - 1, r - 1) = tnJoin[a][r];
379 }
380 out.Q = fj_leading_cols(out.Q, L.nclasses);
381 out.U = fj_leading_cols(out.U, L.nclasses);
382 out.R = fj_leading_cols(out.R, L.nclasses);
383 out.Tp = fj_leading_cols(out.Tp, L.nclasses);
384}
385
386/**
387 * Drive the fork-join fixed point of a transformed model to convergence.
388 *
389 * @param L the base model, whose services are copied back into the transform
390 * at the start of every solve (ModelAdapter.refreshServicesFromBase)
391 * @param tr the transform, mutated in place: its join and Source services carry
392 * the current pass's synchronisation delays and auxiliary arrivals
393 * @param lam the auxiliary arrival rates, `self.fjForkLambda`; warm-started by
394 * the caller across outer iterations and updated here in place
395 * @param opt MVA options (iter_max, iter_tol)
396 * @param inner the inner solve over the auxiliary-expanded model
397 * @return the merged MvaSolution over the base class set (auxiliary columns
398 * dropped, join and Source throughputs kept at their original-class value)
399 */
400template <class T, class InnerSolve>
402 std::vector<T>& lam, const MvaOptions& opt,
403 InnerSolve inner) {
404 const T zero = num_traits<T>::from_int(0);
405 const T two = num_traits<T>::from_int(2);
407
408 // ---- ModelAdapter.refreshServicesFromBase --------------------------------
409 // MMT ONLY. `fjFixedPoint.m` reuses the MMT transform across outer
410 // iterations and has to undo the previous one's converged services; the H-T
411 // arm rebuilds its transform on every call (`ModelAdapter.ht` is invoked
412 // unconditionally at forkIter == 1) and has no auxiliary open stream, so
413 // there is nothing here for it to reset.
414 if (!tr.heidelberger_trivedi) {
415 // Base-derived slots are re-read from the base model. The
416 // transformation-owned ones -- the join's synchronisation delays and the
417 // auxiliary arrivals -- are RESET to what a cold transform would hold, not
418 // left alone: they still carry the previous outer iteration's converged
419 // values, and keeping them would silently warm-start the fork loop from a
420 // different point than the reference does.
421 for (std::size_t i = 1; i <= L.stations.size(); ++i)
422 for (std::size_t k = 1; k <= L.classes.size(); ++k)
423 V.set_service(i, k, L.service[i - 1][k - 1]);
424 for (std::size_t s : tr.auxclasses) {
425 const std::size_t r = tr.fjclassmap[s];
426 for (std::size_t i = 1; i <= L.stations.size(); ++i) {
427 if (tr.is_join_station(i)) continue;
428 V.set_service(i, s, L.service[i - 1][r - 1]);
429 }
430 }
431 for (std::size_t js : tr.joinStations)
432 for (std::size_t k = 1; k <= V.classes.size(); ++k)
434 for (std::size_t s : tr.auxclasses)
435 V.set_service(tr.sourceStation, s,
436 tr.auxdisabled[s]
440 }
441
442 // QN starts at the Immediate sentinel and QN_1 at zero, so the first two
443 // passes can never be mistaken for a converged pair.
445 Matrix<T> QN_1(1, L.nclasses, zero);
446 MvaSolution<T> out;
447 bool forkLoop = true;
448 int forkIter = 0;
449 while (forkLoop && forkIter < opt.iter_max) {
450 ++forkIter;
451 // `fjFixedPoint.m:77`: the auxiliary arrival update is guarded on the
452 // method NOT being H-T, whose auxiliary classes are closed and carry no
453 // arrival rate to re-set.
454 if (forkIter > 1 && !tr.heidelberger_trivedi) {
455 // the auxiliary stream carries the fanout-1 branches the circulating
456 // job did not take
457 for (std::size_t s : tr.auxclasses) {
458 if (tr.auxdisabled[s] || !(tr.fanout[s] > 0.0)) continue;
459 V.set_service(
460 tr.sourceStation, s,
462 T(num_traits<T>::from_double(tr.fanout[s] - 1.0) * lam[s])));
463 }
464 }
465 V.refresh_chains();
466
468 if (QN_1.rows() == QN.rows() && QN_1.cols() == QN.cols()) {
469 double moved = 0.0;
470 for (std::size_t i = 0; i < QN.rows(); ++i)
471 for (std::size_t j = 0; j < QN.cols(); ++j)
472 moved = std::max(moved, std::fabs(num_traits<T>::to_double(QN_1(i, j)) -
473 num_traits<T>::to_double(QN(i, j))));
475 "fork-join iteration %d: queue lengths moved by at most %.3e", forkIter, moved);
476 } else {
478 "fork-join iteration %d: transformed model rebuilt", forkIter);
479 }
480 }
481
482 if (fj_converged(QN_1, QN, opt.iter_tol) && forkIter > 2)
483 forkLoop = false;
484 else
485 QN_1 = QN;
486
487 out = inner(V);
488
489 if (tr.heidelberger_trivedi) {
490 fj_ht_sync_delays(L, tr, out);
491 fj_ht_merge(L, tr, out);
492 QN = out.Q;
493 continue;
494 }
495
496 // ---- synchronisation delays and the forkLambda update ----------------
497 // One pass per fork, as in the reference: each fork drives only ITS OWN
498 // auxiliary classes, off its own join.
499 for (std::size_t fa = 0; fa < tr.forks.size(); ++fa) {
500 const std::size_t fnode = tr.forks[fa].node;
501 const std::size_t jstat = tr.forks[fa].joinStation;
502
503 // A FORK WITH NO JOIN. `fjFixedPoint.m:142-152` drives forkLambda
504 // from the fork's OWN firing rate rather than a join's throughput,
505 // and charges no synchronisation delay, there being no station to
506 // charge it to. `fj_sort_forks` has already left such a fork outer
507 // with itself as parent, which is what `sortForks` returns for it.
508 if (jstat == 0) {
509 const std::vector<T> tnfork = fj_fork_tput(L, tr, out.Tp, fa);
510 for (std::size_t s : tr.auxclasses) {
511 if (tr.fjforkmap[s] != fa) continue;
512 lam[s] = T((lam[s] + tnfork[tr.fjclassmap[s] - 1]) / two);
513 }
514 continue;
515 }
516
517 for (std::size_t s : tr.auxclasses) {
518 if (tr.fjforkmap[s] != fa) continue;
519 const std::size_t r = tr.fjclassmap[s];
520 T acc = zero;
521 for (std::size_t s2 : tr.auxclasses)
522 if (tr.fjclassmap[s2] == r) acc += out.Tp(jstat - 1, s2 - 1);
523 out.Tp(jstat - 1, r - 1) =
524 T(out.Tp(jstat - 1, r - 1) + acc - out.Tp(jstat - 1, s - 1));
525 lam[s] = T((lam[s] + out.Tp(jstat - 1, r - 1)) / two);
526
527 // An INNER fork's delay is charged by fj_find_paths while the
528 // enclosing branch is walked, so computing it again here would
529 // count it twice; only an outer fork's own delay is set here.
530 if (!tr.forks[fa].outer[r]) continue;
531
532 std::vector<T> ri;
533 std::vector<std::size_t> merge{r, s};
534 fj_find_paths(tr, fnode, tr.forks[fa].joinNode, r, merge, out.Q, out.Tp, zero, ri,
535 {});
536 T sync = zero;
537 if (!ri.empty()) {
538 // tasksPerLink = w sends w IDENTICAL tasks down each link, so the
539 // join synchronises on w*B siblings and not on B: the sibling set
540 // is each branch's completion time REPLICATED w times, and the
541 // order statistic is taken over that multiset. Scaling E[X_(k)] by
542 // w instead (what this did before) is w*H_B/mu where the answer is
543 // H_(w*B)/mu, which OVER-states the delay by more the larger w is.
544 // w = 1 replicates to itself, so nothing moves on an ordinary fork.
545 double wd = tr.forks[fa].fanOut;
546 std::size_t w = (wd >= 1.0) ? static_cast<std::size_t>(wd + 0.5) : 1;
547 if (w > 1) {
548 const std::vector<T> base = ri;
549 for (std::size_t k = 1; k < w; ++k)
550 ri.insert(ri.end(), base.begin(), base.end());
551 }
552 T mean = zero;
553 for (const T& x : ri) mean += x;
554 mean = T(mean / num_traits<T>::from_int(static_cast<long>(ri.size())));
555 // The join fires on the k-th sibling completion, k = the sibling
556 // count on a standard join and the declared quorum on a PARTIAL
557 // one. The quorum is declared against the SIBLING count w*B, which
558 // is the replicated length.
559 const T d0 = fj_expected_ordstat(
560 ri, fj_join_quorum(L, tr.forks[fa].joinNode, ri.size()));
561 const T raw = T(d0 - mean);
562 // The quorum can be met BEFORE the branch this transform's own
563 // token walks, and then the parent ought to leave ahead of it.
564 // The MMT cannot express that: its token is a job of the closed
565 // chain and must finish its branch, and that closed token is what
566 // keeps the branch stable, so it cannot be made open either. The
567 // delay floors at zero, which OVER-states the cycle time.
568 // The reference raises a line_warning here; this port has no
569 // warning channel out of the fixed point (see mva_dispatch.h).
570 sync = raw > zero ? raw : zero;
571 }
572 const Distrib<T> d = fj_exp_fit_mean(sync);
573 V.set_service(jstat, s, d);
574 V.set_service(jstat, r, d);
575 }
576 }
577
578 // ---- merge the auxiliary classes back --------------------------------
579 // EVERY join and the Source keep their ORIGINAL-class throughputs: the
580 // auxiliary tokens are extra fork branches, not extra completions of the
581 // class, so adding them there would double the rate at which the fork is
582 // seen to fire.
583 const std::size_t nrows = out.Tp.rows();
584 std::vector<std::vector<T>> tnJoin(tr.joinStations.size(),
585 std::vector<T>(L.nclasses, zero));
586 std::vector<T> tnSource(L.nclasses, zero);
587 for (std::size_t a = 0; a < tr.joinStations.size(); ++a)
588 for (std::size_t k = 0; k < L.nclasses; ++k)
589 tnJoin[a][k] = out.Tp(tr.joinStations[a] - 1, k);
590 for (std::size_t k = 0; k < L.nclasses; ++k)
591 tnSource[k] = out.Tp(tr.sourceStation - 1, k);
592 for (std::size_t s : tr.auxclasses) {
593 const std::size_t r = tr.fjclassmap[s];
594 for (std::size_t i = 0; i < nrows; ++i) {
595 out.Q(i, r - 1) = T(out.Q(i, r - 1) + out.Q(i, s - 1));
596 out.U(i, r - 1) = T(out.U(i, r - 1) + out.U(i, s - 1));
597 out.Tp(i, r - 1) = T(out.Tp(i, r - 1) + out.Tp(i, s - 1));
598 out.R(i, r - 1) = out.Tp(i, r - 1) != zero
599 ? T(out.Q(i, r - 1) / out.Tp(i, r - 1))
600 : zero;
601 }
602 }
603 for (std::size_t a = 0; a < tr.joinStations.size(); ++a)
604 for (std::size_t k = 0; k < L.nclasses; ++k)
605 out.Tp(tr.joinStations[a] - 1, k) = tnJoin[a][k];
606 for (std::size_t k = 0; k < L.nclasses; ++k)
607 out.Tp(tr.sourceStation - 1, k) = tnSource[k];
608
609 // drop the auxiliary columns; they are all appended after the original
610 // ones, so the original block is the leading submatrix
611 out.Q = fj_leading_cols(out.Q, L.nclasses);
612 out.U = fj_leading_cols(out.U, L.nclasses);
613 out.R = fj_leading_cols(out.R, L.nclasses);
614 out.Tp = fj_leading_cols(out.Tp, L.nclasses);
615 QN = out.Q;
616 }
617 // A fork model that comes back non-finite has not been solved. It happens
618 // when the auxiliary open stream saturates a branch station: the mixed MVA
619 // then divides by a non-positive slack and the NaN propagates, where it
620 // reads as a solved model with a few blank entries. Refuse by name instead.
621 for (std::size_t i = 0; i < out.Q.rows(); ++i)
622 for (std::size_t k = 0; k < out.Q.cols(); ++k)
623 if (!std::isfinite(num_traits<T>::to_double(out.Q(i, k))) ||
624 !std::isfinite(num_traits<T>::to_double(out.U(i, k))) ||
625 !std::isfinite(num_traits<T>::to_double(out.R(i, k))) ||
626 !std::isfinite(num_traits<T>::to_double(out.Tp(i, k))))
627 throw NumericError(
628 "SolverMVA: the fork-join fixed point of model '" + L.name +
629 "' returned a non-finite metric; the auxiliary open classes the transform "
630 "adds have saturated one of the branch stations");
631 return out;
632}
633
634} // namespace mva
635} // namespace line
636
637#endif // LINE_SOLVERS_MVA_FJ_DRIVER_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
A network plus its refreshed NetworkStruct.
std::vector< Matrix< T > > nodevisits
(nchains) each (nnodes x nclasses)
std::size_t stateful_of_station(std::size_t st) const
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
void refresh_chains()
Port of MNetwork.refreshChains followed by sn_refresh_visits.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
void set_service(std::size_t station, std::size_t cls, const Distrib< T > &d)
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< NodeDef > nodes
every node, in creation order
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
static bool owns_log()
True only inside the OUTERMOST open run; gates EMISSION.
static void step(const char *fmt,...)
Write one progress line.
The exception types the port throws.
The fork-join transform SolverMVA applies before solving a layer that contains a Fork.
Running progress log of a LINE solver run (the "solver console").
void fj_find_paths(FjMmt< T > &tr, std::size_t curNode, std::size_t endNode, std::size_t curClass, const std::vector< std::size_t > &merge, const Matrix< T > &QN, const Matrix< T > &TN, const T &t0, std::vector< T > &out, std::vector< std::pair< std::size_t, std::size_t > > visited)
Port of ModelAdapter.findPathsCS: the response time along every path from a fork to ITS join,...
Definition fj_driver.h:95
void fj_ht_sync_delays(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, const MvaSolution< T > &out)
Port of the heidelberger-trivedi arm of fjFixedPoint.m:212-255: the synchronisation delays of one pas...
Definition fj_driver.h:269
Matrix< T > fj_leading_cols(const Matrix< T > &A, std::size_t n)
The leading n columns of a class-indexed metric.
Definition fj_driver.h:164
Distrib< T > fj_exp_fit_mean(const T &mean)
Exp.fitMean(m), including its clamp.
Definition fj_mmt.h:252
T fj_expected_ordstat(const std::vector< T > &means, std::size_t k)
The instant the join fires: E[X_(k)] of independent exponentials with the given means,...
Definition fj_mmt.h:204
bool fj_converged(const Matrix< T > &A, const Matrix< T > &B, double iter_tol)
The mixed absolute/relative stopping test of the fork-join loop.
Definition fj_driver.h:173
std::size_t fj_join_quorum(const qn::NetworkStruct< T > &sn, std::size_t joinNode, std::size_t nbranches)
The number of siblings the Join node fires on, out of nbranches forked.
Definition fj_mmt.h:227
void fj_ht_merge(const qn::NetworkStruct< T > &L, const FjMmt< T > &tr, MvaSolution< T > &out)
Port of the heidelberger-trivedi arm of fjFixedPoint.m:263-291: fold the auxiliary columns back into ...
Definition fj_driver.h:346
std::vector< T > fj_fork_tput(const qn::NetworkStruct< T > &L, const FjMmt< T > &tr, const Matrix< T > &TN, std::size_t fa)
Port of fjFixedPoint.m:130-136: the firing throughput of fork fa, per class of the BASE model.
Definition fj_driver.h:211
T fj_finite_or_zero(const T &x)
A metric read out of a solve, with a non-finite entry read as zero.
Definition fj_driver.h:235
T fj_node_time(const qn::NetworkStruct< T > &V, std::size_t nd, const std::vector< std::size_t > &merge, const Matrix< T > &QN, const Matrix< T > &TN)
Port of ModelAdapter.findPathsCS' per-node charge: the queue length of the merge set at this station ...
Definition fj_driver.h:56
MvaSolution< T > fj_fixed_point(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, std::vector< T > &lam, const MvaOptions &opt, InnerSolve inner)
Drive the fork-join fixed point of a transformed model to convergence.
Definition fj_driver.h:401
A queueing network and its refreshed NetworkStruct.
SolverMVA over a SolverLN layer.
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
static Distrib disabled_dist()
Definition lang_types.h:857
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
The transformed layer and the bookkeeping the fixed point needs to drive it and to merge its results ...
Definition fj_mmt.h:113
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:674
static constexpr double FineTol
Definition lang_types.h:668
static constexpr double Zero
Definition lang_types.h:670
The options SolverMVA reads.
Definition mva_types.h:31
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96