1function [QN, UN, RN, TN, CN, XN, totiter, ld] = solver_mam_ldqbd(sn, options)
2% SOLVER_MAM_LDQBD Solve single-
class Delay/Queue networks using LD-QBD
4% Uses a Level-Dependent Quasi-Birth-Death (LD-QBD) process to compute
5% performance metrics
for single-
class networks with one infinite-server station
6% and one FCFS Queue (multi-server, PH service supported).
8% Exactness: exact
for exponential service at any number of servers, and
for PH
9% service at a single server. For PH service with c > 1 servers it
is an
10% approximation:
the c parallel PH servers are collapsed into one PH process
11% scaled by min(n,c), which ignores
the phase of each individual busy server
12% (
the exact chain tracks
the multiset of
the min(n,c) in-service phases).
13% Measured against SolverCTMC
the residual error
is ~1e-2 relative on Erlang and
14% HyperExp, still ~16x below
the dec.source error on
the same models.
16% Two regimes are handled:
17% CLOSED: one Delay (INF) + one Queue, finite population N. Level n = jobs at
18%
the queue (0 <= n <= N); arrival rate from
the delay
is (N-n)*lambda.
19% OPEN: one Source (EXT) + one Queue, open
class (Poisson arrivals). Level
20% n = jobs at
the queue, truncated at M_trunc; arrival rate
is the
21% constant external rate lambda. M_trunc
is taken from options.cutoff
22% or chosen so
the truncated tail probability
is negligible.
24% Both regimes share
the same block-tridiagonal generator, differing only in
the
25% per-level arrival rate and
the top level. Service
is min(n,c)*mu (exact M/M/c
26% boundary) or its PH generalisation.
28% Copyright (c) 2012-2026, Imperial College London
31%% Validate model structure
37 line_error(mfilename, 'LDQBD method
requires a single-
class model.');
40nDelay = sum(sn.sched == SchedStrategy.INF);
41nQueue = sum(sn.sched == SchedStrategy.FCFS);
42nSource = sum(sn.sched == SchedStrategy.EXT);
46 if nSource ~= 1 || nQueue ~= 1 || M ~= 2
47 line_error(mfilename,
'Open LDQBD method requires exactly one Source and one Queue station.');
50 if nDelay ~= 1 || nQueue ~= 1 || M ~= 2
51 line_error(mfilename,
'Closed LDQBD method requires exactly one Delay and one Queue station.');
56queueIdx = find(sn.sched == SchedStrategy.FCFS);
58 srcIdx = find(sn.sched == SchedStrategy.EXT);
60 delayIdx = find(sn.sched == SchedStrategy.INF);
63%% Service process at
the queue
66nservers = sn.nservers;
67PH_queue = PH{queueIdx}{1};
68nServers = nservers(queueIdx);
70if numel(PH_queue{1}) == 1
71 mu = -PH_queue{1}; % exponential service rate
76 nPhases = size(PH_queue{1}, 1);
80 alpha = map_pie(PH_queue);
81 mean_service = map_mean(PH_queue);
84%% Per-level service
factor: load-dependent scaling
if set,
else min(n,c)
85% sn.lldscaling(queueIdx, n)
is the load-dependent multiplier on
the base rate;
86% when absent, a c-server queue scales as min(n,c). This
is the level-dependent
87%
factor that
the LD-QBD applies in each downward (departure) block.
88hasLLD = sn_has_load_dependence(sn) && ~isempty(sn.lldscaling) ...
89 && size(sn.lldscaling, 1) >= queueIdx && any(sn.lldscaling(queueIdx, :) ~= 1);
91 lld = sn.lldscaling(queueIdx, :);
92 lldlimit = numel(lld);
93 sfMax = lld(lldlimit); % saturated
factor (capacity ceiling)
100%% Arrival rate per level and number of levels
103 % External Poisson arrivals only (MAP/MMPP arrivals are not yet supported).
104 arrProc = PH{srcIdx}{1};
105 if numel(arrProc{1}) > 1
106 line_error(mfilename, [
'Open LDQBD method currently supports Poisson (exponential) ' ...
107 'arrivals only; the Source uses a MAP/MMPP process.']);
109 lambda = rates(srcIdx, 1);
110 lambda_eff = lambda * rt(srcIdx, queueIdx);
111 rho = lambda_eff * mean_service / sfMax; % sfMax = saturated capacity
factor
113 line_error(mfilename, sprintf([
'Open LDQBD method requires a stable queue ' ...
114 '(rho = %.4f >= 1). Increase service capacity or reduce the arrival rate.'], rho));
116 % Truncation level:
explicit cutoff,
else enough levels
for a negligible tail.
117 if isfield(options,
'cutoff') && isscalar(options.cutoff) && isfinite(options.cutoff)
118 Nlev = max(nServers + 1, round(options.cutoff));
121 Nlev = nServers + ceil(log(tailTol) / log(rho));
122 Nlev = min(max(Nlev, nServers + 10), 100000);
124 arrRate = lambda_eff * ones(1, Nlev + 1); % arrRate(n+1)
is the rate out of level n
125 arrRate(Nlev + 1) = 0; % truncation: no arrivals above
the top level
127 lambda_d = rates(delayIdx, 1);
128 lambda_eff = lambda_d * rt(delayIdx, queueIdx);
130 arrRate = (N - (0:N)) * lambda_eff; % finite-source rate (N-n)*lambda_eff, 0 at n=N
133% Per-level service
factor sf(n), n = 1..Nlev
137 sf(n) = lld(min(n, lldlimit));
139 sf(n) = min(n, nServers);
143%% Construct LD-QBD block-tridiagonal generator
144% Q0^(n): upward (arrival) Q1^(n): local Q2^(n): downward (departure)
146Q1 = cell(Nlev + 1, 1);
151 Q0{n+1} = arrRate(n+1);
156 departure_rate = sf(n) * mu;
158 Q1{n+1} = -(arrRate(n+1) + departure_rate);
164 Q0{1} = arrRate(1) * alpha; % level 0 -> 1: start service in a phase
166 Q0{n+1} = arrRate(n+1) * eye(nPhases); % level n -> n+1: preserve phase
168 Q1{1} = -arrRate(1); % level 0: only arrivals
170 Q1{n+1} = sf(n) * D0 - arrRate(n+1) * eye(nPhases);
172 Q2{1} = sf(1) * D1 * ones(nPhases, 1); % level 1 -> 0: empty
the queue
174 Q2{n} = sf(n) * D1; % level n -> n-1: complete and restart
179ldqbd_options =
struct(
'epsilon', options.tol,
'maxIter', options.iter_max,
'verbose',
false);
180[R, pi_ldqbd] = ldqbd(Q0, Q1, Q2, ldqbd_options); %#ok<ASGLU>
182%% Performance metrics (per-level stationary distribution pi_ldqbd)
183mean_queue = (0:Nlev) * pi_ldqbd
';
185% Utilization: probability busy for a (load-dependent) single server, else the
186% average fraction of c servers in use.
187if hasLLD || nServers == 1
188 util_ps = 1 - pi_ldqbd(1);
192 util_ps = util_ps + (min(n, nServers) / nServers) * pi_ldqbd(n+1);
204 % Accepted/served throughput = arrival rate minus truncation blocking
205 % (= lambda_eff when the truncation tail is negligible).
206 X = lambda_eff * (1 - pi_ldqbd(Nlev + 1));
208 R_queue = mean_queue / X;
212 % Source station: pass-through, no queueing.
218 QN(queueIdx, 1) = mean_queue;
219 UN(queueIdx, 1) = util_ps;
220 RN(queueIdx, 1) = R_queue;
225 mean_delay = N - mean_queue;
226 X = mean_delay * lambda_eff;
228 R_queue = mean_queue / X;
232 R_delay = 1 / rates(delayIdx, 1);
233 % Delay station metrics. The delay completes service at mean_delay*lambda_d;
234 % only the fraction rt(delayIdx,queueIdx) of those completions proceeds to
235 % the queue, so TN at the delay is NOT the queue flow X whenever the delay
236 % has a self-loop or otherwise routes elsewhere (rt < 1).
237 QN(delayIdx, 1) = mean_delay;
238 UN(delayIdx, 1) = mean_delay; % infinite server: U = Q
239 RN(delayIdx, 1) = R_delay;
240 TN(delayIdx, 1) = mean_delay * lambda_d;
241 % Queue station metrics
242 QN(queueIdx, 1) = mean_queue;
243 UN(queueIdx, 1) = util_ps;
244 RN(queueIdx, 1) = R_queue;
247 CN(1) = R_delay + R_queue;
250totiter = 1; % LDQBD is a direct method
252%% Optional: expose the LD-QBD blocks and parameters (for the SolverENV
253% state-vector analyzer's MAM backend). Built only when requested.
261 delayRate = rates(delayIdx, 1);
264 ld =
struct(
'Q0', {Q0},
'Q1', {Q1},
'Q2', {Q2}, ...
265 'Nlev', Nlev,
'nPhases', nPhases,
'isPH', isPH,
'isOpen', isOpen, ...
266 'queueIdx', queueIdx,
'refIdx', refIdx,
'M', M, ...
267 'nServers', nServers,
'mean_service', mean_service,
'hasLLD', hasLLD, ...
268 'lambda_eff', lambda_eff,
'delayRate', delayRate,
'N', Npop);