LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fj_ht.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_MVA_FJ_HT_H
6#define LINE_SOLVERS_MVA_FJ_HT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The Heidelberger-Trivedi fork-join transform, `options.config.fork_join='ht'`.
12 *
13 * Port of matlab/src/io/@@ModelAdapter/ht.m (Heidelberger and Trivedi, "Queueing
14 * network models for parallel processing with asynchronous tasks", IEEE TC
15 * C-31(11), 1982). It is the second arm of the fork-join fixed point that
16 * `fj_driver.h` drives, beside the MMT transform of `fj_mmt.h`, and it answers a
17 * different question: where MMT keeps the circulating job on ONE branch and
18 * carries the remaining branches by auxiliary OPEN classes, H-T sends the
19 * circulating job STRAIGHT PAST the branches and gives every branch its own
20 * auxiliary CLOSED class, one per (forked class, branch), whose population
21 * matches the original's. The four moves are:
22 *
23 * 1. THE FORK BECOMES A ROUTER, and the ORIGINAL classes are routed from it
24 * directly to the join. The original job therefore spends no time on the
25 * branches; the whole fork-join span is charged to it as one delay at the
26 * join.
27 * 2. THE JOIN BECOMES A DELAY. Its service for the original class is the
28 * instant the join fires, `E[X_(k)] * fanOut`, and for an auxiliary class
29 * the residual `E[X_(k)] - R_branch` that branch still waits at the
30 * synchronisation point.
31 * 3. AN AUXILIARY DELAY IS ADDED PER JOIN, "Auxiliary Delay - <join>". The
32 * join routes into it and it routes back to the fork, so an auxiliary token
33 * cycles fork -> its own branch -> join -> auxiliary delay -> fork. Its
34 * service for an auxiliary class is the response time the ORIGINAL class
35 * accumulates OUTSIDE the span, which is what keeps the auxiliary token's
36 * cycle time equal to the original's.
37 * 4. ONE CLOSED AUXILIARY CLASS PER BRANCH AND PER FORKED CLASS, of the
38 * original's population, referencing the auxiliary delay. It carries the
39 * original's service demand at every station of its branch, so each branch
40 * is loaded by a population equal to the original's, which is what a fork
41 * actually generates.
42 *
43 * WHAT IT REFUSES, by name and for the same reasons the reference does:
44 * - `tasksPerLink > 1`. The transform has no way to send w identical tasks
45 * down a link, since a branch carries exactly one auxiliary class.
46 * - an OPEN class through the fork. The auxiliary class is a ClosedClass of
47 * the original's population, and an open class has none.
48 * - a fork with no join. H-T charges the whole span at the synchronisation
49 * point, so without one there is nothing to charge.
50 *
51 * The synchronisation delays themselves, and the merge-back of the auxiliary
52 * columns, live in `fj_driver.h`: the reference keeps both arms in one
53 * `fjFixedPoint`, and so does this port.
54 */
55
56#include <algorithm>
57#include <cmath>
58#include <limits>
59#include <map>
60#include <string>
61#include <utility>
62#include <vector>
63
66#include "line/util/error.h"
67
68namespace line {
69namespace mva {
70
71namespace detail {
72
73/**
74 * `P{a,b} = P{r,s}` of the reference: copy a whole routing block onto another
75 * class pair. An absent source block is an all-zero one, in which case nothing
76 * is copied and the destination keeps whatever the caller writes into it -- the
77 * same state the reference reaches, its cell array holding a zero matrix there.
78 */
79template <class T>
80void fj_copy_block(qn::NetworkStruct<T>& V, std::size_t r, std::size_t s, std::size_t a,
81 std::size_t b) {
82 const typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>::const_iterator it =
83 V.P.find(std::make_pair(r, s));
84 if (it == V.P.end()) return;
85 const Matrix<T> src = it->second; // by value: set_route below may rehash V.P
86 const T zero = num_traits<T>::from_int(0);
87 for (std::size_t i = 0; i < src.rows(); ++i)
88 for (std::size_t j = 0; j < src.cols(); ++j)
89 if (src(i, j) > zero || src(i, j) < zero) V.set_route(a, b, i + 1, j + 1, src(i, j));
90}
91
92/** `P{r,s}(dst,:) = P{r,s}(src,:)` for every class pair the routing holds. */
93template <class T>
94void fj_copy_row_all_blocks(qn::NetworkStruct<T>& V, std::size_t src, std::size_t dst) {
95 std::vector<std::pair<std::size_t, std::size_t>> keys;
96 for (typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>::const_iterator it =
97 V.P.begin();
98 it != V.P.end(); ++it)
99 keys.push_back(it->first);
100 for (std::size_t k = 0; k < keys.size(); ++k) {
101 typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>::iterator it =
102 V.P.find(keys[k]);
103 if (it == V.P.end() || it->second.rows() < src || it->second.rows() < dst) continue;
104 for (std::size_t j = 0; j < it->second.cols(); ++j)
105 it->second(dst - 1, j) = it->second(src - 1, j);
106 }
107}
108
109} // namespace detail
110
111/**
112 * Build the H-T transform of `L`. Returns an inactive record when the layer
113 * holds no fork, in which case the caller solves the layer directly.
114 */
115template <class T>
117 const T zero = num_traits<T>::from_int(0);
118 const T one = num_traits<T>::from_int(1);
119 FjMmt<T> tr;
120 tr.heidelberger_trivedi = true;
121 tr.norig = L.nclasses;
122
123 std::vector<std::size_t> forkNodes;
124 for (std::size_t i = 0; i < L.nodes.size(); ++i)
125 if (L.nodes[i].nodetype == NodeType::Fork) forkNodes.push_back(i + 1);
126 if (forkNodes.empty()) return tr;
127
128 // ---- 1. one record per fork, with its join, its fan-out and the ORDERED
129 // head node of each of its branches, read off the BASE routing.
130 // The reference reads the heads from `outputStrategy{r}{3}`, which is the
131 // order the links were declared in; this port has only the routing matrix,
132 // so it takes them in ascending node index. The branches are interchangeable
133 // in everything that follows -- each auxiliary class walks one of them and
134 // the order statistic is over the whole set -- so the ordering names the
135 // classes, it does not change the answer.
136 std::vector<std::vector<std::vector<std::size_t>>> heads(forkNodes.size());
137 for (std::size_t a = 0; a < forkNodes.size(); ++a) {
138 typename FjMmt<T>::ForkRec F;
139 F.node = forkNodes[a];
140 F.fanOut = L.nodes[F.node - 1].tasks_per_link;
141 if (F.fanOut > 1.0)
142 throw UnsupportedError(
143 "fj_ht: the fork node '" + L.nodes[F.node - 1].name + "' of model '" + L.name +
144 "' sends more than one task per link (tasksPerLink=" +
145 std::to_string(static_cast<long>(F.fanOut + 0.5)) +
146 "); multiple tasks per link are not supported in H-T, use fork_join='mmt'");
147 for (std::size_t p = 0; p < L.fj.size(); ++p) {
148 if (L.fj[p].first != F.node) continue;
149 if (F.joinNode != 0)
150 throw UnsupportedError("fj_ht: model '" + L.name + "' pairs fork node " +
151 std::to_string(F.node) +
152 " with more than one join station; the reference supports "
153 "one join per fork");
154 F.joinNode = L.fj[p].second;
155 }
156 if (F.joinNode == 0)
157 throw UnsupportedError(
158 "fj_ht: the fork node '" + L.nodes[F.node - 1].name + "' of model '" + L.name +
159 "' has no join; the H-T method charges the whole fork-join span at the "
160 "synchronisation point, so it needs one. Use fork_join='mmt'");
161 F.origfanout.assign(L.nclasses + 1, 0);
162 heads[a].assign(L.nclasses + 1, std::vector<std::size_t>());
163 for (std::size_t r = 1; r <= L.nclasses; ++r) {
164 for (std::size_t j = 1; j <= L.nodes.size(); ++j) {
165 bool linked = false;
166 for (std::size_t s = 1; s <= L.nclasses && !linked; ++s)
167 if (L.get_route(r, s, F.node, j) > zero) linked = true;
168 if (linked) heads[a][r].push_back(j);
169 }
170 F.origfanout[r] = heads[a][r].size();
171 }
172 tr.forks.push_back(F);
173 }
174
175 tr.V = L;
177
178 // ---- 2. every fork becomes a Router. Its out-edges are NOT divided as the
179 // MMT transform divides them: the original class is rerouted straight to the
180 // join below and never takes a branch at all.
181 for (std::size_t a = 0; a < tr.forks.size(); ++a)
182 V.nodes[tr.forks[a].node - 1].nodetype = NodeType::Router;
183 V.fj.clear();
184
185 // ---- 3. every Join becomes a zero-service Delay and gains its auxiliary
186 // Delay. As in the MMT transform, EVERY Join of the model is converted, not
187 // only the ones a fork claims, so an unpaired join does not survive as a
188 // Join the inner solver would then reject.
189 for (std::size_t j = 1; j <= L.nodes.size(); ++j) {
190 if (L.nodes[j - 1].nodetype != NodeType::Join) continue;
191 const std::size_t st = V.nodes[j - 1].station;
192 if (st == 0)
193 throw InputError("fj_ht: the join node '" + V.nodes[j - 1].name + "' of model '" +
194 L.name + "' is not a station");
195 V.stations[st - 1].nodetype = NodeType::Delay;
196 V.stations[st - 1].sched = SchedStrategy::INF;
197 V.stations[st - 1].nservers = std::numeric_limits<double>::infinity();
198 V.nodes[j - 1].nodetype = NodeType::Delay;
199 for (std::size_t k = 1; k <= V.classes.size(); ++k)
201 tr.joinStations.push_back(st);
202
204 ad.name = "Auxiliary Delay - " + V.nodes[j - 1].name;
205 ad.nodetype = NodeType::Delay;
206 ad.sched = SchedStrategy::INF;
207 ad.nservers = std::numeric_limits<double>::infinity();
208 const std::size_t adstat = V.add_station(ad);
209 const std::size_t adnode = V.station_to_node[adstat - 1];
210 tr.auxDelayStation[j] = adstat;
211 tr.auxDelayNode[j] = adnode;
212 for (std::size_t k = 1; k <= V.classes.size(); ++k)
213 V.set_service(adstat, k, Distrib<T>::immediate());
214
215 // The auxiliary delay INHERITS the join's out-edges and the join is then
216 // routed into it, so every class leaves the span through the new delay.
217 // The reference copies the row in every block and rewrites only the
218 // DIAGONAL one, which is what keeps a class switch declared at the join
219 // pointing where it pointed.
220 detail::fj_copy_row_all_blocks(V, j, adnode);
221 for (std::size_t r = 1; r <= V.classes.size(); ++r) {
222 detail::fj_clear_row(V, r, r, j);
223 V.set_route(r, r, j, adnode, one);
224 }
225 }
226 for (std::size_t a = 0; a < tr.forks.size(); ++a)
227 tr.forks[a].joinStation = V.nodes[tr.forks[a].joinNode - 1].station;
228
229 // ---- 4. one auxiliary CLOSED class per forked class and per branch -------
230 tr.fjclassmap.assign(V.classes.size() + 1, 0);
231 tr.fjforkmap.assign(V.classes.size() + 1, 0);
232 tr.fanout.assign(V.classes.size() + 1, 0.0);
233 tr.auxdisabled.assign(V.classes.size() + 1, false);
234 tr.auxbranch.assign(V.classes.size() + 1, 0);
235
236 for (std::size_t fa = 0; fa < tr.forks.size(); ++fa) {
237 const typename FjMmt<T>::ForkRec& F = tr.forks[fa];
238 const std::size_t adstat = tr.auxDelayStation[F.joinNode];
239 const std::size_t adnode = tr.auxDelayNode[F.joinNode];
240
241 // the classes that reach this fork, i.e. `Vnodes(f,:) > 0`
242 std::vector<bool> forked(L.nclasses + 1, false);
243 for (std::size_t c = 0; c < L.nchains; ++c)
244 for (std::size_t r = 1; r <= L.nclasses; ++r)
245 if (L.nodevisits[c](F.node - 1, r - 1) > zero) forked[r] = true;
246
247 for (std::size_t c = 0; c < L.nchains; ++c) {
248 const std::vector<std::size_t>& ic = L.inchain[c];
249 bool any = false;
250 for (std::size_t x = 0; x < ic.size(); ++x)
251 if (forked[ic[x]]) any = true;
252 if (!any) continue;
253
254 // aux[r] holds this chain's auxiliary classes for original class r,
255 // in branch order; empty for a class that does not reach the fork.
256 std::vector<std::vector<std::size_t>> aux(L.nclasses + 1);
257 for (std::size_t x = 0; x < ic.size(); ++x) {
258 const std::size_t r = ic[x];
259 if (!(L.nodevisits[c](F.node - 1, r - 1) > zero)) continue;
260 if (std::isinf(L.classes[r - 1].population))
261 throw UnsupportedError(
262 "fj_ht: class '" + L.classes[r - 1].name + "' of model '" + L.name +
263 "' is open and reaches a fork; the H-T method can be used only on closed "
264 "models, use fork_join='mmt'");
265 for (std::size_t par = 1; par <= F.origfanout[r]; ++par) {
266 qn::JobClass xc;
267 xc.name = V.classes[r - 1].name + "." + V.nodes[F.node - 1].name + ".B" +
268 std::to_string(par);
269 xc.type = JobClassType::CLOSED;
270 // tasksPerLink * population, and tasksPerLink is 1 here: a
271 // fork emitting more was refused above.
272 xc.population = F.fanOut * L.classes[r - 1].population;
273 xc.refstat = adstat;
274 xc.completes = true;
275 xc.is_ref_class = false;
276 xc.attr_kind = -1;
277 xc.attr_idx = 0;
278 xc.prio = 0; // `ClosedClass(..., 0)` of the reference
279 const std::size_t s = V.add_class(xc);
280
281 // The service demand of the original class, at the BASE
282 // stations only: a Join is Immediate (the driver overwrites
283 // it with the residual synchronisation delay), a Source or a
284 // Fork is a no-op, and every OTHER auxiliary delay is left
285 // disabled, which is where the reference's `1:sn.nnodes`
286 // over the BASE struct leaves it.
287 for (std::size_t i = 1; i <= L.stations.size(); ++i) {
288 const NodeType nt = L.stations[i - 1].nodetype;
289 if (nt == NodeType::Join) {
291 } else if (nt == NodeType::Source || nt == NodeType::Fork) {
292 // no-op
293 } else {
294 V.set_service(i, s, L.service[i - 1][r - 1]);
295 }
296 }
297 V.set_service(adstat, s, Distrib<T>::immediate());
298
299 tr.fjclassmap.resize(V.classes.size() + 1, 0);
300 tr.fjforkmap.resize(V.classes.size() + 1, 0);
301 tr.fanout.resize(V.classes.size() + 1, 0.0);
302 tr.auxdisabled.resize(V.classes.size() + 1, false);
303 tr.auxbranch.resize(V.classes.size() + 1, 0);
304 tr.fjclassmap[s] = r;
305 tr.fjforkmap[s] = fa;
306 tr.fanout[s] = static_cast<double>(F.origfanout[r]) * F.fanOut;
307 tr.auxbranch[s] = par;
308 tr.auxclasses.push_back(s);
309 aux[r].push_back(s);
310 }
311 }
312
313 // ---- the routing of the auxiliary classes, and of the originals --
314 for (std::size_t x = 0; x < ic.size(); ++x) {
315 const std::size_t r = ic[x];
316 if (aux[r].empty()) continue;
317 for (std::size_t y = 0; y < ic.size(); ++y) {
318 const std::size_t s = ic[y];
319 if (aux[s].empty()) continue;
320 for (std::size_t par = 1; par <= aux[r].size() && par <= aux[s].size(); ++par) {
321 const std::size_t a = aux[r][par - 1], b = aux[s][par - 1];
322 detail::fj_copy_block(V, r, s, a, b);
323 // the auxiliary token takes ITS OWN branch, waits at the
324 // join, and returns to the fork through the auxiliary
325 // delay that carries the rest of the original's cycle
326 detail::fj_clear_row(V, a, b, F.node);
327 V.set_route(a, b, F.node, heads[fa][r][par - 1], one);
328 V.set_route(a, b, F.joinNode, adnode, one);
329 detail::fj_clear_row(V, a, b, adnode);
330 V.set_route(a, b, adnode, F.node, one);
331 }
332 // The original class is routed straight to the join, so it
333 // does not interfere with the auxiliary tokens on the
334 // branches; the whole span is charged to it as the join's
335 // own service time.
336 detail::fj_clear_row(V, r, s, F.node);
337 V.set_route(r, s, F.node, F.joinNode, one);
338 }
339 }
340 }
341 }
342
343 tr.fjclassmap.resize(V.classes.size() + 1, 0);
344 tr.fjforkmap.resize(V.classes.size() + 1, 0);
345 tr.fanout.resize(V.classes.size() + 1, 0.0);
346 tr.auxdisabled.resize(V.classes.size() + 1, false);
347 tr.auxbranch.resize(V.classes.size() + 1, 0);
348
349 // `outer` and `parent` are read by the MMT arm of the driver only, but they
350 // are filled here too so that a record built by either transform answers the
351 // same questions. A fork whose span holds another fork is inner on that
352 // class, exactly as in `fj_mmt`.
355
356 // The effective routing has to be re-derived, not just the chains: every
357 // edit above is written through `set_route`, i.e. into `P`, while every
358 // consumer reads `route_eff`, which returns `Peff` whenever `Peff` is
359 // non-empty. See the same note in `fj_mmt`.
360 V.refresh_routing();
361 V.refresh_chains();
362 // And the capacities, for the reason spelled out at the tail of `fj_mmt`:
363 // this transform adds a delay station and a block of auxiliary classes, so
364 // the copied `cap`/`classcap` no longer span the stations `buffer_size`
365 // indexes.
367 return tr;
368}
369
370/**
371 * `options.config.fork_join` -> the transform it names.
372 *
373 * The reference spells the same switch twice (`fjFixedPoint.m:47` and `:130`);
374 * this port resolves it once, at the point the transform is built. An unknown
375 * name is refused rather than silently taking the default, since answering with
376 * the MMT transform under another method's name is the one failure mode a
377 * method switch must not have.
378 */
379template <class T>
380FjMmt<T> fj_fork_join_transform(const qn::NetworkStruct<T>& L, const std::string& method) {
381 if (method.empty() || method == "default" || method == "mmt" || method == "fjt")
382 return fj_mmt(L);
383 if (method == "ht" || method == "heidelberger-trivedi") return fj_ht(L);
384 throw InputError("fork-join: '" + method +
385 "' is not a fork-join method; options.config.fork_join is one of "
386 "'default', 'mmt', 'fjt' (the MMT transform) or 'ht', "
387 "'heidelberger-trivedi'");
388}
389
390} // namespace mva
391} // namespace line
392
393#endif // LINE_SOLVERS_MVA_FJ_HT_H
InputError(const std::string &what)
Definition error.h:39
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.
T get_route(std::size_t r, std::size_t s, std::size_t i, std::size_t j) const
P{r,s}(i,j), AS THE USER SET IT.
std::vector< Matrix< T > > nodevisits
(nchains) each (nnodes x nclasses)
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
void refresh_chains()
Port of MNetwork.refreshChains followed by sn_refresh_visits.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
void set_service(std::size_t station, std::size_t cls, const Distrib< T > &d)
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< NodeDef > nodes
every node, in creation order
std::vector< std::pair< std::size_t, std::size_t > > fj
fj(f,j): the Join node j that closes the Fork node f, 1-based.
std::size_t add_station(const Station< T > &st)
Add a station, which is also a node, and grow the service table.
std::vector< std::size_t > station_to_node
(nstations) 1-based node index
void refresh_routing()
Port of the part of MNetwork.refreshRoutingMatrix this port reaches: the expansion of a routing STRAT...
The exception types the port throws.
The fork-join transform SolverMVA applies before solving a layer that contains a Fork.
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
void fj_widen_csmatrix(FjMmt< T > &tr)
Widen every explicit ClassSwitch matrix to the auxiliary-expanded class set.
Definition fj_mmt.h:407
FjMmt< T > fj_fork_join_transform(const qn::NetworkStruct< T > &L, const std::string &method)
options.config.fork_join -> the transform it names.
Definition fj_ht.h:380
FjMmt< T > fj_mmt(const qn::NetworkStruct< T > &L)
Build the transformed layer.
Definition fj_mmt.h:444
FjMmt< T > fj_ht(const qn::NetworkStruct< T > &L)
Build the H-T transform of L.
Definition fj_ht.h:116
void fj_sort_forks(FjMmt< T > &tr)
Port of ModelAdapter.sortForks: fill in outer and parent on every fork record.
Definition fj_mmt.h:357
A queueing network and its refreshed NetworkStruct.
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
One record per Fork of the base layer, ascending in node index.
Definition fj_mmt.h:115
std::size_t node
1-based node index, shared with the base layer
Definition fj_mmt.h:116
std::size_t joinNode
0 when the fork has no join
Definition fj_mmt.h:117
double fanOut
sn.nodeparam{f}.fanOut, which MATLAB sets to the fork's tasksPerLink and NOT to its number of output ...
Definition fj_mmt.h:126
std::vector< std::size_t > origfanout
(nclasses+1) the fork's number of output links for each class.
Definition fj_mmt.h:128
The transformed layer and the bookkeeping the fixed point needs to drive it and to merge its results ...
Definition fj_mmt.h:113
One job class of the network.
std::size_t refstat
1-based reference station
bool completes
Whether passage through the reference station is a COMPLETION.
int attr_kind
LayeredNetworkElement of the element it stands for.
std::size_t attr_idx
index of that element
double population
infinite for an open class
bool is_ref_class
marks the chain's reference class
One station of the network.
SchedStrategy sched
double nservers
may be infinite (a Delay, or an inf-scheduled task)