LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fj_tag.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_LANG_QN_FJ_TAG_H
6#define LINE_LANG_QN_FJ_TAG_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * Port of `matlab/src/api/fj/sn_fj_validate.m` and
12 * `matlab/src/io/@@ModelAdapter/fjtag.m`: the model-to-model transform that makes
13 * a closed fork-join network EXACTLY solvable by a Markov chain.
14 *
15 * WHY A TRANSFORM AND NOT A JOIN HANDLER. A join must release one parent job
16 * once ALL siblings of THAT parent have arrived. A per-class count vector at the
17 * join cannot express "of that parent": with two parents outstanding and two
18 * branches, the counts (1,1) are consistent both with one sibling from each
19 * parent (nothing may join) and with both siblings of one parent (a join must
20 * fire). Any solver that reads only counts has to guess, and guessing is what
21 * makes a join an approximation.
22 *
23 * WHAT THE TRANSFORM DOES. For each (fork f, class r) with matched join j, each
24 * branch b = 1..B and each TAG t = 1..T, it mints an auxiliary closed class
25 * A(f,r,b,t) with population 0. The tag names the parent job. A fork firing
26 * consumes one parent held at the fork and emits one sibling per branch in the
27 * classes of ONE tag -- the lowest free one -- and the join fires only when
28 * every branch of some tag is present. Identity matching is then exact even when
29 * siblings overtake one another, and the price is B*T extra classes per (fork,
30 * class): the state space grows accordingly, which is why this is the exact
31 * solver's route and `fj_mmt` remains the mean-value one.
32 *
33 * WHY T IS THE CHAIN POPULATION AND NOT THE CLASS POPULATION. Class switching
34 * OUTSIDE the fork-join section can concentrate the whole chain in class r -- a
35 * class switch on the edge into the fork does exactly that -- so the number of
36 * concurrently outstanding forked jobs is bounded by the chain, not by the
37 * declared population of r.
38 *
39 * WHY THE LOWEST FREE TAG. Tags are interchangeable, so without a canonical
40 * choice each firing would produce T! equivalent successors and the chain would
41 * carry a permutation group's worth of duplicate states. Allocating the lowest
42 * free tag makes exactly one `fjsync` entry enabled per (fork, class) in any
43 * state.
44 *
45 * HOW THIS DIFFERS FROM THE REFERENCE'S ROUTE. `fjtag.m` copies the Network
46 * OBJECT, adds `ClosedClass` objects, re-links it and calls `getStruct`. Here
47 * the transform is applied to the refreshed `NetworkStruct` directly, as
48 * `tag_chain` and `fj_mmt` already are: the auxiliary routing is written into P
49 * and one `refresh_struct()` re-derives the chains, capacities and rt. The
50 * post-edits that follow are the reference's own -- it overrides the visits and
51 * the auxiliary capacities after `getStruct` for the same reason, because the
52 * engines read them only as a zero-versus-nonzero support gate.
53 */
54
55#include <algorithm>
56#include <cmath>
57#include <cstddef>
58#include <limits>
59#include <map>
60#include <string>
61#include <vector>
62
64#include "line/util/error.h"
65
66namespace line {
67namespace qn {
68
69/** The augmented struct and everything needed to read its results back. */
70template <class T>
71struct FjTagged {
73 /** fjclassmap[a-1] is the ORIGINAL class of auxiliary class a, 0 for originals. */
74 std::vector<std::size_t> fjclassmap;
75 std::vector<std::size_t> fjforkmap, fjjoinmap, fjbranchmap, fjtagmap;
76 std::vector<FjSync<T>> fjsync;
77 std::map<std::size_t, FjJoinParam> joinparam; ///< keyed by 1-based Join node
78 std::size_t korig = 0;
79};
80
81namespace fj_detail {
82
83/** Total per-class node visits, `cellsum(sn.nodevisits)`. */
84template <class T>
85std::vector<std::vector<double>> node_visit_sum(const NetworkStruct<T>& sn) {
86 const std::size_t I = sn.nodes.size(), K = sn.nclasses;
87 std::vector<std::vector<double>> V(I, std::vector<double>(K, 0.0));
88 for (std::size_t c = 0; c < sn.nodevisits.size(); ++c)
89 for (std::size_t i = 0; i < I; ++i)
90 for (std::size_t r = 0; r < K; ++r)
91 V[i][r] += num_traits<T>::to_double(sn.nodevisits[c](i, r));
92 return V;
93}
94
95} // namespace fj_detail
96
97/**
98 * Port of `sn_fj_validate`: is this fork-join model inside the exact solver's
99 * reach?
100 *
101 * The checks here are the STRUCTURAL ones that can be asked of the original
102 * struct; the branch-level ones (class switching on a branch, nesting, sibling
103 * traps) can only be asked during branch discovery and are raised there.
104 */
105template <class T>
107 const std::vector<std::vector<double>> Vn = fj_detail::node_visit_sum(sn);
108 for (std::size_t f = 1; f <= sn.nodes.size(); ++f) {
109 if (sn.nodes[f - 1].nodetype != NodeType::Fork) continue;
110 std::size_t nj = 0;
111 for (std::size_t p = 0; p < sn.fj.size(); ++p)
112 if (sn.fj[p].first == f) ++nj;
113 if (nj == 0)
114 throw UnsupportedError(
115 "sn_fj_validate: the Fork node '" + sn.nodes[f - 1].name +
116 "' has no matched Join; the exact fork-join implementation needs the pair, since "
117 "the tag pool is sized by the join that consumes it");
118 if (nj > 1)
119 throw UnsupportedError("sn_fj_validate: the Fork node '" + sn.nodes[f - 1].name +
120 "' has several matched Joins, which the exact fork-join "
121 "implementation does not support");
122 // TASKS PER LINK. The tag-augmented construction carries an integer
123 // weight per branch, so a link whose count is an integer is served
124 // exactly, whether or not it differs from its siblings'. What it cannot
125 // carry is a count that is not fixed at build time: a DISTRIBUTION has
126 // to be drawn per firing, and the auxiliary class capacity would have to
127 // be its support's maximum with a different tag occupancy per draw.
128 const qn::NodeDef& fk = sn.nodes[f - 1];
129 const qn::ForkParam<T>* fp = sn.fork_param_of(f);
130 const double w = fk.tasks_per_link;
131 if (fp == 0) {
132 if (w != std::floor(w))
133 throw UnsupportedError(
134 "sn_fj_validate: the Fork node '" + fk.name +
135 "' has a non-integer tasksPerLink; a sibling is a job and cannot be emitted "
136 "in fractions");
137 } else {
138 bool uncertain = false;
139 for (std::size_t k = 0; k < fp->fan_out_link.rows(); ++k)
140 for (std::size_t r = 0; r < fp->fan_out_link.cols(); ++r) {
141 const double p = num_traits<T>::to_double(fp->fan_out_prob(k, r));
142 if (p == 0.0) continue; // link not taken
143 if (!fp->fan_out_dist[k][r].disabled)
144 throw UnsupportedError(
145 "sn_fj_validate: the Fork node '" + fk.name +
146 "' draws its tasks per link from a distribution, which the exact "
147 "fork-join implementation cannot carry: the tag occupancy would "
148 "differ per firing. Use SolverJMT or SolverLDES, which draw the "
149 "degree at the fork epoch");
150 const double lv = num_traits<T>::to_double(fp->fan_out_link(k, r));
151 if (lv != std::floor(lv))
152 throw UnsupportedError(
153 "sn_fj_validate: the Fork node '" + fk.name +
154 "' has a non-integer tasksPerLink; a sibling is a job and cannot be "
155 "emitted in fractions");
156 // A PER-LINK COUNT is carried: `ForkInfo::wlink` holds one
157 // integer per branch and it reaches the auxiliary capacity,
158 // the join's required count and the emission list. The only
159 // thing that has to be fixed at build time is that it is an
160 // integer and does not change per firing, which is what the
161 // two tests above ask.
162 if (p != 1.0) uncertain = true;
163 }
164 // BRANCH PROBABILITIES. A branch that may decline makes the SET of
165 // siblings random, so a firing has 2^B outcomes and the join's
166 // required count is a function of which subset fired. The tag
167 // construction records no such per-firing state -- `required[r]` is
168 // fixed when the state space is built -- so this path would emit
169 // every branch anyway and answer with the CERTAIN-fork number. It is
170 // refused by name instead: silently returning the answer to a
171 // different model is the one outcome worth avoiding. MATLAB and
172 // Python refuse it here for the same reason.
173 if (uncertain)
174 throw UnsupportedError(
175 "sn_fj_validate: the Fork node '" + fk.name +
176 "' has a branch activation probability below one, so the SET of siblings is "
177 "random and the tag construction fixes it when the state space is built. Use "
178 "SolverJMT or SolverLDES, which draw the activation at the fork epoch, or "
179 "SolverMVA, whose MMT transform sees the expected degree");
180 }
181 for (std::size_t r = 1; r <= sn.nclasses; ++r) {
182 if (!(Vn[f - 1][r - 1] > 0)) continue;
183 // The tag pool is the chain population, so the WHOLE chain must be
184 // closed -- an open class anywhere in it makes the pool unbounded.
185 std::size_t c = 0;
186 for (std::size_t cc = 0; cc < sn.chains.size(); ++cc)
187 if (sn.chains[cc][r - 1]) { c = cc + 1; break; }
188 bool open = !std::isfinite(sn.njobs()[r - 1]);
189 if (c != 0)
190 for (std::size_t s = 0; s < sn.nclasses; ++s)
191 if (sn.chains[c - 1][s] && !std::isfinite(sn.njobs()[s])) open = true;
192 if (open)
193 throw UnsupportedError(
194 "sn_fj_validate: class '" + sn.classes[r - 1].name +
195 "' is routed through the Fork node '" + sn.nodes[f - 1].name +
196 "' and belongs to an OPEN chain. The tag pool is sized by the chain "
197 "population, which is unbounded here; use SolverMVA (fj_mmt) or SolverSSA");
198 }
199 }
200 for (std::size_t j = 1; j <= sn.nodes.size(); ++j) {
201 if (sn.nodes[j - 1].nodetype != NodeType::Join) continue;
202 bool matched = false;
203 std::size_t fsrc = 0;
204 for (std::size_t p = 0; p < sn.fj.size(); ++p)
205 if (sn.fj[p].second == j) { matched = true; fsrc = sn.fj[p].first; }
206 if (!matched)
207 throw UnsupportedError("sn_fj_validate: the Join node '" + sn.nodes[j - 1].name +
208 "' has no matched Fork");
209 // JOIN STRATEGY. PARTIAL is served: `FjJoinParam::required[r][b]` is a
210 // per-branch count, so a quorum is that count LOWERED and not a
211 // different mechanism. What it must be is REACHABLE -- a quorum above
212 // what the fork is certain to emit would never fire, and a join that
213 // never fires turns the chain into an absorbing set rather than an
214 // error.
215 typename std::map<std::size_t, typename NetworkStruct<T>::JoinDecl>::const_iterator jd =
216 sn.joindecl.find(j);
217 if (jd != sn.joindecl.end()) {
218 if (jd->second.strategy != lang::JoinStrategy::STD &&
219 jd->second.strategy != lang::JoinStrategy::PARTIAL)
220 throw UnsupportedError(
221 "sn_fj_validate: only JoinStrategy STD and PARTIAL are supported by the exact "
222 "fork-join implementation; the Join node '" + sn.nodes[j - 1].name +
223 "' declares neither");
224 if (jd->second.strategy == lang::JoinStrategy::PARTIAL && jd->second.quorum > 0.0 &&
225 fsrc != 0) {
226 const qn::NodeDef& fk2 = sn.nodes[fsrc - 1];
227 const qn::ForkParam<T>* fp2 = sn.fork_param_of(fsrc);
228 double emitted = 0.0;
229 if (fp2 == 0) {
230 for (std::size_t nd = 1; nd <= sn.nodes.size(); ++nd)
231 for (std::size_t r = 1; r <= sn.nclasses; ++r)
232 if (num_traits<T>::to_double(sn.rtnodes((fsrc - 1) * sn.nclasses + r - 1,
233 (nd - 1) * sn.nclasses + r - 1)) >
234 0) {
235 emitted += fk2.tasks_per_link;
236 break;
237 }
238 } else {
239 for (std::size_t k = 0; k < fp2->fan_out_link.rows(); ++k)
240 for (std::size_t r = 0; r < fp2->fan_out_link.cols(); ++r)
241 if (num_traits<T>::to_double(fp2->fan_out_prob(k, r)) == 1.0)
242 emitted += num_traits<T>::to_double(fp2->fan_out_link(k, r));
243 }
244 if (jd->second.quorum > emitted)
245 throw UnsupportedError(
246 "sn_fj_validate: the Join node '" + sn.nodes[j - 1].name +
247 "' asks for more siblings than the Fork node '" + sn.nodes[fsrc - 1].name +
248 "' is certain to emit, so it could never fire");
249 }
250 }
251 }
252}
253
254/**
255 * Can the exact fork-join construction be asked for this model?
256 *
257 * The fork-join model class `sn_fj_validate` admits, asked as a predicate
258 * rather than thrown, so that a REPORT can reach it: `solver_ctmc_analyzer` and
259 * the SSA runner reach the SAME rules through `fj_tag`, which calls the
260 * validator on its first line. The message the validator throws is about "the
261 * exact fork-join implementation", which both share, so this predicate belongs
262 * beside it rather than inside either solver.
263 *
264 * IT WRAPS THE VALIDATOR RATHER THAN RESTATING IT, and that is the point: the
265 * rules are eight and they move (pairing, join strategy, tasks-per-link, branch
266 * probability, open classes through a fork), so a second copy would be a second
267 * thing to keep in step. There is exactly one body of rules and two ways in --
268 * one that throws, for the run, and this one, which answers.
269 *
270 * WHAT IT REFUSES AND WHY THE ANALYZER IS RIGHT TO. The fork-join PAIRING is a
271 * declaration carried by the Join (`Join(model, name, fork)` in all four
272 * codebases), not a derivation from the routing: a nested model such as
273 * fj_basic_nesting has two forks and two joins whose pairing the routing alone
274 * does not determine. So a Join built without naming its fork leaves `sn.fj`
275 * empty, and "has no matched Join" is the honest answer to a model that
276 * declares none -- not a topology test that failed to see one.
277 *
278 * @param sn the refreshed struct of the model
279 * @return an empty string when the fork-join construction may run
280 */
281template <class T>
282std::string sn_fj_supports(const NetworkStruct<T>& sn) {
283 bool any_fj = sn.has_fork();
284 for (std::size_t i = 0; i < sn.nodes.size() && !any_fj; ++i)
285 if (sn.nodes[i].nodetype == NodeType::Join) any_fj = true;
286 if (!any_fj) return std::string();
287 try {
289 } catch (const line::Error& e) {
290 // the validator's own refusal, turned into an answer
291 return std::string(e.what());
292 }
293 return std::string();
294}
295
296/**
297 * Port of `ModelAdapter.fjtag`.
298 *
299 * @param sn a refreshed struct whose Fork nodes all have a matched Join
300 */
301template <class T>
304
305 const std::size_t K = sn.nclasses;
306 const std::size_t I = sn.nodes.size();
307 const std::vector<std::vector<double>> Vn = fj_detail::node_visit_sum(sn);
308 const T zero = num_traits<T>::from_int(0);
309 const double inf = std::numeric_limits<double>::infinity();
310
311 FjTagged<T> out;
312 out.korig = K;
313 out.V = sn;
314 NetworkStruct<T>& V = out.V;
315 V.isfjaugmented = true;
316
317 // A Fork holds the parent job for the instant between its arrival and the
318 // firing, so in the augmented model it is STATEFUL. `stateful_nodes` must
319 // stay ASCENDING, because `stateful_index` is a position in it and every
320 // network state row is indexed by that position.
321 for (std::size_t i = 0; i < I; ++i)
322 if (V.nodes[i].nodetype == NodeType::Fork) V.nodes[i].stateful = true;
323 V.stateful_nodes.clear();
324 for (std::size_t i = 0; i < I; ++i)
325 if (V.nodes[i].stateful) V.stateful_nodes.push_back(i + 1);
326
327 // One row per (fork, class): the branch structure the post-edits and the
328 // firing list are both built from.
329 struct ForkInfo {
330 std::size_t f = 0, j = 0, r = 0, w = 1;
331 /** Per-branch tasksPerLink, empty when every branch carries `w`. */
332 std::vector<std::size_t> wlink;
333 std::vector<std::size_t> branchheads;
334 std::vector<std::vector<std::size_t>> branchsets;
335 std::vector<std::vector<std::size_t>> auxmatrix; // B x T
336 };
337 std::vector<ForkInfo> info;
338
339 for (std::size_t f = 1; f <= I; ++f) {
340 if (sn.nodes[f - 1].nodetype != NodeType::Fork) continue;
341 std::size_t j = 0;
342 for (std::size_t p = 0; p < sn.fj.size(); ++p)
343 if (sn.fj[p].first == f) j = sn.fj[p].second;
344 const std::size_t w = static_cast<std::size_t>(sn.nodes[f - 1].tasks_per_link);
345
346 for (std::size_t r = 1; r <= K; ++r) {
347 if (!(Vn[f - 1][r - 1] > 0)) continue;
348 ForkInfo fi;
349 fi.f = f;
350 fi.j = j;
351 fi.r = r;
352 fi.w = w == 0 ? 1 : w;
353
354 // Branch heads: the nodes the fork routes class r to directly.
355 for (std::size_t nd = 1; nd <= I; ++nd)
356 if (num_traits<T>::to_double(sn.rtnodes((f - 1) * K + (r - 1),
357 (nd - 1) * K + (r - 1))) > 0)
358 fi.branchheads.push_back(nd);
359 const std::size_t B = fi.branchheads.size();
360 if (B < 2)
361 throw UnsupportedError(
362 "fj_tag: the Fork node '" + sn.nodes[f - 1].name +
363 "' has a single output link for class '" + sn.classes[r - 1].name +
364 "'. A degenerate fork is not a fork -- remove it, or give it a second branch");
365
366 // PER-BRANCH TASKS PER LINK. `w` is the node-wide count and stays
367 // the mean the scalar slot carries; `wlink` is the count of the link
368 // that actually feeds branch b, and it is filled only when the
369 // branches disagree, so a plain fork keeps exactly the shape it had.
370 {
371 const qn::ForkParam<T>* fkn = sn.fork_param_of(f);
372 if (fkn != 0) {
373 std::vector<std::size_t> wv(B, 0);
374 bool uniform = true;
375 for (std::size_t b = 0; b < B; ++b) {
376 const double lv = num_traits<T>::to_double(
377 fkn->fan_out_link(fi.branchheads[b] - 1, r - 1));
378 wv[b] = static_cast<std::size_t>(lv + 0.5);
379 if (wv[b] == 0) wv[b] = 1;
380 if (wv[b] != wv[0]) uniform = false;
381 }
382 if (uniform) {
383 fi.w = wv[0];
384 } else {
385 fi.wlink = wv;
386 }
387 }
388 }
389
390 // Branch discovery: the class-r reachable closure from each head, up
391 // to but excluding the join.
392 fi.branchsets.assign(B, std::vector<std::size_t>());
393 for (std::size_t b = 0; b < B; ++b) {
394 std::vector<std::size_t> visitset(1, fi.branchheads[b]);
395 std::vector<std::size_t> frontier(1, fi.branchheads[b]);
396 while (!frontier.empty()) {
397 const std::size_t cn = frontier.front();
398 frontier.erase(frontier.begin());
399 if (sn.nodes[cn - 1].nodetype == NodeType::Fork)
400 throw UnsupportedError(
401 "fj_tag: nested fork-join (the Fork node '" + sn.nodes[cn - 1].name +
402 "' sits on a branch of '" + sn.nodes[f - 1].name +
403 "') is not supported: a sibling would need a tag from each enclosing "
404 "fork and this transform mints one tag dimension");
405 if (sn.nodes[cn - 1].nodetype == NodeType::Join && cn != j)
406 throw UnsupportedError(
407 "fj_tag: overlapping fork-join pairs (the Join node '" +
408 sn.nodes[cn - 1].name + "' sits on a branch of '" +
409 sn.nodes[f - 1].name + "', which closes at '" + sn.nodes[j - 1].name +
410 "') are not supported");
411 for (std::size_t nd = 1; nd <= I; ++nd)
412 for (std::size_t s = 1; s <= K; ++s) {
414 sn.rtnodes((cn - 1) * K + (r - 1), (nd - 1) * K + (s - 1))) >
415 0))
416 continue;
417 if (s != r)
418 throw UnsupportedError(
419 "fj_tag: class switching between the Fork node '" +
420 sn.nodes[f - 1].name + "' and its Join is not supported: the "
421 "sibling classes are minted per ORIGINAL class, so a sibling "
422 "that switches has no auxiliary twin to switch into");
423 if (nd == j) continue;
424 if (std::find(visitset.begin(), visitset.end(), nd) == visitset.end()) {
425 visitset.push_back(nd);
426 frontier.push_back(nd);
427 }
428 }
429 }
430 // Every branch node must REACH the join. A sibling that can be
431 // trapped away from it never joins, so the parent never
432 // completes and the chain has an absorbing set the reference
433 // refuses rather than solves.
434 std::vector<std::size_t> canreach(1, j);
435 bool changed = true;
436 while (changed) {
437 changed = false;
438 for (std::size_t a = 0; a < visitset.size(); ++a) {
439 const std::size_t cn = visitset[a];
440 if (std::find(canreach.begin(), canreach.end(), cn) != canreach.end())
441 continue;
442 for (std::size_t b2 = 0; b2 < canreach.size(); ++b2)
444 sn.rtnodes((cn - 1) * K + (r - 1),
445 (canreach[b2] - 1) * K + (r - 1))) > 0) {
446 canreach.push_back(cn);
447 changed = true;
448 break;
449 }
450 }
451 }
452 for (std::size_t a = 0; a < visitset.size(); ++a)
453 if (std::find(canreach.begin(), canreach.end(), visitset[a]) == canreach.end())
454 throw UnsupportedError(
455 "fj_tag: the Join node '" + sn.nodes[j - 1].name +
456 "' is unreachable from the branch node '" +
457 sn.nodes[visitset[a] - 1].name +
458 "'; a sibling trapped there never joins and its parent never completes");
459 fi.branchsets[b] = visitset;
460 }
461
462 // The tag pool: the population of the CHAIN, not of the class.
463 std::size_t c = 0;
464 for (std::size_t cc = 0; cc < sn.chains.size(); ++cc)
465 if (sn.chains[cc][r - 1]) { c = cc + 1; break; }
466 double tot = 0;
467 if (c == 0) {
468 tot = sn.njobs()[r - 1];
469 } else {
470 for (std::size_t s = 0; s < K; ++s)
471 if (sn.chains[c - 1][s]) tot += sn.njobs()[s];
472 }
473 const std::size_t Tt = static_cast<std::size_t>(tot + 0.5);
474 if (Tt == 0)
475 throw InputError("fj_tag: the chain of class '" + sn.classes[r - 1].name +
476 "' routed through '" + sn.nodes[f - 1].name +
477 "' carries no jobs, so no fork firing can ever occur");
478
479 fi.auxmatrix.assign(B, std::vector<std::size_t>(Tt, 0));
480 for (std::size_t t = 1; t <= Tt; ++t)
481 for (std::size_t b = 0; b < B; ++b) {
482 JobClass ac = sn.classes[r - 1];
483 ac.name = sn.classes[r - 1].name + "_f" + std::to_string(f) + "_b" +
484 std::to_string(b + 1) + "_t" + std::to_string(t);
485 ac.type = JobClassType::CLOSED;
486 ac.population = 0.0;
487 ac.completes = false;
488 ac.is_ref_class = false;
489 const std::size_t a = V.add_class(ac);
490 fi.auxmatrix[b][t - 1] = a;
491
492 // The sibling is served exactly as the parent would have
493 // been, at every station of its own branch.
494 for (std::size_t x = 0; x < fi.branchsets[b].size(); ++x) {
495 const std::size_t cn = fi.branchsets[b][x];
496 const std::size_t ist = sn.nodes[cn - 1].station;
497 if (ist == 0 || sn.nodes[cn - 1].nodetype == NodeType::Join) continue;
498 V.set_service(ist, a, sn.service[ist - 1][r - 1]);
499 }
500 // The sibling routing is the parent's, restricted to the
501 // branch. The auxiliary class TERMINATES at the join: it has
502 // no outgoing row there, because the join consumes it and
503 // releases a parent instead.
504 for (std::size_t x = 0; x < fi.branchsets[b].size(); ++x) {
505 const std::size_t cn = fi.branchsets[b][x];
506 for (std::size_t nd = 1; nd <= I; ++nd) {
507 const T p = sn.get_route(r, r, cn, nd);
508 if (num_traits<T>::to_double(p) != 0) V.set_route(a, a, cn, nd, p);
509 }
510 }
511 }
512 info.push_back(fi);
513 }
514 }
515
516 const std::size_t Kaug = V.classes.size();
517 out.fjclassmap.assign(Kaug, 0);
518 out.fjforkmap.assign(Kaug, 0);
519 out.fjjoinmap.assign(Kaug, 0);
520 out.fjbranchmap.assign(Kaug, 0);
521 out.fjtagmap.assign(Kaug, 0);
522 for (std::size_t row = 0; row < info.size(); ++row) {
523 const ForkInfo& fi = info[row];
524 for (std::size_t b = 0; b < fi.auxmatrix.size(); ++b)
525 for (std::size_t t = 0; t < fi.auxmatrix[b].size(); ++t) {
526 const std::size_t a = fi.auxmatrix[b][t];
527 out.fjclassmap[a - 1] = fi.r;
528 out.fjforkmap[a - 1] = fi.f;
529 out.fjjoinmap[a - 1] = fi.j;
530 out.fjbranchmap[a - 1] = b + 1;
531 out.fjtagmap[a - 1] = t + 1;
532 }
533 }
534
535 // Pad every per-class table to the widened class set BEFORE the refresh, so
536 // that an untouched auxiliary class looks exactly as the refresh would have
537 // found it: unbounded capacity, derived drop rule, no routing strategy of its
538 // own. `add_class` grows only the service table.
539 for (std::size_t i = 0; i < V.stations.size(); ++i) {
540 Station<T>& st = V.stations[i];
541 if (st.classcap.size() < Kaug) st.classcap.resize(Kaug, inf);
542 if (st.droprule.size() < Kaug) st.droprule.resize(Kaug, 0);
543 if (!st.schedparam.empty() && st.schedparam.size() < Kaug)
544 st.schedparam.resize(Kaug, zero);
545 if (!st.cdscalingpeak.empty() && st.cdscalingpeak.size() < Kaug)
546 st.cdscalingpeak.resize(Kaug, zero);
547 if (!st.jdscalingpeak.empty() && st.jdscalingpeak.size() < Kaug)
548 st.jdscalingpeak.resize(Kaug, zero);
549 }
550 for (std::size_t row = 0; row < info.size(); ++row) {
551 const ForkInfo& fi = info[row];
552 for (std::size_t b = 0; b < fi.auxmatrix.size(); ++b)
553 for (std::size_t t = 0; t < fi.auxmatrix[b].size(); ++t) {
554 const std::size_t a = fi.auxmatrix[b][t];
555 for (std::size_t x = 0; x < fi.branchsets[b].size(); ++x) {
556 const std::size_t cn = fi.branchsets[b][x];
557 if (sn.nodes[cn - 1].station == 0) continue;
558 Station<T>& st = V.stations[sn.nodes[cn - 1].station - 1];
559 if (!st.schedparam.empty())
560 st.schedparam[a - 1] = st.schedparam[fi.r - 1];
561 }
562 }
563 }
564 for (std::size_t i = 0; i < I; ++i) {
565 NodeDef& nd = V.nodes[i];
566 if (nd.routing.empty()) continue;
567 if (nd.routing.size() < Kaug) nd.routing.resize(Kaug, RoutingStrategy::PROB);
568 for (std::size_t row = 0; row < info.size(); ++row) {
569 const ForkInfo& fi = info[row];
570 for (std::size_t b = 0; b < fi.auxmatrix.size(); ++b)
571 for (std::size_t t = 0; t < fi.auxmatrix[b].size(); ++t)
572 nd.routing[fi.auxmatrix[b][t] - 1] =
573 sn.nodes[i].routing.size() >= fi.r ? sn.nodes[i].routing[fi.r - 1]
574 : RoutingStrategy::PROB;
575 }
576 }
577 // An explicit ClassSwitch matrix is (nclasses x nclasses); widen it with the
578 // identity on the auxiliary block. A sibling never switches class -- the
579 // transform refuses a branch that switches -- so the identity is not a
580 // default, it is the only admissible row.
581 for (typename std::map<std::size_t, Matrix<T>>::iterator it = V.csmatrix.begin();
582 it != V.csmatrix.end(); ++it) {
583 const Matrix<T> old = it->second;
584 Matrix<T> C(Kaug, Kaug, zero);
585 for (std::size_t x = 0; x < old.rows() && x < Kaug; ++x)
586 for (std::size_t y = 0; y < old.cols() && y < Kaug; ++y) C(x, y) = old(x, y);
587 for (std::size_t a = K; a < Kaug; ++a) C(a, a) = num_traits<T>::from_int(1);
588 it->second = C;
589 }
590
591 V.nclasses = Kaug;
592 V.refresh_struct();
593
594 // ---- post-edits, exactly as `fjtag.m` applies them after getStruct ----
595 //
596 // WHY THE VISITS ARE OVERWRITTEN. An auxiliary class has population 0 and a
597 // routing block that terminates at the join, so it is a TRANSIENT class whose
598 // visit ratios are not defined by any traffic equation -- the refresh either
599 // divides by a zero visit at the reference station or spreads mass over a
600 // sub-stochastic block. The engines read auxiliary visits only as a
601 // zero-versus-nonzero support gate ("may this class appear at this node"), so
602 // that is what is written: 1 on the branch support, 0 elsewhere. The ORIGINAL
603 // classes' visits are restored from the pre-augmentation struct for the same
604 // reason: making the fork stateful changes the stateful index space and the
605 // fork's own row, which the original model's visits never had.
606 for (std::size_t r = 1; r <= K; ++r) {
607 std::size_t corig = 0, cnew = 0;
608 for (std::size_t cc = 0; cc < sn.chains.size(); ++cc)
609 if (sn.chains[cc][r - 1]) { corig = cc + 1; break; }
610 for (std::size_t cc = 0; cc < V.chains.size(); ++cc)
611 if (V.chains[cc][r - 1]) { cnew = cc + 1; break; }
612 if (corig == 0 || cnew == 0) continue;
613 for (std::size_t isf = 1; isf <= V.stateful_nodes.size(); ++isf) {
614 const std::size_t ind = V.stateful_nodes[isf - 1];
615 const std::size_t isf_old = sn.stateful_index(ind);
616 if (isf_old != 0) {
617 V.visits[cnew - 1](isf - 1, r - 1) = sn.visits[corig - 1](isf_old - 1, r - 1);
618 } else {
619 // The stateful Fork had no row before. Its visit is read only as
620 // a capacity gate, so a nonzero marker is all it needs.
621 V.visits[cnew - 1](isf - 1, r - 1) =
622 num_traits<T>::from_double(Vn[ind - 1][r - 1] > 0 ? 1.0 : 0.0);
623 }
624 }
625 for (std::size_t i = 0; i < I; ++i)
626 V.nodevisits[cnew - 1](i, r - 1) = sn.nodevisits[corig - 1](i, r - 1);
627 }
628 for (std::size_t row = 0; row < info.size(); ++row) {
629 const ForkInfo& fi = info[row];
630 std::size_t cnew = 0;
631 for (std::size_t cc = 0; cc < V.chains.size(); ++cc)
632 if (V.chains[cc][fi.r - 1]) { cnew = cc + 1; break; }
633 if (cnew == 0) continue;
634 for (std::size_t b = 0; b < fi.auxmatrix.size(); ++b) {
635 std::vector<std::size_t> support = fi.branchsets[b];
636 support.push_back(fi.j);
637 // THIS BRANCH's count, not the node-wide one. Capping the auxiliary
638 // class of a branch that was sent 3 tasks at the node-wide mean of 2
639 // makes the third sibling unplaceable, and the chain deadlocks with
640 // no error to say why.
641 const std::size_t wb = fi.wlink.empty() ? fi.w : fi.wlink[b];
642 for (std::size_t t = 0; t < fi.auxmatrix[b].size(); ++t) {
643 const std::size_t a = fi.auxmatrix[b][t];
644 for (std::size_t c2 = 0; c2 < V.nchains; ++c2) {
645 for (std::size_t x = 0; x < V.visits[c2].rows(); ++x)
646 V.visits[c2](x, a - 1) = zero;
647 for (std::size_t x = 0; x < V.nodevisits[c2].rows(); ++x)
648 V.nodevisits[c2](x, a - 1) = zero;
649 }
650 for (std::size_t i = 0; i < V.stations.size(); ++i)
651 V.classcap[i][a - 1] = 0.0;
652 for (std::size_t x = 0; x < support.size(); ++x) {
653 const std::size_t cn = support[x];
654 V.nodevisits[cnew - 1](cn - 1, a - 1) = num_traits<T>::from_int(1);
655 const std::size_t isf = V.stateful_index(cn);
656 if (isf != 0)
657 V.visits[cnew - 1](isf - 1, a - 1) = num_traits<T>::from_int(1);
658 // Each auxiliary class holds at most `tasksPerLink` siblings
659 // network-wide: one tag is outstanding at a time under STD.
660 if (V.nodes[cn - 1].station != 0)
661 V.classcap[V.nodes[cn - 1].station - 1][a - 1] =
662 static_cast<double>(wb);
663 }
664 }
665 }
666 FjJoinParam& jp = out.joinparam[fi.j];
667 jp.fork = fi.f;
668 if (std::find(jp.origclasses.begin(), jp.origclasses.end(), fi.r) == jp.origclasses.end())
669 jp.origclasses.push_back(fi.r);
670 jp.auxmatrix[fi.r] = fi.auxmatrix;
671 // Siblings of branch b a firing consumes. Under STD it is the tasks that
672 // branch was sent -- the per-destination count when the fork declares
673 // one, the node-wide count otherwise. Under PARTIAL the Join's own
674 // quorum is what it waits for, so the quorum LOWERS this count rather
675 // than replacing the mechanism.
676 std::vector<std::size_t> reqb(fi.auxmatrix.size(), fi.w);
677 if (!fi.wlink.empty())
678 for (std::size_t b = 0; b < reqb.size() && b < fi.wlink.size(); ++b)
679 reqb[b] = fi.wlink[b];
680 typename std::map<std::size_t, typename NetworkStruct<T>::JoinDecl>::const_iterator jdq =
681 sn.joindecl.find(fi.j);
682 if (jdq != sn.joindecl.end() && jdq->second.strategy == lang::JoinStrategy::PARTIAL &&
683 jdq->second.quorum > 0.0) {
684 const std::size_t q = static_cast<std::size_t>(jdq->second.quorum + 0.5);
685 for (std::size_t b = 0; b < reqb.size(); ++b)
686 if (q < reqb[b]) reqb[b] = q;
687 }
688 jp.required[fi.r] = reqb;
689 }
690
691 // The firing list: one entry per (fork, class, tag).
692 for (std::size_t row = 0; row < info.size(); ++row) {
693 const ForkInfo& fi = info[row];
694 const std::size_t Tt = fi.auxmatrix.empty() ? 0 : fi.auxmatrix[0].size();
695 for (std::size_t t = 1; t <= Tt; ++t) {
696 FjSync<T> e;
697 e.fork = fi.f;
698 e.join = fi.j;
699 e.cls = fi.r;
700 e.tag = t;
701 e.branchheads = fi.branchheads;
702 for (std::size_t b = 0; b < fi.auxmatrix.size(); ++b)
703 e.auxclasses.push_back(fi.auxmatrix[b][t - 1]);
704 e.auxall = fi.auxmatrix;
705 e.weight = fi.w;
706 e.weightlink = fi.wlink; // empty whenever every branch agrees
707 out.fjsync.push_back(e);
708 }
709 }
710
711 V.fjclassmap = out.fjclassmap;
712 V.fjjoinparam = out.joinparam;
713 return out;
714}
715
716} // namespace qn
717} // namespace line
718
719#endif // LINE_LANG_QN_FJ_TAG_H
Base error for the multiprecision C++ port.
Definition error.h:31
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
void set_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
P{r,s}(i,j) = p, with 1-based NODE and class indices.
std::size_t add_class(const JobClass &cl)
Add a class and grow the service table.
std::vector< Matrix< T > > nodevisits
(nchains) each (nnodes x nclasses)
std::size_t stateful_index(std::size_t ind) const
1-based stateful index of node ind, 0 when the node is not stateful.
std::vector< std::size_t > stateful_nodes
1-based node indices, ascending
std::vector< std::vector< bool > > chains
(nchains x nclasses)
std::map< std::size_t, FjJoinParam > fjjoinparam
sn.nodeparam{j}.fj for each Join node: the tag matrix and the required sibling multiplicity after_eve...
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
void refresh_struct()
The whole chain, in MATLAB's refreshStruct order.
void set_service(std::size_t station, std::size_t cls, const Distrib< T > &d)
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::vector< double > > classcap
std::map< std::size_t, Matrix< T > > csmatrix
The class-switch matrix of a ClassSwitch node, by 1-based NODE index.
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
std::vector< std::size_t > fjclassmap
sn.fjclassmap: the ORIGINAL class of each auxiliary sibling class, 0 for an original class.
bool isfjaugmented
sn.isfjaugmented: this struct came out of fj_tag, so its Fork nodes are STATEFUL and its Join nodes c...
The exception types the port throws.
void sn_fj_validate(const NetworkStruct< T > &sn)
Port of sn_fj_validate: is this fork-join model inside the exact solver's reach?
Definition fj_tag.h:106
std::string sn_fj_supports(const NetworkStruct< T > &sn)
Can the exact fork-join construction be asked for this model?
Definition fj_tag.h:282
FjTagged< T > fj_tag(const NetworkStruct< T > &sn)
Port of ModelAdapter.fjtag.
Definition fj_tag.h:302
A queueing network and its refreshed NetworkStruct.
sn.nodeparam{j}.fj: what a Join node needs to fire on identity.
std::vector< std::size_t > origclasses
std::map< std::size_t, std::vector< std::vector< std::size_t > > > auxmatrix
std::map< std::size_t, std::vector< std::size_t > > required
One fork firing synchronization: sn.fjsync{k}.
std::size_t fork
1-based Fork node
std::vector< std::size_t > weightlink
Per-branch tasksPerLink, EMPTY when every branch carries weight.
std::size_t weight
tasksPerLink: siblings emitted per branch
std::vector< std::size_t > branchheads
1-based node per branch
std::size_t join
1-based Join node that closes it
std::size_t cls
1-based ORIGINAL class being forked
std::vector< std::vector< std::size_t > > auxall
(B x T) every auxiliary class of this (fork, class), for the tag scan.
std::vector< std::size_t > auxclasses
the tag's auxiliary class per branch
std::size_t tag
1-based tag this entry allocates
The augmented struct and everything needed to read its results back.
Definition fj_tag.h:71
std::vector< FjSync< T > > fjsync
Definition fj_tag.h:76
NetworkStruct< T > V
Definition fj_tag.h:72
std::size_t korig
Definition fj_tag.h:78
std::vector< std::size_t > fjjoinmap
Definition fj_tag.h:75
std::vector< std::size_t > fjforkmap
Definition fj_tag.h:75
std::vector< std::size_t > fjbranchmap
Definition fj_tag.h:75
std::map< std::size_t, FjJoinParam > joinparam
keyed by 1-based Join node
Definition fj_tag.h:77
std::vector< std::size_t > fjclassmap
fjclassmap[a-1] is the ORIGINAL class of auxiliary class a, 0 for originals.
Definition fj_tag.h:74
std::vector< std::size_t > fjtagmap
Definition fj_tag.h:75
Variable forking levels, the twin of MATLAB sn.nodeparam{f}.fanOutLink / .fanOutProb / ....
std::vector< std::vector< lang::Distrib< T > > > fan_out_dist
One job class of the network.
bool completes
Whether passage through the reference station is a COMPLETION.
double population
infinite for an open class
bool is_ref_class
marks the chain's reference class
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.
double tasks_per_link
Fork.output.tasksPerLink == MATLAB sn.nodeparam{f}.fanOut: how many tasks a fork emits per outgoing l...
One station of the network.
std::vector< T > jdscalingpeak
sn.jdscalingpeak for this station: the declared peak joint-dependent scaling per class.
std::vector< int > droprule
Per-class blocking rule as an INT, with 0 meaning "not set".
std::vector< T > cdscalingpeak
sn.cdscalingpeak for this station: the DECLARED peak rate scaling per class, empty when the station i...
std::vector< T > schedparam
sn.schedparam, per class: the DPS / GPS weight, or the SEPT / LEPT rank.
std::vector< double > classcap
Per-class buffer from setChainCapacity; infinite where unset.