LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_fj_nodevisits_mmt.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_SN_SN_FJ_NODEVISITS_MMT_H
6#define LINE_API_SN_SN_FJ_NODEVISITS_MMT_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * Post-MMT node visits of a fork-join model.
12 *
13 * Port of the fork block of `matlab/src/lang/@MNetwork/refreshStruct.m`
14 * (twins: the `sn.fj.any() && !isFJAugmented` tail of `Network.java:refreshStruct`,
15 * and `network.py:_refresh_fork_join_nodevisits`).
16 *
17 * WHY THE ROUTING ANSWER IS NOT THE ANSWER. `sn_refresh_visits` solves one
18 * traffic equation per chain, so a job that a Fork splits into siblings is
19 * counted once: its blunt fork correction leaves every visited node at 1 and a
20 * Join at its in-degree. That is the visit vector of ONE token, not of the
21 * work the fork actually releases. The reference recovers the rest from the MMT
22 * transformation, whose auxiliary open classes ARE the siblings: for every
23 * auxiliary chain the transformed layer grows, the original class's node visits
24 * become
25 *
26 * V_orig(:,r) <- tasksPerLink * ( V_orig(:,r) + V_aux(:,r) )
27 *
28 * with `V_aux` read off the auxiliary chain and the transformed layer's own
29 * Source, Sink and Fork rows zeroed (they carry the auxiliary arrival, not a
30 * visit of the original class). On the two-branch closed fork-join this turns
31 * (Think,F,Q1,Q2,J) = (1,1,1,1,2) into (1,2,1.5,1.5,3), which is what MATLAB,
32 * the JAR and native Python all report.
33 *
34 * `V_orig` ON THE RIGHT IS THE PRE-CORRECTION SNAPSHOT, and the reference is a
35 * value-semantics MATLAB struct where that happens for free: it reads `sn` and
36 * writes `self.sn`. Two forks feeding the same class therefore do not compound
37 * -- the last auxiliary chain wins, over the same untouched snapshot -- so this
38 * port keeps the snapshot explicitly rather than accumulating in place.
39 *
40 * NOT APPLIED to a struct that came out of `fj_tag` (`isfjaugmented`): there the
41 * Fork nodes are already resolved into tagged classes, and the reference skips
42 * the correction for exactly that reason.
43 *
44 * ARITHMETIC: field. One multiplication and one addition per entry.
45 */
46
47#include <cstddef>
48#include <string>
49#include <vector>
50
52#include "line/num/number.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace api {
58
59/**
60 * Rewrite `sn.nodevisits` with the MMT correction. A no-op on a model with no
61 * fork-join pair, on a tag-augmented struct, and whenever the transformation
62 * grows no auxiliary class.
63 */
64template <class T>
66 if (sn.fj.empty() || sn.isfjaugmented) return;
68 if (!tr.active()) return;
69 const qn::NetworkStruct<T>& V = tr.V;
70 if (V.nchains <= sn.nchains) return;
71
72 // Every write reads the PRE-correction visits; see the header.
73 const std::vector<Matrix<T>> X = sn.nodevisits;
74 const std::size_t I = sn.nodes.size();
75
76 // The transformed layer keeps the base nodes and appends its own Source and
77 // Sink, so a row of V maps to a base row by NAME, and a row with no base
78 // counterpart contributes nothing.
79 std::vector<std::size_t> vrow_to_base(V.nodes.size(), 0); // 1-based, 0 = absent
80 for (std::size_t a = 0; a < V.nodes.size(); ++a) {
81 const qn::NodeType nt = V.nodes[a].nodetype;
82 if (nt == qn::NodeType::Source || nt == qn::NodeType::Sink ||
83 nt == qn::NodeType::Fork)
84 continue; // the reference zeroes these rows before reading V_aux
85 for (std::size_t i = 0; i < I; ++i)
86 if (sn.nodes[i].name == V.nodes[a].name) {
87 vrow_to_base[a] = i + 1;
88 break;
89 }
90 }
91
92 for (std::size_t nc = sn.nchains; nc < V.nchains; ++nc) {
93 const std::vector<std::size_t>& aux = V.inchain[nc];
94 if (aux.empty()) continue;
95 const std::size_t a0 = aux[0];
96 if (a0 >= tr.fjclassmap.size() || tr.fjclassmap[a0] == 0) continue;
97 const std::size_t forkIdx = tr.fjforkmap[a0];
98 if (forkIdx >= tr.forks.size()) continue;
99 const T fanOut = num_traits<T>::from_double(tr.forks[forkIdx].fanOut);
100
101 // the base chain of the original class this auxiliary chain mirrors
102 std::size_t origChain = V.nchains;
103 for (std::size_t c = 0; c < sn.nchains; ++c)
104 for (std::size_t r : sn.inchain[c])
105 if (r == tr.fjclassmap[a0]) origChain = c;
106 if (origChain >= sn.nchains) continue;
107
108 for (std::size_t jaux = 0; jaux < aux.size(); ++jaux) {
109 const std::size_t a = aux[jaux];
110 if (a >= tr.fjclassmap.size() || tr.fjclassmap[a] == 0) continue;
111 const std::size_t r = tr.fjclassmap[a]; // original class mirrored by a
112
113 std::vector<T> vaux(I, num_traits<T>::from_int(0));
114 for (std::size_t b = 0; b < V.nodes.size(); ++b)
115 if (vrow_to_base[b] != 0) vaux[vrow_to_base[b] - 1] = V.nodevisits[nc](b, a - 1);
116
117 for (std::size_t i = 0; i < I; ++i)
118 sn.nodevisits[origChain](i, r - 1) = T(fanOut * (X[origChain](i, r - 1) + vaux[i]));
119 }
120 }
121}
122
123} // namespace api
124} // namespace line
125
126#endif // LINE_API_SN_SN_FJ_NODEVISITS_MMT_H
A network plus its refreshed NetworkStruct.
std::vector< Matrix< T > > nodevisits
(nchains) each (nnodes x nclasses)
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< NodeDef > nodes
every node, in creation order
The fork-join transform SolverMVA applies before solving a layer that contains a Fork.
Dense matrix and non-owning view.
void sn_fj_nodevisits_mmt(qn::NetworkStruct< T > &sn)
Rewrite sn.nodevisits with the MMT correction.
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
FjMmt< T > fj_mmt(const qn::NetworkStruct< T > &L)
Build the transformed layer.
Definition fj_mmt.h:444
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
The transformed layer and the bookkeeping the fixed point needs to drive it and to merge its results ...
Definition fj_mmt.h:113