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 exact
5% performance metrics for single-class networks with one infinite-server station
6% and one FCFS Queue (multi-server, PH service supported).
7%
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.
15%
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.
19%
20% Copyright (c) 2012-2026, Imperial College London
21% All rights reserved.
22
23%% Validate model structure
24M = sn.nstations;
25K = sn.nclasses;
26N = sn.njobs';
27
28if K ~= 1
29 line_error(mfilename, 'LDQBD method requires a single-class model.');
30end
31
32nDelay = sum(sn.sched == SchedStrategy.INF);
33nQueue = sum(sn.sched == SchedStrategy.FCFS);
34nSource = sum(sn.sched == SchedStrategy.EXT);
35
36isOpen = ~isfinite(N);
37if isOpen
38 if nSource ~= 1 || nQueue ~= 1 || M ~= 2
39 line_error(mfilename, 'Open LDQBD method requires exactly one Source and one Queue station.');
40 end
41else
42 if nDelay ~= 1 || nQueue ~= 1 || M ~= 2
43 line_error(mfilename, 'Closed LDQBD method requires exactly one Delay and one Queue station.');
44 end
45end
46
47%% Identify stations
48queueIdx = find(sn.sched == SchedStrategy.FCFS);
49if isOpen
50 srcIdx = find(sn.sched == SchedStrategy.EXT);
51else
52 delayIdx = find(sn.sched == SchedStrategy.INF);
53end
54
55%% Service process at the queue
56PH = sn.proc;
57rates = sn.rates;
58nservers = sn.nservers;
59PH_queue = PH{queueIdx}{1};
60nServers = nservers(queueIdx);
61
62if numel(PH_queue{1}) == 1
63 mu = -PH_queue{1}; % exponential service rate
64 nPhases = 1;
65 isPH = false;
66 mean_service = 1/mu;
67else
68 nPhases = size(PH_queue{1}, 1);
69 isPH = true;
70 D0 = PH_queue{1};
71 D1 = PH_queue{2};
72 alpha = map_pie(PH_queue);
73 mean_service = map_mean(PH_queue);
74end
75
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);
82if hasLLD
83 lld = sn.lldscaling(queueIdx, :);
84 lldlimit = numel(lld);
85 sfMax = lld(lldlimit); % saturated factor (capacity ceiling)
86else
87 lld = [];
88 lldlimit = 0;
89 sfMax = nServers;
90end
91
92%% Arrival rate per level and number of levels
93rt = sn.rt;
94if isOpen
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.']);
100 end
101 lambda = rates(srcIdx, 1);
102 lambda_eff = lambda * rt(srcIdx, queueIdx);
103 rho = lambda_eff * mean_service / sfMax; % sfMax = saturated capacity factor
104 if rho >= 1
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));
107 end
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));
111 else
112 tailTol = 1e-10;
113 Nlev = nServers + ceil(log(tailTol) / log(rho));
114 Nlev = min(max(Nlev, nServers + 10), 100000);
115 end
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
118else
119 lambda_d = rates(delayIdx, 1);
120 lambda_eff = lambda_d * rt(delayIdx, queueIdx);
121 Nlev = N;
122 arrRate = (N - (0:N)) * lambda_eff; % finite-source rate (N-n)*lambda_eff, 0 at n=N
123end
124
125% Per-level service factor sf(n), n = 1..Nlev
126sf = zeros(1, Nlev);
127for n = 1:Nlev
128 if hasLLD
129 sf(n) = lld(min(n, lldlimit));
130 else
131 sf(n) = min(n, nServers);
132 end
133end
134
135%% Construct LD-QBD block-tridiagonal generator
136% Q0^(n): upward (arrival) Q1^(n): local Q2^(n): downward (departure)
137Q0 = cell(Nlev, 1);
138Q1 = cell(Nlev + 1, 1);
139Q2 = cell(Nlev, 1);
140
141if ~isPH
142 for n = 0:Nlev-1
143 Q0{n+1} = arrRate(n+1);
144 end
145 for n = 0:Nlev
146 departure_rate = 0;
147 if n > 0
148 departure_rate = sf(n) * mu;
149 end
150 Q1{n+1} = -(arrRate(n+1) + departure_rate);
151 end
152 for n = 1:Nlev
153 Q2{n} = sf(n) * mu;
154 end
155else
156 Q0{1} = arrRate(1) * alpha; % level 0 -> 1: start service in a phase
157 for n = 1:Nlev-1
158 Q0{n+1} = arrRate(n+1) * eye(nPhases); % level n -> n+1: preserve phase
159 end
160 Q1{1} = -arrRate(1); % level 0: only arrivals
161 for n = 1:Nlev
162 Q1{n+1} = sf(n) * D0 - arrRate(n+1) * eye(nPhases);
163 end
164 Q2{1} = sf(1) * D1 * ones(nPhases, 1); % level 1 -> 0: empty the queue
165 for n = 2:Nlev
166 Q2{n} = sf(n) * D1; % level n -> n-1: complete and restart
167 end
168end
169
170%% Solve LD-QBD
171ldqbd_options = struct('epsilon', options.tol, 'maxIter', options.iter_max, 'verbose', false);
172[R, pi_ldqbd] = ldqbd(Q0, Q1, Q2, ldqbd_options); %#ok<ASGLU>
173
174%% Performance metrics (per-level stationary distribution pi_ldqbd)
175mean_queue = (0:Nlev) * pi_ldqbd';
176
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);
181else
182 util_ps = 0;
183 for n = 1:Nlev
184 util_ps = util_ps + (min(n, nServers) / nServers) * pi_ldqbd(n+1);
185 end
186end
187
188QN = zeros(M, K);
189UN = zeros(M, K);
190RN = zeros(M, K);
191TN = zeros(M, K);
192CN = zeros(1, K);
193XN = zeros(1, K);
194
195if isOpen
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));
199 if X > 0
200 R_queue = mean_queue / X;
201 else
202 R_queue = 0;
203 end
204 % Source station: pass-through, no queueing.
205 QN(srcIdx, 1) = 0;
206 UN(srcIdx, 1) = 0;
207 RN(srcIdx, 1) = 0;
208 TN(srcIdx, 1) = X;
209 % Queue station.
210 QN(queueIdx, 1) = mean_queue;
211 UN(queueIdx, 1) = util_ps;
212 RN(queueIdx, 1) = R_queue;
213 TN(queueIdx, 1) = X;
214 XN(1) = X;
215 CN(1) = R_queue;
216else
217 mean_delay = N - mean_queue;
218 X = mean_delay * lambda_eff;
219 if X > 0
220 R_queue = mean_queue / X;
221 else
222 R_queue = 0;
223 end
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;
229 TN(delayIdx, 1) = X;
230 % Queue station metrics
231 QN(queueIdx, 1) = mean_queue;
232 UN(queueIdx, 1) = util_ps / nServers;
233 RN(queueIdx, 1) = R_queue;
234 TN(queueIdx, 1) = X;
235 XN(1) = X;
236 CN(1) = R_delay + R_queue;
237end
238
239totiter = 1; % LDQBD is a direct method
240
241%% Optional: expose the LD-QBD blocks and parameters (for the SolverENV
242% state-vector analyzer's MAM backend). Built only when requested.
243if nargout >= 8
244 if isOpen
245 refIdx = srcIdx;
246 delayRate = NaN;
247 Npop = Inf;
248 else
249 refIdx = delayIdx;
250 delayRate = rates(delayIdx, 1);
251 Npop = N;
252 end
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);
258end
259
260end