LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_node_tables.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_SOLVER_NODE_TABLES_H
6#define LINE_SOLVERS_SOLVER_NODE_TABLES_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The NODE-indexed view of a station result, behind `getAvgNodeTable`.
12 *
13 * ONE DEFINITION, THREE CALLERS. `line-cli -a node` and `-a nodechain` were the
14 * first two, and the comment on `node_metrics` already said why they must not
15 * each build the scatter: "recomputing them in the second arm would be a second
16 * definition of the same scatter, free to disagree with the first". The third
17 * is the example corpus. Four `cache_replc_*` references call
18 * `getAvgNodeTable()` rather than `getAvgTable()` -- a cache model's answer
19 * lives at the Cache and the ClassSwitch, neither of which is a station -- and
20 * their C++ twins printed the station table instead, so every Cache and
21 * ClassSwitch row was absent and the goldens' 71 cells went unmeasured. This
22 * header is what lets a twin print the table its reference printed without a
23 * fourth copy of the rule.
24 *
25 * ARITHMETIC: field. Sums, quotients and the two `sn_*` ports it delegates to.
26 */
27
28#include <algorithm>
29#include <cmath>
30#include <cstddef>
31#include <map>
32#include <string>
33#include <vector>
34
38#include "line/num/number.h"
41#include "line/util/matrix.h"
42
43namespace line {
44namespace solvers {
45
46/**
47 * The station AvgResult of a solver whose runner returns its own solution type,
48 * i.e. SSA and Fluid: their QN/UN/RN/TN are the same six columns the AvgTable
49 * arm already prints, so a VIEW of them is a view of the same numbers.
50 *
51 * The two columns those runners do not carry are filled the way the reference
52 * fills them, and the SAME way their own `-a avg` arms do:
53 * - ResidT is `sn_get_residt_from_respt`, the per-JOB residence time. It is a
54 * pure function of `sn` and RN, which is why SolverMVA's runner calls it
55 * rather than deriving it inside the analyzer, and why a solver that reports
56 * no residence time of its own is not thereby excused from reporting one.
57 * ResidT = RespT holds only when every station is visited once per cycle;
58 * assuming it cost `sdroute_closed` a factor of 3 on the two Queues and
59 * `init_state_ps` a factor of 17 on Queue1.
60 * - ArvR is the throughput except at a Source, which has no arrivals TO ITSELF.
61 * Restating either rule here rather than sharing it would let `-a avg` and
62 * `-a node` disagree on one column of one model, which is the divergence
63 * `-s mva` vs `-s fluid` already produced once on gallery_mm1.
64 */
65template <class T>
67 const line::Matrix<double>& QN,
68 const line::Matrix<double>& UN,
69 const line::Matrix<double>& RN,
70 const line::Matrix<double>& TN,
71 const std::vector<double>& CN,
72 const std::vector<double>& XN,
73 const std::string& method) {
74 const std::size_t M = sn.nstations, R = sn.nclasses;
75 const T zero = line::num_traits<T>::from_int(0);
77 r.QN = line::Matrix<T>(M, R, zero);
78 r.UN = line::Matrix<T>(M, R, zero);
79 r.RN = line::Matrix<T>(M, R, zero);
80 r.WN = line::Matrix<T>(M, R, zero);
81 r.AN = line::Matrix<T>(M, R, zero);
82 r.TN = line::Matrix<T>(M, R, zero);
83 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(R, false));
84 for (std::size_t i = 0; i < M; ++i) {
85 if (sn.stations[i].sched == line::lang::SchedStrategy::EXT)
86 for (std::size_t c = 0; c < R; ++c) srcmask[i][c] = true;
87 for (std::size_t c = 0; c < R; ++c) {
88 r.QN(i, c) = line::num_traits<T>::from_double(QN(i, c));
89 r.UN(i, c) = line::num_traits<T>::from_double(UN(i, c));
90 r.RN(i, c) = line::num_traits<T>::from_double(RN(i, c));
91 r.TN(i, c) = line::num_traits<T>::from_double(TN(i, c));
92 }
93 }
95 // ARRIVAL RATE IS AN INFLOW, NOT A THROUGHPUT. `AN = TN` holds only where
96 // the station's own flow balances, and a fluid fixed point need not: on
97 // cache_replc_routing MATLAB's rmf reports Tput 0 at Delay1 and Delay2 while
98 // 0.4 and 0.6 arrive there, so the shortcut dropped those four rows out of
99 // the node table entirely. `SolverFLD/runAnalyzer.m:372` and
100 // `SolverSSA/runAnalyzer.m:199` both derive AN through sn_get_arvr_from_tput
101 // from the class-expanded routing, exactly as CTMC, NC, MAM, BA and LQNS
102 // already do in this port; the Source mask is `getAvg`'s own zeroMask.
105 // A JSQ/SQ dispatcher's routing row holds only the static split, so the
106 // stations it feeds take their arrival rate (and residence time) from flow balance.
108 for (std::size_t c = 0; c < CN.size(); ++c)
109 r.CN.push_back(line::num_traits<T>::from_double(CN[c]));
110 for (std::size_t c = 0; c < XN.size(); ++c)
111 r.XN.push_back(line::num_traits<T>::from_double(XN[c]));
112 r.method = method;
113 r.actualmethod = method;
114 return r;
115}
116
117/**
118 * The classes `getAvgNodeTable` omits: the auxiliary retrieval classes that
119 * `Cache.setRetrievalSystem` mints, one per item. Both references hide them
120 * (`getAvgNodeTable.m` through `retrievalClassIndices`, NetworkSolver.java the
121 * same), being plumbing rather than user classes. They do carry node rates --
122 * their visits circulate cache -> retrieval queues -> cache -- so the all-zero
123 * row filter does NOT drop them, and without this the table printed one
124 * ArvR = Tput = 2 row per item at the Cache of retrieval_simple.
125 */
126template <class T>
128 std::vector<bool> hidden(sn.nclasses, false);
129 for (const auto& kv : sn.nodeparam) {
130 if (kv.first == 0 || kv.first > sn.nodes.size() ||
131 sn.nodes[kv.first - 1].nodetype != line::qn::NodeType::Cache)
132 continue;
133 for (const std::vector<std::size_t>& row : kv.second.retrieval_classes)
134 for (std::size_t rc : row)
135 if (rc >= 1 && rc <= sn.nclasses) hidden[rc - 1] = true;
136 }
137 return hidden;
138}
139
140/**
141 * The station table scattered to the NODE index space, plus the two flow
142 * columns the reference recomputes there.
143 *
144 * Factored out of `-a node` because `-a nodechain` aggregates exactly these six
145 * matrices by chain: recomputing them in the second arm would be a second
146 * definition of the same scatter, free to disagree with the first.
147 */
148template <class T>
150 line::Matrix<T> QN, UN, RN, WN, AN, TN; ///< (nnodes x nclasses)
151};
152
153template <class T>
155 const line::mva::AvgResult<T>& r) {
156 const std::size_t I = sn.nodes.size(), R = sn.nclasses;
157 const T zero = line::num_traits<T>::from_int(0);
159 m.QN = line::Matrix<T>(I, R, zero);
160 m.UN = line::Matrix<T>(I, R, zero);
161 m.RN = line::Matrix<T>(I, R, zero);
162 m.WN = line::Matrix<T>(I, R, zero);
163 for (std::size_t ist = 0; ist < sn.nstations; ++ist) {
164 const std::size_t ind = sn.station_to_node[ist];
165 if (!ind) continue;
166 for (std::size_t c = 0; c < R; ++c) {
167 m.QN(ind - 1, c) = r.QN(ist, c);
168 m.UN(ind - 1, c) = r.UN(ist, c);
169 m.RN(ind - 1, c) = r.RN(ist, c);
170 m.WN(ind - 1, c) = r.WN(ist, c);
171 }
172 }
173 // THE TWO RECOMPUTED COLUMNS READ THE REFRESHED STRUCT, exactly as the
174 // station table already does (`solver_mva_runner.h`, `refL`). A cache's
175 // hit/miss split is a SOLVER RESULT: `link()` leaves the self-switch at the
176 // offered 1/2-1/2, and both of these columns are visit-weighted, so reading
177 // the base struct here reports the split nobody computed. Measured on
178 // cache_replc_routing, Cache/Router/Sink hit and miss came out 1/1 against a
179 // golden of 0.8/1.2, and on cache_replc_fifo the Cache read-class ArvR came
180 // out 2 against 1 -- the two halves of one guess, summed.
181 //
182 // NULL for every model with no cache branch, which is the base struct and
183 // the behaviour every other model already had.
185 // AND THE SPLIT ITSELF IS PASSED, not left to be inferred from the visits.
186 // A refreshed struct is only ONE of the two ways a solver states what it
187 // measured, and the arms that do not build one -- CTMC, SSA, and the
188 // Source-Cache-Sink branch of MVA and NC -- still report a hit probability
189 // in `AvgResult::cache`. Without it every one of them fell through to the
190 // visit-ratio branch of sn_get_node_tput_from_tput, i.e. to the 1/2-1/2
191 // `link()` offers before anything is solved: 0.5/0.5 against a golden of
192 // 0.4/0.6 on cache_replc_fifo and cache_replc_lru, 1/1 against 1.1127/
193 // 0.8873 on cache_replc_rr. Empty on every model with no Cache node.
194 std::map<std::size_t, line::api::CacheActualProb<T>> cacheprob;
195 for (std::size_t ci = 0; ci < r.cache.caches.size(); ++ci) {
196 const line::solvers::CacheNodeMetrics<T>& cm = r.cache.caches[ci];
197 if (cm.node == 0 || cm.hitprob.empty()) continue;
199 p.hit = cm.hitprob;
200 p.miss = cm.missprob;
202 cacheprob[cm.node] = p;
203 }
205 m.TN = line::api::sn_get_node_tput_from_tput(refsn, r.TN, m.AN, cacheprob);
206 // WHAT A CACHE SENDS ON IS WHAT THE NEXT NODE RECEIVES, and the visit ratios
207 // cannot say so: `sn.nodevisits` still carries the 1/2-1/2 `link()` offered.
208 // As getAvgNode.m does, re-derive the arrivals at the non-station nodes a
209 // cache feeds (ClassSwitch, Sink, a further Cache) from its solved hit/miss
210 // throughput along rtnodes, then the node throughputs from those arrivals. A
211 // cache feeding a cache needs one pass per level, so iterate to the fixed point.
212 bool hascache = false;
213 for (std::size_t pass = 0; pass < I; ++pass) {
214 bool changed = false;
215 for (std::size_t cind = 1; cind <= I; ++cind) {
216 if (refsn.nodes[cind - 1].nodetype != line::qn::NodeType::Cache) continue;
217 hascache = true;
218 typename std::map<std::size_t, line::qn::CacheParam<T>>::const_iterator np =
219 refsn.nodeparam.find(cind);
220 if (np == refsn.nodeparam.end()) continue;
221 for (std::size_t ind = 1; ind <= I; ++ind) {
222 if (ind == cind || refsn.nodes[ind - 1].station != 0) continue;
223 for (std::size_t s = 0; s < R; ++s) {
224 T arv = zero;
225 bool fed = false;
226 for (std::size_t c = 1; c <= R; ++c) {
227 if (!line::api::detail::sn_is_cache_class(np->second.hitclass, np->second.missclass, c))
228 continue;
229 const T p = refsn.rtnodes((cind - 1) * R + (c - 1), (ind - 1) * R + s);
230 if (p > zero) {
231 arv = T(arv + m.TN(cind - 1, c - 1) * p);
232 fed = true;
233 }
234 }
235 if (!fed) continue;
236 const double d = line::num_traits<T>::to_double(T(m.AN(ind - 1, s) - arv));
237 const double scale = std::max(1.0, line::num_traits<T>::to_double(arv));
238 if (std::fabs(d) > line::lang::GlobalConstants::FineTol * scale) {
239 m.AN(ind - 1, s) = arv;
240 changed = true;
241 }
242 }
243 }
244 }
245 if (!changed) break;
246 m.TN = line::api::sn_get_node_tput_from_tput(refsn, r.TN, m.AN, cacheprob);
247 }
248 // The station arrivals were derived from routing that still split each read
249 // evenly; with the node throughputs settled, re-derive them by flow balance.
250 if (hascache) {
251 for (std::size_t ist = 0; ist < refsn.nstations; ++ist) {
252 const std::size_t ind = refsn.station_to_node[ist];
253 if (!ind || refsn.nodes[ind - 1].nodetype == line::qn::NodeType::Source) continue;
254 for (std::size_t rr = 0; rr < R; ++rr) {
255 T arv = zero;
256 for (std::size_t j = 0; j < I; ++j)
257 for (std::size_t s = 0; s < R; ++s) {
258 const T p = refsn.rtnodes(j * R + s, (ind - 1) * R + rr);
259 if (p > zero) arv = T(arv + m.TN(j, s) * p);
260 }
261 m.AN(ind - 1, rr) = arv;
262 }
263 }
264 }
265 return m;
266}
267} // namespace solvers
268} // namespace line
269
270#endif // LINE_SOLVERS_SOLVER_NODE_TABLES_H
What a solver observed about the Cache nodes of a model.
A network plus its refreshed NetworkStruct.
std::map< std::size_t, CacheParam< T > > nodeparam
Cache parameters by 1-based NODE index; only Cache nodes have an entry.
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::size_t > station_to_node
(nstations) 1-based node index
Dense matrix and non-owning view.
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.
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
Matrix< T > filter_metric(const qn::NetworkStruct< T > &L, const Matrix< T > &metric, MetricKind kind, const std::vector< std::vector< bool > > *zero_mask)
Port of filterMetric: what @@NetworkSolver/getAvg does between the analyzer and the caller.
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
Port of sn_get_arvr_from_tput: the arrival rate each station sees, from the throughputs and the class...
void sn_arvr_state_dependent_balance(const qn::NetworkStruct< T > &L, Matrix< T > &AN, const Matrix< T > &TN, Matrix< T > *WN)
std::vector< bool > node_table_hidden_classes(const line::qn::NetworkStruct< T > &sn)
The classes getAvgNodeTable omits: the auxiliary retrieval classes that Cache.setRetrievalSystem mint...
line::mva::AvgResult< T > avg_result_from_sim(const line::qn::NetworkStruct< T > &sn, const line::Matrix< double > &QN, const line::Matrix< double > &UN, const line::Matrix< double > &RN, const line::Matrix< double > &TN, const std::vector< double > &CN, const std::vector< double > &XN, const std::string &method)
The station AvgResult of a solver whose runner returns its own solution type, i.e.
NodeMetrics< T > node_metrics(const line::qn::NetworkStruct< T > &sn, const line::mva::AvgResult< T > &r)
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.
Arrival rates at stations fed by a JSQ or SQ(d) dispatcher, from flow balance.
Ports of matlab/src/api/sn/sn_get_node_arvr_from_tput.m and sn_get_node_tput_from_tput....
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The converged cache split, per ORIGINAL class of one Cache node.
static constexpr double FineTol
Definition lang_types.h:765
The metrics getAvg returns, after filtering.
std::shared_ptr< qn::NetworkStruct< T > > refreshed_struct
The struct whose cache self-switch carries the CONVERGED hit/miss split, filled by the cacheqn branch...
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
Matrix< T > UN
utilization
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node and on every solver that does n...
Matrix< T > AN
arrival rate
One Cache node's measured behaviour.
std::vector< T > hitprob
(K) TRUE hit fraction, EMPTY = not computed
std::vector< T > delayedprob
(K) delayed-hit fraction, EMPTY off a retrieval system
std::size_t node
1-based node index of the Cache
The station table scattered to the NODE index space, plus the two flow columns the reference recomput...
line::Matrix< T > TN
(nnodes x nclasses)