LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_dt.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_NC_SOLVER_NC_DT_H
6#define LINE_SOLVERS_NC_SOLVER_NC_DT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Exact normalizing-constant analysis of a discrete-time (slotted) model.
12 *
13 * Port of matlab/src/solvers/NC/nc_is_dt_model.m and
14 * solver_nc_dt_analyzer.m. The route is requested with
15 * `NcSolverOptions::slotted`, the same switch SolverLDES uses to run on a
16 * discrete time scale, and is never auto-detected: a Geometric service time is
17 * a perfectly ordinary continuous-time model unless the caller says the model
18 * lives on a slot lattice.
19 *
20 * Two families are covered, both from Daduna (2001):
21 *
22 * chapter 2 a Bernoulli server fed by a Bernoulli arrival stream, with an
23 * unbounded buffer (theorem 2.3, corollary 2.7), a finite buffer
24 * (corollary 2.8) or a load-dependent service probability
25 * (example 2.10), evaluated by `dqsys_bernoulli1`;
26 * chapter 3 a closed cycle of Bernoulli servers (theorem 3.2, corollary
27 * 3.4), evaluated by `dpfqn_nc` when the service probabilities
28 * are state independent and by `dpfqn_ncld` otherwise.
29 *
30 * THE ADMISSIBLE FEATURE SET IS NARROW BECAUSE THE PRODUCT FORM IS NARROW.
31 * Beyond the geometric service requirement:
32 *
33 * - a cycle is the only topology; section 4.1 of the reference records that
34 * general discrete-time topologies of FCFS Bernoulli servers have no
35 * product form;
36 * - every station must be a single server. Pestien and Ramakrishnan, quoted
37 * before example 2.10, proved that a multiserver node inside a cycle of
38 * geometrical queues destroys the product form for ANY finite server
39 * count, so the multiserver case is refused rather than approximated;
40 * - class switching is rejected, and a multichain cycle is admitted only
41 * through its aggregate population.
42 *
43 * A model that fails any of these is an ERROR, not a fallback to the
44 * continuous-time analyzer: the continuous-time answer to a slotted question is
45 * a different number, not a worse one.
46 *
47 * Every metric is on the slot lattice: a rate is a per-slot probability and a
48 * time is a number of slots. `NcSolverOptions::slotlength` rescales both to
49 * model time units.
50 */
51
52#include <cmath>
53#include <cstddef>
54#include <limits>
55#include <string>
56#include <vector>
57
63#include "line/util/error.h"
64
65namespace line {
66namespace nc {
67
68/** Classification of a model against the discrete-time product form. */
69template <class T>
70struct DtModel {
71 std::string kind = "none"; ///< "bernoulli1", "cycle" or "none"
72 std::string reason; ///< why not, when kind is "none"
73 std::size_t station = 0; ///< 0-based queueing station (bernoulli1)
74 std::size_t source = 0; ///< 0-based Source station (bernoulli1)
75 T arrival_prob; ///< offered b (bernoulli1)
76 std::vector<T> service_single; ///< p(n), n = 1..L (bernoulli1)
77 std::size_t capacity = 0; ///< 0 means unbounded
78 std::vector<std::size_t> order; ///< stations in cycle order
79 std::vector<std::vector<T> > service; ///< p_j(n), [cycle position][n-1]
80 std::size_t population = 0;
81};
82
83namespace detail {
84
85/** Load-dependent scaling of station `ist` expanded to alpha(1..N). */
86template <class T>
87std::vector<T> dt_lld_vector(const qn::NetworkStruct<T>& sn, std::size_t ist, std::size_t N) {
88 const T one = num_traits<T>::from_int(1);
89 std::vector<T> alpha(N, one);
90 const std::vector<T>& lld = sn.stations[ist].lldscaling;
91 if (lld.empty()) return alpha;
92 bool varies = false;
93 for (std::size_t k = 0; k < lld.size(); ++k) {
94 if (!(lld[k] == one)) { varies = true; break; }
95 }
96 if (!varies) return alpha;
97 // Hold the last declared value beyond the tabulated range, as the
98 // load-dependent normalizing-constant analyzers do.
99 for (std::size_t n = 0; n < N; ++n) {
100 alpha[n] = (n < lld.size()) ? lld[n] : lld[lld.size() - 1];
101 }
102 return alpha;
103}
104
105/** True when station `ist` declares a load dependence. */
106template <class T>
107bool dt_has_lld(const qn::NetworkStruct<T>& sn, std::size_t ist) {
108 const T one = num_traits<T>::from_int(1);
109 const std::vector<T>& lld = sn.stations[ist].lldscaling;
110 for (std::size_t k = 0; k < lld.size(); ++k) {
111 if (!(lld[k] == one)) return true;
112 }
113 return false;
114}
115
116/** Effective buffer capacity of station `ist`, 0 when unbounded. */
117template <class T>
118std::size_t dt_capacity(const qn::NetworkStruct<T>& sn, std::size_t ist, std::size_t r) {
119 double cap = sn.stations[ist].cap;
120 if (!sn.classcap.empty() && ist < sn.classcap.size() && r < sn.classcap[ist].size()) {
121 const double cc = sn.classcap[ist][r];
122 if (cc < cap) cap = cc;
123 }
124 if (!std::isfinite(cap) || cap <= 0) return 0;
125 return static_cast<std::size_t>(cap);
126}
127
128/**
129 * Station order along the cycle, or empty when the routing is not a single
130 * deterministic cycle visiting every station exactly once.
131 */
132template <class T>
133std::vector<std::size_t> dt_cycle_order(const qn::NetworkStruct<T>& sn) {
134 const std::size_t M = sn.nstations, R = sn.nclasses;
135 const api::SnRtStations<T> rt = api::sn_rt_stations(sn);
136 const Matrix<T>& rtst = rt.rtst;
137 const double tol = 1e-8, coarse = 1e-3;
138
139 std::vector<std::size_t> succ(M, 0);
140 for (std::size_t i = 0; i < M; ++i) {
141 long tgt = -1;
142 for (std::size_t j = 0; j < M; ++j) {
143 double w = 0;
144 for (std::size_t r = 0; r < R; ++r) {
145 for (std::size_t s = 0; s < R; ++s) {
146 w += num_traits<T>::to_double(rtst(i * R + r, j * R + s));
147 }
148 }
149 if (w > tol) {
150 if (std::fabs(w - static_cast<double>(R)) > coarse && std::fabs(w - 1.0) > coarse) {
151 return std::vector<std::size_t>(); // fractional routing out of i
152 }
153 if (tgt >= 0) return std::vector<std::size_t>(); // more than one successor
154 tgt = static_cast<long>(j);
155 }
156 }
157 if (tgt < 0 || static_cast<std::size_t>(tgt) == i) return std::vector<std::size_t>();
158 succ[i] = static_cast<std::size_t>(tgt);
159 }
160
161 std::vector<bool> visited(M, false);
162 std::vector<std::size_t> order;
163 std::size_t cur = 0;
164 for (std::size_t k = 0; k < M; ++k) {
165 if (visited[cur]) return std::vector<std::size_t>();
166 visited[cur] = true;
167 order.push_back(cur);
168 cur = succ[cur];
169 }
170 if (cur != 0) return std::vector<std::size_t>();
171 for (std::size_t i = 0; i < M; ++i) {
172 if (!visited[i]) return std::vector<std::size_t>();
173 }
174 return order;
175}
176
177} // namespace detail
178
179/** Classify `sn` against the two discrete-time product-form families. */
180template <class T>
182 DtModel<T> dt;
183 const std::size_t M = sn.nstations, R = sn.nclasses;
184 const T zero = num_traits<T>::from_int(0);
185 const T one = num_traits<T>::from_int(1);
186
187 for (std::size_t i = 0; i < M; ++i) {
188 for (std::size_t r = 0; r < R; ++r) {
189 const T rate = sn.rates(i, r);
190 if (rate > zero) {
191 if (sn.procid(i + 1, r + 1) != qn::ProcessType::GEOMETRIC) {
192 dt.reason = "a station serves a class with a non-Geometric process; a "
193 "discrete-time model needs Geometric service and interarrival times";
194 return dt;
195 }
196 }
197 }
198 }
199 for (std::size_t i = 0; i < M; ++i) {
200 if (sn.stations[i].cdscaling || sn.stations[i].jdscaling) {
201 dt.reason = "class- or joint-dependent scaling is not covered by the discrete-time "
202 "product form";
203 return dt;
204 }
205 }
206
207 const std::vector<double> njobs = sn.njobs();
208 bool is_open = false;
209 for (std::size_t r = 0; r < njobs.size(); ++r) {
210 if (std::isinf(njobs[r])) { is_open = true; break; }
211 }
212
213 if (is_open) {
214 if (R != 1) {
215 dt.reason = "the discrete-time single-node route handles one open class";
216 return dt;
217 }
218 std::size_t src = 0, nsrc = 0, ist = 0, nq = 0;
219 for (std::size_t i = 0; i < M; ++i) {
220 if (sn.stations[i].sched == qn::SchedStrategy::EXT) { src = i; ++nsrc; }
221 else { ist = i; ++nq; }
222 }
223 if (nsrc != 1) {
224 dt.reason = "an open discrete-time model needs exactly one Source";
225 return dt;
226 }
227 if (nq != 1) {
228 dt.reason = "the discrete-time single-node route handles one queueing station";
229 return dt;
230 }
231 if (sn.stations[ist].sched != qn::SchedStrategy::FCFS) {
232 dt.reason = "a Bernoulli server is a FCFS station";
233 return dt;
234 }
235 if (std::isfinite(sn.stations[ist].nservers) && sn.stations[ist].nservers != 1.0) {
236 dt.reason = "a Bernoulli server is a single-server station; use load dependence for "
237 "the multiserver approximation of example 2.10";
238 return dt;
239 }
240 const T b = sn.rates(src, 0), p = sn.rates(ist, 0);
241 if (!(b > zero) || b > one) {
242 dt.reason = "the source arrival probability must lie in (0,1]";
243 return dt;
244 }
245 if (!(p > zero) || p > one) {
246 dt.reason = "the service probability must lie in (0,1]";
247 return dt;
248 }
249 const std::size_t cap = detail::dt_capacity(sn, ist, 0);
250 const bool has_lld = detail::dt_has_lld(sn, ist);
251 if (cap == 0 && has_lld) {
252 dt.reason = "a load-dependent Bernoulli server needs a finite capacity to bound the "
253 "state space";
254 return dt;
255 }
256 if (cap == 0 && !(b < p)) {
257 dt.reason = "an unbounded discrete-time queue needs an arrival probability below the "
258 "service probability";
259 return dt;
260 }
261 if (cap == 0) {
262 dt.service_single.assign(1, p);
263 } else {
264 const std::vector<T> alpha = detail::dt_lld_vector(sn, ist, cap);
265 dt.service_single.resize(cap);
266 for (std::size_t n = 0; n < cap; ++n) {
267 dt.service_single[n] = p * alpha[n];
268 if (!(dt.service_single[n] > zero) || dt.service_single[n] > one) {
269 dt.reason = "load dependence must keep the service probability inside (0,1]";
270 return dt;
271 }
272 }
273 }
274 dt.kind = "bernoulli1";
275 dt.station = ist;
276 dt.source = src;
277 dt.arrival_prob = b;
278 dt.capacity = cap;
279 return dt;
280 }
281
282 double Ntot = 0;
283 for (std::size_t r = 0; r < njobs.size(); ++r) {
284 if (std::isfinite(njobs[r])) Ntot += njobs[r];
285 }
286 if (Ntot <= 0 || Ntot != std::floor(Ntot)) {
287 dt.reason = "the closed population must be a positive integer";
288 return dt;
289 }
290 const std::size_t N = static_cast<std::size_t>(Ntot);
291
292 std::vector<T> p(M, zero);
293 for (std::size_t i = 0; i < M; ++i) {
294 if (sn.stations[i].sched != qn::SchedStrategy::FCFS) {
295 dt.reason = "a cycle of Bernoulli servers is FCFS throughout";
296 return dt;
297 }
298 if (std::isfinite(sn.stations[i].nservers) && sn.stations[i].nservers != 1.0) {
299 dt.reason = "a multiserver node inside a cycle of geometrical queues has no product "
300 "form";
301 return dt;
302 }
303 bool seen = false;
304 T lo = zero, hi = zero;
305 for (std::size_t r = 0; r < R; ++r) {
306 const T v = sn.rates(i, r);
307 if (v > zero) {
308 if (!seen) { lo = v; hi = v; seen = true; }
309 else { if (v < lo) lo = v; if (v > hi) hi = v; }
310 }
311 }
312 if (!seen) {
313 dt.reason = "a station of the cycle serves no class";
314 return dt;
315 }
316 if (num_traits<T>::to_double(hi - lo) > 1e-8 * num_traits<T>::to_double(hi)) {
317 dt.reason = "a station has a class-dependent service probability; the discrete-time "
318 "cycle needs one Bernoulli server per node";
319 return dt;
320 }
321 p[i] = lo;
322 if (!(p[i] > zero) || !(p[i] < one)) {
323 dt.reason = "the product form of theorem 3.2 needs every service probability in (0,1)";
324 return dt;
325 }
326 }
327
328 const std::vector<std::size_t> order = detail::dt_cycle_order(sn);
329 if (order.empty()) {
330 dt.reason = "the stations do not form a single deterministic cycle; discrete-time FCFS "
331 "networks of other topologies have no product form";
332 return dt;
333 }
334
335 dt.service.assign(M, std::vector<T>(N, zero));
336 for (std::size_t k = 0; k < M; ++k) {
337 const std::vector<T> alpha = detail::dt_lld_vector(sn, order[k], N);
338 for (std::size_t n = 0; n < N; ++n) {
339 dt.service[k][n] = p[order[k]] * alpha[n];
340 if (!(dt.service[k][n] > zero) || dt.service[k][n] > one) {
341 dt.reason = "load dependence must keep every service probability inside (0,1]";
342 return dt;
343 }
344 }
345 }
346 dt.kind = "cycle";
347 dt.order = order;
348 dt.population = N;
349 return dt;
350}
351
352/**
353 * Exact discrete-time analysis of `sn`.
354 *
355 * On the cycle route the per-class split is proportional to the per-class
356 * population. Service in the cycle is type independent and FCFS forbids
357 * overtaking, so the cyclic order of the jobs is frozen; the marginal law of
358 * the queue lengths carries no class information, and the long-run share of
359 * station j held by chain g is its population share N_g/N. That is the sense in
360 * which section 3.2 of the reference calls the multichain case a direct
361 * adaptation of the unichain one.
362 */
363template <class T>
365 const DtModel<T> dt = nc_is_dt_model(sn);
366 const std::size_t M = sn.nstations, K = sn.nclasses;
367 const T zero = num_traits<T>::from_int(0);
368 const T one = num_traits<T>::from_int(1);
369
370 NcSolution<T> out;
371 out.sol.Q = Matrix<T>(M, K, zero);
372 out.sol.U = Matrix<T>(M, K, zero);
373 out.sol.R = Matrix<T>(M, K, zero);
374 out.sol.Tp = Matrix<T>(M, K, zero);
375 out.sol.C.assign(K, zero);
376 out.sol.X.assign(K, zero);
377 out.sol.iter = 1;
378
379 if (dt.kind == "bernoulli1") {
380 out.sol.method = "dt.bernoulli1";
381 std::vector<T> b(1, dt.arrival_prob);
383 if (dt.capacity == 0) {
384 // An unbounded buffer is the finite chain taken far enough out that
385 // the geometric tail is below the working precision; the closed form
386 // of corollary 2.7 agrees with it term for term.
387 const double ratio = num_traits<T>::to_double(dt.arrival_prob)
391 std::size_t L = 64;
392 if (ratio > 0 && ratio < 1) {
393 const std::size_t need = static_cast<std::size_t>(std::ceil(std::log(1e-18)
394 / std::log(ratio)));
395 if (need > L) L = need;
396 }
397 if (L > 100000) L = 100000;
399 } else {
401 }
402 out.sol.Q(dt.station, 0) = r.meanQueueLength;
403 out.sol.U(dt.station, 0) = r.utilization;
404 out.sol.Tp(dt.station, 0) = r.throughput;
405 out.sol.R(dt.station, 0) = r.meanSojournTime;
406 out.sol.X[0] = r.throughput;
407 out.sol.C[0] = r.meanSojournTime;
408 // The Source row carries the offered stream, as on the continuous-time route.
409 out.sol.Tp(dt.source, 0) = dt.arrival_prob;
410 out.sol.lG = std::log(num_traits<T>::to_double(r.normConst));
411 } else if (dt.kind == "cycle") {
412 const std::size_t N = dt.population;
413 bool state_independent = true;
414 for (std::size_t k = 0; k < dt.service.size() && state_independent; ++k) {
415 for (std::size_t n = 1; n < dt.service[k].size(); ++n) {
416 if (!(dt.service[k][n] == dt.service[k][0])) { state_independent = false; break; }
417 }
418 }
419 std::vector<T> Qs(M, zero), Us(M, zero), Ts(M, zero);
420 if (state_independent) {
421 // Propositions 3.18 and 3.19 end to end, with the index of corollary
422 // 3.20(a) corrected (see dpfqn_nc.h).
423 out.sol.method = "dt.cycle";
424 std::vector<T> p(M);
425 for (std::size_t k = 0; k < M; ++k) p[k] = dt.service[k][0];
427 const T x = nc.throughput();
428 for (std::size_t k = 0; k < M; ++k) {
429 const T q = one - p[k];
430 T ratio = one, tail = zero;
431 for (std::size_t n = 1; n <= N; ++n) {
432 ratio = ratio * q / p[k];
433 tail = tail + ratio / q * nc.G1[N - n + 1] / nc.G;
434 }
435 Qs[dt.order[k]] = tail; // E[X_j] = sum_{n>=1} P(X_j>=n)
436 Us[dt.order[k]] = x / p[k]; // P(X_j >= 1)
437 Ts[dt.order[k]] = x;
438 }
439 out.sol.lG = nc.lG;
440 } else {
441 out.sol.method = "dt.cycleld";
443 for (std::size_t k = 0; k < M; ++k) {
444 const std::vector<T> marg = nc.marginal(k);
445 T q = zero, t = zero;
446 for (std::size_t n = 0; n <= N; ++n) {
447 q = q + marg[n] * num_traits<T>::from_int(static_cast<long>(n));
448 if (n >= 1) t = t + marg[n] * dt.service[k][n - 1];
449 }
450 Qs[dt.order[k]] = q;
451 Us[dt.order[k]] = one - marg[0];
452 Ts[dt.order[k]] = t;
453 }
454 out.sol.lG = nc.lG;
455 }
456
457 const std::vector<double> njobs = sn.njobs();
458 for (std::size_t i = 0; i < M; ++i) {
459 for (std::size_t r = 0; r < K; ++r) {
460 const T share = num_traits<T>::from_double(
461 std::isfinite(njobs[r]) ? njobs[r] / static_cast<double>(N) : 0.0);
462 out.sol.Q(i, r) = Qs[i] * share;
463 out.sol.U(i, r) = Us[i] * share;
464 out.sol.Tp(i, r) = Ts[i] * share;
465 if (out.sol.Tp(i, r) > zero) {
466 out.sol.R(i, r) = out.sol.Q(i, r) / out.sol.Tp(i, r);
467 }
468 }
469 }
470 for (std::size_t r = 0; r < K; ++r) {
471 const std::size_t ref = sn.classes[r].refstat;
472 if (ref >= 1 && ref <= M) out.sol.X[r] = out.sol.Tp(ref - 1, r);
473 if (out.sol.X[r] > zero && std::isfinite(njobs[r])) {
474 out.sol.C[r] = num_traits<T>::from_double(njobs[r]) / out.sol.X[r];
475 }
476 }
477 } else {
478 throw InputError("solver_nc_dt: the slotted option was requested but the model is not a "
479 "discrete-time product-form model: " + dt.reason);
480 }
481
482 out.actualmethod = out.sol.method;
483 if (opt.slotlength != 1.0) {
484 const T d = num_traits<T>::from_double(opt.slotlength);
485 for (std::size_t i = 0; i < M; ++i) {
486 for (std::size_t r = 0; r < K; ++r) {
487 out.sol.Tp(i, r) = out.sol.Tp(i, r) / d;
488 out.sol.R(i, r) = out.sol.R(i, r) * d;
489 }
490 }
491 for (std::size_t r = 0; r < K; ++r) {
492 out.sol.X[r] = out.sol.X[r] / d;
493 out.sol.C[r] = out.sol.C[r] * d;
494 }
495 }
496 return out;
497}
498
499} // namespace nc
500} // namespace line
501
502#endif // LINE_SOLVERS_NC_SOLVER_NC_DT_H
InputError(const std::string &what)
Definition error.h:39
A network plus its refreshed NetworkStruct.
Normalizing constants of a discrete-time closed cycle of Bernoulli servers.
State dependent Bernoulli server on a discrete time scale.
The exception types the port throws.
SnRtStations< T > sn_rt_stations(const qn::NetworkStruct< T > &sn)
DtNcLdResult< T > dpfqn_ncld(const std::vector< std::vector< T > > &P, std::size_t N)
Theorem 3.2 for a cycle whose service probabilities depend on the local queue length.
Definition dpfqn_nc.h:205
DtNcResult< T > dpfqn_nc(const std::vector< T > &p, std::size_t N)
Propositions 3.18 and 3.19 for a cycle with state independent service probabilities.
Definition dpfqn_nc.h:113
Bernoulli1Result< T > dqsys_bernoulli1(const std::vector< T > &b, const std::vector< T > &p, std::size_t L)
Finite buffer of L jobs.
NcSolution< T > solver_nc_dt(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Exact discrete-time analysis of sn.
DtModel< T > nc_is_dt_model(const qn::NetworkStruct< T > &sn)
Classify sn against the two discrete-time product-form families.
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Port of matlab/src/api/sn/sn_rt_stations.m.
Constants of the state dependent cycle, in one common scale.
Definition dpfqn_nc.h:87
Constants of the state independent cycle, in one common scale.
Definition dpfqn_nc.h:76
Steady-state quantities of a finite-buffer Bernoulli server.
T throughput
carried departures per slot
T meanSojournTime
in slots, by Little's law
Classification of a model against the discrete-time product form.
std::vector< std::vector< T > > service
p_j(n), [cycle position][n-1]
std::vector< T > service_single
p(n), n = 1..L (bernoulli1)
std::size_t station
0-based queueing station (bernoulli1)
std::size_t capacity
0 means unbounded
T arrival_prob
offered b (bernoulli1)
std::string reason
why not, when kind is "none"
std::string kind
"bernoulli1", "cycle" or "none"
std::size_t population
std::size_t source
0-based Source station (bernoulli1)
std::vector< std::size_t > order
stations in cycle order
The [Q,U,R,T,C,X,lG] of the reference, plus the algorithm that ran.
Definition nc_types.h:113
mva::MvaSolution< T > sol
Definition nc_types.h:114
std::string actualmethod
Definition nc_types.h:115
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33