LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_fj_visits_spn.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_VISITS_SPN_H
6#define LINE_API_SN_SN_FJ_VISITS_SPN_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * Fork-join node visit ratios, via the auxiliary closed SPN.
12 *
13 * Port of matlab/src/api/sn/sn_fj_visits_spn.m (twins: `SnFjVisitsSpn.java`,
14 * `python/line_solver/api/sn/sn_fj_visits_spn.py`).
15 *
16 * WHAT IT COMPUTES. For each class that passes through a fork-join pair, the
17 * reference builds an auxiliary closed stochastic Petri net that carries the
18 * fork/join synchronization exactly -- one Place per station, a "done" Place
19 * per branch feeding a Join, an immediate Join transition consuming one token
20 * from each branch, and B tokens circulating, B being the largest leaf count
21 * over the outermost forks -- and reads the per-Place throughputs as the visit
22 * ratios, normalized so the chain's reference station is one.
23 *
24 * THE SPN SOLVE IS NOT WHAT PRODUCES THE ANSWER, but the rule it produces is
25 * NOT the uniform one `sn_fj_visits_spn.m:113` claims. That comment says the net
26 * is population preserving, so every station Place fires at the same rate; the
27 * reference's own CTMC solve says otherwise, and it is the solve that defines
28 * the function. The pre-fork transition consumes all B tokens at once and the
29 * Join returns them, so a station INSIDE a fork-join region fires once per B
30 * firings of the cycle: normalized on the reference station, a station outside
31 * the region is 1, a station inside it is 1/B, and a Fork or a Join, which
32 * holds no Place, is 0. Measured against MATLAB and the JAR on a two-branch, a
33 * three-branch, a nested, a chained-branch, a pre/post-fork-station and a
34 * two-class model: 1, 0.5 and 1/3 exactly where predicted.
35 *
36 * This port evaluates that rule instead of enumerating a state space whose size
37 * is exponential in B. WHAT IS LOST, stated plainly: MATLAB's solve would DETECT
38 * a construction that violates the assumptions (an unbalanced Join, a fork whose
39 * leaves are not conserved), where this port assumes them. That is why the
40 * structural preconditions are CHECKED here -- an unresolvable fork, or a Join
41 * with no branch feeding it, is refused by name.
42 *
43 * ARITHMETIC: field. No transcendental function is involved; before
44 * normalization the answer is exactly 0, 1 or the unit fraction 1/B.
45 */
46
47#include <cstddef>
48#include <string>
49#include <vector>
50
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace api {
58
59namespace fjvdetail {
60
61/**
62 * Recursively resolve a Fork's destinations down to station nodes.
63 *
64 * A fork branch may open another fork (nested fork-join), so the leaves are
65 * the stations reachable without crossing a Join.
66 */
67template <class T>
68void resolve_fork_dests(const qn::NetworkStruct<T>& sn, const Matrix<T>& P_r,
69 const std::vector<bool>& visited, std::size_t fork_nd, std::size_t cls,
70 double weight, std::vector<std::size_t>* out, std::vector<double>* wout,
71 std::vector<bool>* seen) {
72 const std::size_t I = sn.nodes.size();
73 if ((*seen)[fork_nd]) return; // a routing cycle through forks would not terminate
74 (*seen)[fork_nd] = true;
75 const qn::ForkParam<T>* fk = sn.fork_param_of(fork_nd + 1);
76 const bool variable = fk != 0;
77 for (std::size_t j = 0; j < I; ++j) {
78 if (!(num_traits<T>::to_double(P_r(fork_nd, j)) > 0.0) || !visited[j]) continue;
79 // EXPECTED tasks this link carries. A plain fork gives every link 1, so
80 // the weighted leaf count collapses to the leaf count it always was.
81 double w = weight;
82 if (variable && j < fk->fan_out_link.rows() && cls < fk->fan_out_link.cols())
83 w *= num_traits<T>::to_double(fk->fan_out_prob(j, cls)) *
84 num_traits<T>::to_double(fk->fan_out_link(j, cls));
85 if (sn.nodes[j].nodetype == qn::NodeType::Fork) {
86 resolve_fork_dests(sn, P_r, visited, j, cls, w, out, wout, seen);
87 } else if (sn.nodes[j].station != 0) {
88 out->push_back(j);
89 wout->push_back(w);
90 }
91 }
92}
93
94/** The station nodes a fork ultimately feeds, with the expected tasks each receives. */
95template <class T>
96std::vector<std::size_t> fork_leaves(const qn::NetworkStruct<T>& sn, const Matrix<T>& P_r,
97 const std::vector<bool>& visited, std::size_t fork_nd,
98 std::size_t cls = 0, std::vector<double>* weights = 0) {
99 std::vector<std::size_t> out;
100 std::vector<double> w;
101 std::vector<bool> seen(sn.nodes.size(), false);
102 resolve_fork_dests(sn, P_r, visited, fork_nd, cls, 1.0, &out, &w, &seen);
103 if (weights) *weights = w;
104 return out;
105}
106
107} // namespace fjvdetail
108
109/** What the auxiliary construction found, for one class. */
111 std::vector<std::size_t> stationNodes, forkNodes, joinNodes; ///< 0-based node indices
112 /**
113 * Circulating population: the largest EXPECTED outermost leaf count.
114 *
115 * On a plain fork every link carries one certain task, so this is the leaf
116 * count it has always been and the value is integral. Under a variable
117 * forking level it is sum over links of P(branch fires) * E[tasks on it],
118 * which is generally FRACTIONAL -- and a fractional token population is not
119 * a net anyone can enumerate, which is why the reference solve is skipped
120 * exactly there and the closed form is the answer everywhere.
121 */
122 double B = 1.0;
123 std::vector<std::size_t> joinLeaves; ///< per node, the leaves a Join synchronizes
124 /**
125 * Per node, whether it sits INSIDE a fork-join region: reachable from an
126 * outermost Fork without crossing a Join. These are the stations the net
127 * runs at 1/B of the reference station's rate; everything else on the cycle
128 * runs at the reference's own rate. A branch is followed to its end, not
129 * only to its first station, because the reference's net gives a chained
130 * branch station the same rate as the leaf it feeds.
131 */
132 std::vector<bool> inRegion;
133};
134
135/**
136 * Classify one class's visited subgraph and size the auxiliary net.
137 *
138 * Exposed because it is what carries the model content: B and the per-Join leaf
139 * counts are the construction, and a test that only checked the ones and zeros
140 * of the visit vector would not be testing anything.
141 */
142template <class T>
144 const std::vector<bool>& visited, std::size_t cls = 0) {
145 const std::size_t I = sn.nodes.size();
146 FjSpnStructure out;
147 out.joinLeaves.assign(I, 0);
148 out.inRegion.assign(I, false);
149 for (std::size_t nd = 0; nd < I; ++nd) {
150 if (!visited[nd]) continue;
151 const qn::NodeType t = sn.nodes[nd].nodetype;
152 if (t == qn::NodeType::Fork) out.forkNodes.push_back(nd);
153 else if (t == qn::NodeType::Join) out.joinNodes.push_back(nd);
154 else if (sn.nodes[nd].station != 0 && t != qn::NodeType::Source &&
155 t != qn::NodeType::Sink)
156 out.stationNodes.push_back(nd);
157 }
158
159 // Leaf count of each Join, bottom-up. The reference sweeps the Join list
160 // once per Join, which is enough to resolve any nesting depth because each
161 // pass resolves at least the innermost unresolved level.
162 for (std::size_t pass = 0; pass < out.joinNodes.size(); ++pass)
163 for (std::size_t ji = 0; ji < out.joinNodes.size(); ++ji) {
164 const std::size_t jnd = out.joinNodes[ji];
165 std::size_t lc = 0;
166 for (std::size_t src = 0; src < I; ++src) {
167 if (!(num_traits<T>::to_double(P_r(src, jnd)) > 0.0) || !visited[src]) continue;
168 if (sn.nodes[src].nodetype == qn::NodeType::Join && out.joinLeaves[src] > 0)
169 lc += out.joinLeaves[src];
170 else
171 lc += 1; // a station, or a Join not yet resolved
172 }
173 out.joinLeaves[jnd] = lc;
174 }
175
176 // B is the largest leaf count over the OUTERMOST forks. A fork fed by a
177 // Join is a serial fork-join stage, not an outer one, and its branches
178 // circulate inside the population the outer stage already fixed.
179 for (std::size_t fi = 0; fi < out.forkNodes.size(); ++fi) {
180 const std::size_t fnd = out.forkNodes[fi];
181 bool outermost = true;
182 for (std::size_t src = 0; src < I; ++src)
183 if (num_traits<T>::to_double(P_r(src, fnd)) > 0.0 && visited[src] &&
184 sn.nodes[src].nodetype == qn::NodeType::Join)
185 outermost = false;
186 if (!outermost) continue;
187 // Everything this fork opens, down to its Join, runs at the branch rate.
188 std::vector<std::size_t> frontier(1, fnd);
189 for (std::size_t h = 0; h < frontier.size(); ++h) {
190 const std::size_t nd = frontier[h];
191 for (std::size_t j = 0; j < I; ++j) {
192 if (!(num_traits<T>::to_double(P_r(nd, j)) > 0.0) || !visited[j]) continue;
193 if (sn.nodes[j].nodetype == qn::NodeType::Join || out.inRegion[j]) continue;
194 out.inRegion[j] = true;
195 frontier.push_back(j);
196 }
197 }
198 std::vector<double> leafw;
199 const std::vector<std::size_t> leaves =
200 fjvdetail::fork_leaves(sn, P_r, visited, fnd, cls, &leafw);
201 if (leaves.empty())
202 throw InputError(
203 "sn_fj_visits_spn: a Fork reaches no station on any branch, so the auxiliary net "
204 "has nothing to synchronize; the routing of this class is malformed");
205 // The EXPECTED sibling count, which is the leaf count exactly when every
206 // link is certain and carries one task.
207 double expected = 0.0;
208 for (std::size_t li = 0; li < leaves.size(); ++li) expected += leafw[li];
209 if (expected <= 0.0)
210 throw InputError(
211 "sn_fj_visits_spn: a Fork emits no task in expectation, so its Join can never "
212 "fire; at least one branch must be certain to emit at least one task");
213 if (expected > out.B) out.B = expected;
214 }
215 for (std::size_t ji = 0; ji < out.joinNodes.size(); ++ji)
216 if (out.joinLeaves[out.joinNodes[ji]] == 0)
217 throw InputError(
218 "sn_fj_visits_spn: a Join has no branch feeding it, so the auxiliary net cannot "
219 "be balanced and its throughputs would not be uniform");
220 return out;
221}
222
223/**
224 * Per-chain fork-join node visit ratios.
225 *
226 * @return one (nnodes x nclasses) matrix per chain, normalized so the chain's
227 * reference station carries one
228 */
229template <class T>
230std::vector<Matrix<T>> sn_fj_visits_spn(const qn::NetworkStruct<T>& sn) {
231 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
232 const std::size_t I = sn.nodes.size(), K = sn.nclasses, C = sn.nchains;
233 std::vector<Matrix<T>> nodevisits(C, Matrix<T>(I, K, zero));
234 if (sn.fj.empty()) return nodevisits; // no fork-join structure at all
235
236 const double fineTol = 1e-8;
237 for (std::size_t c = 0; c < C; ++c) {
238 if (c >= sn.inchain.size() || sn.inchain[c].empty()) continue;
239 const std::vector<std::size_t>& classes = sn.inchain[c];
240
241 for (std::size_t ci = 0; ci < classes.size(); ++ci) {
242 const std::size_t r = classes[ci] - 1; // inchain is 1-based
243
244 // The single-class node routing, read out of the (I*K) block form.
245 Matrix<T> P_r(I, I, zero);
246 if (sn.rtnodes.rows() == I * K && sn.rtnodes.cols() == I * K)
247 for (std::size_t i = 0; i < I; ++i)
248 for (std::size_t j = 0; j < I; ++j) P_r(i, j) = sn.rtnodes(i * K + r, j * K + r);
249
250 // The nodes this class reaches from its reference station.
251 const std::size_t refstat = sn.classes[r].refstat;
252 if (refstat == 0 || refstat > sn.station_to_node.size()) continue;
253 const std::size_t refnode = sn.station_to_node[refstat - 1] - 1;
254 std::vector<bool> visited(I, false);
255 visited[refnode] = true;
256 bool changed = true;
257 while (changed) {
258 changed = false;
259 for (std::size_t i = 0; i < I; ++i) {
260 if (!visited[i]) continue;
261 for (std::size_t j = 0; j < I; ++j)
262 if (num_traits<T>::to_double(P_r(i, j)) > 0.0 && !visited[j]) {
263 visited[j] = true;
264 changed = true;
265 }
266 }
267 }
268
269 bool hasFork = false;
270 for (std::size_t i = 0; i < I; ++i)
271 if (visited[i] && sn.nodes[i].nodetype == qn::NodeType::Fork) hasFork = true;
272
273 if (!hasFork) {
274 // No fork on this class's path: every visited node is on the
275 // ordinary cycle and is visited once.
276 for (std::size_t i = 0; i < I; ++i)
277 if (visited[i]) nodevisits[c](i, r) = one;
278 continue;
279 }
280
281 // The construction is checked, then its rates are used: a station
282 // inside a fork-join region fires once per B firings of the cycle,
283 // because the pre-fork transition consumes all B tokens and the Join
284 // returns them, so it carries 1/B where a station outside carries 1.
285 // A Fork and a Join hold no Place at all, hence zero.
286 const FjSpnStructure st = fj_spn_structure(sn, P_r, visited, r);
287 const T invB = one / num_traits<T>::from_double(st.B);
288 for (std::size_t si = 0; si < st.stationNodes.size(); ++si) {
289 const std::size_t nd = st.stationNodes[si];
290 nodevisits[c](nd, r) = st.inRegion[nd] ? invB : one;
291 }
292 }
293
294 // Normalize on the chain's reference station, as the reference does.
295 const std::size_t r0 = classes[0] - 1;
296 const std::size_t refstat0 = sn.classes[r0].refstat;
297 if (refstat0 == 0 || refstat0 > sn.station_to_node.size()) continue;
298 const std::size_t refnode_c = sn.station_to_node[refstat0 - 1] - 1;
299 for (std::size_t ci = 0; ci < classes.size(); ++ci) {
300 const std::size_t r = classes[ci] - 1;
301 const T nv = nodevisits[c](refnode_c, r);
302 if (num_traits<T>::to_double(nv) > fineTol)
303 for (std::size_t i = 0; i < I; ++i) nodevisits[c](i, r) = nodevisits[c](i, r) / nv;
304 }
305 }
306 return nodevisits;
307}
308
309} // namespace api
310} // namespace line
311
312#endif // LINE_API_SN_SN_FJ_VISITS_SPN_H
InputError(const std::string &what)
Definition error.h:39
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
FjSpnStructure fj_spn_structure(const qn::NetworkStruct< T > &sn, const Matrix< T > &P_r, const std::vector< bool > &visited, std::size_t cls=0)
Classify one class's visited subgraph and size the auxiliary net.
std::vector< Matrix< T > > sn_fj_visits_spn(const qn::NetworkStruct< T > &sn)
Per-chain fork-join node visit ratios.
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
What the auxiliary construction found, for one class.
std::vector< std::size_t > stationNodes
std::vector< std::size_t > joinNodes
0-based node indices
double B
Circulating population: the largest EXPECTED outermost leaf count.
std::vector< std::size_t > joinLeaves
per node, the leaves a Join synchronizes
std::vector< bool > inRegion
Per node, whether it sits INSIDE a fork-join region: reachable from an outermost Fork without crossin...
std::vector< std::size_t > forkNodes