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 exact
5% performance metrics
for single-
class networks with one infinite-server station
6% and one FCFS Queue (multi-server, PH service supported).
8% Two regimes are handled:
9% CLOSED: one Delay (INF) + one Queue, finite population N. Level n = jobs at
10% the queue (0 <= n <= N); arrival rate from the delay
is (N-n)*lambda.
11% OPEN: one Source (EXT) + one Queue, open
class (Poisson arrivals). Level
12% n = jobs at the queue, truncated at M_trunc; arrival rate
is the
13% constant external rate lambda. M_trunc
is taken from options.cutoff
14% or chosen so the truncated tail probability
is negligible.
16% Both regimes share the same block-tridiagonal generator, differing only in the
17% per-level arrival rate and the top level. Service
is min(n,c)*mu (exact M/M/c
18% boundary) or its PH generalisation.
20% Copyright (c) 2012-2026, Imperial College London
23%% Validate model structure
29 line_error(mfilename, 'LDQBD method
requires a single-
class model.');
32nDelay = sum(sn.sched == SchedStrategy.INF);
33nQueue = sum(sn.sched == SchedStrategy.FCFS);
34nSource = sum(sn.sched == SchedStrategy.EXT);
38 if nSource ~= 1 || nQueue ~= 1 || M ~= 2
39 line_error(mfilename,
'Open LDQBD method requires exactly one Source and one Queue station.');
42 if nDelay ~= 1 || nQueue ~= 1 || M ~= 2
43 line_error(mfilename,
'Closed LDQBD method requires exactly one Delay and one Queue station.');
48queueIdx = find(sn.sched == SchedStrategy.FCFS);
50 srcIdx = find(sn.sched == SchedStrategy.EXT);
52 delayIdx = find(sn.sched == SchedStrategy.INF);
55%% Service process at the queue
58nservers = sn.nservers;
59PH_queue = PH{queueIdx}{1};
60nServers = nservers(queueIdx);
62if numel(PH_queue{1}) == 1
63 mu = -PH_queue{1}; % exponential service rate
68 nPhases = size(PH_queue{1}, 1);
72 alpha = map_pie(PH_queue);
73 mean_service = map_mean(PH_queue);
76%% Per-level service factor: load-dependent scaling
if set,
else min(n,c)
77% sn.lldscaling(queueIdx, n)
is the load-dependent multiplier on the base rate;
78% when absent, a c-server queue scales as min(n,c). This
is the level-dependent
79% factor that the LD-QBD applies in each downward (departure) block.
80hasLLD = sn_has_load_dependence(sn) && ~isempty(sn.lldscaling) ...
81 && size(sn.lldscaling, 1) >= queueIdx && any(sn.lldscaling(queueIdx, :) ~= 1);
83 lld = sn.lldscaling(queueIdx, :);
84 lldlimit = numel(lld);
85 sfMax = lld(lldlimit); % saturated factor (capacity ceiling)
92%% Arrival rate per level and number of levels
95 % External Poisson arrivals only (MAP/MMPP arrivals are not yet supported).
96 arrProc = PH{srcIdx}{1};
97 if numel(arrProc{1}) > 1
98 line_error(mfilename, [
'Open LDQBD method currently supports Poisson (exponential) ' ...
99 'arrivals only; the Source uses a MAP/MMPP process.']);
101 lambda = rates(srcIdx, 1);
102 lambda_eff = lambda * rt(srcIdx, queueIdx);
103 rho = lambda_eff * mean_service / sfMax; % sfMax = saturated capacity factor
105 line_error(mfilename, sprintf([
'Open LDQBD method requires a stable queue ' ...
106 '(rho = %.4f >= 1). Increase service capacity or reduce the arrival rate.'], rho));
108 % Truncation level:
explicit cutoff,
else enough levels
for a negligible tail.
109 if isfield(options,
'cutoff') && isscalar(options.cutoff) && isfinite(options.cutoff)
110 Nlev = max(nServers + 1, round(options.cutoff));
113 Nlev = nServers + ceil(log(tailTol) / log(rho));
114 Nlev = min(max(Nlev, nServers + 10), 100000);
116 arrRate = lambda_eff * ones(1, Nlev + 1); % arrRate(n+1)
is the rate out of level n
117 arrRate(Nlev + 1) = 0; % truncation: no arrivals above the top level
119 lambda_d = rates(delayIdx, 1);
120 lambda_eff = lambda_d * rt(delayIdx, queueIdx);
122 arrRate = (N - (0:N)) * lambda_eff; % finite-source rate (N-n)*lambda_eff, 0 at n=N
125% Per-level service factor sf(n), n = 1..Nlev
129 sf(n) = lld(min(n, lldlimit));
131 sf(n) = min(n, nServers);
135%% Construct LD-QBD block-tridiagonal generator
136% Q0^(n): upward (arrival) Q1^(n): local Q2^(n): downward (departure)
138Q1 = cell(Nlev + 1, 1);
143 Q0{n+1} = arrRate(n+1);
148 departure_rate = sf(n) * mu;
150 Q1{n+1} = -(arrRate(n+1) + departure_rate);
156 Q0{1} = arrRate(1) * alpha; % level 0 -> 1: start service in a phase
158 Q0{n+1} = arrRate(n+1) * eye(nPhases); % level n -> n+1: preserve phase
160 Q1{1} = -arrRate(1); % level 0: only arrivals
162 Q1{n+1} = sf(n) * D0 - arrRate(n+1) * eye(nPhases);
164 Q2{1} = sf(1) * D1 * ones(nPhases, 1); % level 1 -> 0: empty the queue
166 Q2{n} = sf(n) * D1; % level n -> n-1: complete and restart
171ldqbd_options =
struct(
'epsilon', options.tol,
'maxIter', options.iter_max,
'verbose',
false);
172[R, pi_ldqbd] = ldqbd(Q0, Q1, Q2, ldqbd_options); %#ok<ASGLU>
174%% Performance metrics (per-level stationary distribution pi_ldqbd)
175mean_queue = (0:Nlev) * pi_ldqbd
';
177% Utilization: probability busy for a (load-dependent) single server, else the
178% average fraction of c servers in use.
179if hasLLD || nServers == 1
180 util_ps = 1 - pi_ldqbd(1);
184 util_ps = util_ps + (min(n, nServers) / nServers) * pi_ldqbd(n+1);
196 % Accepted/served throughput = arrival rate minus truncation blocking
197 % (= lambda_eff when the truncation tail is negligible).
198 X = lambda_eff * (1 - pi_ldqbd(Nlev + 1));
200 R_queue = mean_queue / X;
204 % Source station: pass-through, no queueing.
210 QN(queueIdx, 1) = mean_queue;
211 UN(queueIdx, 1) = util_ps;
212 RN(queueIdx, 1) = R_queue;
217 mean_delay = N - mean_queue;
218 X = mean_delay * lambda_eff;
220 R_queue = mean_queue / X;
224 R_delay = 1 / rates(delayIdx, 1);
225 % Delay station metrics
226 QN(delayIdx, 1) = mean_delay;
227 UN(delayIdx, 1) = mean_delay; % infinite server: U = Q
228 RN(delayIdx, 1) = R_delay;
230 % Queue station metrics
231 QN(queueIdx, 1) = mean_queue;
232 UN(queueIdx, 1) = util_ps / nServers;
233 RN(queueIdx, 1) = R_queue;
236 CN(1) = R_delay + R_queue;
239totiter = 1; % LDQBD is a direct method
241%% Optional: expose the LD-QBD blocks and parameters (for the SolverENV
242% state-vector analyzer's MAM backend). Built only when requested.
250 delayRate = rates(delayIdx, 1);
253 ld =
struct(
'Q0', {Q0},
'Q1', {Q1},
'Q2', {Q2}, ...
254 'Nlev', Nlev,
'nPhases', nPhases,
'isPH', isPH,
'isOpen', isOpen, ...
255 'queueIdx', queueIdx,
'refIdx', refIdx,
'M', M, ...
256 'nServers', nServers,
'mean_service', mean_service,
'hasLLD', hasLLD, ...
257 'lambda_eff', lambda_eff,
'delayRate', delayRate,
'N', Npop);