LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
npfqn_sqd.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_NPFQN_SQD_H
6#define LINE_API_NPFQN_SQD_H
7
8/**
9 * @file
10 * @ingroup api_npfqn
11 * Smith Queue Decomposition (SQD): approximate MVA for closed networks under
12 * Blocking-After-Service (manufacturing / transfer blocking).
13 *
14 * Templated port of matlab/src/api/npfqn/npfqn_sqd.m. Each finite-buffer
15 * station is described by a load-dependent EFFECTIVE service rate calibrated
16 * from an M/M/1/K blocking probability, and downstream blocking is propagated
17 * through the effective routing between service stations. The recursion is an
18 * AMVA-style sweep over the population 1..N:
19 *
20 * mu_i(n) = n V1_i exp( -[ max(0, (n-1)/beta_i) ]^{gamma_i} ) (13)
21 * W^buf_i = (1 + n) / mu_i(n) (18)
22 * W^svr_i = ST_i (1 + L^svr_i) + (server blocking time) (17)
23 * X = pop / sum_i V_i (W^buf_i + W^svr_i)
24 * L^buf_i = X V_i W^buf_i, L^svr_i = X V_i W^svr_i
25 *
26 * with V1_i deflated by the blocking probability after every population step.
27 * Delay (INF / EXT) stations are infinite-capacity pure delays. The method is
28 * single-chain: it consumes the CHAIN-AGGREGATED demands, i.e. the first
29 * column of MATLAB's sn_get_demands_chain output.
30 *
31 * SIGNATURE. MATLAB's entry point is `npfqn_sqd(sn, N, ...)` and its first act
32 * is to unpack `sn` into six plain arrays; after that line the routine is pure
33 * numerics with no reference to the model layer at all. The port therefore
34 * takes those arrays directly, exactly as the whole pfqn_* family takes L, N, Z
35 * rather than a Network. THE NAME IS UNCHANGED so that the registry keeps its
36 * one-to-one mapping to the MATLAB source. To reconstruct the MATLAB call, a
37 * caller unpacks:
38 *
39 * ST = STchain(:,1) from sn_get_demands_chain(sn)
40 * V = Vchain(:,1) from sn_get_demands_chain(sn)
41 * isDelay(i) = sn.sched(i) is SchedStrategy.INF or .EXT
42 * cap(i) = infinity when isDelay(i), else sn.cap(i), with any
43 * sn.cap(i) > 1e14 also treated as infinite
44 * rt = sn.rt (stateful-indexed routing matrix)
45 * stationToStateful = sn.stationToStateful (ONE-based, as MATLAB stores it)
46 * nclasses = sn.nclasses
47 * N = sn.nclosedjobs when the caller passes nothing
48 *
49 * `computeEffectiveRouting` is NOT hoisted out: collapsing pass-through delay
50 * nodes into a station-to-station chain is algorithm content, not unpacking, so
51 * it is ported here and reads `rt` in the same class-1 slice the reference does,
52 * rt((sf_i-1) nclasses + 1, (sf_j-1) nclasses + 1).
53 *
54 * NO ARITY-COLLIDING OVERLOAD. MATLAB accepts one to seven positional
55 * arguments; every overload here takes eight or nine, and of unrelated types,
56 * so a call transcribed from MATLAB cannot bind to one of them and silently
57 * solve a different model.
58 *
59 * Arithmetic: TRANSCENDENTAL, double and Real only. `exp` in (13), `log` in the
60 * (beta, gamma) calibration, and real powers in both the calibration and the
61 * M/M/1/K blocking probability rho^K / (1 - rho^{K+1}).
62 *
63 * UNEXPLAINED CONSTANTS, reproduced verbatim and NOT rationalized here.
64 * `INITIAL_V1 = 692.192` is the default initial value of the effective-rate
65 * scale V1_i, and `CALIBRATION_EPSILON = 0.05` is the target V_b of the fixed
66 * heuristic calibration (mode 1). Neither is derived anywhere in the reference;
67 * both arrive from the original SolverDBT contribution (Avinash Bommareddy,
68 * Imperial College London FYP, 2026). 692.192 in particular has no stated
69 * units or provenance, and the results DO depend on it whenever the population
70 * sweep is short enough that the deflation V1 <- V1 (1 - pBlock) has not washed
71 * the initial value out. Treat it as an inherited magic number pending an
72 * answer from the contributor, not as a tuned parameter.
73 *
74 * REFERENCE DEFECTS: none found. Reproduces MATLAB to 1e-15 relative on every
75 * combination of the four option switches tried.
76 */
77
78#include <cmath>
79#include <cstddef>
80#include <limits>
81#include <string>
82#include <vector>
83
84#include "line/num/number.h"
85#include "line/util/error.h"
86#include "line/util/matrix.h"
87
88namespace line {
89namespace npfqn {
90
91/** Return value of npfqn_sqd, mirroring [X, Q, U, R]. */
92template <class T>
93struct SqdResult {
94 std::vector<T> X; ///< (M) per-station throughput
95 std::vector<T> Q; ///< (M) per-station queue length
96 std::vector<T> U; ///< (M) per-station utilization, capped at one
97 std::vector<T> R; ///< (M) per-station residence time
98};
99
100/** The four option switches of the reference, with its defaults. */
101template <class T>
103 /// 0 = base (beta = K, gamma = 1), 1 = fixed heuristic, 2 = blocking-aware
105 /// include the server-blocking-time term in W^svr (MATLAB serverBlockingTime)
107 /// true = MATLAB neighborMode 'ownserver', false = 'downstream'
108 bool ownServerNeighbor = false;
109 /// true = MATLAB v1Policy 'fresh', false = 'compound'
110 bool freshV1 = false;
111 /// per-station initial V1; empty selects INITIAL_V1 for every station
112 std::vector<T> initialV1;
113};
114
115namespace detail {
116
117/** Infinite-capacity test; the routine is gated to inexact T, so this is safe. */
118template <class T>
119bool sqd_is_inf(const T& v) {
120 return !std::isfinite(num_traits<T>::to_double(v));
121}
122
123/** Steady-state blocking probability of an M/M/1/K queue at load rho. */
124template <class T>
125T sqd_mm1k_blocking(const T& K, const T& rho) {
126 using std::pow;
127 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
128 if (rho <= num_traits<T>::from_double(1e-15)) return zero;
129 if (num_abs(T(rho - one)) < num_traits<T>::from_double(1e-9))
130 return T(one / T(K + one));
131 return T(T(one - rho) * pow(rho, K) / T(one - pow(rho, T(K + one))));
132}
133
134/** Calibrate (beta, gamma) of the load-dependent effective service rate. */
135template <class T>
136void sqd_beta_gamma(const T& K, const T& pBlockDown, int mode, const T& calEps, T& beta,
137 T& gamma) {
138 using std::log;
139 using std::pow;
140 const T one = num_traits<T>::from_int(1);
141 const T a = num_traits<T>::from_int(2);
142 if (K <= a || sqd_is_inf(K)) {
143 beta = K;
144 gamma = one;
145 return;
146 }
147 const T b = K;
148 T Va, Vb;
149 if (mode == 2) { // blocking-aware
150 T pK = pBlockDown;
151 const T lo = num_traits<T>::from_double(1e-6);
152 const T hi = T(one - lo);
153 if (pK > hi) pK = hi;
154 if (pK < lo) pK = lo;
155 Va = T(one - pK * T(a - one) / T(b - one));
156 Vb = T(one - pK);
157 } else if (mode == 1) { // fixed heuristic
158 Va = T(T(b - a) / b);
159 Vb = calEps;
160 } else { // mode 0: base
161 beta = K;
162 gamma = one;
163 return;
164 }
165 const T va_hi = num_traits<T>::from_double(0.999);
166 const T va_lo = num_traits<T>::from_double(0.01);
167 const T vb_lo = num_traits<T>::from_double(1e-6);
168 if (Va > va_hi) Va = va_hi;
169 if (Va < va_lo) Va = va_lo;
170 if (Vb < vb_lo) Vb = vb_lo;
171 if (Vb >= Va) {
172 beta = K;
173 gamma = one;
174 return;
175 }
176 const T lnVa = log(Va);
177 const T lnVb = log(Vb);
178 gamma = T(log(T(lnVa / lnVb)) / log(T(T(a - one) / T(b - one))));
179 const T g_lo = num_traits<T>::from_double(0.5);
180 const T g_hi = num_traits<T>::from_int(10);
181 if (gamma < g_lo) gamma = g_lo;
182 if (gamma > g_hi) gamma = g_hi;
183 beta = T(T(a - one) / pow(T(-lnVa), T(one / gamma)));
184 if (!std::isfinite(num_traits<T>::to_double(gamma)) ||
185 !std::isfinite(num_traits<T>::to_double(beta)) ||
186 beta <= num_traits<T>::from_int(0)) {
187 beta = K;
188 gamma = one;
189 }
190}
191
192/**
193 * Station-to-station effective routing, collapsing pass-through delay nodes.
194 * Reads the class-1 slice of the stateful-indexed routing matrix, exactly as
195 * MATLAB's computeEffectiveRouting does.
196 */
197template <class T>
198Matrix<T> sqd_effective_routing(const Matrix<T>& rt,
199 const std::vector<std::size_t>& stationToStateful,
200 std::size_t nclasses, const std::vector<bool>& isDelay) {
201 const std::size_t M = isDelay.size();
202 const T zero = num_traits<T>::from_int(0);
203 Matrix<T> p(M, M, zero);
204 const auto rtat = [&](std::size_t i, std::size_t j) {
205 // stationToStateful is one-based, as MATLAB stores it
206 const std::size_t ri = (stationToStateful[i] - 1) * nclasses;
207 const std::size_t rj = (stationToStateful[j] - 1) * nclasses;
208 if (ri >= rt.rows() || rj >= rt.cols())
209 throw InputError("npfqn_sqd: stationToStateful indexes outside the routing matrix");
210 return rt(ri, rj);
211 };
212 for (std::size_t i = 0; i < M; ++i) {
213 if (isDelay[i]) continue;
214 for (std::size_t j = 0; j < M; ++j) {
215 const T p_ij = rtat(i, j);
216 if (p_ij <= zero) continue;
217 if (!isDelay[j]) {
218 p(i, j) += p_ij;
219 } else {
220 for (std::size_t k = 0; k < M; ++k) {
221 if (isDelay[k]) continue;
222 const T p_jk = rtat(j, k);
223 if (p_jk > zero) p(i, k) += p_ij * p_jk;
224 }
225 }
226 }
227 }
228 return p;
229}
230
231} // namespace detail
232
233/**
234 * @brief Smith Queue Decomposition (SQD): approximate MVA for closed networks
235 * under Blocking-After-Service (manufacturing / transfer blocking).
236 *
237 * @param ST (M) chain-aggregated service times, STchain(:,1)
238 * @param V (M) chain-aggregated visit ratios, Vchain(:,1)
239 * @param cap (M) buffer capacities; use infinity for unbounded
240 * @param isDelay (M) true at the INF / EXT stations
241 * @param rt stateful-indexed routing matrix (sn.rt)
242 * @param stationToStateful (M) ONE-based station-to-stateful map
243 * @param nclasses class count, the stride of rt
244 * @param N total closed population
245 * @param opt the four option switches and the initial V1
246 */
247template <class T>
248SqdResult<T> npfqn_sqd(const std::vector<T>& ST, const std::vector<T>& V,
249 const std::vector<T>& cap, const std::vector<bool>& isDelay,
250 const Matrix<T>& rt, const std::vector<std::size_t>& stationToStateful,
251 std::size_t nclasses, int N, const SqdOptions<T>& opt) {
253 "npfqn_sqd requires transcendental arithmetic (the effective-rate "
254 "calibration uses exp, log and real powers)");
255 using std::exp;
256 using std::pow;
257 const std::size_t M = ST.size();
258 if (M == 0) throw InputError("npfqn_sqd: empty station list");
259 if (V.size() != M || cap.size() != M || isDelay.size() != M ||
260 stationToStateful.size() != M)
261 throw InputError("npfqn_sqd: the per-station inputs disagree on the station count");
262 if (nclasses == 0) throw InputError("npfqn_sqd: the class count must be positive");
263 if (N < 0) throw InputError("npfqn_sqd: the population must be nonnegative");
264 if (!opt.initialV1.empty() && opt.initialV1.size() != M)
265 throw InputError("npfqn_sqd: initialV1 must have one entry per station");
266
267 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
268 const T INITIAL_V1 = num_traits<T>::from_double(692.192);
269 const T CALIBRATION_EPSILON = num_traits<T>::from_double(0.05);
270 const T tiny = num_traits<T>::from_double(1e-10);
271 const T eps15 = num_traits<T>::from_double(1e-15);
272
273 const Matrix<T> pEff =
274 detail::sqd_effective_routing(rt, stationToStateful, nclasses, isDelay);
275
276 std::vector<T> V1(M, zero), v1init(M, zero), L_buf(M, zero), L_svr(M, zero);
277 for (std::size_t i = 0; i < M; ++i) {
278 v1init[i] = opt.initialV1.empty() ? INITIAL_V1 : opt.initialV1[i];
279 V1[i] = v1init[i];
280 }
281 T X = zero;
282 std::vector<T> W_buf(M, zero), W_svr(M, zero);
283
284 for (int pop = 1; pop <= N; ++pop) {
285 // ---- wait times ------------------------------------------------------
286 for (std::size_t i = 0; i < M; ++i) {
287 if (isDelay[i]) {
288 W_buf[i] = zero;
289 W_svr[i] = ST[i];
290 continue;
291 }
292 T pBlockDown = zero;
293 if (opt.ownServerNeighbor) {
294 if (!detail::sqd_is_inf(cap[i]))
295 pBlockDown = detail::sqd_mm1k_blocking(cap[i], T(X * V[i] * ST[i]));
296 } else {
297 for (std::size_t j = 0; j < M; ++j)
298 if (!isDelay[j] && pEff(i, j) > zero && !detail::sqd_is_inf(cap[j]))
299 pBlockDown += pEff(i, j) *
300 detail::sqd_mm1k_blocking(cap[j], T(X * V[j] * ST[j]));
301 }
302
303 T n = zero;
304 if (opt.ownServerNeighbor) {
305 n = L_svr[i];
306 } else {
307 for (std::size_t j = 0; j < M; ++j) n += pEff(i, j) * L_svr[j];
308 }
309
310 T mu_n;
311 if (n < tiny) {
312 mu_n = V1[i];
313 } else {
314 T beta, gamma;
315 detail::sqd_beta_gamma(cap[i], pBlockDown, opt.calibrationMode,
316 CALIBRATION_EPSILON, beta, gamma);
317 T base = T(T(n - one) / beta);
318 if (base < zero) base = zero;
319 const T expArg = pow(base, gamma);
320 mu_n = T(n * V1[i] * exp(T(-expArg))); // Eq. 13
321 }
322 if (mu_n < tiny) mu_n = tiny;
323
324 W_buf[i] = T(one / mu_n * T(one + n)); // Eq. 18
325 W_svr[i] = T(ST[i] * T(one + L_svr[i])); // Eq. 17
326
327 if (opt.serverBlockingTime) {
328 T bt = zero;
329 for (std::size_t j = 0; j < M; ++j) {
330 if (isDelay[j] || !(pEff(i, j) > zero) || detail::sqd_is_inf(cap[j])) continue;
331 const T pBj = detail::sqd_mm1k_blocking(cap[j], T(X * V[j] * ST[j]));
332 const T denom = T(ST[i] + ST[j]);
333 T theta = zero;
334 if (denom > eps15) theta = T(ST[j] / denom);
335 bt += pEff(i, j) * pBj * ST[j] * theta;
336 }
337 W_svr[i] += bt;
338 }
339 }
340
341 // ---- throughput ------------------------------------------------------
342 T sumVW = zero;
343 for (std::size_t i = 0; i < M; ++i) sumVW += V[i] * T(W_buf[i] + W_svr[i]);
344 X = (sumVW > eps15) ? T(num_traits<T>::from_int(pop) / sumVW) : zero;
345
346 // ---- queue lengths ---------------------------------------------------
347 for (std::size_t i = 0; i < M; ++i) {
348 L_buf[i] = X * V[i] * W_buf[i];
349 L_svr[i] = X * V[i] * W_svr[i];
350 }
351
352 // ---- deflate V1 ------------------------------------------------------
353 if (pop < N) {
354 for (std::size_t i = 0; i < M; ++i) {
355 if (isDelay[i]) continue;
356 T pBlock = zero;
357 if (opt.ownServerNeighbor) {
358 if (!detail::sqd_is_inf(cap[i]))
359 pBlock = detail::sqd_mm1k_blocking(cap[i], T(X * V[i] * ST[i]));
360 } else {
361 for (std::size_t j = 0; j < M; ++j)
362 if (!isDelay[j] && pEff(i, j) > zero && !detail::sqd_is_inf(cap[j]))
363 pBlock += pEff(i, j) *
364 detail::sqd_mm1k_blocking(cap[j], T(X * V[j] * ST[j]));
365 }
366 V1[i] = opt.freshV1 ? T(v1init[i] * T(one - pBlock))
367 : T(V1[i] * T(one - pBlock));
368 if (V1[i] < tiny) V1[i] = tiny;
369 }
370 }
371 }
372
373 SqdResult<T> res;
374 res.X.assign(M, zero);
375 res.Q.assign(M, zero);
376 res.U.assign(M, zero);
377 res.R.assign(M, zero);
378 for (std::size_t i = 0; i < M; ++i) {
379 const T T_i = X * V[i];
380 const T Q_i = T(L_buf[i] + L_svr[i]);
381 const T W_tot = T(W_buf[i] + W_svr[i]);
382 res.X[i] = T_i;
383 res.Q[i] = Q_i;
384 res.R[i] = (T_i > eps15) ? T(Q_i / T_i) : W_tot;
385 const T u = T(T_i * ST[i]);
386 res.U[i] = (u < one) ? u : one;
387 }
388 return res;
389}
390
391/**
392 * Overload with the reference's default option switches. Eight arguments, so it
393 * cannot collide with a MATLAB call transcribed positionally (the reference
394 * accepts at most seven, starting with an sn).
395 */
396template <class T>
397SqdResult<T> npfqn_sqd(const std::vector<T>& ST, const std::vector<T>& V,
398 const std::vector<T>& cap, const std::vector<bool>& isDelay,
399 const Matrix<T>& rt, const std::vector<std::size_t>& stationToStateful,
400 std::size_t nclasses, int N) {
401 return npfqn_sqd(ST, V, cap, isDelay, rt, stationToStateful, nclasses, N, SqdOptions<T>());
402}
403
404} // namespace npfqn
405} // namespace line
406
407#endif // LINE_API_NPFQN_SQD_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
Dense matrix and non-owning view.
SqdResult< T > npfqn_sqd(const std::vector< T > &ST, const std::vector< T > &V, const std::vector< T > &cap, const std::vector< bool > &isDelay, const Matrix< T > &rt, const std::vector< std::size_t > &stationToStateful, std::size_t nclasses, int N, const SqdOptions< T > &opt)
Smith Queue Decomposition (SQD): approximate MVA for closed networks under Blocking-After-Service (ma...
Definition npfqn_sqd.h:248
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
The four option switches of the reference, with its defaults.
Definition npfqn_sqd.h:102
bool ownServerNeighbor
true = MATLAB neighborMode 'ownserver', false = 'downstream'
Definition npfqn_sqd.h:108
int calibrationMode
0 = base (beta = K, gamma = 1), 1 = fixed heuristic, 2 = blocking-aware
Definition npfqn_sqd.h:104
bool freshV1
true = MATLAB v1Policy 'fresh', false = 'compound'
Definition npfqn_sqd.h:110
bool serverBlockingTime
include the server-blocking-time term in W^svr (MATLAB serverBlockingTime)
Definition npfqn_sqd.h:106
std::vector< T > initialV1
per-station initial V1; empty selects INITIAL_V1 for every station
Definition npfqn_sqd.h:112
Return value of npfqn_sqd, mirroring [X, Q, U, R].
Definition npfqn_sqd.h:93
std::vector< T > R
(M) per-station residence time
Definition npfqn_sqd.h:97
std::vector< T > Q
(M) per-station queue length
Definition npfqn_sqd.h:95
std::vector< T > U
(M) per-station utilization, capped at one
Definition npfqn_sqd.h:96
std::vector< T > X
(M) per-station throughput
Definition npfqn_sqd.h:94