LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_mam_ldqbd.m
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
3%
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).
7%
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.
15%
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.
23%
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.
27%
28% Copyright (c) 2012-2026, Imperial College London
29% All rights reserved.
30
31%% Validate model structure
32M = sn.nstations;
33K = sn.nclasses;
34N = sn.njobs';
35
36if K ~= 1
37 line_error(mfilename, 'LDQBD method requires a single-class model.');
38end
39
40nDelay = sum(sn.sched == SchedStrategy.INF);
41nQueue = sum(sn.sched == SchedStrategy.FCFS);
42nSource = sum(sn.sched == SchedStrategy.EXT);
43
44isOpen = ~isfinite(N);
45if isOpen
46 if nSource ~= 1 || nQueue ~= 1 || M ~= 2
47 line_error(mfilename, 'Open LDQBD method requires exactly one Source and one Queue station.');
48 end
49else
50 if nDelay ~= 1 || nQueue ~= 1 || M ~= 2
51 line_error(mfilename, 'Closed LDQBD method requires exactly one Delay and one Queue station.');
52 end
53end
54
55%% Identify stations
56queueIdx = find(sn.sched == SchedStrategy.FCFS);
57if isOpen
58 srcIdx = find(sn.sched == SchedStrategy.EXT);
59else
60 delayIdx = find(sn.sched == SchedStrategy.INF);
61end
62
63%% Service process at the queue
64PH = sn.proc;
65rates = sn.rates;
66nservers = sn.nservers;
67PH_queue = PH{queueIdx}{1};
68nServers = nservers(queueIdx);
69
70if numel(PH_queue{1}) == 1
71 mu = -PH_queue{1}; % exponential service rate
72 nPhases = 1;
73 isPH = false;
74 mean_service = 1/mu;
75else
76 nPhases = size(PH_queue{1}, 1);
77 isPH = true;
78 D0 = PH_queue{1};
79 D1 = PH_queue{2};
80 alpha = map_pie(PH_queue);
81 mean_service = map_mean(PH_queue);
82end
83
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);
90if hasLLD
91 lld = sn.lldscaling(queueIdx, :);
92 lldlimit = numel(lld);
93 sfMax = lld(lldlimit); % saturated factor (capacity ceiling)
94else
95 lld = [];
96 lldlimit = 0;
97 sfMax = nServers;
98end
99
100%% Arrival rate per level and number of levels
101rt = sn.rt;
102if isOpen
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.']);
108 end
109 lambda = rates(srcIdx, 1);
110 lambda_eff = lambda * rt(srcIdx, queueIdx);
111 rho = lambda_eff * mean_service / sfMax; % sfMax = saturated capacity factor
112 if rho >= 1
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));
115 end
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));
119 else
120 tailTol = 1e-10;
121 Nlev = nServers + ceil(log(tailTol) / log(rho));
122 Nlev = min(max(Nlev, nServers + 10), 100000);
123 end
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
126else
127 lambda_d = rates(delayIdx, 1);
128 lambda_eff = lambda_d * rt(delayIdx, queueIdx);
129 Nlev = N;
130 arrRate = (N - (0:N)) * lambda_eff; % finite-source rate (N-n)*lambda_eff, 0 at n=N
131end
132
133% Per-level service factor sf(n), n = 1..Nlev
134sf = zeros(1, Nlev);
135for n = 1:Nlev
136 if hasLLD
137 sf(n) = lld(min(n, lldlimit));
138 else
139 sf(n) = min(n, nServers);
140 end
141end
142
143%% Construct LD-QBD block-tridiagonal generator
144% Q0^(n): upward (arrival) Q1^(n): local Q2^(n): downward (departure)
145Q0 = cell(Nlev, 1);
146Q1 = cell(Nlev + 1, 1);
147Q2 = cell(Nlev, 1);
148
149if ~isPH
150 for n = 0:Nlev-1
151 Q0{n+1} = arrRate(n+1);
152 end
153 for n = 0:Nlev
154 departure_rate = 0;
155 if n > 0
156 departure_rate = sf(n) * mu;
157 end
158 Q1{n+1} = -(arrRate(n+1) + departure_rate);
159 end
160 for n = 1:Nlev
161 Q2{n} = sf(n) * mu;
162 end
163else
164 Q0{1} = arrRate(1) * alpha; % level 0 -> 1: start service in a phase
165 for n = 1:Nlev-1
166 Q0{n+1} = arrRate(n+1) * eye(nPhases); % level n -> n+1: preserve phase
167 end
168 Q1{1} = -arrRate(1); % level 0: only arrivals
169 for n = 1:Nlev
170 Q1{n+1} = sf(n) * D0 - arrRate(n+1) * eye(nPhases);
171 end
172 Q2{1} = sf(1) * D1 * ones(nPhases, 1); % level 1 -> 0: empty the queue
173 for n = 2:Nlev
174 Q2{n} = sf(n) * D1; % level n -> n-1: complete and restart
175 end
176end
177
178%% Solve LD-QBD
179ldqbd_options = struct('epsilon', options.tol, 'maxIter', options.iter_max, 'verbose', false);
180[R, pi_ldqbd] = ldqbd(Q0, Q1, Q2, ldqbd_options); %#ok<ASGLU>
181
182%% Performance metrics (per-level stationary distribution pi_ldqbd)
183mean_queue = (0:Nlev) * pi_ldqbd';
184
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);
189else
190 util_ps = 0;
191 for n = 1:Nlev
192 util_ps = util_ps + (min(n, nServers) / nServers) * pi_ldqbd(n+1);
193 end
194end
195
196QN = zeros(M, K);
197UN = zeros(M, K);
198RN = zeros(M, K);
199TN = zeros(M, K);
200CN = zeros(1, K);
201XN = zeros(1, K);
202
203if isOpen
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));
207 if X > 0
208 R_queue = mean_queue / X;
209 else
210 R_queue = 0;
211 end
212 % Source station: pass-through, no queueing.
213 QN(srcIdx, 1) = 0;
214 UN(srcIdx, 1) = 0;
215 RN(srcIdx, 1) = 0;
216 TN(srcIdx, 1) = X;
217 % Queue station.
218 QN(queueIdx, 1) = mean_queue;
219 UN(queueIdx, 1) = util_ps;
220 RN(queueIdx, 1) = R_queue;
221 TN(queueIdx, 1) = X;
222 XN(1) = X;
223 CN(1) = R_queue;
224else
225 mean_delay = N - mean_queue;
226 X = mean_delay * lambda_eff;
227 if X > 0
228 R_queue = mean_queue / X;
229 else
230 R_queue = 0;
231 end
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;
245 TN(queueIdx, 1) = X;
246 XN(1) = X;
247 CN(1) = R_delay + R_queue;
248end
249
250totiter = 1; % LDQBD is a direct method
251
252%% Optional: expose the LD-QBD blocks and parameters (for the SolverENV
253% state-vector analyzer's MAM backend). Built only when requested.
254if nargout >= 8
255 if isOpen
256 refIdx = srcIdx;
257 delayRate = NaN;
258 Npop = Inf;
259 else
260 refIdx = delayIdx;
261 delayRate = rates(delayIdx, 1);
262 Npop = N;
263 end
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);
269end
270
271end
Definition Station.m:245