LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
tag_chain.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_TAG_CHAIN_H
6#define LINE_LANG_QN_TAG_CHAIN_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * Port of matlab/src/io/@@ModelAdapter/tagChain.m: the model-to-model transform
12 * that isolates ONE job of a chain so that its passage can be observed.
13 *
14 * WHAT IT DOES. Every class of the chain gets a TAGGED twin: same service, same
15 * routing, same reference station, but its own class index. The chain's
16 * population is moved by one job -- the class the caller names loses one, its
17 * twin gains it -- so the transformed model has the same total population and
18 * the same stationary behaviour, with one job now distinguishable from the
19 * rest. A twin exists for EVERY class of the chain and not only for the tagged
20 * one because the tagged job class-switches as it circulates; without the twins
21 * it would leave the tagged block on its first switch and stop being tagged.
22 *
23 * WHY IT IS A MODEL TRANSFORM AND NOT A POST-PROCESSING STEP. The quantity the
24 * response-time CDF needs is the law of the state SEEN BY ONE JOB between its
25 * arrival and its departure. The generator of the untagged model has no event
26 * that says "this particular job arrived": arrivals of a class are
27 * indistinguishable, and the filtration can only split the generator on class-
28 * and node-level events. Minting a class with exactly one job in it turns the
29 * question into one the filtration can answer.
30 *
31 * THE POPULATION BOOKKEEPING IS NOT COSMETIC. The twin carries population 1 at
32 * the tagged class and 0 at every other class of the chain, and the original
33 * class is decremented by 1. Keeping the original population would enlarge the
34 * chain by one job and change the very response time being measured.
35 *
36 * WHAT THE REFERENCE DOES THAT THIS DOES NOT. tagChain.m works on a Network
37 * object and re-links it, so it re-derives the class-switch nodes from the
38 * linked routing matrix P{r,s}. Here the transform is applied to the refreshed
39 * NetworkStruct directly, exactly as fj_mmt does, and the tagged routing block
40 * is written into P and into any explicit ClassSwitch matrix before a single
41 * refresh_struct() re-derives the chains, the capacities and rt. The two routes
42 * agree because a chain is closed under class switching: the block of P (and of
43 * the class-switch matrix) over the chain's own classes is already stochastic,
44 * so copying it onto the tagged classes needs no rebalancing.
45 */
46
47#include <cmath>
48#include <cstddef>
49#include <limits>
50#include <map>
51#include <string>
52#include <vector>
53
55#include "line/util/error.h"
56
57namespace line {
58namespace qn {
59
60/**
61 * The tagged model and the class bookkeeping a caller needs to read it back.
62 *
63 * `tagged[a]` is the twin of `orig[a]`, both 1-based and in chain order, and
64 * `taggedjob` is the one twin that actually holds a job. A caller filtering the
65 * generator wants the whole of `tagged`, not just `taggedjob`: the single job
66 * moves between the twins as it switches class.
67 */
68template <class T>
71 std::vector<std::size_t> tagged;
72 std::vector<std::size_t> orig;
73 std::size_t taggedjob = 0;
74};
75
76namespace tag_detail {
77
78/**
79 * Refuse, by name, every feature whose per-class parameter tables this
80 * transform would have to widen and cannot widen meaningfully.
81 *
82 * The list is not a portability shortfall of the C++ side: tagChain.m widens
83 * only the service processes, the output strategies and the capacities, so on
84 * any of these models the reference produces a tagged model whose extra class
85 * is missing from the very table that decides its dynamics. Silently doing the
86 * same here would return a CDF for a model the caller did not describe.
87 */
88template <class T>
89void tag_chain_check(const NetworkStruct<T>& sn) {
90 if (!sn.nodeparam.empty())
91 throw UnsupportedError(
92 "tag_chain: the model has a Cache node, whose item access costs and hit/miss routing "
93 "are indexed by class; a tagged twin of a read class has no entry in them");
94 if (!sn.transparam.empty())
95 throw UnsupportedError(
96 "tag_chain: the model has a Transition (SPN) node, whose enabling and firing arcs are "
97 "indexed by class and mode; the transform has no rule for the tagged twin's arcs");
98 if (sn.has_fork() || !sn.fj.empty())
99 throw UnsupportedError(
100 "tag_chain: the model has a Fork/Join, where the tagged job becomes several sibling "
101 "tasks and the response time is no longer the passage of one job");
102 if (!sn.regions.empty())
103 throw UnsupportedError(
104 "tag_chain: the model has a finite capacity region, whose per-class caps and drop "
105 "rules are declared over the original class set and cannot be widened");
106 if (!sn.retrialparam.empty())
107 throw UnsupportedError(
108 "tag_chain: the model has a retrial station, whose orbit process is declared per "
109 "class");
110 if (!sn.pollingparam.empty())
111 throw UnsupportedError(
112 "tag_chain: the model has a polling station, whose switchover walks are declared per "
113 "buffer, one buffer per class");
114 if (!sn.pasparam.empty())
115 throw UnsupportedError(
116 "tag_chain: the model has a pass-and-swap / order-independent station, whose service "
117 "rate function and swap graph are indexed by class");
118 for (std::size_t r = 0; r < sn.issignal.size(); ++r)
119 if (sn.issignal[r])
120 throw UnsupportedError(
121 "tag_chain: the model has a G-network signal class, which can annihilate the "
122 "tagged job without a departure and leaves its passage undefined");
123 for (std::size_t r = 0; r < sn.syncreply.size(); ++r)
124 if (sn.syncreply[r] != 0)
125 throw UnsupportedError(
126 "tag_chain: the model has synchronous call/reply blocking, whose reply class is "
127 "declared per calling class and has no tagged twin");
128 for (std::size_t i = 0; i < sn.stations.size(); ++i) {
129 if (sn.stations[i].cdscaling || sn.stations[i].jdscaling)
130 throw UnsupportedError("tag_chain: station '" + sn.stations[i].name +
131 "' is class dependent; its scaling is a function of the "
132 "per-class population vector, whose width the tagging changes");
133 if (sn.stations[i].svc_rate_fun)
134 throw UnsupportedError("tag_chain: station '" + sn.stations[i].name +
135 "' carries a service rate function over ordered class lists, "
136 "which has no value at the tagged twin");
137 }
138}
139
140} // namespace tag_detail
141
142/**
143 * Tag one class of one chain. `chain` and `jobclass` are 1-based; `chain`
144 * indexes `sn.inchain`.
145 *
146 * The chain must be CLOSED. The reference does not check it, and its tagged
147 * twin of an open class ends up with a population of 1 on a class that has no
148 * population at all -- a model whose state space is not the one the caller
149 * asked about. Here that is refused by name instead.
150 */
151template <class T>
152TaggedChain<T> tag_chain(const NetworkStruct<T>& sn, std::size_t chain, std::size_t jobclass,
153 const std::string& suffix = ".tagged") {
154 if (chain == 0 || chain > sn.inchain.size())
155 throw InputError("tag_chain: chain index out of range");
156 const std::vector<std::size_t>& ic = sn.inchain[chain - 1];
157 if (jobclass == 0 || jobclass > sn.classes.size())
158 throw InputError("tag_chain: class index out of range");
159 bool member = false;
160 for (std::size_t a = 0; a < ic.size(); ++a)
161 if (ic[a] == jobclass) member = true;
162 if (!member) throw InputError("tag_chain: the class to tag does not belong to the given chain");
163 for (std::size_t a = 0; a < ic.size(); ++a)
164 if (!std::isfinite(sn.classes[ic[a] - 1].population))
165 throw UnsupportedError("tag_chain: class '" + sn.classes[ic[a] - 1].name +
166 "' of the chain to tag is open; tagging moves one job out of a "
167 "finite population and an open chain has none");
168 if (sn.classes[jobclass - 1].population < 1.0)
169 throw InputError("tag_chain: class '" + sn.classes[jobclass - 1].name +
170 "' has no job to tag");
171
172 tag_detail::tag_chain_check(sn);
173
174 TaggedChain<T> out;
175 out.V = sn;
176 NetworkStruct<T>& V = out.V;
177 const std::size_t I = sn.nodes.size();
178 const std::size_t M = sn.stations.size();
179 const double inf = std::numeric_limits<double>::infinity();
180 const T zero = num_traits<T>::from_int(0);
181
182 out.orig = ic;
183 for (std::size_t a = 0; a < ic.size(); ++a) {
184 const std::size_t r = ic[a];
185 JobClass tc = sn.classes[r - 1];
186 tc.name = sn.classes[r - 1].name + suffix;
187 tc.population = (r == jobclass) ? 1.0 : 0.0;
188 const std::size_t t = V.add_class(tc);
189 const std::size_t K2 = V.classes.size();
190 out.tagged.push_back(t);
191 if (r == jobclass) out.taggedjob = t;
192
193 for (std::size_t i = 1; i <= M; ++i) {
194 const Distrib<T>& d = sn.service[i - 1][r - 1];
195 Station<T>& st = V.stations[i - 1];
196 // An unset capacity is infinite and an unset rule is derived, so
197 // padding with those two sentinels leaves every untouched class
198 // exactly as the refresh would have found it.
199 if (st.classcap.size() < K2) st.classcap.resize(K2, inf);
200 if (st.droprule.size() < K2) st.droprule.resize(K2, 0);
201 if (!st.schedparam.empty() && st.schedparam.size() < K2)
202 st.schedparam.resize(K2, zero);
203 if (!st.cdscalingpeak.empty() && st.cdscalingpeak.size() < K2)
204 st.cdscalingpeak.resize(K2, zero);
205 st.droprule[t - 1] = static_cast<int>(DropStrategy::WAITQ);
206
207 if (d.disabled) {
208 // A class the station never serves keeps a twin so the class
209 // indexing of the tagged block mirrors the original one, but it
210 // is disabled here and given no buffer at all.
212 st.classcap[r - 1] = 0.0;
213 st.classcap[t - 1] = 0.0;
214 if (!st.schedparam.empty()) st.schedparam[t - 1] = zero;
215 continue;
216 }
217 V.set_service(i, t, d);
218 // ONE buffer slot for the twin, one fewer for the original: the
219 // station still holds the same number of chain jobs, and the tagged
220 // job is guaranteed a slot it never has to compete for.
221 st.classcap[t - 1] = 1.0;
222 if (std::isfinite(st.classcap[r - 1])) st.classcap[r - 1] -= 1.0;
223 if (!st.schedparam.empty()) st.schedparam[t - 1] = st.schedparam[r - 1];
224 if (!st.cdscalingpeak.empty()) st.cdscalingpeak[t - 1] = st.cdscalingpeak[r - 1];
225 }
226
227 // A node whose routing is state dependent decides per CLASS, so the
228 // twin has to inherit the strategy and not fall back on the PROB
229 // default that an unsized row would give it.
230 for (std::size_t i = 0; i < I; ++i) {
231 NodeDef& nd = V.nodes[i];
232 if (nd.routing.empty()) continue;
233 if (nd.routing.size() < K2) nd.routing.resize(K2, RoutingStrategy::PROB);
234 nd.routing[t - 1] = sn.nodes[i].routing.size() >= r ? sn.nodes[i].routing[r - 1]
235 : RoutingStrategy::PROB;
236 }
237 }
238
239 // The tagged block of the routing is the chain's own block, copied. Nothing
240 // routes between the tagged and the untagged blocks: a tagged job stays
241 // tagged for its whole passage, which is the entire point of the transform.
242 for (std::size_t x = 0; x < ic.size(); ++x)
243 for (std::size_t y = 0; y < ic.size(); ++y)
244 for (std::size_t i = 1; i <= I; ++i)
245 for (std::size_t j = 1; j <= I; ++j) {
246 const T p = sn.get_route(ic[x], ic[y], i, j);
247 if (num_traits<T>::to_double(p) != 0)
248 V.set_route(out.tagged[x], out.tagged[y], i, j, p);
249 }
250
251 // An explicit ClassSwitch matrix is (nclasses x nclasses) and is applied on
252 // the way out of the node, so it needs the same block copy. Its rows stay
253 // stochastic without rebalancing because a chain is by definition closed
254 // under class switching.
255 const std::size_t K2 = V.classes.size();
256 for (typename std::map<std::size_t, Matrix<T>>::iterator it = V.csmatrix.begin();
257 it != V.csmatrix.end(); ++it) {
258 const Matrix<T> old = it->second;
259 Matrix<T> C(K2, K2, zero);
260 for (std::size_t i = 0; i < old.rows() && i < K2; ++i)
261 for (std::size_t j = 0; j < old.cols() && j < K2; ++j) C(i, j) = old(i, j);
262 for (std::size_t x = 0; x < ic.size(); ++x)
263 for (std::size_t y = 0; y < ic.size(); ++y)
264 C(out.tagged[x] - 1, out.tagged[y] - 1) = old(ic[x] - 1, ic[y] - 1);
265 it->second = C;
266 }
267
268 // Last, so that every table above was still read at its original width.
269 V.classes[jobclass - 1].population -= 1.0;
270 V.refresh_struct();
271 return out;
272}
273
274} // namespace qn
275} // namespace line
276
277#endif // LINE_LANG_QN_TAG_CHAIN_H
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< 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::map< std::size_t, Matrix< T > > csmatrix
The class-switch matrix of a ClassSwitch node, by 1-based NODE index.
The exception types the port throws.
TaggedChain< T > tag_chain(const NetworkStruct< T > &sn, std::size_t chain, std::size_t jobclass, const std::string &suffix=".tagged")
Tag one class of one chain.
Definition tag_chain.h:152
A queueing network and its refreshed NetworkStruct.
static Distrib disabled_dist()
Definition lang_types.h:857
One job class of the network.
double population
infinite for an open class
A node of the network.
std::vector< RoutingStrategy > routing
sn.routing, per class.
One station of the network.
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.
The tagged model and the class bookkeeping a caller needs to read it back.
Definition tag_chain.h:69
std::size_t taggedjob
Definition tag_chain.h:73
NetworkStruct< T > V
Definition tag_chain.h:70
std::vector< std::size_t > tagged
Definition tag_chain.h:71
std::vector< std::size_t > orig
Definition tag_chain.h:72