LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_ldqbd.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_MAM_SOLVER_MAM_LDQBD_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_LDQBD_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_mam_ldqbd.m`: the level-dependent QBD analyzer for a
12 * single-class network of one infinite server and one FCFS queue.
13 *
14 * WHY THIS MATTERS MORE THAN ITS SIZE SUGGESTS. It is the one branch of the MAM
15 * ladder where the reference prefers an EXACT method over the `dec.source`
16 * decomposition, and `solver_mam_analyzer.m` routes `default` here for a
17 * single-class closed Delay+Queue. The level-dependent arrival rate
18 * `(N - n) lambda` captures the population constraint that `dec.source` can only
19 * approximate through its throughput fixed point, so on this shape the two
20 * answers are not close: on the test model below the LD-QBD queue length is
21 * 1.36 against dec.source's 1.42, and the LD-QBD one is right.
22 *
23 * EXACTNESS: exact for exponential service at any number of servers, and for PH
24 * service at any number of servers. The multiserver PH chain comes from
25 * `ldqbd_mphc`, whose level coordinate is the MULTISET of the phases the
26 * min(n,c) busy servers sit in. The collapsed single-phase approximation the
27 * reference carried until 2026-08-18 -- one PH process run at min(n,c) times its
28 * speed, ~1e-2 relative against SolverCTMC -- is gone from every codebase.
29 *
30 * TWO REGIMES, one generator. Closed: level n is the number at the queue,
31 * 0 <= n <= N, and the arrival rate out of level n is `(N-n) lambda_eff`, which
32 * vanishes at n = N and closes the chain by itself. Open: Poisson arrivals at a
33 * constant `lambda_eff`, truncated at a level chosen so the tail probability is
34 * below 1e-10 (or at `options.cutoff`). Only the per-level arrival rate and the
35 * top level differ.
36 *
37 * ARITHMETIC. `ldqbd_R` runs a backward recursion of matrix inverses and its
38 * `pinv` fallback needs singular vectors, so the whole path is gated on
39 * transcendental arithmetic and refuses by name under exact/Rational.
40 */
41
42#include <algorithm>
43#include <cmath>
44#include <cstddef>
45#include <string>
46#include <vector>
47
48#include "line/api/mam/ldqbd.h"
56#include "line/util/error.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace mam {
61
62/**
63 * The LD-QBD blocks and parameters, the reference's optional eighth output.
64 *
65 * Built unconditionally here rather than behind a `nargout` test: C++ has no
66 * such thing, the cost is a few pointers, and the SolverENV state-vector
67 * analyzer is the consumer the reference built it for.
68 */
69template <class T>
71 std::vector<Matrix<T>> Q0, Q1, Q2;
72 std::size_t Nlev = 0;
73 std::size_t nPhases = 1;
74 bool isPH = false;
75 bool isOpen = false;
76 std::size_t queueIdx = 0, refIdx = 0, M = 0;
77 double nServers = 1.0;
79 bool hasLLD = false;
80 /** Per-level service factor sf(n), with sf[0] unused so it lines up by level. */
81 std::vector<T> sf;
82 /**
83 * The capacity that normalizes the utilization: max(c, max(alpha)), the
84 * LARGEST factor the load-dependence table declares rather than the
85 * saturated one, since a non-monotone alpha peaks in the middle. Same rule
86 * as CTMC's ceff, which is what makes the two report the same number.
87 */
88 double utilPeak = 1.0;
91 double N = 0.0;
92 /**
93 * The station alternates OFF -> setup -> busy -> delay-off around the
94 * service, so the chain carries phases the block builder has no place for.
95 * When this is set the blocks above describe a server that is ALWAYS warm
96 * and must not be used: the closed regime hands the whole chain to
97 * `qbd_setupdelayoff_closed` instead.
98 */
99 bool hasSetup = false;
102};
103
104/** What the analyzer returns: the metrics plus the blocks it built them from. */
105template <class T>
110
111/**
112 * Port of `solver_mam_ldqbd.m`.
113 *
114 * @param L the refreshed struct; must be single-class, two stations, and
115 * either Delay+Queue (closed) or Source+Queue (open)
116 * @param opt the MAM options; `cutoff` bounds the open truncation
117 */
118template <class T>
120 if constexpr (!num_traits<T>::has_transcendental) {
121 throw UnsupportedError(
122 "solver_mam_ldqbd: the level-dependent QBD recursion inverts a matrix per level and "
123 "falls back to a pseudo-inverse (singular vectors) when a level is singular, neither "
124 "of which is exact arithmetic; rerun with --arith double or --arith real");
125 } else {
127 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
128 const std::size_t M = L.nstations, K = L.nclasses;
129
130 if (K != 1)
131 throw UnsupportedError("solver_mam_ldqbd: the LDQBD method requires a single-class model");
132
133 std::size_t nDelay = 0, nQueue = 0, nSource = 0;
134 std::size_t delayIdx = 0, queueIdx = 0, srcIdx = 0;
135 for (std::size_t i = 1; i <= M; ++i) {
136 const SchedStrategy s = L.stations[i - 1].sched;
137 if (s == SchedStrategy::INF) { ++nDelay; delayIdx = i; }
138 else if (s == SchedStrategy::FCFS) { ++nQueue; queueIdx = i; }
139 else if (s == SchedStrategy::EXT) { ++nSource; srcIdx = i; }
140 }
141 const bool isOpen = std::isinf(L.classes[0].population);
142 if (isOpen) {
143 if (nSource != 1 || nQueue != 1 || M != 2)
144 throw UnsupportedError(
145 "solver_mam_ldqbd: the open LDQBD method requires exactly one Source and one "
146 "Queue station");
147 } else {
148 if (nDelay != 1 || nQueue != 1 || M != 2)
149 throw UnsupportedError(
150 "solver_mam_ldqbd: the closed LDQBD method requires exactly one Delay and one "
151 "Queue station");
152 }
153
154 // ---- the service process at the queue --------------------------------
155 const Map<T> PHq = lang::dist_to_map(L.service[queueIdx - 1][0]);
156 const double nServers = L.stations[queueIdx - 1].nservers;
157 const std::size_t nPhases = PHq.D0.rows();
158 const bool isPH = nPhases > 1;
159 T mu = zero, mean_service = zero;
160 std::vector<T> alpha;
161 if (!isPH) {
162 mu = T(-PHq.D0(0, 0));
163 if (!(num_traits<T>::to_double(mu) > 0.0))
164 throw InputError("solver_mam_ldqbd: the queue has a non-positive service rate");
165 mean_service = T(one / mu);
166 } else {
167 alpha = map_pie(PHq);
168 mean_service = map_mean(PHq);
169 }
170
171 // ---- setup and delay-off at the queue --------------------------------
172 // Refused BY NAME outside the closed, single-server, exponential,
173 // load-independent case rather than answered as if the server were always
174 // warm, which is what this solver did until 2026-09 and is BUG-78.
175 bool hasSetup = false;
176 T alpharate = zero, alphascv = one, betarate = zero, betascv = one;
177 {
178 const typename std::map<std::size_t, qn::SetupDelayOffParam<T>>::const_iterator sit =
179 L.setupparam.find(queueIdx);
180 if (sit != L.setupparam.end()) {
181 lang::Distrib<T> su, doff;
182 if (sit->second.last(su, doff) && !doff.disabled) {
183 if (isOpen)
184 throw InputError(
185 "solver_mam_ldqbd: open LDQBD does not model a setup/delay-off server; "
186 "use method 'dec.source', whose qbd_setupdelayoff covers the open case");
187 if (isPH || nServers > 1)
188 throw InputError(
189 "solver_mam_ldqbd: closed LDQBD models a setup/delay-off server with "
190 "exponential service at a single server only; this station has "
191 "phase-type service or several servers");
192 hasSetup = true;
193 alpharate = T(one / su.mean);
194 alphascv = su.scv;
195 betarate = T(one / doff.mean);
196 betascv = doff.scv;
197 }
198 }
199 }
200
201 // ---- the per-level service factor ------------------------------------
202 // sn.lldscaling when present, else min(n, c).
203 const std::vector<T>& lld = L.stations[queueIdx - 1].lldscaling;
204 bool hasLLD = false;
205 for (const T& v : lld)
206 if (v != one) hasLLD = true;
207 const double sfMax = hasLLD ? num_traits<T>::to_double(lld.back()) : nServers;
208 // The capacity that normalizes the utilization is the LARGEST factor the
209 // table declares, not the saturated one: a non-monotone alpha peaks in the
210 // middle. Same rule as CTMC's ceff = max(nservers, max(lldscaling(ist,:))),
211 // which is what makes the two report the same number.
212 double utilPeak = nServers;
213 if (hasLLD)
214 for (const T& v : lld) utilPeak = std::max(utilPeak, num_traits<T>::to_double(v));
215
216 // ---- the per-level arrival rate and the number of levels -------------
217 const std::size_t Kc = K;
218 auto rt_at = [&](std::size_t from, std::size_t to) -> T {
219 const std::size_t a = (L.stateful_of_station(from) - 1) * Kc;
220 const std::size_t b = (L.stateful_of_station(to) - 1) * Kc;
221 if (a >= L.rt.rows() || b >= L.rt.cols()) return zero;
222 return L.rt(a, b);
223 };
224
225 std::size_t Nlev = 0;
226 T lambda_eff = zero, delayRate = zero;
227 std::vector<T> arrRate;
228 if (isOpen) {
229 const Map<T> arv = lang::dist_to_map(L.service[srcIdx - 1][0]);
230 if (arv.D0.rows() > 1)
231 throw UnsupportedError(
232 "solver_mam_ldqbd: the open LDQBD method currently supports Poisson (exponential) "
233 "arrivals only; the Source uses a MAP/MMPP process");
234 const T lambda = L.rates(srcIdx - 1, 0);
235 lambda_eff = T(lambda * rt_at(srcIdx, queueIdx));
236 const double rho =
237 num_traits<T>::to_double(T(lambda_eff * mean_service)) / (sfMax > 0.0 ? sfMax : 1.0);
238 if (rho >= 1.0)
239 throw NumericError(
240 "solver_mam_ldqbd: the open LDQBD method requires a stable queue (rho = " +
241 std::to_string(rho) +
242 " >= 1). Increase service capacity or reduce the arrival rate");
243 const std::size_t c = static_cast<std::size_t>(
244 std::isfinite(nServers) ? std::llround(nServers) : 1);
245 if (opt.cutoff > 0) {
246 Nlev = std::max(c + 1, opt.cutoff);
247 } else {
248 const double tailTol = 1e-10;
249 const long lv =
250 static_cast<long>(c) + static_cast<long>(std::ceil(std::log(tailTol) / std::log(rho)));
251 Nlev = static_cast<std::size_t>(
252 std::min<long>(std::max<long>(lv, static_cast<long>(c) + 10), 100000));
253 }
254 arrRate.assign(Nlev + 1, lambda_eff);
255 arrRate[Nlev] = zero; // truncation: no arrivals above the top level
256 } else {
257 delayRate = L.rates(delayIdx - 1, 0);
258 lambda_eff = T(delayRate * rt_at(delayIdx, queueIdx));
259 const double Nd = L.classes[0].population;
260 Nlev = static_cast<std::size_t>(std::llround(Nd));
261 arrRate.assign(Nlev + 1, zero);
262 // Finite-source rate (N - n) lambda_eff, which is zero at n = N and
263 // closes the chain without a truncation.
264 for (std::size_t n = 0; n <= Nlev; ++n)
265 arrRate[n] = T(num_traits<T>::from_double(Nd - static_cast<double>(n)) * lambda_eff);
266 }
267 if (Nlev < 1)
268 throw UnsupportedError(
269 "solver_mam_ldqbd: the model has no levels to solve (a zero population)");
270
271 std::vector<T> sf(Nlev + 1, zero); // sf[n] for n = 1..Nlev
272 for (std::size_t n = 1; n <= Nlev; ++n) {
273 if (hasLLD)
274 sf[n] = lld[std::min(n, lld.size()) - 1];
275 else
277 std::min(static_cast<double>(n), std::isfinite(nServers) ? nServers : 1.0));
278 }
279
280 // ---- the block-tridiagonal generator ---------------------------------
281 // Q0[n] level n -> n+1 (arrival), Q1[n] local, Q2[n] level n -> n-1.
282 // Q2 carries an unused entry at index 0 so the three line up by level, as
283 // the C++ ldqbd takes them.
284 std::vector<Matrix<T>> Q0(Nlev), Q1(Nlev + 1), Q2(Nlev + 1);
285 if (!isPH) {
286 for (std::size_t n = 0; n + 1 <= Nlev; ++n) Q0[n] = Matrix<T>(1, 1, arrRate[n]);
287 for (std::size_t n = 0; n <= Nlev; ++n) {
288 const T dep = (n > 0) ? T(sf[n] * mu) : zero;
289 Q1[n] = Matrix<T>(1, 1, T(-(arrRate[n] + dep)));
290 }
291 Q2[0] = Matrix<T>(1, 1, zero);
292 for (std::size_t n = 1; n <= Nlev; ++n) Q2[n] = Matrix<T>(1, 1, T(sf[n] * mu));
293 } else {
294 // PH service: the level carries the MULTISET of the phases the min(n,c)
295 // busy servers sit in, which is exact at any number of servers. At c = 1
296 // the multiset is just the phase, so this reproduces the single-server
297 // blocks (sf(n) D0 - arr I, sf(n) D1) entry for entry. `sf` is indexed
298 // from 1 here and from 0 there, hence the shifted copy.
299 std::vector<T> sf1(Nlev, zero);
300 for (std::size_t n = 1; n <= Nlev; ++n) sf1[n - 1] = sf[n];
302 ldqbd_mphc(PHq.D0, PHq.D1, alpha, nServers, arrRate, sf1);
303 Q0 = blk.Q0;
304 Q1 = blk.Q1;
305 Q2 = blk.Q2;
306 }
307
308 std::vector<T> p;
309 T mean_queue = zero, x_setup = zero;
310 if (hasSetup) {
311 // SETUP AND DELAY-OFF, the closed vacation queue. The level-dependent
312 // chain this needs is the one above with two extra phase families -- the
313 // setup above level 0 and the delay-off at level 0 -- and
314 // `qbd_setupdelayoff_closed` builds and solves exactly that, so it is
315 // called rather than duplicated. Without it the blocks above describe a
316 // server that is ALWAYS warm and the answer is byte-identical across any
317 // setup mean (BUG-78).
319 num_traits<T>::from_double(L.classes[0].population), T(one / lambda_eff), mu,
320 alpharate, alphascv, betarate, betascv);
321 mean_queue = cr.QN;
322 x_setup = cr.XN;
323 } else {
324 const LdqbdResult<T> res = ldqbd(Q0, Q1, Q2);
325 p = res.pi.pi;
326 if (p.size() != Nlev + 1)
327 throw NumericError("solver_mam_ldqbd: the LD-QBD solve returned the wrong level count");
328
329 for (std::size_t n = 0; n <= Nlev; ++n)
330 mean_queue += num_traits<T>::from_int(static_cast<int>(n)) * p[n];
331 }
332
333 // Utilization is the fraction of the station's PEAK capacity in use,
334 // sum_n p(n)*sf(n)/utilPeak, the work-based convention CTMC, MVA, NC and
335 // serial SSA all report. Without load dependence sf(n) = min(n,c) and
336 // utilPeak = c, giving the mean fraction of the c servers in use; at c = 1
337 // that is sf(n) = 1 for every n >= 1, so the sum collapses to 1 - p(0).
338 //
339 // It used to report 1 - p(0) under load dependence, i.e. P(busy), which
340 // reads a station running alpha(n) times faster as no busier than one at
341 // its nominal rate: 0.9587 against CTMC's 0.6612 on a 4-job closed model
342 // with alpha = [1 1.5 2 2.5].
343 T util = zero;
344 if (hasSetup) {
345 // With a setup the server is DELIVERING work only in the busy phase, so
346 // the level occupancy over-counts it: a level is occupied during the
347 // setup too. The utilization law gives the same work-based number
348 // without the per-phase vector, X*E[S]/peak, which is what the level sum
349 // reduces to without a setup.
350 util = T(x_setup * mean_service / num_traits<T>::from_double(utilPeak));
351 } else {
352 const T peak = num_traits<T>::from_double(utilPeak);
353 for (std::size_t n = 1; n <= Nlev; ++n) util += T(sf[n] / peak * p[n]);
354 }
355
357 mva::MvaSolution<T>& s = out.sol;
358 s.Q = Matrix<T>(M, K, zero);
359 s.U = Matrix<T>(M, K, zero);
360 s.R = Matrix<T>(M, K, zero);
361 s.Tp = Matrix<T>(M, K, zero);
362 s.C.assign(K, zero);
363 s.X.assign(K, zero);
364 s.iter = 1; // LDQBD is a direct method
365
366 if (isOpen) {
367 // Served throughput = arrival rate less the truncation blocking.
368 const T X = T(lambda_eff * T(one - p[Nlev]));
369 const T Rq = (X > zero) ? T(mean_queue / X) : zero;
370 s.Tp(srcIdx - 1, 0) = X;
371 s.Q(queueIdx - 1, 0) = mean_queue;
372 s.U(queueIdx - 1, 0) = util;
373 s.R(queueIdx - 1, 0) = Rq;
374 s.Tp(queueIdx - 1, 0) = X;
375 s.X[0] = X;
376 s.C[0] = Rq;
377 } else {
378 const T mean_delay = T(num_traits<T>::from_double(L.classes[0].population) - mean_queue);
379 const T X = T(mean_delay * lambda_eff);
380 const T Rq = (X > zero) ? T(mean_queue / X) : zero;
381 const T Rd = T(one / delayRate);
382 // The delay completes at mean_delay * lambda_d, of which only the
383 // fraction rt(delay, queue) proceeds to the queue, so its throughput is
384 // NOT the queue flow X whenever the delay routes elsewhere.
385 s.Q(delayIdx - 1, 0) = mean_delay;
386 s.U(delayIdx - 1, 0) = mean_delay; // infinite server: U = Q
387 s.R(delayIdx - 1, 0) = Rd;
388 s.Tp(delayIdx - 1, 0) = T(mean_delay * delayRate);
389 s.Q(queueIdx - 1, 0) = mean_queue;
390 s.U(queueIdx - 1, 0) = util;
391 s.R(queueIdx - 1, 0) = Rq;
392 s.Tp(queueIdx - 1, 0) = X;
393 s.X[0] = X;
394 s.C[0] = T(Rd + Rq);
395 }
396
397 LdqbdBlocks<T>& b = out.ld;
398 b.Q0 = Q0;
399 b.Q1 = Q1;
400 b.Q2 = Q2;
401 b.Nlev = Nlev;
402 b.nPhases = nPhases;
403 b.isPH = isPH;
404 b.isOpen = isOpen;
405 b.queueIdx = queueIdx;
406 b.refIdx = isOpen ? srcIdx : delayIdx;
407 b.M = M;
408 b.nServers = nServers;
409 b.mean_service = mean_service;
410 b.hasLLD = hasLLD;
411 b.sf = sf;
412 b.utilPeak = utilPeak;
413 b.lambda_eff = lambda_eff;
414 b.hasSetup = hasSetup;
415 b.alpharate = alpharate;
416 b.alphascv = alphascv;
417 b.betarate = betarate;
418 b.betascv = betascv;
419 b.delayRate = isOpen ? zero : delayRate;
420 b.N = isOpen ? std::numeric_limits<double>::infinity() : L.classes[0].population;
421 return out;
422 } // if constexpr has_transcendental
423}
424
425} // namespace mam
426} // namespace line
427
428#endif // LINE_SOLVERS_MAM_SOLVER_MAM_LDQBD_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t stateful_of_station(std::size_t st) const
std::vector< std::vector< Distrib< T > > > service
service[i][r], 0-based station and class; a disabled entry marks a pair never visited.
std::map< std::size_t, SetupDelayOffParam< T > > setupparam
Setup / delay-off, keyed by 1-based STATION index.
Matrix< T > rt
sn.rt and sn.rtnodes: the class-expanded routing.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Level-dependent QBD processes with finitely many levels: the rate matrices R^(n) by the backward matr...
Port of ldqbd_mphc.m and ph_multisets.m: the exact level-dependent QBD blocks of an M/PH/c queue.
The option and result types SolverMAM shares with its analyzers.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
The option and result types every MVA analyzer shares.
mam::Map< T > dist_to_map(const Distrib< T > &d)
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
LdqbdMphcBlocks< T > ldqbd_mphc(const Matrix< T > &D0, const Matrix< T > &D1, const std::vector< T > &alpha, double c, const std::vector< T > &arrRate, const std::vector< T > &sf)
Block-tridiagonal generator of an M/PH/c queue with level-dependent arrivals.
Definition ldqbd_mphc.h:111
LdqbdSolution< T > solver_mam_ldqbd(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_ldqbd.m.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
LdqbdResult< T > ldqbd(const std::vector< Matrix< T > > &q0, const std::vector< Matrix< T > > &q1, const std::vector< Matrix< T > > &q2)
Solve a level-dependent QBD: rate matrices and stationary law (ldqbd.m).
Definition ldqbd.h:246
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
SetupDelayoffClosed< T > qbd_setupdelayoff_closed(const T &N, const T &Z, const T &mu, const T &alpharate, const T &alphascv, const T &betarate, const T &betascv)
Mean queue length and throughput of a FINITE-POPULATION queue with setup delay and delay-off,...
A queueing network and its refreshed NetworkStruct.
Mean queue length of an M/M/1 queue with a setup delay and a delayed-off period, solved as a QBD.
The LD-QBD blocks and parameters, the reference's optional eighth output.
std::vector< Matrix< T > > Q0
std::vector< Matrix< T > > Q1
std::vector< Matrix< T > > Q2
std::vector< T > sf
Per-level service factor sf(n), with sf[0] unused so it lines up by level.
double utilPeak
The capacity that normalizes the utilization: max(c, max(alpha)), the LARGEST factor the load-depende...
bool hasSetup
The station alternates OFF -> setup -> busy -> delay-off around the service, so the chain carries pha...
The three block lists of a level-dependent QBD, as ldqbd takes them.
Definition ldqbd_mphc.h:88
R and the stationary distribution together (ldqbd.m).
Definition ldqbd.h:239
LdqbdPi< T > pi
Definition ldqbd.h:241
What the analyzer returns: the metrics plus the blocks it built them from.
mva::MvaSolution< T > sol
The options SolverMAM reads.
Definition mam_types.h:29
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
Mean queue length and throughput of the CLOSED setup/delay-off queue.
T QN
mean number of jobs at the station
T XN
throughput of the station
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
std::vector< T > C
Definition mva_types.h:98