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 <string>
29#include <vector>
30
33#include "line/util/error.h"
34
35namespace line {
36namespace qn {
37template <class T>
38class Network;
39}
40
41namespace io {
42
43namespace detail {
44
45inline bool qn2lqn_routing_node(qn::NodeType t) {
46 return t == qn::NodeType::Queue || t == qn::NodeType::Delay ||
47 t == qn::NodeType::ClassSwitch || t == qn::NodeType::Router ||
48 t == qn::NodeType::Logger || t == qn::NodeType::Fork ||
49 t == qn::NodeType::Join;
50}
51
52inline std::string qn2lqn_indexed(const char* prefix, std::size_t i, std::size_t r) {
53 return std::string(prefix) + std::to_string(i + 1) + "_" + std::to_string(r + 1);
54}
55
56inline std::string qn2lqn_pseudo(const char* prefix, std::size_t c, std::size_t i,
57 std::size_t r) {
58 return std::string(prefix) + "_" + std::to_string(c + 1) + "_" +
59 std::to_string(i + 1) + "_" + std::to_string(r + 1);
60}
61
62} // namespace detail
63
64/** Port of MATLAB `QN2LQN(model)`, over the refreshed C++ NetworkStruct. */
65template <class T>
67 typedef num_traits<T> nt;
68 const T one = nt::from_int(1);
69 const std::size_t I = sn.nodes.size();
70 const std::size_t K = sn.nclasses;
71 const std::size_t C = sn.nchains;
72
73 if (I == 0 || K == 0 || C == 0)
74 throw InputError("QN2LQN: the queueing network has no closed chain to convert");
75 if (sn.nodevisits.size() != C || sn.inchain.size() != C)
76 throw InputError("QN2LQN: the NetworkStruct visits or chains are not refreshed");
77 for (std::size_t r = 0; r < K; ++r)
78 if (!std::isfinite(sn.classes[r].population))
79 throw UnsupportedError(
80 "QN2LQN: open classes are not supported; SolverQNS sends open models to "
81 "qnsolver directly");
82
83 std::vector<std::size_t> chain_of(K, C);
84 for (std::size_t c = 0; c < C; ++c)
85 for (std::size_t x = 0; x < sn.inchain[c].size(); ++x) {
86 const std::size_t r1 = sn.inchain[c][x];
87 if (r1 == 0 || r1 > K)
88 throw InputError("QN2LQN: chain " + std::to_string(c + 1) +
89 " contains an out-of-range class index");
90 if (chain_of[r1 - 1] != C)
91 throw InputError("QN2LQN: class " + std::to_string(r1) +
92 " belongs to more than one chain");
93 chain_of[r1 - 1] = c;
94 }
95 for (std::size_t r = 0; r < K; ++r)
96 if (chain_of[r] == C)
97 throw InputError("QN2LQN: class " + std::to_string(r + 1) +
98 " belongs to no chain");
99
101 const std::string pseudo_host = sn.name.empty() ? std::string("QN") : sn.name;
102 b.processor(pseudo_host, std::numeric_limits<double>::infinity(), lang::SchedStrategy::INF);
103
104 std::vector<std::string> ref_task(C), ref_entry(C);
105 for (std::size_t c = 0; c < C; ++c) {
106 double jobs = 0.0;
107 for (std::size_t x = 0; x < sn.inchain[c].size(); ++x)
108 jobs += sn.classes[sn.inchain[c][x] - 1].population;
109 ref_task[c] = "RefTask_" + std::to_string(c + 1);
110 ref_entry[c] = "Chain_" + std::to_string(c + 1);
111 b.task(ref_task[c], jobs, lang::SchedStrategy::REF, pseudo_host);
112 b.entry(ref_entry[c], ref_task[c]);
113 }
114
115 // MATLAB's cell arrays E/A/PA are represented by empty names for absent
116 // elements. A name is sufficient because LqnBuilder resolves handles by
117 // name at finalization.
118 std::vector<std::string> entries(I * K), service_acts(I * K);
119 const auto ir = [K](std::size_t i, std::size_t r) { return i * K + r; };
120 const auto cir = [I, K](std::size_t c, std::size_t i, std::size_t r) {
121 return (c * I + i) * K + r;
122 };
123
124 for (std::size_t i = 0; i < I; ++i) {
125 const qn::NodeDef& nd = sn.nodes[i];
126 if (nd.nodetype != qn::NodeType::Queue && nd.nodetype != qn::NodeType::Delay)
127 continue;
128 if (nd.station == 0 || nd.station > sn.stations.size())
129 throw InputError("QN2LQN: service node '" + nd.name + "' has no station row");
130 const std::size_t ist = nd.station - 1;
131 b.processor(nd.name, sn.stations[ist].nservers, sn.stations[ist].sched);
132 const std::string task = "T_" + nd.name;
133 b.task(task, std::numeric_limits<double>::infinity(), lang::SchedStrategy::INF, nd.name);
134 for (std::size_t r = 0; r < K; ++r) {
135 const std::size_t c = chain_of[r];
136 if (sn.nodevisits[c].rows() != I || sn.nodevisits[c].cols() != K)
137 throw InputError("QN2LQN: nodevisits has the wrong dimensions for chain " +
138 std::to_string(c + 1));
139 if (!(nt::to_double(sn.nodevisits[c](i, r)) > 0.0)) continue;
140 if (ist >= sn.service.size() || r >= sn.service[ist].size() ||
141 sn.service[ist][r].disabled)
142 throw InputError("QN2LQN: visited station '" + nd.name + "', class '" +
143 sn.classes[r].name + "' has no service distribution");
144 entries[ir(i, r)] = detail::qn2lqn_indexed("E", i, r);
145 service_acts[ir(i, r)] = detail::qn2lqn_indexed("Q", i, r);
146 b.entry(entries[ir(i, r)], task);
147 b.activity(service_acts[ir(i, r)], sn.service[ist][r], task);
148 b.bound_to(service_acts[ir(i, r)], entries[ir(i, r)]);
149 b.replies_to(service_acts[ir(i, r)], entries[ir(i, r)]);
150 }
151 }
152
153 std::vector<std::string> pseudo(C * I * K);
154 std::vector<bool> bound(C, false);
155 std::vector<std::size_t> bound_i(C, 0), bound_r(C, 0);
156 const auto incoming = [&](std::size_t i, std::size_t r) {
157 const std::size_t col = i * K + r;
158 if (sn.rtnodes.cols() <= col) return false;
159 for (std::size_t row = 0; row < sn.rtnodes.rows(); ++row)
160 if (nt::to_double(sn.rtnodes(row, col)) > 0.0) return true;
161 return false;
162 };
163
164 for (std::size_t i = 0; i < I; ++i) {
165 const qn::NodeType type = sn.nodes[i].nodetype;
166 if (type == qn::NodeType::ClassSwitch || type == qn::NodeType::Router ||
167 type == qn::NodeType::Logger || type == qn::NodeType::Fork ||
168 type == qn::NodeType::Join) {
169 const char* prefix = (type == qn::NodeType::Fork || type == qn::NodeType::Join)
170 ? "FJ"
171 : "CS";
172 for (std::size_t r = 0; r < K; ++r) {
173 if (!incoming(i, r)) continue;
174 const std::size_t c = chain_of[r];
175 pseudo[cir(c, i, r)] = detail::qn2lqn_pseudo(prefix, c, i, r);
176 b.activity(pseudo[cir(c, i, r)], lang::Distrib<T>::immediate(), ref_task[c]);
177 }
178 } else if (type == qn::NodeType::Queue || type == qn::NodeType::Delay) {
179 for (std::size_t r = 0; r < K; ++r) {
180 const std::size_t c = chain_of[r];
181 if (service_acts[ir(i, r)].empty()) continue;
182 pseudo[cir(c, i, r)] = detail::qn2lqn_indexed("A", i, r);
183 b.activity(pseudo[cir(c, i, r)], lang::Distrib<T>::immediate(), ref_task[c]);
184 const std::size_t first_r = sn.inchain[c].front() - 1;
185 const std::size_t refstat = sn.classes[first_r].refstat;
186 if (refstat == 0 || refstat > sn.station_to_node.size())
187 throw InputError("QN2LQN: chain " + std::to_string(c + 1) +
188 " has an invalid reference station");
189 const std::size_t refnode = sn.station_to_node[refstat - 1] - 1;
190 if (i == refnode && r == first_r) {
191 b.bound_to(pseudo[cir(c, i, r)], ref_entry[c]);
192 bound[c] = true;
193 bound_i[c] = i;
194 bound_r[c] = r;
195 }
196 b.sync_call(pseudo[cir(c, i, r)], entries[ir(i, r)], one);
197 }
198 } else if (type != qn::NodeType::Source && type != qn::NodeType::Sink) {
199 throw UnsupportedError("QN2LQN: node '" + sn.nodes[i].name + "' has type " +
200 std::string(lang::node_type_to_text(type)) +
201 ", which the MATLAB conversion does not map");
202 }
203 }
204 for (std::size_t c = 0; c < C; ++c)
205 if (!bound[c])
206 throw InputError("QN2LQN: chain " + std::to_string(c + 1) +
207 " has no visited reference station to bind to its entry");
208
209 for (std::size_t c = 0; c < C; ++c) {
210 for (std::size_t i = 0; i < I; ++i) {
211 if (!detail::qn2lqn_routing_node(sn.nodes[i].nodetype)) continue;
212 for (std::size_t rx = 0; rx < sn.inchain[c].size(); ++rx) {
213 const std::size_t r = sn.inchain[c][rx] - 1;
214 const std::string& pre = pseudo[cir(c, i, r)];
215 if (pre.empty() || !incoming(i, r)) continue;
216 std::vector<std::string> posts;
217 std::vector<T> probs;
218 for (std::size_t j = 0; j < I; ++j) {
219 if (!detail::qn2lqn_routing_node(sn.nodes[j].nodetype) ||
220 sn.nodes[j].nodetype == qn::NodeType::Join)
221 continue;
222 for (std::size_t sx = 0; sx < sn.inchain[c].size(); ++sx) {
223 const std::size_t s = sn.inchain[c][sx] - 1;
224 const T pr = sn.rtnodes(i * K + r, j * K + s);
225 if (!(nt::to_double(pr) > 0.0)) continue;
226 if (j == bound_i[c] && s == bound_r[c]) {
227 const std::string end = detail::qn2lqn_pseudo("End", c, i, r);
228 b.activity(end, lang::Distrib<T>::immediate(), ref_task[c]);
229 posts.push_back(end);
230 } else {
231 const std::string& post = pseudo[cir(c, j, s)];
232 if (post.empty())
233 throw InputError("QN2LQN: route from node '" + sn.nodes[i].name +
234 "' reaches a node/class with no pseudo-activity");
235 posts.push_back(post);
236 }
237 probs.push_back(pr);
238 }
239 }
240 if (posts.empty()) continue;
241 if (sn.nodes[i].nodetype == qn::NodeType::Fork)
242 b.and_fork(pre, posts);
243 else
244 b.or_fork(pre, posts, probs);
245 }
246 }
247 }
248
249 // Join destinations were deliberately omitted above. They are the post
250 // side of an AND join (or a serial edge for a degenerate one-input join).
251 for (std::size_t c = 0; c < C; ++c)
252 for (std::size_t j = 0; j < I; ++j) {
253 if (sn.nodes[j].nodetype != qn::NodeType::Join) continue;
254 for (std::size_t sx = 0; sx < sn.inchain[c].size(); ++sx) {
255 const std::size_t s = sn.inchain[c][sx] - 1;
256 const std::string& post = pseudo[cir(c, j, s)];
257 if (post.empty()) continue;
258 std::vector<std::string> pres;
259 for (std::size_t i = 0; i < I; ++i) {
260 if (!detail::qn2lqn_routing_node(sn.nodes[i].nodetype)) continue;
261 for (std::size_t rx = 0; rx < sn.inchain[c].size(); ++rx) {
262 const std::size_t r = sn.inchain[c][rx] - 1;
263 if (nt::to_double(sn.rtnodes(i * K + r, j * K + s)) <= 0.0) continue;
264 const std::string& pre = pseudo[cir(c, i, r)];
265 if (!pre.empty()) pres.push_back(pre);
266 }
267 }
268 if (pres.size() > 1)
269 b.and_join(pres, post);
270 else if (pres.size() == 1)
271 b.serial(pres.front(), post);
272 }
273 }
274
275 // Finalize once here as a structural validation: every entry must have a
276 // bound activity and every precedence/call target must exist.
277 (void)b.build();
278 return b.model();
279}
280
281/** MATLAB-compatible model-level entry point; refreshes the Network first. */
282template <class T>
284 return qn2lqn(model.get_struct());
285}
286
287} // namespace io
288} // namespace line
289
290#endif // LINE_IO_QN2LQN_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
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.
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:61
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:66
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:341
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
A queueing network and its refreshed NetworkStruct.
static Distrib immediate()
The Immediate singleton.
Definition lang_types.h:846
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:399
A node of the network.