LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ln_zero_capacity.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_LN_LN_ZERO_CAPACITY_H
6#define LINE_SOLVERS_LN_LN_ZERO_CAPACITY_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Reference tasks trapped at a zero-capacity element of a layered network.
12 *
13 * Port of matlab/src/solvers/LN/ln_zero_capacity.m.
14 *
15 * A processor or a non-reference task of multiplicity 0 never serves a request.
16 * A reference task that reaches one through synchronous calls with positive
17 * probability ends, in steady state, with all of its users stuck on that call
18 * path (nested blocking): every throughput it drives is 0, the response times
19 * along the path are Inf, and each task, entry and activity on the path holds
20 * the requests stuck below it, the dead task holding those queued at it. A
21 * trapped request holds task threads only, never a processor, so the other
22 * reference tasks are solved as if the trapped ones were absent.
23 *
24 * Every vector is indexed by LQN element, 1-based, (nidx+1) long:
25 * dead zero-capacity processors and tasks
26 * trapped reference tasks whose users are all trapped
27 * onroute elements on a blocked call path
28 * held requests each element holds in the trapped state
29 * ignore elements no live reference task or open arrival reaches, to drop from the layers
30 * live true when some reference task or open arrival is still served
31 * reason non-empty when the trapped state is ambiguous or does not exist
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <cstddef>
37#include <cstdio>
38#include <limits>
39#include <string>
40#include <vector>
41
43#include "line/num/number.h"
44
45namespace line {
46namespace ln {
47
48/** The structural answer of ln_zero_capacity; see the file header. */
50 std::vector<bool> dead, trapped, onroute, ignore;
51 std::vector<double> held;
52 bool live = true;
53 std::string reason;
54
55 bool any_trapped() const {
56 for (bool b : trapped)
57 if (b) return true;
58 return false;
59 }
60};
61
62namespace zc_detail {
63
64inline std::string fmt_g(double v) {
65 if (std::isinf(v)) return v > 0 ? "Inf" : "-Inf";
66 char buf[64];
67 std::snprintf(buf, sizeof(buf), "%g", v);
68 return buf;
69}
70
71template <class T>
73public:
74 explicit ZeroCapacity(const lqn::LqnStruct<T>& l) : lqn(l), N(l.nidx) {
75 zc.dead.assign(N + 1, false);
76 zc.trapped.assign(N + 1, false);
77 zc.onroute.assign(N + 1, false);
78 zc.ignore.assign(N + 1, false);
79 zc.held.assign(N + 1, 0.0);
80 }
81
83 using lang::CallType;
84 using lang::LqnElement;
85 const std::size_t NT = lqn.tshift + lqn.ntasks;
86 bool anydead = false;
87 for (std::size_t idx = 1; idx <= NT; ++idx) {
88 zc.dead[idx] = lqn.mult[idx] == 0.0 && lqn.sched[idx] != lang::SchedStrategy::INF && !lqn.isref[idx];
89 anydead = anydead || zc.dead[idx];
90 }
91 if (!anydead) return zc;
92
93 // Entries from which a zero-capacity element is reached through synchronous calls.
94 // blockAct[e] is the activity a request to e is stuck at, next[e] the entry it waits
95 // on there, -1 when the activity waits for its dead processor, 0 when e's task is dead.
96 state.assign(N + 1, 0);
97 reach.assign(N + 1, false);
98 blockAct.assign(N + 1, 0);
99 next.assign(N + 1, 0);
100 for (std::size_t e = lqn.eshift + 1; e <= lqn.eshift + lqn.nentries; ++e) {
101 entry_route(e);
102 if (!zc.reason.empty()) return zc;
103 }
104
105 // Requests that no trapped state can absorb
106 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
107 const std::size_t tgt = lqn.callpair_dst[c];
108 if (lqn.calltype[c] == CallType::FWD && reach[tgt]) {
109 zc.reason = "Entry " + lqn.names[lqn.callpair_src[c]] + " forwards requests to " + lqn.names[tgt] +
110 ", which reaches a zero-capacity element: forwarding into a blocked call path is "
111 "not supported.";
112 return zc;
113 }
114 }
115 std::vector<bool> arv(N + 1, false);
116 for (std::size_t e = lqn.eshift + 1; e <= lqn.eshift + lqn.nentries && e < lqn.has_arrival.size(); ++e) {
117 arv[e] = lqn.has_arrival[e];
118 if (arv[e] && reach[e]) {
119 zc.reason = "Entry " + lqn.names[e] + " has an open arrival stream and reaches a zero-capacity "
120 "element, where its requests accumulate without bound: the model has no steady state.";
121 return zc;
122 }
123 }
124
125 // Trapped reference tasks and the requests held along their call paths
126 std::vector<std::size_t> refs;
127 for (std::size_t t = 1; t <= lqn.ntasks; ++t)
128 if (lqn.isref[lqn.tshift + t]) refs.push_back(lqn.tshift + t);
129 std::vector<double> incoming(N + 1, 0.0), nroutes(N + 1, 0.0);
130 for (std::size_t t : refs) {
131 std::vector<std::size_t> ents;
132 for (std::size_t e : lqn.entriesof[t])
133 if (reach[e]) ents.push_back(e);
134 if (ents.empty()) continue;
135 if (ents.size() > 1) {
136 zc.reason = "Reference task " + lqn.names[t] + " reaches a zero-capacity element from " +
137 std::to_string(ents.size()) +
138 " of its entries: how its users divide between the blocked paths depends on the "
139 "initial state.";
140 return zc;
141 }
142 zc.trapped[t] = true;
143 double h = lqn.mult[t];
144 mark(t, h);
145 std::size_t e = ents[0];
146 bool first = true;
147 while (true) {
148 const std::size_t tt = lqn.parent[e];
149 if (zc.dead[tt]) {
150 mark(e, h); // requests queued at a task with no threads
151 mark(tt, h);
152 break;
153 }
154 if (!first) {
155 incoming[tt] += h;
156 nroutes[tt] += 1.0;
157 h = std::min(h, task_cap(tt));
158 mark(tt, h);
159 }
160 first = false;
161 mark(e, h);
162 mark(blockAct[e], h);
163 if (next[e] < 0) {
164 zc.onroute[lqn.parent[tt]] = true; // its dead processor
165 break;
166 }
167 e = std::size_t(next[e]);
168 }
169 }
170 for (std::size_t tt = 1; tt <= N; ++tt)
171 if (nroutes[tt] > 1.0 && incoming[tt] > task_cap(tt)) {
172 zc.reason = "Task " + lqn.names[tt] + " has " + fmt_g(lqn.mult[tt]) +
173 " threads and is on the blocked call paths of several reference tasks that send it " +
174 fmt_g(incoming[tt]) + " requests: which of them hold its threads depends on the "
175 "initial state.";
176 return zc;
177 }
178 for (std::size_t idx = 1; idx <= NT; ++idx)
179 if (zc.onroute[idx] && lqn.repl[idx] > 1.0) {
180 zc.reason = lqn.names[idx] +
181 " is replicated and on a call path blocked at a zero-capacity element, which is not "
182 "supported.";
183 return zc;
184 }
185
186 // What the reference tasks that are not trapped, and the open arrivals, still reach
187 std::vector<std::size_t> starts;
188 for (std::size_t t : refs)
189 if (!zc.trapped[t]) starts.push_back(t);
190 for (std::size_t e = 1; e <= N; ++e)
191 if (arv[e]) starts.push_back(e);
192 const std::vector<bool> live = reach_from(starts);
193 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
194 const std::size_t tgt = lqn.callpair_dst[c];
195 if (lqn.calltype[c] == CallType::ASYNC && live[lqn.callpair_src[c]] &&
196 (reach[tgt] || zc.dead[lqn.parent[tgt]])) {
197 zc.reason = "Activity " + lqn.names[lqn.callpair_src[c]] + " calls " + lqn.names[tgt] +
198 " asynchronously, which reaches a zero-capacity element where its requests accumulate "
199 "without bound: the model has no steady state.";
200 return zc;
201 }
202 }
203 for (std::size_t idx = 1; idx <= N; ++idx)
204 if (live[idx] && zc.dead[idx]) {
205 zc.reason = lqn.names[idx] +
206 " has zero capacity and is reached by a reference task that is not trapped there, "
207 "through a call that cannot block it.";
208 return zc;
209 }
210 for (std::size_t tt = lqn.tshift + 1; tt <= NT; ++tt)
211 if (live[tt] && zc.held[tt] > 0.0 && std::isfinite(task_cap(tt)) && !lqn.isref[tt]) {
212 zc.reason = "Task " + lqn.names[tt] +
213 " is reached by a reference task that is not trapped, while " + fmt_g(zc.held[tt]) +
214 " of its " + fmt_g(lqn.mult[tt]) +
215 " threads are held forever by requests blocked at a zero-capacity element.";
216 return zc;
217 }
218 zc.live = false;
219 for (std::size_t i = 1; i <= N; ++i) zc.live = zc.live || live[i];
220
221 // Drop from the layers the processors and tasks only the trapped part reaches
222 std::vector<std::size_t> tstarts;
223 for (std::size_t t : refs)
224 if (zc.trapped[t]) tstarts.push_back(t);
225 const std::vector<bool> fromTrapped = reach_from(tstarts);
226 std::vector<bool> drop(N + 1, false);
227 for (std::size_t i = 1; i <= NT; ++i) drop[i] = (fromTrapped[i] || zc.dead[i]) && !live[i];
228 for (std::size_t idx = lqn.eshift + 1; idx <= lqn.ashift + lqn.nacts; ++idx) drop[idx] = drop[lqn.parent[idx]];
229 zc.ignore = drop;
230 return zc;
231 }
232
233private:
234 const lqn::LqnStruct<T>& lqn;
235 const std::size_t N;
237 std::vector<int> state; // 0 unvisited, 1 on the stack, 2 done
238 std::vector<bool> reach;
239 std::vector<std::size_t> blockAct;
240 std::vector<long> next;
241
242 void mark(std::size_t idx, double h) {
243 zc.onroute[idx] = true;
244 zc.held[idx] += h;
245 }
246
247 /** Threads of a task, Inf at an infinite server or for anything that is not a task. */
248 double task_cap(std::size_t tt) const {
249 if (tt > lqn.tshift && tt <= lqn.tshift + lqn.ntasks && lqn.sched[tt] != lang::SchedStrategy::INF)
250 return lqn.mult[tt];
251 return std::numeric_limits<double>::infinity();
252 }
253
254 static bool has(const std::vector<std::size_t>& v, std::size_t x) {
255 return std::find(v.begin(), v.end(), x) != v.end();
256 }
257
258 /** True when a request to entry e is trapped at a zero-capacity element. */
259 bool entry_route(std::size_t e) {
260 using lang::CallType;
261 if (state[e] == 2) return reach[e];
262 bool r = false;
263 if (state[e] == 1 || !zc.reason.empty()) return false; // a call cycle cannot reach anything new
264 state[e] = 1;
265 const std::size_t tk = lqn.parent[e];
266 if (zc.dead[tk]) {
267 r = true;
268 } else {
269 const std::vector<std::size_t> starts = acts_after(e);
270 const std::vector<std::size_t> acts = act_closure(starts, {});
271 if (zc.dead[lqn.parent[tk]]) {
272 // the first activity never gets the processor
273 if (starts.size() != 1) {
274 zc.reason = "Entry " + lqn.names[e] + " starts " + std::to_string(starts.size()) +
275 " activities on zero-capacity processor " + lqn.names[lqn.parent[tk]] +
276 ": how its requests divide between them depends on the initial state.";
277 } else {
278 r = true;
279 blockAct[e] = starts[0];
280 next[e] = -1;
281 }
282 } else {
283 std::vector<std::size_t> cand;
284 std::vector<std::vector<std::size_t>> candCall;
285 for (std::size_t a : acts) {
286 std::vector<std::size_t> dc;
287 if (a < lqn.callsof.size())
288 for (std::size_t ci : lqn.callsof[a])
289 if (lqn.calltype[ci] == CallType::SYNC &&
290 num_traits<T>::to_double(lqn.callproc_mean[ci]) > 0.0 &&
291 entry_route(lqn.callpair_dst[ci]))
292 dc.push_back(ci);
293 if (!dc.empty()) {
294 cand.push_back(a);
295 candCall.push_back(dc);
296 }
297 }
298 // an activity behind another blocked one on every path is never reached
299 std::vector<std::size_t> c2;
300 std::vector<std::vector<std::size_t>> cc2;
301 for (std::size_t k = 0; k < cand.size(); ++k) {
302 std::vector<std::size_t> others;
303 for (std::size_t j = 0; j < cand.size(); ++j)
304 if (j != k) others.push_back(cand[j]);
305 if (has(act_closure(starts, others), cand[k])) {
306 c2.push_back(cand[k]);
307 cc2.push_back(candCall[k]);
308 }
309 }
310 if (c2.size() > 1 || (c2.size() == 1 && cc2[0].size() > 1)) {
311 zc.reason = "Entry " + lqn.names[e] +
312 " reaches a zero-capacity element along more than one call path: how its "
313 "requests divide between them depends on the initial state.";
314 } else if (c2.size() == 1) {
315 r = true;
316 blockAct[e] = c2[0];
317 next[e] = long(lqn.callpair_dst[cc2[0][0]]);
318 }
319 }
320 }
321 reach[e] = r;
322 state[e] = 2;
323 return r;
324 }
325
326 /** Activities that directly follow entry or activity i in its own task. */
327 std::vector<std::size_t> acts_after(std::size_t i) const {
328 std::vector<std::size_t> s;
329 for (std::size_t j : lqn.graph.succ(i))
330 if (j <= N && lqn.type[j] == lang::LqnElement::ACTIVITY && lqn.parent[j] == lqn.parent[i])
331 s.push_back(j);
332 return s;
333 }
334
335 /** Activities reached from `starts` without passing through `blocked`. */
336 std::vector<std::size_t> act_closure(const std::vector<std::size_t>& starts,
337 const std::vector<std::size_t>& blocked) const {
338 std::vector<std::size_t> seen, stack;
339 for (std::size_t s : starts)
340 if (!has(blocked, s)) stack.push_back(s);
341 while (!stack.empty()) {
342 const std::size_t a = stack.back();
343 stack.pop_back();
344 if (has(seen, a)) continue;
345 seen.push_back(a);
346 for (std::size_t b : acts_after(a))
347 if (!has(seen, b) && !has(blocked, b)) stack.push_back(b);
348 }
349 return seen;
350 }
351
352 /** Elements a request from the tasks or entries in `starts` can visit. */
353 std::vector<bool> reach_from(const std::vector<std::size_t>& starts) const {
354 std::vector<bool> seen(N + 1, false);
355 std::vector<std::size_t> stack = starts;
356 while (!stack.empty()) {
357 const std::size_t i = stack.back();
358 stack.pop_back();
359 if (seen[i]) continue;
360 seen[i] = true;
361 std::vector<std::size_t> nxt;
362 switch (lqn.type[i]) {
364 nxt.push_back(lqn.parent[i]);
365 if (lqn.isref[i]) nxt.insert(nxt.end(), lqn.entriesof[i].begin(), lqn.entriesof[i].end());
366 break;
369 nxt.push_back(lqn.parent[i]);
370 for (std::size_t j : lqn.graph.succ(i))
371 if (j <= N) nxt.push_back(j);
372 break;
373 }
374 default:
375 break;
376 }
377 for (std::size_t j : nxt)
378 if (j >= 1 && !seen[j]) stack.push_back(j);
379 }
380 return seen;
381 }
382};
383
384} // namespace zc_detail
385
386/** Port of ln_zero_capacity.m; see the file header. */
387template <class T>
391
392} // namespace ln
393} // namespace line
394
395#endif // LINE_SOLVERS_LN_LN_ZERO_CAPACITY_H
ZeroCapacity(const lqn::LqnStruct< T > &l)
LayeredNetworkStruct, the flattened description of a layered queueing network.
LqnElement
LQN element kinds, with the values of MATLAB LayeredNetworkElement.
Definition lang_types.h:471
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:474
std::string fmt_g(double v)
LnZeroCapacity ln_zero_capacity(const lqn::LqnStruct< T > &lqn)
Port of ln_zero_capacity.m; see the file header.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
The structural answer of ln_zero_capacity; see the file header.
std::vector< double > held
std::vector< bool > onroute
std::vector< bool > trapped
std::vector< bool > dead
std::vector< bool > ignore
std::vector< std::size_t > parent
(nidx+1) host of a task, task of an entry/activity
Definition lqn_struct.h:215