LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_node_metrics.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_NODE_METRICS_H
6#define LINE_API_SN_SN_NODE_METRICS_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * Ports of matlab/src/api/sn/sn_get_node_arvr_from_tput.m and
12 * sn_get_node_tput_from_tput.m: the NODE-level arrival rate and throughput
13 * tables behind `getAvgNode`, `getAvgNodeTable` and the node chain getters.
14 *
15 * Station metrics are what a solver returns; node metrics are what the user
16 * asks for when the model has nodes that are not stations -- a Router, a
17 * ClassSwitch, a Cache, a Fork or a Join. Those nodes have no service and no
18 * queue, so their only meaningful averages are the rates flowing through them,
19 * and those come from the station throughputs plus the NODE-level routing
20 * `sn.rtnodes` and the per-chain node visits.
21 *
22 * REFERENCE STATION INDEX. Both functions read `sn.refstat(c)` with c a CHAIN
23 * index, while `sn.refstat` is indexed by CLASS. The two agree whenever the
24 * chain's classes are numbered from its own index, which every single-chain
25 * and every non-switching model satisfies, and the refresh already refuses a
26 * chain whose classes disagree on their reference station. Reproduced as the
27 * reference has it: correcting the index here alone would make these tables
28 * disagree with MATLAB, the JAR and native Python at once.
29 *
30 * CACHE HIT AND MISS RATES. The reference splits a Cache node's flow with the
31 * ACTUAL hit and miss probabilities the cache fixed point converged to, which
32 * this struct does not store on the node -- `refresh_cacheqn_actual_visits`
33 * folds them into the visits instead. They are therefore optional arguments:
34 * a caller holding the converged probabilities passes them and gets the
35 * reference's first branch, and a caller that does not gets its second, the
36 * visit-ratio split, which is what the folded visits already encode.
37 *
38 * ARITHMETIC: field. Sums and quotients only.
39 */
40
41#include <cmath>
42#include <cstddef>
43#include <map>
44#include <vector>
45
47#include "line/num/number.h"
48
49namespace line {
50namespace api {
51
52/**
53 * The converged cache split, per ORIGINAL class of one Cache node.
54 *
55 * Empty vectors mean "not available", which selects the reference's
56 * visit-ratio branch. `delayed_hit` may stay empty even when the other two are
57 * given; it is zero then, as the reference defaults it.
58 */
59template <class T>
61 std::vector<T> hit, miss, delayed_hit;
62};
63
64namespace detail {
65
66/** True when class `r` (1-based) is a hit or miss class of ANY Cache node. */
67template <class T>
68bool sn_is_any_cache_class(const qn::NetworkStruct<T>& sn, std::size_t r) {
69 for (const auto& kv : sn.nodeparam) {
70 const qn::CacheParam<T>& cp = kv.second;
71 for (std::size_t x : cp.hitclass)
72 if (x == r) return true;
73 for (std::size_t x : cp.missclass)
74 if (x == r) return true;
75 }
76 return false;
77}
78
79/**
80 * `sum(TN(refstat, inchain))`, the chain's total throughput at its reference
81 * station, over the classes that HAVE a reference visit.
82 *
83 * THIS SUM AND `sn_chain_refvisits` ARE THE TWO HALVES OF ONE RATIO -- a rate
84 * per reference visit -- so they have to run over the same classes or the
85 * quotient is not that rate. The restriction is inert on every model but a
86 * cache one, because a class with no visit at a station has no throughput there
87 * either. A CACHE IS THE ONE EXCEPTION: its over-route sends a hit or miss
88 * class THROUGH a station that never serves it, `getAvgHandles` keeps the Tput
89 * handle of a cache class so that pass-through flow IS reported, and the
90 * refreshed struct gives the same pair no visit. The reference never has the
91 * term at all -- its implicit ClassSwitch absorbs the switch before the station
92 * -- so counting it here alone scaled every cache node rate by the ratio of the
93 * two sums: exactly 2x on cache_replc_fifo, whose Delay carries the whole
94 * hit+miss flow a second time.
95 */
96template <class T>
97T sn_chain_tput(const qn::NetworkStruct<T>& sn, const Matrix<T>& TN, std::size_t c,
98 std::size_t refstat, const std::vector<std::size_t>& inchain) {
99 T acc = num_traits<T>::from_int(0);
100 if (refstat == 0 || refstat > sn.nstations) return acc;
101 const T zero = num_traits<T>::from_int(0);
102 const bool have_visits = c < sn.visits.size();
103 const std::size_t isf = have_visits ? sn.stateful_of_station(refstat) - 1 : 0;
104 for (std::size_t r : inchain) {
105 if (have_visits && sn.visits[c](isf, r - 1) == zero && sn_is_any_cache_class(sn, r))
106 continue;
107 acc = T(acc + TN(refstat - 1, r - 1));
108 }
109 return acc;
110}
111
112/** `sum(sn.visits{c}(stationToStateful(refstat), inchain))`. */
113template <class T>
114T sn_chain_refvisits(const qn::NetworkStruct<T>& sn, std::size_t c, std::size_t refstat,
115 const std::vector<std::size_t>& inchain) {
116 T acc = num_traits<T>::from_int(0);
117 if (refstat == 0 || refstat > sn.nstations || c >= sn.visits.size()) return acc;
118 const std::size_t isf = sn.stateful_of_station(refstat) - 1;
119 for (std::size_t r : inchain) acc = T(acc + sn.visits[c](isf, r - 1));
120 return acc;
121}
122
123/**
124 * `sn.refstat(c)` as the reference spells it: the CLASS-indexed reference
125 * station read at the chain's index. See the file header.
126 */
127template <class T>
128std::size_t sn_chain_refstat(const qn::NetworkStruct<T>& sn, std::size_t c) {
129 if (c >= sn.classes.size()) return 0;
130 return sn.classes[c].refstat;
131}
132
133/** True when a converged cache probability is NaN, i.e. was never measured. */
134template <class T>
135bool sn_prob_is_nan(const T& p) {
136 return std::isnan(num_traits<T>::to_double(p));
137}
138
139/** True when class `r` (1-based) is one of the node's hit or miss classes. */
140inline bool sn_is_cache_class(const std::vector<std::size_t>& hit,
141 const std::vector<std::size_t>& miss, std::size_t r) {
142 for (std::size_t x : hit)
143 if (x == r) return true;
144 for (std::size_t x : miss)
145 if (x == r) return true;
146 return false;
147}
148
149} // namespace detail
150
151/**
152 * Port of sn_get_node_arvr_from_tput.
153 *
154 * `AN` is the STATION arrival-rate table (sn_get_arvr_from_tput). Stations
155 * copy their own row; every other node takes the chain's reference throughput
156 * scaled by its node visits.
157 */
158template <class T>
160 const Matrix<T>& AN) {
161 const T zero = num_traits<T>::from_int(0);
162 const std::size_t I = sn.nodes.size(), C = sn.nchains, M = sn.nstations, R = sn.nclasses;
163 Matrix<T> ANn(I, R, zero);
164 if (TN.rows() == 0 || AN.rows() == 0) return ANn;
165
166 for (std::size_t ist = 0; ist < M; ++ist) {
167 const std::size_t ind = sn.station_to_node[ist];
168 if (ind == 0) continue;
169 for (std::size_t r = 0; r < R; ++r) ANn(ind - 1, r) = AN(ist, r);
170 }
171 for (std::size_t ind = 1; ind <= I; ++ind) {
172 if (sn.nodes[ind - 1].nodetype == qn::NodeType::Source) continue;
173 const bool is_cache = sn.nodes[ind - 1].nodetype == qn::NodeType::Cache;
174 const bool is_station = sn.nodes[ind - 1].station != 0;
175 for (std::size_t c = 0; c < C; ++c) {
176 const std::vector<std::size_t>& inchain = sn.inchain[c];
177 const std::size_t refstat = detail::sn_chain_refstat(sn, c);
178 const T den = detail::sn_chain_refvisits(sn, c, refstat, inchain);
179 if (!(den > zero)) continue;
180 const T tot = detail::sn_chain_tput(sn, TN, c, refstat, inchain);
181 for (std::size_t r : inchain) {
182 if (is_cache) {
183 typename std::map<std::size_t, qn::CacheParam<T>>::const_iterator it =
184 sn.nodeparam.find(ind);
185 if (it != sn.nodeparam.end() &&
186 detail::sn_is_cache_class(it->second.hitclass, it->second.missclass, r))
187 continue;
188 } else if (is_station) {
189 continue;
190 }
191 ANn(ind - 1, r - 1) = T(sn.nodevisits[c](ind - 1, r - 1) / den * tot);
192 }
193 }
194 }
195 return ANn;
196}
197
198/**
199 * Port of sn_get_node_tput_from_tput.
200 *
201 * Three passes, in the reference's order. A Cache node first splits the
202 * arriving flow into its hit and miss classes; every station then overwrites
203 * its own node row with its station throughput; finally every non-station node
204 * that is not a Sink accumulates what `sn.rtnodes` carries out of it.
205 *
206 * `cache_prob` maps a 1-based Cache NODE index to its converged split; an
207 * absent or empty entry selects the visit-ratio branch.
208 */
209template <class T>
211 const qn::NetworkStruct<T>& sn, const Matrix<T>& TN, const Matrix<T>& ANn,
212 const std::map<std::size_t, CacheActualProb<T>>& cache_prob =
213 std::map<std::size_t, CacheActualProb<T>>()) {
214 const T zero = num_traits<T>::from_int(0);
215 const std::size_t I = sn.nodes.size(), C = sn.nchains, M = sn.nstations, R = sn.nclasses;
216 Matrix<T> TNn(I, R, zero);
217 if (TN.rows() == 0) return TNn;
218
219 // pass 1: the Cache hit and miss split
220 for (std::size_t ind = 1; ind <= I; ++ind) {
221 if (sn.nodes[ind - 1].nodetype != qn::NodeType::Cache) continue;
222 typename std::map<std::size_t, qn::CacheParam<T>>::const_iterator np =
223 sn.nodeparam.find(ind);
224 if (np == sn.nodeparam.end()) continue;
225 const std::vector<std::size_t>& hitclass = np->second.hitclass;
226 const std::vector<std::size_t>& missclass = np->second.missclass;
227 typename std::map<std::size_t, CacheActualProb<T>>::const_iterator cp =
228 cache_prob.find(ind);
229 const bool have_actual = cp != cache_prob.end() && !cp->second.hit.empty();
230 for (std::size_t c = 0; c < C; ++c) {
231 const std::vector<std::size_t>& inchain = sn.inchain[c];
232 const std::size_t refstat = detail::sn_chain_refstat(sn, c);
233 const T tot = detail::sn_chain_tput(sn, TN, c, refstat, inchain);
234 for (std::size_t r : inchain) {
235 if (have_actual) {
236 for (std::size_t o = 0; o < hitclass.size(); ++o) {
237 T arv = zero;
238 if (ANn.rows() == I && o < R) {
239 arv = ANn(ind - 1, o);
240 } else {
241 bool in = false;
242 for (std::size_t q : inchain)
243 if (q == o + 1) in = true;
244 if (in && refstat >= 1 && refstat <= M) arv = TN(refstat - 1, o);
245 }
246 // A NaN PROBABILITY IS "NOT MEASURED", NOT A RATE, and the reference
247 // skips it (`~isnan(actualHitProb(origClass))` and its twins). The
248 // retrieval classes are why it matters: each carries the MISS class
249 // as its own missclass, so the returning fetch logs the miss that
250 // started it, but no solver reports a split for it. Summing its NaN
251 // made the Cache and Sink MissClass rows NaN on every retrieval_*.
252 if (hitclass[o] == r && o < cp->second.hit.size() &&
253 !detail::sn_prob_is_nan(cp->second.hit[o])) {
254 T p = cp->second.hit[o];
255 if (o < cp->second.delayed_hit.size() &&
256 !detail::sn_prob_is_nan(cp->second.delayed_hit[o]))
257 p = T(p + cp->second.delayed_hit[o]);
258 TNn(ind - 1, r - 1) = T(TNn(ind - 1, r - 1) + arv * p);
259 } else if (o < missclass.size() && missclass[o] == r &&
260 o < cp->second.miss.size() &&
261 !detail::sn_prob_is_nan(cp->second.miss[o])) {
262 TNn(ind - 1, r - 1) = T(TNn(ind - 1, r - 1) + arv * cp->second.miss[o]);
263 }
264 }
265 } else if (detail::sn_is_cache_class(hitclass, missclass, r)) {
266 const T den = detail::sn_chain_refvisits(sn, c, refstat, inchain);
267 if (den > zero)
268 TNn(ind - 1, r - 1) =
269 T(sn.nodevisits[c](ind - 1, r - 1) / den * tot);
270 }
271 }
272 }
273 }
274
275 // pass 2: every station's own throughput
276 for (std::size_t ist = 0; ist < M; ++ist) {
277 const std::size_t ind = sn.station_to_node[ist];
278 if (ind == 0) continue;
279 for (std::size_t r = 0; r < R; ++r) TNn(ind - 1, r) = TN(ist, r);
280 }
281
282 // pass 3: what the node routing carries out of a non-station node
283 if (ANn.rows() != I || sn.rtnodes.rows() != I * R) return TNn;
284 for (std::size_t ind = 1; ind <= I; ++ind) {
285 const qn::NodeType nt = sn.nodes[ind - 1].nodetype;
286 if (nt == qn::NodeType::Sink) continue;
287 if (nt == qn::NodeType::Source) {
288 const std::size_t ist = sn.nodes[ind - 1].station;
289 if (ist != 0)
290 for (std::size_t s = 0; s < R; ++s) TNn(ind - 1, s) = TN(ist - 1, s);
291 continue;
292 }
293 // a Join is accumulated even though it IS a station: the reference's
294 // Join arm has no isstation guard, and a Join's throughput is the rate
295 // its outgoing edges carry, not the rate its (auxiliary) station serves
296 if (nt != qn::NodeType::Join && sn.nodes[ind - 1].station != 0) continue;
297 for (std::size_t c = 0; c < C; ++c) {
298 const std::vector<std::size_t>& inchain = sn.inchain[c];
299 for (std::size_t r : inchain) {
300 bool anystateful = false;
301 if (c < sn.visits.size())
302 for (std::size_t a = 0; a < sn.visits[c].rows(); ++a)
303 if (sn.visits[c](a, r - 1) != zero) anystateful = true;
304 if (!anystateful) continue;
305 for (std::size_t s : inchain)
306 for (std::size_t jnd = 1; jnd <= I; ++jnd) {
307 if (nt == qn::NodeType::Cache && ind == jnd) continue;
308 TNn(ind - 1, s - 1) =
309 T(TNn(ind - 1, s - 1) +
310 ANn(ind - 1, r - 1) *
311 sn.rtnodes((ind - 1) * R + (r - 1), (jnd - 1) * R + (s - 1)));
312 }
313 }
314 }
315 }
316 return TNn;
317}
318
319} // namespace api
320} // namespace line
321
322#endif // LINE_API_SN_SN_NODE_METRICS_H
std::size_t rows() const
Definition matrix.h:89
A network plus its refreshed NetworkStruct.
Matrix< T > sn_get_node_tput_from_tput(const qn::NetworkStruct< T > &sn, const Matrix< T > &TN, const Matrix< T > &ANn, const std::map< std::size_t, CacheActualProb< T > > &cache_prob=std::map< std::size_t, CacheActualProb< T > >())
Port of sn_get_node_tput_from_tput.
Matrix< T > sn_get_node_arvr_from_tput(const qn::NetworkStruct< T > &sn, const Matrix< T > &TN, const Matrix< T > &AN)
Port of sn_get_node_arvr_from_tput.
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:326
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
The converged cache split, per ORIGINAL class of one Cache node.
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass