LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qn2lqn.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_IO_QN2LQN_H
6#define LINE_IO_QN2LQN_H
7
8/**
9 * @file
10 * @ingroup line_io
11 * Convert a closed queueing network into the layered model used by SolverLQNS.
12 *
13 * Port of matlab/src/io/QN2LQN.m. A chain becomes a reference task, every
14 * Queue or Delay becomes an infinite-multiplicity server task, and the node
15 * routing becomes activity precedences on the reference task. Forks use an
16 * AND fork and Joins an AND join; the other routing nodes use probabilistic OR
17 * forks.
18 *
19 * C++ keeps visits on two explicit index spaces. The MATLAB routine indexes
20 * `sn.visits` with a node index, while this port uses `nodevisits`, the
21 * node-indexed twin, so a Router/ClassSwitch before a station cannot shift the
22 * service activity onto the wrong row.
23 */
24
25#include <cmath>
26#include <cstddef>
27#include <limits>
28#include <map>
29#include <string>
30#include <vector>
31
34#include "line/util/error.h"
35
36namespace line {
37namespace qn {
38template <class T>
39class Network;
40}
41
42namespace io {
43
44namespace detail {
45
46inline bool qn2lqn_routing_node(qn::NodeType t) {
47 return t == qn::NodeType::Queue || t == qn::NodeType::Delay ||
48 t == qn::NodeType::ClassSwitch || t == qn::NodeType::Router ||
49 t == qn::NodeType::Logger || t == qn::NodeType::Fork ||
50 t == qn::NodeType::Join;
51}
52
53inline std::string qn2lqn_indexed(const char* prefix, std::size_t i, std::size_t r) {
54 return std::string(prefix) + std::to_string(i + 1) + "_" + std::to_string(r + 1);
55}
56
57inline std::string qn2lqn_pseudo(const char* prefix, std::size_t c, std::size_t i,
58 std::size_t r) {
59 return std::string(prefix) + "_" + std::to_string(c + 1) + "_" +
60 std::to_string(i + 1) + "_" + std::to_string(r + 1);
61}
62
63} // namespace detail
64
65/** Port of MATLAB `QN2LQN(model)`, over the refreshed C++ NetworkStruct. */
66template <class T>
68 typedef num_traits<T> nt;
69 const T one = nt::from_int(1);
70 const std::size_t I = sn.nodes.size();
71 const std::size_t K = sn.nclasses;
72 const std::size_t C = sn.nchains;
73
74 if (I == 0 || K == 0 || C == 0)
75 throw InputError("QN2LQN: the queueing network has no closed chain to convert");
76 if (sn.nodevisits.size() != C || sn.inchain.size() != C)
77 throw InputError("QN2LQN: the NetworkStruct visits or chains are not refreshed");
78 for (std::size_t r = 0; r < K; ++r)
79 if (!std::isfinite(sn.classes[r].population))
80 throw UnsupportedError(ErrorKind{"OpenClass"},
81 "QN2LQN: open classes are not supported; SolverLQNS sends open Networks to "
82 "qnsolver directly");
83
84 std::vector<std::size_t> chain_of(K, C);
85 for (std::size_t c = 0; c < C; ++c)
86 for (std::size_t x = 0; x < sn.inchain[c].size(); ++x) {
87 const std::size_t r1 = sn.inchain[c][x];
88 if (r1 == 0 || r1 > K)
89 throw InputError("QN2LQN: chain " + std::to_string(c + 1) +
90 " contains an out-of-range class index");
91 if (chain_of[r1 - 1] != C)
92 throw InputError("QN2LQN: class " + std::to_string(r1) +
93 " belongs to more than one chain");
94 chain_of[r1 - 1] = c;
95 }
96 for (std::size_t r = 0; r < K; ++r)
97 if (chain_of[r] == C)
98 throw InputError("QN2LQN: class " + std::to_string(r + 1) +
99 " belongs to no chain");
100
102 const std::string pseudo_host = sn.name.empty() ? std::string("QN") : sn.name;
103 b.processor(pseudo_host, std::numeric_limits<double>::infinity(), lang::SchedStrategy::INF);
104
105 std::vector<std::string> ref_task(C), ref_entry(C);
106 for (std::size_t c = 0; c < C; ++c) {
107 double jobs = 0.0;
108 for (std::size_t x = 0; x < sn.inchain[c].size(); ++x)
109 jobs += sn.classes[sn.inchain[c][x] - 1].population;
110 ref_task[c] = "RefTask_" + std::to_string(c + 1);
111 ref_entry[c] = "Chain_" + std::to_string(c + 1);
112 b.task(ref_task[c], jobs, lang::SchedStrategy::REF, pseudo_host);
113 b.entry(ref_entry[c], ref_task[c]);
114 }
115
116 // MATLAB's cell arrays E/A/PA are represented by empty names for absent
117 // elements. A name is sufficient because LqnBuilder resolves handles by
118 // name at finalization.
119 std::vector<std::string> entries(I * K), service_acts(I * K);
120 const auto ir = [K](std::size_t i, std::size_t r) { return i * K + r; };
121 const auto cir = [I, K](std::size_t c, std::size_t i, std::size_t r) {
122 return (c * I + i) * K + r;
123 };
124
125 for (std::size_t i = 0; i < I; ++i) {
126 const qn::NodeDef& nd = sn.nodes[i];
127 if (nd.nodetype != qn::NodeType::Queue && nd.nodetype != qn::NodeType::Delay)
128 continue;
129 if (nd.station == 0 || nd.station > sn.stations.size())
130 throw InputError("QN2LQN: service node '" + nd.name + "' has no station row");
131 const std::size_t ist = nd.station - 1;
132 b.processor(nd.name, sn.stations[ist].nservers, sn.stations[ist].sched);
133 const std::string task = "T_" + nd.name;
134 b.task(task, std::numeric_limits<double>::infinity(), lang::SchedStrategy::INF, nd.name);
135 for (std::size_t r = 0; r < K; ++r) {
136 const std::size_t c = chain_of[r];
137 if (sn.nodevisits[c].rows() != I || sn.nodevisits[c].cols() != K)
138 throw InputError("QN2LQN: nodevisits has the wrong dimensions for chain " +
139 std::to_string(c + 1));
140 if (!(nt::to_double(sn.nodevisits[c](i, r)) > 0.0)) continue;
141 if (ist >= sn.service.size() || r >= sn.service[ist].size() ||
142 sn.service[ist][r].disabled)
143 throw InputError("QN2LQN: visited station '" + nd.name + "', class '" +
144 sn.classes[r].name + "' has no service distribution");
145 entries[ir(i, r)] = detail::qn2lqn_indexed("E", i, r);
146 service_acts[ir(i, r)] = detail::qn2lqn_indexed("Q", i, r);
147 b.entry(entries[ir(i, r)], task);
148 b.activity(service_acts[ir(i, r)], sn.service[ist][r], task);
149 b.bound_to(service_acts[ir(i, r)], entries[ir(i, r)]);
150 b.replies_to(service_acts[ir(i, r)], entries[ir(i, r)]);
151 }
152 }
153
154 std::vector<std::string> pseudo(C * I * K);
155 std::vector<bool> bound(C, false);
156 std::vector<std::size_t> bound_i(C, 0), bound_r(C, 0);
157 const auto incoming = [&](std::size_t i, std::size_t r) {
158 const std::size_t col = i * K + r;
159 if (sn.rtnodes.cols() <= col) return false;
160 for (std::size_t row = 0; row < sn.rtnodes.rows(); ++row)
161 if (nt::to_double(sn.rtnodes(row, col)) > 0.0) return true;
162 return false;
163 };
164
165 for (std::size_t i = 0; i < I; ++i) {
166 const qn::NodeType type = sn.nodes[i].nodetype;
167 if (type == qn::NodeType::ClassSwitch || type == qn::NodeType::Router ||
168 type == qn::NodeType::Logger || type == qn::NodeType::Fork ||
169 type == qn::NodeType::Join) {
170 const char* prefix = (type == qn::NodeType::Fork || type == qn::NodeType::Join)
171 ? "FJ"
172 : "CS";
173 for (std::size_t r = 0; r < K; ++r) {
174 if (!incoming(i, r)) continue;
175 const std::size_t c = chain_of[r];
176 pseudo[cir(c, i, r)] = detail::qn2lqn_pseudo(prefix, c, i, r);
177 b.activity(pseudo[cir(c, i, r)], lang::Distrib<T>::immediate(), ref_task[c]);
178 }
179 } else if (type == qn::NodeType::Queue || type == qn::NodeType::Delay) {
180 for (std::size_t r = 0; r < K; ++r) {
181 const std::size_t c = chain_of[r];
182 if (service_acts[ir(i, r)].empty()) continue;
183 pseudo[cir(c, i, r)] = detail::qn2lqn_indexed("A", i, r);
184 b.activity(pseudo[cir(c, i, r)], lang::Distrib<T>::immediate(), ref_task[c]);
185 const std::size_t first_r = sn.inchain[c].front() - 1;
186 const std::size_t refstat = sn.classes[first_r].refstat;
187 if (refstat == 0 || refstat > sn.station_to_node.size())
188 throw InputError("QN2LQN: chain " + std::to_string(c + 1) +
189 " has an invalid reference station");
190 const std::size_t refnode = sn.station_to_node[refstat - 1] - 1;
191 if (i == refnode && r == first_r) {
192 b.bound_to(pseudo[cir(c, i, r)], ref_entry[c]);
193 bound[c] = true;
194 bound_i[c] = i;
195 bound_r[c] = r;
196 }
197 b.sync_call(pseudo[cir(c, i, r)], entries[ir(i, r)], one);
198 }
199 } else if (type != qn::NodeType::Source && type != qn::NodeType::Sink) {
200 throw UnsupportedError(ErrorKind{"Export"}, "QN2LQN: node '" + sn.nodes[i].name + "' has type " +
201 std::string(lang::node_type_to_text(type)) +
202 ", which the MATLAB conversion does not map");
203 }
204 }
205 for (std::size_t c = 0; c < C; ++c)
206 if (!bound[c])
207 throw InputError("QN2LQN: chain " + std::to_string(c + 1) +
208 " has no visited reference station to bind to its entry");
209
210 // lqns admits an activity in ONE post list only, so the forks of a chain are
211 // collected first and a target named by several gets one Immediate stub per
212 // fork, OR-joined into it (as QN2LQN.m does).
213 struct ForkRec {
214 std::string pre;
215 std::vector<std::string> posts;
216 std::vector<T> probs;
217 bool is_and;
218 };
219 for (std::size_t c = 0; c < C; ++c) {
220 std::vector<ForkRec> forks;
221 for (std::size_t i = 0; i < I; ++i) {
222 if (!detail::qn2lqn_routing_node(sn.nodes[i].nodetype)) continue;
223 for (std::size_t rx = 0; rx < sn.inchain[c].size(); ++rx) {
224 const std::size_t r = sn.inchain[c][rx] - 1;
225 const std::string& pre = pseudo[cir(c, i, r)];
226 if (pre.empty() || !incoming(i, r)) continue;
227 std::vector<std::string> posts;
228 std::vector<T> probs;
229 for (std::size_t j = 0; j < I; ++j) {
230 if (!detail::qn2lqn_routing_node(sn.nodes[j].nodetype) ||
231 sn.nodes[j].nodetype == qn::NodeType::Join)
232 continue;
233 for (std::size_t sx = 0; sx < sn.inchain[c].size(); ++sx) {
234 const std::size_t s = sn.inchain[c][sx] - 1;
235 const T pr = sn.rtnodes(i * K + r, j * K + s);
236 if (!(nt::to_double(pr) > 0.0)) continue;
237 if (j == bound_i[c] && s == bound_r[c]) {
238 const std::string end = detail::qn2lqn_pseudo("End", c, i, r);
239 b.activity(end, lang::Distrib<T>::immediate(), ref_task[c]);
240 posts.push_back(end);
241 } else {
242 const std::string& post = pseudo[cir(c, j, s)];
243 if (post.empty())
244 throw InputError("QN2LQN: route from node '" + sn.nodes[i].name +
245 "' reaches a node/class with no pseudo-activity");
246 posts.push_back(post);
247 }
248 probs.push_back(pr);
249 }
250 }
251 if (posts.empty()) continue;
252 forks.push_back(ForkRec{pre, posts, probs, sn.nodes[i].nodetype == qn::NodeType::Fork});
253 }
254 }
255 std::map<std::string, std::size_t> nin;
256 for (const ForkRec& f : forks)
257 for (const std::string& t : f.posts) ++nin[t];
258 std::map<std::string, std::vector<std::string>> stubs;
259 for (std::size_t k = 0; k < forks.size(); ++k) {
260 ForkRec& f = forks[k];
261 for (std::string& t : f.posts) {
262 if (nin[t] < 2) continue;
263 const std::string stub = "In_" + t + "_" + std::to_string(k + 1);
264 b.activity(stub, lang::Distrib<T>::immediate(), ref_task[c]);
265 stubs[t].push_back(stub);
266 t = stub;
267 }
268 if (f.is_and)
269 b.and_fork(f.pre, f.posts);
270 else
271 b.or_fork(f.pre, f.posts, f.probs);
272 }
273 for (const auto& kv : stubs) b.or_join(kv.second, kv.first);
274 }
275
276 // Join destinations were deliberately omitted above. They are the post
277 // side of an AND join (or a serial edge for a degenerate one-input join).
278 for (std::size_t c = 0; c < C; ++c)
279 for (std::size_t j = 0; j < I; ++j) {
280 if (sn.nodes[j].nodetype != qn::NodeType::Join) continue;
281 for (std::size_t sx = 0; sx < sn.inchain[c].size(); ++sx) {
282 const std::size_t s = sn.inchain[c][sx] - 1;
283 const std::string& post = pseudo[cir(c, j, s)];
284 if (post.empty()) continue;
285 std::vector<std::string> pres;
286 for (std::size_t i = 0; i < I; ++i) {
287 if (!detail::qn2lqn_routing_node(sn.nodes[i].nodetype)) continue;
288 for (std::size_t rx = 0; rx < sn.inchain[c].size(); ++rx) {
289 const std::size_t r = sn.inchain[c][rx] - 1;
290 if (nt::to_double(sn.rtnodes(i * K + r, j * K + s)) <= 0.0) continue;
291 const std::string& pre = pseudo[cir(c, i, r)];
292 if (!pre.empty()) pres.push_back(pre);
293 }
294 }
295 if (pres.size() > 1)
296 b.and_join(pres, post);
297 else if (pres.size() == 1)
298 b.serial(pres.front(), post);
299 }
300 }
301
302 // Finalize once here as a structural validation: every entry must have a
303 // bound activity and every precedence/call target must exist.
304 (void)b.build();
305 return b.model();
306}
307
308/** MATLAB-compatible model-level entry point; refreshes the Network first. */
309template <class T>
311 return qn2lqn(model.get_struct());
312}
313
314} // namespace io
315} // namespace line
316
317#endif // LINE_IO_QN2LQN_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
void bound_to(const std::string &act, const std::string &entry_name)
Bind an activity to an entry: it is the entry's first activity.
std::size_t activity(const std::string &name, const Distrib< T > &hostdem, const std::string &on_task)
Add an activity on a task, with its host demand.
LqnStruct< T > build() const
Flatten into the struct SolverLN consumes.
std::size_t processor(const std::string &name, double mult, SchedStrategy sched, double repl=1.0)
Add a processor.
Definition lqn_builder.h:49
void replies_to(const std::string &act, const std::string &entry_name)
Mark an activity as the one that replies to an entry.
void and_fork(const std::string &pre, const std::vector< std::string > &posts)
pre -> every post, concurrently.
void and_join(const std::vector< std::string > &pres, const std::string &post, std::size_t quorum=0)
all pres (or quorum of them) -> post.
void serial(const std::string &pre, const std::string &post)
pre -> post, a plain sequence.
void sync_call(const std::string &act, const std::string &dest_entry, const T &mean)
A synchronous call from an activity to an entry of another task.
const LqnModel< T > & model() const
void or_fork(const std::string &pre, const std::vector< std::string > &posts, const std::vector< T > &probs)
pre -> one of the posts, with the given branch probabilities.
void or_join(const std::vector< std::string > &pres, const std::string &post)
any of the pres -> post.
std::size_t entry(const std::string &name, const std::string &on_task)
Add an entry on a task.
std::size_t task(const std::string &name, double mult, SchedStrategy sched, const std::string &on_processor, double repl=1.0)
Add a task on a processor.
Definition lqn_builder.h:66
A network plus its refreshed NetworkStruct.
A queueing network under construction.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
The exception types the port throws.
Build a layered queueing network in code, as the MATLAB constructors do.
lqn::LqnModel< T > qn2lqn(const qn::NetworkStruct< T > &sn)
Port of MATLAB QN2LQN(model), over the refreshed C++ NetworkStruct.
Definition qn2lqn.h:67
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:348
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
static Distrib immediate()
The point mass at zero.
Definition lang_types.h:986
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:411
A node of the network.