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
41#include "line/util/error.h"
42
43namespace line {
44namespace mva {
45
46using lang::Distrib;
48
49/**
50 * Port of ModelAdapter.findPathsCS' per-node charge: the queue length of the
51 * merge set at this station divided by its throughput, which is the time a job
52 * of that set spends at the node. See the reference comment carried in
53 * solver_ln.h: a non-station node, and a station the merge set never completes
54 * at, contribute nothing.
55 */
56template <class T>
57T fj_node_time(const qn::NetworkStruct<T>& V, std::size_t nd,
58 const std::vector<std::size_t>& merge, const Matrix<T>& QN,
59 const Matrix<T>& TN) {
60 const T zero = num_traits<T>::from_int(0);
61 const std::size_t st = V.nodes[nd - 1].station;
62 if (st == 0) return zero;
63 T q = zero, t = zero;
64 for (std::size_t k : merge) {
65 q += QN(st - 1, k - 1);
66 t += TN(st - 1, k - 1);
67 }
68 if (!(t > zero)) return zero;
69 return T(q / t);
70}
71
72/**
73 * `fjR` of `fjFixedPoint.m`: the order-statistic sojourn d0 the fixed point charges
74 * each fork-join section, per (fork node, ORIGINAL class), overwritten on every pass
75 * so the converged one wins. `set` stands in for the reference's NaN initialiser,
76 * which an exact arithmetic has no value for.
77 */
78template <class T>
80 Matrix<T> R; ///< (nnodes x nclasses) d0, 0-based storage
81 std::vector<std::vector<bool>> set; ///< whether R(node, class) was ever written
82 void init(std::size_t nnodes, std::size_t nclasses) {
83 R = Matrix<T>(nnodes, nclasses, num_traits<T>::from_int(0));
84 set.assign(nnodes, std::vector<bool>(nclasses, false));
85 }
86 /** Record d0 for a 1-based (node, class); a class outside the base set is ignored. */
87 void put(std::size_t node, std::size_t cls, const T& d0) {
88 if (node == 0 || cls == 0 || node > R.rows() || cls > R.cols()) return;
89 R(node - 1, cls - 1) = d0;
90 set[node - 1][cls - 1] = true;
91 }
92};
93
94/** One nested fork walked by fj_find_paths: the second output `inner` of findPathsCS. */
95template <class T>
96struct FjInnerD0 {
97 std::size_t node = 0; ///< 1-based fork node
98 std::size_t cls = 0; ///< 1-based class arriving at it, possibly auxiliary
99 T d0; ///< the order statistic the outer path is charged for it
100};
101
102/**
103 * `fjSectionMetrics` of `fjFixedPoint.m`: one row per Fork/Join pair of the BASE
104 * model. FJTput is the class throughput the solve reports at the Join's station,
105 * FJRespT the d0 recorded for the pair's fork, and FJQLen = FJTput * FJRespT
106 * (Little). A class with no throughput there, or no d0, reports zero in all three.
107 */
108template <class T>
110 const FjSectionAcc<T>& acc) {
111 const T zero = num_traits<T>::from_int(0);
113 for (std::size_t p = 0; p < out.npairs(); ++p) {
114 const std::size_t f = out.forks[p];
115 const std::size_t jst = L.nodes[out.joins[p] - 1].station;
116 for (std::size_t r = 0; r < L.nclasses; ++r) {
117 T x = zero;
118 if (jst != 0 && jst <= TN.rows() && r < TN.cols()) x = TN(jst - 1, r);
119 const bool has_d0 = f <= acc.set.size() && r < acc.set[f - 1].size() &&
120 acc.set[f - 1][r] &&
121 std::isfinite(num_traits<T>::to_double(acc.R(f - 1, r)));
122 if (x > zero && has_d0) {
123 out.Tput(p, r) = x;
124 out.RespT(p, r) = acc.R(f - 1, r);
125 out.QLen(p, r) = T(x * acc.R(f - 1, r));
126 }
127 }
128 }
129 return out;
130}
131
132/**
133 * Port of ModelAdapter.findPathsCS: the response time along every path from a
134 * fork to ITS join, in the given class. `merge`'s first entry is the class being
135 * traversed and its remaining entries the auxiliary classes of the forks whose
136 * branches are being walked; the end node's own time is subtracted back off,
137 * since the synchronisation delay is what is being computed and must not be
138 * counted into the branch it delays.
139 *
140 * NESTED FORKS ARE COLLAPSED IN PLACE. Meeting a fork part-way along a branch,
141 * the walk recurses to THAT fork's join, forms its `E[max]` there and then
142 * continues from its join with `t0 + E[max]` -- so an inner fork contributes the
143 * time its own branches take in parallel, not the time they would take in series.
144 * It also WRITES the inner join's synchronisation delay for the merge set as a
145 * side effect, which is why `V` is taken by non-const reference here: the
146 * reference does the same, and it is the only place an inner join's delay is set
147 * (the driver's own loop sets it only for outer forks).
148 *
149 * The `visited` set of (node, class) pairs is the reference's own cycle guard: a
150 * path returning to a pair it already holds is a routing loop, such as the .Aux
151 * self-loop of a call whose mean exceeds one, not a new branch. It replaces the
152 * depth cap this port used while it handled a single fork, which could not tell a
153 * legitimate deep nest from a cycle.
154 */
155template <class T>
156void fj_find_paths(FjMmt<T>& tr, std::size_t curNode, std::size_t endNode, std::size_t curClass,
157 const std::vector<std::size_t>& merge, const Matrix<T>& QN, const Matrix<T>& TN,
158 const T& t0, std::vector<T>& out,
159 std::vector<std::pair<std::size_t, std::size_t>> visited,
160 std::vector<FjInnerD0<T>>* inner_d0 = nullptr) {
162 const T zero = num_traits<T>::from_int(0);
163 if (curNode == endNode) {
164 out.push_back(T(t0 - fj_node_time(V, curNode, merge, QN, TN)));
165 return;
166 }
167 const std::pair<std::size_t, std::size_t> here(curNode, curClass);
168 if (std::find(visited.begin(), visited.end(), here) != visited.end()) return;
169 visited.push_back(here);
170
171 for (std::size_t s = 1; s <= V.classes.size(); ++s)
172 for (std::size_t nd = 1; nd <= V.nodes.size(); ++nd) {
173 // CS-aware: a synthesized ClassSwitch on a branch carries its switch
174 // in `csmatrix`, not in `P`, so a walk over `P` alone would end the
175 // path there and lose the rest of the branch's response time.
176 if (!(detail::fj_route_cs(V, curClass, s, curNode, nd) > zero)) continue;
177 std::vector<std::size_t> m2 = merge;
178 m2[0] = s;
179
180 // Is `nd` another fork of this transform? Its node type is Router by
181 // now, so the fork records are the only way to tell.
182 std::size_t inner = tr.forks.size();
183 for (std::size_t b = 0; b < tr.forks.size(); ++b)
184 if (tr.forks[b].node == nd && tr.forks[b].joinNode != 0) inner = b;
185
186 if (inner < tr.forks.size()) {
187 const std::size_t ijoin = tr.forks[inner].joinNode;
188 const std::size_t istat = tr.forks[inner].joinStation;
189 // the inner fork's own auxiliary class for the class arriving at it
190 std::size_t iaux = 0;
191 for (std::size_t x : tr.auxclasses)
192 if (tr.fjforkmap[x] == inner && tr.fjclassmap[x] == s) iaux = x;
193 std::vector<std::size_t> m3 = m2;
194 if (iaux != 0) m3.push_back(iaux);
195
196 std::vector<T> paths;
197 fj_find_paths(tr, nd, ijoin, s, m3, QN, TN, zero, paths, visited, inner_d0);
198 T d0 = zero;
199 if (!paths.empty()) {
200 T mean = zero;
201 for (const T& x : paths) mean += x;
202 mean = T(mean / num_traits<T>::from_int(static_cast<long>(paths.size())));
203 // The join fires on the k-th branch completion, k = the branch
204 // count on a standard join and the declared quorum on a PARTIAL one.
205 d0 = fj_expected_ordstat(paths, fj_join_quorum(V, ijoin, paths.size()));
206 // The inner join's delay, for every class of the merge set.
207 // Note the reference charges E[max] - mean here WITHOUT the
208 // fanOut factor it applies at an outer fork. Under a quorum the
209 // k-th completion can precede a branch's own, and then the join
210 // adds no delay: the parent left on a sibling.
211 const Distrib<T> d =
212 fj_exp_fit_mean(d0 > mean ? T(d0 - mean) : num_traits<T>::from_int(0));
213 if (istat != 0)
214 for (std::size_t cls : m3) V.set_service(istat, cls, d);
215 }
216 // findPathsCS' second output: the nested pair's d0, after those of the
217 // pairs nested inside it and before those met past its join
218 if (inner_d0 != nullptr) {
219 FjInnerD0<T> rec;
220 rec.node = nd;
221 rec.cls = s;
222 rec.d0 = d0;
223 inner_d0->push_back(rec);
224 }
225 fj_find_paths(tr, ijoin, endNode, s, m2, QN, TN, T(t0 + d0), out, visited,
226 inner_d0);
227 } else {
228 fj_find_paths(tr, nd, endNode, s, m2, QN, TN,
229 T(t0 + fj_node_time(V, nd, m2, QN, TN)), out, visited, inner_d0);
230 }
231 }
232}
233
234/** The leading `n` columns of a class-indexed metric. */
235template <class T>
236Matrix<T> fj_leading_cols(const Matrix<T>& A, std::size_t n) {
237 Matrix<T> out(A.rows(), n, num_traits<T>::from_int(0));
238 for (std::size_t i = 0; i < A.rows(); ++i)
239 for (std::size_t j = 0; j < n && j < A.cols(); ++j) out(i, j) = A(i, j);
240 return out;
241}
242
243/** The mixed absolute/relative stopping test of the fork-join loop. */
244template <class T>
245bool fj_converged(const Matrix<T>& A, const Matrix<T>& B, double iter_tol) {
246 if (A.rows() != B.rows() || A.cols() != B.cols()) return false;
247 for (std::size_t i = 0; i < A.rows(); ++i)
248 for (std::size_t j = 0; j < A.cols(); ++j) {
249 const double d = std::fabs(num_traits<T>::to_double(A(i, j)) -
250 num_traits<T>::to_double(B(i, j)));
251 if (d > GlobalConstants::Zero +
252 iter_tol * std::fabs(num_traits<T>::to_double(B(i, j))))
253 return false;
254 }
255 return true;
256}
257
258/**
259 * Port of `fjFixedPoint.m:130-136`: the firing throughput of fork `fa`, per
260 * class of the BASE model.
261 *
262 * A fork is not a station and has no throughput of its own, so the reference
263 * derives one from the visit ratios: the fork's node visits, divided by the
264 * class's total visits to its reference station, times the throughput observed
265 * there. The divisor is the CHAIN's visit total at the reference station and
266 * the multiplier the CHAIN's throughput there, because visits are normalised
267 * per chain and a class-only ratio would not be scale-free under class
268 * switching.
269 *
270 * TWO INDEX SPACES MEET HERE, exactly as in the reference. `visits` is indexed
271 * by STATEFUL node and `refstat` is a STATION, so the reference station must be
272 * mapped through `stateful_of_station` before it indexes `visits`; `nodevisits`
273 * is indexed by NODE, which is what `parent` names. The throughput `TN` comes
274 * from the solve of the TRANSFORMED model and is indexed by its station rows,
275 * which the transform leaves aligned with the base model's -- the same
276 * assumption `fjFixedPoint.m` makes when it writes `TN(nodeToStation(joinIdx))`.
277 *
278 * `parent` is this fork's own index when it is not nested: an inner fork fires
279 * as often as the outer one it sits behind, so the visits that measure it are
280 * the OUTER fork's, which is what `sortForks` returns in `parent_forks`.
281 */
282template <class T>
283std::vector<T> fj_fork_tput(const qn::NetworkStruct<T>& L, const FjMmt<T>& tr, const Matrix<T>& TN,
284 std::size_t fa) {
285 const T zero = num_traits<T>::from_int(0);
286 std::vector<T> tnfork(L.nclasses, zero);
287 const std::size_t pnode = tr.forks[tr.forks[fa].parent].node;
288 for (std::size_t c = 0; c < L.nchains; ++c) {
289 const std::vector<std::size_t>& ic = L.inchain[c];
290 for (std::size_t r : ic) {
291 const std::size_t rs = L.classes[r - 1].refstat;
292 const std::size_t isf = L.stateful_of_station(rs);
293 T den = zero, tsum = zero;
294 for (std::size_t k : ic) {
295 den += L.visits[c](isf - 1, k - 1);
296 tsum += TN(rs - 1, k - 1);
297 }
298 if (!(den > zero)) continue;
299 tnfork[r - 1] = T(T(L.nodevisits[c](pnode - 1, r - 1) / den) * tsum);
300 }
301 }
302 return tnfork;
303}
304
305/** A metric read out of a solve, with a non-finite entry read as zero. */
306template <class T>
307T fj_finite_or_zero(const T& x) {
308 if (!std::isfinite(num_traits<T>::to_double(x))) return num_traits<T>::from_int(0);
309 return x;
310}
311
312/**
313 * Port of the `heidelberger-trivedi` arm of `fjFixedPoint.m:212-255`: the
314 * synchronisation delays of one pass, written onto the transformed model.
315 *
316 * The branch response time of an auxiliary class is its TOTAL response time over
317 * the model, less what it spends at the join (the residual synchronisation delay
318 * this function is about to overwrite) and at the auxiliary delay (the original
319 * class's time outside the span). That leaves exactly the time the branch takes.
320 * From those,
321 *
322 * d0 = E[X_(k)] fires the join. k is the branch count on a standard join and
323 * the declared quorum on a PARTIAL one.
324 * di = d0 - ri is what branch i still waits at the join once it has finished.
325 * r0 is the original class's cycle response time less its own time
326 * at the join, i.e. everything outside the span, which is what
327 * the auxiliary delay must hold so that the auxiliary token
328 * cycles at the original's rate.
329 *
330 * The join charges `d0 * fanOut` to the ORIGINAL class, which is the whole span:
331 * `fj_ht` routed it straight past the branches. `fanOut` is 1 on every model that
332 * reaches here -- `fj_ht` refuses tasksPerLink > 1 by name -- and is kept as a
333 * named factor because the reference multiplies by it.
334 *
335 * UNDER A QUORUM `d0` can precede a branch's own completion, and then that branch
336 * waits no further; `di` floors at zero, which is a boundary of the transform and
337 * not a choice. The reference raises a line_warning there; this port has no
338 * warning channel out of the fixed point (see mva_dispatch.h).
339 */
340template <class T>
342 FjSectionAcc<T>* fjacc = nullptr) {
343 const T zero = num_traits<T>::from_int(0);
345 for (std::size_t fa = 0; fa < tr.forks.size(); ++fa) {
346 const std::size_t fnode = tr.forks[fa].node;
347 const std::size_t jnode = tr.forks[fa].joinNode;
348 if (jnode == 0) continue;
349 const std::size_t jstat = tr.forks[fa].joinStation;
350 const std::size_t adstat = tr.auxDelayStation[jnode];
351 for (std::size_t c = 0; c < L.nchains; ++c) {
352 const std::vector<std::size_t>& ic = L.inchain[c];
353 for (std::size_t xi = 0; xi < ic.size(); ++xi) {
354 const std::size_t r = ic[xi];
355 if (num_traits<T>::to_double(L.nodevisits[c](fnode - 1, r - 1)) == 0.0) continue;
356 // EVERY auxiliary class of r, which is what `find(fjclassmap
357 // == r)` selects, split into the branches THIS join waits on
358 // and the ones another fork owns.
359 //
360 // The reference separates them by arithmetic rather than by a
361 // test: its `RN` is `QN./TN`, so a class with no throughput at
362 // this join and at this auxiliary delay reads NaN there, and
363 // the two RAW subtractions carry that NaN into `ri`.
364 // `fj_ordstat_exp` then DROPS the NaN entries before it counts,
365 // and `di = d0 - ri` leaves them NaN, so `Exp.fitMean(di)` gives
366 // the foreign class the GLACIAL service at this join described
367 // at the write below. `r0`, by contrast, is written at the
368 // auxiliary delay for ALL of them, which is what lets the other
369 // fork's classes reach it at all -- and that changes the
370 // transformed model's visit ratios from the second pass on.
371 // Both halves are reproduced here.
372 std::vector<std::size_t> br, foreign;
373 std::vector<T> ri;
374 for (std::size_t s : tr.auxclasses) {
375 if (tr.fjclassmap[s] != r) continue;
376 if (num_traits<T>::to_double(out.Tp(adstat - 1, s - 1)) == 0.0 ||
377 num_traits<T>::to_double(out.Tp(jstat - 1, s - 1)) == 0.0) {
378 foreign.push_back(s);
379 continue;
380 }
381 T acc = zero;
382 for (std::size_t i = 0; i < out.R.rows(); ++i)
383 acc += fj_finite_or_zero(out.R(i, s - 1));
384 // the two subtracted terms are read RAW, as in the reference:
385 // its NaN sweep applies to the summed matrix only
386 acc = T(acc - out.R(adstat - 1, s - 1));
387 br.push_back(s);
388 ri.push_back(T(acc - out.R(jstat - 1, s - 1)));
389 }
390 if (br.empty()) continue;
391 const T d0 = fj_expected_ordstat(ri, fj_join_quorum(L, jnode, br.size()));
392 if (fjacc != nullptr) fjacc->put(fnode, r, d0); // fjR(f,r) = d0
393
394 T r0 = zero;
395 for (std::size_t i = 0; i < out.R.rows(); ++i) {
396 T row = zero;
397 for (std::size_t yi = 0; yi < ic.size(); ++yi) row += out.R(i, ic[yi] - 1);
398 r0 += fj_finite_or_zero(row);
399 }
400 r0 = T(r0 - out.R(jstat - 1, r - 1));
401
402 V.set_service(jstat, r,
404 tr.forks[fa].fanOut))));
405 const Distrib<T> outside = fj_exp_fit_mean(r0);
406 for (std::size_t b = 0; b < br.size(); ++b) {
407 T di = T(d0 - ri[b]);
408 if (di < zero) di = zero;
409 V.set_service(jstat, br[b], fj_exp_fit_mean(di));
410 V.set_service(adstat, br[b], outside);
411 }
412 // The reference writes `Exp.fitMean(di(idx))` for EVERY aux class
413 // of r, and for the OTHER fork's classes di is NaN. fitMean is
414 // `Exp(min(Immediate, max(Zero, 1/MEAN)))` and MATLAB's max IGNORES
415 // NaN, so that is `Exp(GlobalConstants.Zero)`: a SERVED station of
416 // mean 1e14, NOT a disabled one.
417 //
418 // The difference is structural rather than numeric. Disabling drops
419 // the (station,class) pair from the `served` mask, which zeroes the
420 // join's row AND column, kills the fork's row, and strands Delay1 as
421 // an isolated weak component that ctmc_solve's equal-weight-per-
422 // component rule hands half the mass -- a visit of 4 at a station the
423 // class never reaches. Served-but-glacial keeps the chain connected,
424 // so those states stay transient and their visits stay 0.
425 const Distrib<T> glacial = Distrib<T>::exp_rate(
427 for (std::size_t s : foreign) {
428 V.set_service(jstat, s, glacial);
429 V.set_service(adstat, s, outside);
430 }
431 }
432 }
433 }
434}
435
436/**
437 * Port of the `heidelberger-trivedi` arm of `fjFixedPoint.m:263-291`: fold the
438 * auxiliary columns back into the classes they stand for.
439 *
440 * EVERY JOIN LOSES ITS ORIGINAL-CLASS METRICS FIRST. The original class was
441 * routed straight past the branches and charged the whole span at the join, so
442 * what the solve reports for it there is the transform's own bookkeeping and not
443 * a queue the model has; the branches' figures, which the auxiliary classes
444 * carry, are what belongs there. Throughput is the exception: the join's
445 * original-class rate is how often the fork-join completes, so it is saved and
446 * put back after the merge, where the reference puts it back too.
447 *
448 * The auxiliary DELAY rows are left in place rather than deleted. The reference
449 * deletes them; they are appended after every base station, so the leading block
450 * the caller reads is the same either way, and deleting them would only move the
451 * join rows the restore step above indexes by their pre-deletion position.
452 */
453template <class T>
455 const T zero = num_traits<T>::from_int(0);
456 std::vector<bool> orig(tr.V.classes.size() + 1, false);
457 for (std::size_t s : tr.auxclasses) orig[tr.fjclassmap[s]] = true;
458
459 std::vector<std::vector<T>> tnJoin(tr.joinStations.size(),
460 std::vector<T>(L.nclasses + 1, zero));
461 for (std::size_t a = 0; a < tr.joinStations.size(); ++a) {
462 const std::size_t js = tr.joinStations[a];
463 for (std::size_t r = 1; r <= L.nclasses; ++r) {
464 if (!orig[r]) continue;
465 tnJoin[a][r] = out.Tp(js - 1, r - 1);
466 out.Q(js - 1, r - 1) = zero;
467 out.R(js - 1, r - 1) = zero;
468 out.Tp(js - 1, r - 1) = zero;
469 out.U(js - 1, r - 1) = zero;
470 }
471 }
472 const std::size_t nrows = out.Tp.rows();
473 for (std::size_t s : tr.auxclasses) {
474 const std::size_t r = tr.fjclassmap[s];
475 for (std::size_t i = 0; i < nrows; ++i) {
476 out.Q(i, r - 1) = T(out.Q(i, r - 1) + out.Q(i, s - 1));
477 out.U(i, r - 1) = T(out.U(i, r - 1) + out.U(i, s - 1));
478 out.Tp(i, r - 1) = T(out.Tp(i, r - 1) + out.Tp(i, s - 1));
479 out.R(i, r - 1) =
480 out.Tp(i, r - 1) != zero ? T(out.Q(i, r - 1) / out.Tp(i, r - 1)) : zero;
481 }
482 }
483 for (std::size_t a = 0; a < tr.joinStations.size(); ++a) {
484 const std::size_t js = tr.joinStations[a];
485 for (std::size_t r = 1; r <= L.nclasses; ++r)
486 if (orig[r]) out.Tp(js - 1, r - 1) = tnJoin[a][r];
487 }
488 out.Q = fj_leading_cols(out.Q, L.nclasses);
489 out.U = fj_leading_cols(out.U, L.nclasses);
490 out.R = fj_leading_cols(out.R, L.nclasses);
491 out.Tp = fj_leading_cols(out.Tp, L.nclasses);
492}
493
494/**
495 * Drive the fork-join fixed point of a transformed model to convergence.
496 *
497 * @param L the base model, whose services are copied back into the transform
498 * at the start of every solve (ModelAdapter.refreshServicesFromBase)
499 * @param tr the transform, mutated in place: its join and Source services carry
500 * the current pass's synchronisation delays and auxiliary arrivals
501 * @param lam the auxiliary arrival rates, `self.fjForkLambda`; warm-started by
502 * the caller across outer iterations and updated here in place
503 * @param opt MVA options (iter_max, iter_tol)
504 * @param inner the inner solve over the auxiliary-expanded model
505 * @param warning when non-null, receives the reference's line_warning text if the
506 * loop exhausts iter_max, and is left untouched otherwise
507 * @return the merged MvaSolution over the base class set (auxiliary columns
508 * dropped, join and Source throughputs kept at their original-class value);
509 * `converged` is false when the loop exhausted iter_max
510 */
511template <class T, class InnerSolve>
513 std::vector<T>& lam, const MvaOptions& opt,
514 InnerSolve inner, std::string* warning = nullptr) {
515 const T zero = num_traits<T>::from_int(0);
516 const T two = num_traits<T>::from_int(2);
518
519 // ---- ModelAdapter.refreshServicesFromBase --------------------------------
520 // MMT ONLY. `fjFixedPoint.m` reuses the MMT transform across outer
521 // iterations and has to undo the previous one's converged services; the H-T
522 // arm rebuilds its transform on every call (`ModelAdapter.ht` is invoked
523 // unconditionally at forkIter == 1) and has no auxiliary open stream, so
524 // there is nothing here for it to reset.
525 if (!tr.heidelberger_trivedi) {
526 // Base-derived slots are re-read from the base model. The
527 // transformation-owned ones -- the join's synchronisation delays and the
528 // auxiliary arrivals -- are RESET to what a cold transform would hold, not
529 // left alone: they still carry the previous outer iteration's converged
530 // values, and keeping them would silently warm-start the fork loop from a
531 // different point than the reference does.
532 for (std::size_t i = 1; i <= L.stations.size(); ++i)
533 for (std::size_t k = 1; k <= L.classes.size(); ++k)
534 V.set_service(i, k, L.service[i - 1][k - 1]);
535 for (std::size_t s : tr.auxclasses) {
536 const std::size_t r = tr.fjclassmap[s];
537 for (std::size_t i = 1; i <= L.stations.size(); ++i) {
538 if (tr.is_join_station(i)) continue;
539 V.set_service(i, s, L.service[i - 1][r - 1]);
540 }
541 }
542 for (std::size_t js : tr.joinStations)
543 for (std::size_t k = 1; k <= V.classes.size(); ++k)
545 for (std::size_t s : tr.auxclasses)
546 V.set_service(tr.sourceStation, s,
547 tr.auxdisabled[s]
551 }
552
553 // QN starts at the Immediate sentinel and QN_1 at zero, so the first two
554 // passes can never be mistaken for a converged pair.
556 Matrix<T> QN_1(1, L.nclasses, zero);
557 MvaSolution<T> out;
558 // fjR of fjFixedPoint.m, the section sojourn per (fork node, original class)
559 FjSectionAcc<T> fjR;
560 fjR.init(L.nodes.size(), L.nclasses);
561 bool forkLoop = true;
562 int forkIter = 0;
563 while (forkLoop && forkIter < opt.iter_max) {
564 ++forkIter;
565 // `fjFixedPoint.m:77`: the auxiliary arrival update is guarded on the
566 // method NOT being H-T, whose auxiliary classes are closed and carry no
567 // arrival rate to re-set.
568 if (forkIter > 1 && !tr.heidelberger_trivedi) {
569 // the auxiliary stream carries the fanout-1 branches the circulating
570 // job did not take
571 for (std::size_t s : tr.auxclasses) {
572 if (tr.auxdisabled[s] || !(tr.fanout[s] > 0.0)) continue;
573 V.set_service(
574 tr.sourceStation, s,
576 T(num_traits<T>::from_double(tr.fanout[s] - 1.0) * lam[s])));
577 }
578 }
579 V.refresh_chains();
580
582 if (QN_1.rows() == QN.rows() && QN_1.cols() == QN.cols()) {
583 double moved = 0.0;
584 for (std::size_t i = 0; i < QN.rows(); ++i)
585 for (std::size_t j = 0; j < QN.cols(); ++j)
586 moved = std::max(moved, std::fabs(num_traits<T>::to_double(QN_1(i, j)) -
587 num_traits<T>::to_double(QN(i, j))));
589 "fork-join iteration %d: queue lengths moved by at most %.3e", forkIter, moved);
590 } else {
592 "fork-join iteration %d: transformed model rebuilt", forkIter);
593 }
594 }
595
596 if (fj_converged(QN_1, QN, opt.iter_tol) && forkIter > 2)
597 forkLoop = false;
598 else
599 QN_1 = QN;
600
601 out = inner(V);
602
603 if (tr.heidelberger_trivedi) {
604 fj_ht_sync_delays(L, tr, out, &fjR);
605 fj_ht_merge(L, tr, out);
606 QN = out.Q;
607 continue;
608 }
609
610 // ---- synchronisation delays and the forkLambda update ----------------
611 // One pass per fork, as in the reference: each fork drives only ITS OWN
612 // auxiliary classes, off its own join.
613 for (std::size_t fa = 0; fa < tr.forks.size(); ++fa) {
614 const std::size_t fnode = tr.forks[fa].node;
615 const std::size_t jstat = tr.forks[fa].joinStation;
616
617 // A FORK WITH NO JOIN. `fjFixedPoint.m:142-152` drives forkLambda
618 // from the fork's OWN firing rate rather than a join's throughput,
619 // and charges no synchronisation delay, there being no station to
620 // charge it to. `fj_sort_forks` has already left such a fork outer
621 // with itself as parent, which is what `sortForks` returns for it.
622 if (jstat == 0) {
623 const std::vector<T> tnfork = fj_fork_tput(L, tr, out.Tp, fa);
624 for (std::size_t s : tr.auxclasses) {
625 if (tr.fjforkmap[s] != fa) continue;
626 lam[s] = T((lam[s] + tnfork[tr.fjclassmap[s] - 1]) / two);
627 }
628 continue;
629 }
630
631 for (std::size_t s : tr.auxclasses) {
632 if (tr.fjforkmap[s] != fa) continue;
633 const std::size_t r = tr.fjclassmap[s];
634 T acc = zero;
635 for (std::size_t s2 : tr.auxclasses)
636 if (tr.fjclassmap[s2] == r) acc += out.Tp(jstat - 1, s2 - 1);
637 out.Tp(jstat - 1, r - 1) =
638 T(out.Tp(jstat - 1, r - 1) + acc - out.Tp(jstat - 1, s - 1));
639 lam[s] = T((lam[s] + out.Tp(jstat - 1, r - 1)) / two);
640
641 // An INNER fork's delay is charged by fj_find_paths while the
642 // enclosing branch is walked, so computing it again here would
643 // count it twice; only an outer fork's own delay is set here.
644 if (!tr.forks[fa].outer[r]) continue;
645
646 std::vector<T> ri;
647 std::vector<std::size_t> merge{r, s};
648 std::vector<FjInnerD0<T>> innerFJ;
649 fj_find_paths(tr, fnode, tr.forks[fa].joinNode, r, merge, out.Q, out.Tp, zero, ri,
650 {}, &innerFJ);
651 // nested pairs are charged d0 on the outer path; an auxiliary class
652 // maps back to the original class it stands for
653 for (const FjInnerD0<T>& q : innerFJ) {
654 std::size_t cq = q.cls;
655 if (cq > L.nclasses) cq = cq < tr.fjclassmap.size() ? tr.fjclassmap[cq] : 0;
656 if (cq >= 1 && cq <= L.nclasses) fjR.put(q.node, cq, q.d0);
657 }
658 T sync = zero;
659 if (!ri.empty()) {
660 // tasksPerLink = w sends w IDENTICAL tasks down each link, so the
661 // join synchronises on w*B siblings and not on B: the sibling set
662 // is each branch's completion time REPLICATED w times, and the
663 // order statistic is taken over that multiset. Scaling E[X_(k)] by
664 // w instead (what this did before) is w*H_B/mu where the answer is
665 // H_(w*B)/mu, which OVER-states the delay by more the larger w is.
666 // w = 1 replicates to itself, so nothing moves on an ordinary fork.
667 double wd = tr.forks[fa].fanOut;
668 std::size_t w = (wd >= 1.0) ? static_cast<std::size_t>(wd + 0.5) : 1;
669 if (w > 1) {
670 const std::vector<T> base = ri;
671 for (std::size_t k = 1; k < w; ++k)
672 ri.insert(ri.end(), base.begin(), base.end());
673 }
674 T mean = zero;
675 for (const T& x : ri) mean += x;
676 mean = T(mean / num_traits<T>::from_int(static_cast<long>(ri.size())));
677 // The join fires on the k-th sibling completion, k = the sibling
678 // count on a standard join and the declared quorum on a PARTIAL
679 // one. The quorum is declared against the SIBLING count w*B, which
680 // is the replicated length.
681 const T d0 = fj_expected_ordstat(
682 ri, fj_join_quorum(L, tr.forks[fa].joinNode, ri.size()));
683 fjR.put(fnode, r, d0); // stored before the syncDelay floor below
684 const T raw = T(d0 - mean);
685 // The quorum can be met BEFORE the branch this transform's own
686 // token walks, and then the parent ought to leave ahead of it.
687 // The MMT cannot express that: its token is a job of the closed
688 // chain and must finish its branch, and that closed token is what
689 // keeps the branch stable, so it cannot be made open either. The
690 // delay floors at zero, which OVER-states the cycle time.
691 // The reference raises a line_warning here; this port has no
692 // warning channel out of the fixed point (see mva_dispatch.h).
693 sync = raw > zero ? raw : zero;
694 }
695 const Distrib<T> d = fj_exp_fit_mean(sync);
696 V.set_service(jstat, s, d);
697 V.set_service(jstat, r, d);
698 }
699 }
700
701 // ---- merge the auxiliary classes back --------------------------------
702 // EVERY join and the Source keep their ORIGINAL-class throughputs: the
703 // auxiliary tokens are extra fork branches, not extra completions of the
704 // class, so adding them there would double the rate at which the fork is
705 // seen to fire.
706 const std::size_t nrows = out.Tp.rows();
707 std::vector<std::vector<T>> tnJoin(tr.joinStations.size(),
708 std::vector<T>(L.nclasses, zero));
709 std::vector<T> tnSource(L.nclasses, zero);
710 for (std::size_t a = 0; a < tr.joinStations.size(); ++a)
711 for (std::size_t k = 0; k < L.nclasses; ++k)
712 tnJoin[a][k] = out.Tp(tr.joinStations[a] - 1, k);
713 for (std::size_t k = 0; k < L.nclasses; ++k)
714 tnSource[k] = out.Tp(tr.sourceStation - 1, k);
715 for (std::size_t s : tr.auxclasses) {
716 const std::size_t r = tr.fjclassmap[s];
717 for (std::size_t i = 0; i < nrows; ++i) {
718 out.Q(i, r - 1) = T(out.Q(i, r - 1) + out.Q(i, s - 1));
719 out.U(i, r - 1) = T(out.U(i, r - 1) + out.U(i, s - 1));
720 out.Tp(i, r - 1) = T(out.Tp(i, r - 1) + out.Tp(i, s - 1));
721 out.R(i, r - 1) = out.Tp(i, r - 1) != zero
722 ? T(out.Q(i, r - 1) / out.Tp(i, r - 1))
723 : zero;
724 }
725 }
726 for (std::size_t a = 0; a < tr.joinStations.size(); ++a)
727 for (std::size_t k = 0; k < L.nclasses; ++k)
728 out.Tp(tr.joinStations[a] - 1, k) = tnJoin[a][k];
729 for (std::size_t k = 0; k < L.nclasses; ++k)
730 out.Tp(tr.sourceStation - 1, k) = tnSource[k];
731
732 // drop the auxiliary columns; they are all appended after the original
733 // ones, so the original block is the leading submatrix
734 out.Q = fj_leading_cols(out.Q, L.nclasses);
735 out.U = fj_leading_cols(out.U, L.nclasses);
736 out.R = fj_leading_cols(out.R, L.nclasses);
737 out.Tp = fj_leading_cols(out.Tp, L.nclasses);
738 QN = out.Q;
739 }
740 // fjFixedPoint.m warns and returns the interim solution; a caller that
741 // passed no sink still sees it through `converged` (BUG-100).
742 if (forkLoop && forkIter >= opt.iter_max) {
743 out.converged = false;
744 if (warning != nullptr)
745 *warning = std::string("The fork-join (") +
746 (tr.heidelberger_trivedi ? "ht" : "mmt") +
747 ") fixed point did not converge in options.iter_max=" +
748 std::to_string(opt.iter_max) +
749 " iterations; returning the interim solution.";
750 }
751 // A fork model that comes back non-finite has not been solved. It happens
752 // when the auxiliary open stream saturates a branch station: the mixed MVA
753 // then divides by a non-positive slack and the NaN propagates, where it
754 // reads as a solved model with a few blank entries. Refuse by name instead.
755 for (std::size_t i = 0; i < out.Q.rows(); ++i)
756 for (std::size_t k = 0; k < out.Q.cols(); ++k)
757 if (!std::isfinite(num_traits<T>::to_double(out.Q(i, k))) ||
758 !std::isfinite(num_traits<T>::to_double(out.U(i, k))) ||
759 !std::isfinite(num_traits<T>::to_double(out.R(i, k))) ||
760 !std::isfinite(num_traits<T>::to_double(out.Tp(i, k))))
761 throw NumericError(ErrorKind{"Instability"},
762 "SolverMVA: the fork-join fixed point of model '" + L.name +
763 "' returned a non-finite metric; the auxiliary open classes the transform "
764 "adds have saturated one of the branch stations");
765 // fjSectionMetrics: FJTput off the merged Join throughput, FJRespT off fjR
766 out.fj = fj_section_metrics(L, out.Tp, fjR);
767 return out;
768}
769
770} // namespace mva
771} // namespace line
772
773#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:112
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.
Fork-join section metrics: FJQLen, FJRespT and FJTput of every Fork/Join pair.
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_ht_sync_delays(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, const MvaSolution< T > &out, FjSectionAcc< T > *fjacc=nullptr)
Port of the heidelberger-trivedi arm of fjFixedPoint.m:212-255: the synchronisation delays of one pas...
Definition fj_driver.h:341
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:236
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:245
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:454
MvaSolution< T > fj_fixed_point(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, std::vector< T > &lam, const MvaOptions &opt, InnerSolve inner, std::string *warning=nullptr)
Drive the fork-join fixed point of a transformed model to convergence.
Definition fj_driver.h:512
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:283
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:307
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, std::vector< FjInnerD0< T > > *inner_d0=nullptr)
Port of ModelAdapter.findPathsCS: the response time along every path from a fork to ITS join,...
Definition fj_driver.h:156
solvers::FjMetrics< T > fj_section_metrics(const qn::NetworkStruct< T > &L, const Matrix< T > &TN, const FjSectionAcc< T > &acc)
fjSectionMetrics of fjFixedPoint.m: one row per Fork/Join pair of the BASE model.
Definition fj_driver.h:109
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:57
FjMetrics< T > fj_metrics_zero(const S &sn, std::size_t nclasses)
An empty-but-shaped result: every pair of sn, all three matrices zero.
Definition fj_metrics.h:73
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
SolverMVA over a SolverLN layer.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:764
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
static Distrib disabled_dist()
Definition lang_types.h:997
static Distrib immediate()
The point mass at zero.
Definition lang_types.h:986
One nested fork walked by fj_find_paths: the second output inner of findPathsCS.
Definition fj_driver.h:96
T d0
the order statistic the outer path is charged for it
Definition fj_driver.h:99
std::size_t node
1-based fork node
Definition fj_driver.h:97
std::size_t cls
1-based class arriving at it, possibly auxiliary
Definition fj_driver.h:98
The transformed layer and the bookkeeping the fixed point needs to drive it and to merge its results ...
Definition fj_mmt.h:113
fjR of fjFixedPoint.m: the order-statistic sojourn d0 the fixed point charges each fork-join section,...
Definition fj_driver.h:79
void init(std::size_t nnodes, std::size_t nclasses)
Definition fj_driver.h:82
std::vector< std::vector< bool > > set
whether R(node, class) was ever written
Definition fj_driver.h:81
void put(std::size_t node, std::size_t cls, const T &d0)
Record d0 for a 1-based (node, class); a class outside the base set is ignored.
Definition fj_driver.h:87
Matrix< T > R
(nnodes x nclasses) d0, 0-based storage
Definition fj_driver.h:80
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:771
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double Zero
Definition lang_types.h:767
The options SolverMVA reads.
Definition mva_types.h:32
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:97
std::optional< bool > converged
Whether the fixed point met its tolerance, or empty when the handler reports none.
Definition mva_types.h:110
solvers::FjMetrics< T > fj
Fork-join section metrics set by the fork-join fixed point (fj_driver.h); empty otherwise.
Definition mva_types.h:121
self.result.FJ: one row per Fork/Join pair, one column per class.
Definition fj_metrics.h:43
std::vector< std::size_t > forks
1-based fork node of each pair
Definition fj_metrics.h:44
Matrix< T > Tput
FJTput, P x R.
Definition fj_metrics.h:48
Matrix< T > QLen
FJQLen, P x R.
Definition fj_metrics.h:46
std::size_t npairs() const
Definition fj_metrics.h:53
std::vector< std::size_t > joins
1-based join node of each pair
Definition fj_metrics.h:45
Matrix< T > RespT
FJRespT, P x R.
Definition fj_metrics.h:47