LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
me_oqn_blk.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_API_ME_ME_OQN_BLK_H
6#define LINE_API_ME_ME_OQN_BLK_H
7
8/**
9 * @file
10 * @ingroup api_me
11 * Maximum Entropy algorithm for single-class OPEN networks with FINITE BUFFERS,
12 * under loss or transfer blocking.
13 *
14 * Templated port of `matlab/src/api/me/me_oqn_blk.m`: Kouvatsos (1994) Section
15 * 4 for the loss case, and Tahilramani, Manjunath and Bose (1999) for transfer
16 * blocking.
17 *
18 * TWO POLICIES, AND ONLY ONE OF THEM IS EASY.
19 *
20 * LOSS (blockrule 0). A job finding the destination full is discarded. Every
21 * station is then a censored GE/GE/c/0;N queue and the network is a
22 * straightforward ME decomposition.
23 *
24 * TRANSFER BLOCKING (blockrule 1, BAS). A job that completes at i and finds j
25 * full is held in i's SERVER, which can serve nobody else until j has room.
26 * That is NOT WORK CONSERVING, so no product-form approximation applies to
27 * the network as it stands. The reference first makes it work conserving by
28 * inserting a GE/GE/inf HOLDING NODE h_ij on every routing pair with a
29 * finite-buffer destination: the holding node absorbs the blocked job so i's
30 * server is released, and the delay it charges is the residual life of the
31 * minimum of the c_j services in progress at j, inflated geometrically
32 * because the released job may find j full again. Station i's own service is
33 * inflated by the same blocking probability, so the jobs queued behind the
34 * blocked one still see the server busy. THE HELD JOBS ARE ADDED BACK TO
35 * STATION i at the end -- they are physically in i's servers, and reporting
36 * them at the holding node would lose them from the station table.
37 *
38 * WHY THE SELF-LOOP IS ELIMINATED FIRST. The flow decomposition assumes RENEWAL
39 * arrival streams, which immediate feedback breaks. A job returning straight to
40 * i receives a geometric number of passes, so the composite service has rate
41 * mu(1-p_ii) and scv p_ii + (1-p_ii)Cs; the loop is removed and the residual
42 * routing renormalized.
43 *
44 * TWO CONVENTIONS THAT ARE EASY TO GET WRONG, both carried deliberately.
45 * An EXTERNAL arrival finding the buffer full is LOST whatever the drop rule --
46 * there is no upstream server to hold it in -- which is both the source's
47 * convention and what SolverCTMC does, returning the same answer for DROP and
48 * BAS on a source-fed finite queue. And the reported UTILIZATION is LINE's, the
49 * carried flow times the nominal mean service time per server, so a server held
50 * blocked after service does NOT count as busy; the ME solution's own
51 * E[min(n,c)]/c is computed under the INFLATED service and would include the
52 * blocking time, so it is recomputed at the end.
53 */
54
55#include <algorithm>
56#include <cmath>
57#include <cstddef>
58#include <limits>
59#include <vector>
60
64#include "line/num/number.h"
65#include "line/util/error.h"
66#include "line/util/lu.h"
67#include "line/util/matrix.h"
68
69namespace line {
70namespace me {
71
72/** Controls of the blocking fixed point, `me_oqn_blk`'s options struct. */
74 double tol = 1e-6;
75 long maxiter = 1000;
76 /** Relaxation weight on the blocking probabilities, the source's scheme. */
77 double damping = 0.5;
78};
79
80/** What `me_oqn_blk` returns, per station. */
81template <class T>
83 std::vector<T> Q; ///< mean number present, the jobs held blocked included
84 std::vector<T> W; ///< mean response time, Q / T
85 std::vector<T> T_; ///< throughput, the CARRIED flow
86 std::vector<T> U; ///< utilization on LINE's convention (see the header)
87 std::vector<T> Ca; ///< interarrival scv of the offered flow
88 std::vector<T> Cd; ///< interdeparture scv
89 std::vector<T> PBa; ///< probability an arrival finds the station full
90 std::vector<T> lambda; ///< OFFERED arrival rate, the attempts included
91 long iter = 0;
92 bool converged = false;
93};
94
95/**
96 * Port of `me_oqn_blk`.
97 *
98 * @param M number of stations
99 * @param lambda0 external arrival rates (M)
100 * @param Ca0 external interarrival scv (M), at least 1 where lambda0 > 0
101 * @param mu service rates (M)
102 * @param Cs service scv (M), at least 1 at every finite-buffer station
103 * @param P routing (M x M), `P(i,j) = p_ij`; a row sum below one sends
104 * the residual flow out of the network
105 * @param c servers per station (M); 0 marks an infinite server, as
106 * everywhere else in `api/me`
107 * @param N buffer capacity per station (M) in jobs, service included;
108 * 0 marks an unbounded buffer
109 * @param blockrule per station (M): 0 = loss, 1 = transfer blocking
110 * @param opt tolerance, iteration budget and relaxation weight
111 */
112template <class T>
113MeBlkResult<T> me_oqn_blk(std::size_t M, const std::vector<T>& lambda0,
114 const std::vector<T>& Ca0, const std::vector<T>& mu,
115 const std::vector<T>& Cs, const Matrix<T>& P,
116 const std::vector<long>& c, const std::vector<long>& N,
117 const std::vector<int>& blockrule,
118 const MeBlkOptions& opt = MeBlkOptions()) {
120 "me_oqn_blk requires transcendental arithmetic: its building blocks assemble "
121 "the censored state law in logarithms");
122 const T zero = num_traits<T>::from_int(0);
123 const T one = num_traits<T>::from_int(1);
124 const T two = num_traits<T>::from_int(2);
125
126 if (lambda0.size() != M || Ca0.size() != M || mu.size() != M || Cs.size() != M ||
127 c.size() != M || N.size() != M || blockrule.size() != M)
128 throw InputError("me_oqn_blk: every per-station vector must have M entries");
129 if (P.rows() != M || P.cols() != M)
130 throw InputError("me_oqn_blk: the routing matrix must be M x M");
131
132 // 0 marks "unbounded" for N and "infinite server" for c, the convention the
133 // rest of api/me uses in place of MATLAB's Inf.
134 std::vector<char> finiteBuf(M, 0), bas(M, 0);
135 for (std::size_t i = 0; i < M; ++i) {
136 finiteBuf[i] = (N[i] > 0 && c[i] > 0) ? 1 : 0;
137 bas[i] = (finiteBuf[i] && blockrule[i] == 1) ? 1 : 0;
138 if (finiteBuf[i] && num_traits<T>::to_double(Cs[i]) < 1.0 - 1e-12)
139 throw InputError(
140 "me_oqn_blk: a finite buffer requires a service scv of at least 1 at station " +
141 std::to_string(i + 1) + ": the GE distribution is not defined for scv < 1.");
142 if (lambda0[i] > zero && num_traits<T>::to_double(Ca0[i]) < 1.0 - 1e-12)
143 throw InputError(
144 "me_oqn_blk: a finite buffer requires an external interarrival scv of at least 1 "
145 "at station " + std::to_string(i + 1) + ".");
146 }
147
148 // Immediate feedback elimination: the flow decomposition assumes renewal
149 // streams, which a self loop breaks.
150 Matrix<T> Pf = P;
151 std::vector<T> muf = mu, Csf = Cs;
152 for (std::size_t i = 0; i < M; ++i) {
153 const T pii = P(i, i);
154 if (pii > zero) {
155 const T q = T(one - pii);
156 muf[i] = T(mu[i] * q);
157 Csf[i] = T(pii + q * Cs[i]);
158 for (std::size_t j = 0; j < M; ++j) Pf(i, j) = T(P(i, j) / q);
159 Pf(i, i) = zero;
160 }
161 }
162
163 // Residual-life rate of the minimum of the c_j services in progress at j. A
164 // GE service is zero with probability 1-sigma and exponential with rate
165 // mu*sigma otherwise, so its equilibrium residual life is exponential with
166 // rate mu*sigma and the minimum over c_j busy servers has rate c*mu*sigma.
167 std::vector<T> sigmaS(M, one), muRes(M, zero);
168 for (std::size_t i = 0; i < M; ++i) {
169 sigmaS[i] = T(two / (Csf[i] + one));
170 muRes[i] = T(num_traits<T>::from_int(c[i]) * muf[i] * sigmaS[i]);
171 }
172
173 MeBlkResult<T> res;
174 res.Ca.assign(M, one);
175 res.Cd = Csf;
176 res.Q.assign(M, zero);
177 res.U.assign(M, zero);
178 res.T_.assign(M, zero);
179 res.lambda.assign(M, zero);
180 res.PBa.assign(M, zero);
181 Matrix<T> PBs(M, M, zero), PBh(M, M, zero);
182 std::vector<T> PBe(M, zero);
183
184 double delta = std::numeric_limits<double>::infinity();
185 long it = 0;
186 for (it = 1; it <= opt.maxiter; ++it) {
187 const std::vector<T> Ca_old = res.Ca;
188 const Matrix<T> PBs_old = PBs;
189 const std::vector<T> PBe_old = PBe;
190
191 // Service inflation at the blocking stations: the fraction PBf(i) of
192 // i's completions is followed by a blocking period.
193 std::vector<T> PBf(M, zero);
194 for (std::size_t i = 0; i < M; ++i)
195 for (std::size_t j = 0; j < M; ++j)
196 if (Pf(i, j) > zero && bas[j]) PBf[i] += T(Pf(i, j) * PBs(i, j));
197 for (std::size_t i = 0; i < M; ++i)
198 if (num_traits<T>::to_double(PBf[i]) >= 1.0 - 1e-9)
199 throw NumericError(
200 "me_oqn_blk: the transfer-blocking fixed point saturates, a station is blocked "
201 "with probability one. The network has no stable operating point under BAS.");
202 std::vector<T> muEff(M, zero), CsEff(M, one);
203 for (std::size_t i = 0; i < M; ++i) {
204 muEff[i] = T(muf[i] * (one - PBf[i]));
205 CsEff[i] = T(PBf[i] + Csf[i] * (one - PBf[i]));
206 }
207
208 // Flow balance on the CARRIED flow. Under loss a fraction PB of a
209 // stream is discarded; under transfer blocking every job eventually
210 // enters, the delay being charged to the holding node.
211 Matrix<T> A(M, M, zero);
212 std::vector<T> b(M, zero);
213 for (std::size_t j = 0; j < M; ++j) {
214 b[j] = T(lambda0[j] * (one - PBe[j]));
215 for (std::size_t i = 0; i < M; ++i)
216 if (Pf(i, j) > zero)
217 A(i, j) = (finiteBuf[j] && !bas[j]) ? T(Pf(i, j) * (one - PBs(i, j)))
218 : Pf(i, j);
219 }
220 // T = (I - A')^{-1} b
221 Matrix<T> S(M, M, zero);
222 for (std::size_t r = 0; r < M; ++r) {
223 for (std::size_t k = 0; k < M; ++k) S(r, k) = T(-A(k, r));
224 S(r, r) = T(S(r, r) + one);
225 }
226 res.T_ = solve(S, b);
227 for (T& v : res.T_)
228 if (v < zero) v = zero;
229
230 // Offered (attempt) rates. A job blocked under BAS re-attempts from the
231 // holding node, so its stream contributes carried/(1-PB) attempts.
232 std::vector<T> attExt(M, zero);
233 Matrix<T> attInt(M, M, zero);
234 const T tiny = num_traits<T>::from_double(1e-12);
235 for (std::size_t j = 0; j < M; ++j) {
236 attExt[j] = lambda0[j];
237 for (std::size_t i = 0; i < M; ++i)
238 if (Pf(i, j) > zero) {
239 if (finiteBuf[j] && bas[j]) {
240 T den = T(one - PBs(i, j));
241 if (den < tiny) den = tiny;
242 attInt(i, j) = T(res.T_[i] * Pf(i, j) / den);
243 } else {
244 attInt(i, j) = T(res.T_[i] * Pf(i, j));
245 }
246 }
247 }
248 for (std::size_t j = 0; j < M; ++j) {
249 T tot = attExt[j];
250 for (std::size_t i = 0; i < M; ++i) tot += attInt(i, j);
251 res.lambda[j] = tot;
252 if (tot > zero) {
253 T acc = T(attExt[j] * PBe[j]);
254 for (std::size_t i = 0; i < M; ++i) acc += T(attInt(i, j) * PBs(i, j));
255 res.PBa[j] = T(acc / tot);
256 } else {
257 res.PBa[j] = zero;
258 }
259 }
260
261 // Interarrival scv of the offered flow, by GE splitting and merging: a
262 // stream thinned with probability p has scv 1-p+p*Cd, and the merge
263 // satisfies 1/(Cm+1) = sum_s (lam_s/lam)/(Cs+1).
264 Matrix<T> CaStreamInt(M, M, one);
265 for (std::size_t j = 0; j < M; ++j) {
266 if (!(res.lambda[j] > zero)) continue;
267 T sum_inv = zero;
268 if (attExt[j] > zero)
269 sum_inv += T((attExt[j] / res.lambda[j]) / (Ca0[j] + one));
270 for (std::size_t i = 0; i < M; ++i)
271 if (attInt(i, j) > zero) {
272 CaStreamInt(i, j) = T(one - Pf(i, j) + Pf(i, j) * res.Cd[i]);
273 sum_inv += T((attInt(i, j) / res.lambda[j]) / (CaStreamInt(i, j) + one));
274 }
275 if (sum_inv > zero) res.Ca[j] = T(-one + one / sum_inv);
276 }
277
278 // Station solution in isolation, and the per-STREAM blocking
279 // probabilities: eq. (4.3) evaluated with each stream's own scv.
280 std::vector<T> PBe_new(M, zero);
281 Matrix<T> PBs_new(M, M, zero), PBh_new(M, M, zero);
282 for (std::size_t j = 0; j < M; ++j) {
283 if (c[j] <= 0) {
284 // Infinite server: no queueing and no blocking.
285 res.Q[j] = (muEff[j] > zero) ? T(res.lambda[j] / muEff[j]) : zero;
286 res.U[j] = res.Q[j];
287 res.Cd[j] = res.Ca[j];
288 continue;
289 }
290 const T cT = num_traits<T>::from_int(c[j]);
291 if (!finiteBuf[j]) {
292 // Unbounded buffer: the infinite-capacity GE blocks.
293 T rho = zero;
294 if (muEff[j] > zero) rho = T(res.lambda[j] / (cT * muEff[j]));
295 if (num_traits<T>::to_double(rho) >= 1.0) {
297 std::numeric_limits<double>::infinity());
298 res.U[j] = one;
299 res.Cd[j] = CsEff[j];
300 } else if (c[j] == 1) {
301 res.Q[j] = T(rho * (res.Ca[j] + one) / two +
302 rho * rho * (CsEff[j] + res.Ca[j]) / (two * (one - rho)));
303 res.U[j] = rho;
304 res.Cd[j] = T(rho * rho * CsEff[j] + (one - rho) * res.Ca[j] +
305 rho * (one - rho));
306 } else {
307 res.Q[j] = me_gegec_mql(res.lambda[j], res.Ca[j], muEff[j], CsEff[j], c[j]);
308 res.U[j] = rho;
309 res.Cd[j] = T(rho * rho * CsEff[j] + (one - rho) * res.Ca[j] +
310 rho * (one - rho));
311 }
312 continue;
313 }
314 const GegecnResult<T> gj =
315 me_gegecn(res.lambda[j], res.Ca[j], muEff[j], CsEff[j], c[j], 0, N[j]);
316 res.Q[j] = gj.L;
317 res.U[j] = gj.U;
318 // Interdeparture scv at the CENSORED utilization: with losses the
319 // offered load can exceed one while the busy fraction cannot.
320 res.Cd[j] = T(gj.U * gj.U * CsEff[j] + (one - gj.U) * res.Ca[j] +
321 gj.U * (one - gj.U));
322 if (attExt[j] > zero)
323 PBe_new[j] = me_gegecn_pb(gj.p, 0L, N[j], c[j], CsEff[j], Ca0[j]);
324 for (std::size_t i = 0; i < M; ++i)
325 if (attInt(i, j) > zero) {
326 PBs_new(i, j) =
327 me_gegecn_pb(gj.p, 0L, N[j], c[j], CsEff[j], CaStreamInt(i, j));
328 if (bas[j]) {
329 // The flow released by h_ij is a Bernoulli sample of i's
330 // departures with probability p_ij PB^i_j, carried
331 // through a GE/GE/inf queue whose interdeparture scv
332 // equals its interarrival scv.
333 const T q = T(Pf(i, j) * PBs(i, j));
334 const T CaH = T(one - q + q * res.Cd[i]);
335 PBh_new(i, j) = me_gegecn_pb(gj.p, 0L, N[j], c[j], CsEff[j], CaH);
336 }
337 }
338 }
339
340 // Relaxation on the blocking probabilities.
341 const T w = num_traits<T>::from_double(opt.damping);
342 const T w1 = T(one - w);
343 for (std::size_t j = 0; j < M; ++j) PBe[j] = T(w1 * PBe[j] + w * PBe_new[j]);
344 for (std::size_t i = 0; i < M; ++i)
345 for (std::size_t j = 0; j < M; ++j) {
346 PBs(i, j) = T(w1 * PBs(i, j) + w * PBs_new(i, j));
347 PBh(i, j) = T(w1 * PBh(i, j) + w * PBh_new(i, j));
348 }
349
350 delta = 0.0;
351 for (std::size_t j = 0; j < M; ++j) {
352 delta = std::max(delta, std::fabs(num_traits<T>::to_double(res.Ca[j]) -
353 num_traits<T>::to_double(Ca_old[j])));
354 delta = std::max(delta, std::fabs(num_traits<T>::to_double(PBe[j]) -
355 num_traits<T>::to_double(PBe_old[j])));
356 for (std::size_t i = 0; i < M; ++i)
357 delta = std::max(delta, std::fabs(num_traits<T>::to_double(PBs(i, j)) -
358 num_traits<T>::to_double(PBs_old(i, j))));
359 }
360 if (delta < opt.tol) {
361 res.converged = true;
362 break;
363 }
364 }
365 res.iter = std::min(it, opt.maxiter);
366
367 // Holding-node occupancy. The jobs held in h_ij are physically blocked in
368 // the SERVERS OF STATION i, so they are added back to i rather than
369 // reported at a node the station table does not have.
370 for (std::size_t i = 0; i < M; ++i) {
371 T held = zero;
372 for (std::size_t j = 0; j < M; ++j)
373 if (bas[j] && Pf(i, j) > zero && PBs(i, j) > zero) {
374 const T rateH = T(res.T_[i] * Pf(i, j) * PBs(i, j));
375 const T muH = T(muRes[j] * (one - PBh(i, j)));
376 if (muH > zero) held += T(rateH / muH);
377 }
378 res.Q[i] += held;
379 }
380
381 // Utilization on LINE's convention: the carried flow times the NOMINAL mean
382 // service time per server, so a server held blocked after service does not
383 // count as busy. The ME solution reports E[min(n,c)]/c under the INFLATED
384 // service, which would include the blocking time.
385 for (std::size_t i = 0; i < M; ++i) {
386 if (muf[i] > zero) {
387 res.U[i] = (c[i] <= 0) ? T(res.T_[i] / muf[i])
388 : T(res.T_[i] / (num_traits<T>::from_int(c[i]) * muf[i]));
389 } else {
390 res.U[i] = zero;
391 }
392 }
393 res.W.assign(M, zero);
394 for (std::size_t i = 0; i < M; ++i)
395 if (res.T_[i] > zero) res.W[i] = T(res.Q[i] / res.T_[i]);
396 return res;
397}
398
399} // namespace me
400} // namespace line
401
402#endif // LINE_API_ME_ME_OQN_BLK_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
Mean queue length of a stable infinite-capacity GE/GE/c/FCFS queue.
Censored GE/GE/c/K;N queue by entropy maximisation.
Shared declarations for the maximum-entropy (Kouvatsos) queueing network algorithms.
GegecnResult< T > me_gegecn(const T &lambda, const T &Ca, const T &mu, const T &Cs, long c, long K, long N)
Port of me_gegecn.
Definition me_gegecn.h:75
T me_gegecn_pb(const std::vector< T > &p, long K, long N, long c, const T &Cs, const T &Ca)
Port of me_gegecn_pb.
T me_gegec_mql(const T &lambda, const T &Ca, const T &mu, const T &Cs, long c)
Port of me_gegec_mql.
MeBlkResult< T > me_oqn_blk(std::size_t M, const std::vector< T > &lambda0, const std::vector< T > &Ca0, const std::vector< T > &mu, const std::vector< T > &Cs, const Matrix< T > &P, const std::vector< long > &c, const std::vector< long > &N, const std::vector< int > &blockrule, const MeBlkOptions &opt=MeBlkOptions())
Port of me_oqn_blk.
Definition me_oqn_blk.h:113
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
What me_gegecn returns: the law and the four means read off it.
Definition me_gegecn.h:53
T U
utilization, E[min(n,c)]/c
Definition me_gegecn.h:56
T L
mean number in the queue, sum_n n p(n)
Definition me_gegecn.h:55
std::vector< T > p
p[idx] = Pr{n = K + idx}, idx = 0..N-K
Definition me_gegecn.h:54
Controls of the blocking fixed point, me_oqn_blk's options struct.
Definition me_oqn_blk.h:73
double damping
Relaxation weight on the blocking probabilities, the source's scheme.
Definition me_oqn_blk.h:77
What me_oqn_blk returns, per station.
Definition me_oqn_blk.h:82
std::vector< T > U
utilization on LINE's convention (see the header)
Definition me_oqn_blk.h:86
std::vector< T > Q
mean number present, the jobs held blocked included
Definition me_oqn_blk.h:83
std::vector< T > W
mean response time, Q / T
Definition me_oqn_blk.h:84
std::vector< T > Cd
interdeparture scv
Definition me_oqn_blk.h:88
std::vector< T > T_
throughput, the CARRIED flow
Definition me_oqn_blk.h:85
std::vector< T > PBa
probability an arrival finds the station full
Definition me_oqn_blk.h:89
std::vector< T > lambda
OFFERED arrival rate, the attempts included.
Definition me_oqn_blk.h:90
std::vector< T > Ca
interarrival scv of the offered flow
Definition me_oqn_blk.h:87