LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_mam_retrial.m
1function [QN,UN,RN,TN,CN,XN,totiter] = solver_mam_retrial(sn, options)
2% [QN,UN,RN,TN,CN,XN,TOTITER] = SOLVER_MAM_RETRIAL(SN, OPTIONS)
3%
4% Solves queueing models with customer impatience:
5%
6% 1. RETRIAL (orbit impatience): BMAP/PH/N/N bufferless retrial queues
7% Reference: Dudin et al., "Analysis of BMAP/PH/N-Type Queueing System
8% with Flexible Retrials Admission Control", Mathematics 2025, 13(9), 1434.
9%
10% 2. RENEGING (queue abandonment): MAP/M/s+G queues with patience
11% Reference: O. Gursoy, K. A. Mehr, N. Akar, "The MAP/M/s + G Call Center
12% Model with Generally Distributed Patience Times"
13%
14% Copyright (c) 2012-2026, Imperial College London
15% All rights reserved.
16
17% Check for reneging (queue abandonment) first
18[isReneging, renegingInfo] = detectRenegingTopology(sn);
19if isReneging
20 [QN,UN,RN,TN,CN,XN,totiter] = solveReneging(sn, options, renegingInfo);
21 return;
22end
23
24% Check for retrial topology
25[isRetrial, retInfo] = qsys_is_retrial(sn);
26if ~isRetrial
27 line_error(mfilename, 'No valid impatience configuration detected (retrial or reneging).');
28end
29
30% Initialize output arrays
31M = sn.nstations;
32K = sn.nclasses;
33QN = zeros(M, K);
34UN = zeros(M, K);
35RN = zeros(M, K);
36TN = zeros(M, K);
37CN = zeros(1, K);
38XN = zeros(1, K);
39
40% Extract indices
41sourceIdx = retInfo.sourceIdx;
42queueIdx = retInfo.stationIdx;
43classIdx = retInfo.classIdx;
44N = retInfo.N;
45
46% Extract arrival process (BMAP) from source
47% sn.proc{sourceIdx}{classIdx} contains the arrival process
48arrivalProc = sn.proc{sourceIdx}{classIdx};
49
50% Convert arrival process to BMAP matrices D = {D0, D1, ...}
51D = extractBMAPMatrices(arrivalProc);
52
53if isempty(D)
54 line_error(mfilename, 'Could not extract BMAP matrices from arrival process.');
55end
56
57% Extract PH service distribution from queue
58% sn.proc{queueIdx}{classIdx} contains {alpha, A} for PH
59serviceProc = sn.proc{queueIdx}{classIdx};
60[beta, S] = extractPHParams(serviceProc);
61
62if isempty(beta) || isempty(S)
63 line_error(mfilename, 'Could not extract PH parameters from service process.');
64end
65
66% Extract retrial rate alpha from network configuration
67% Try to get from sn.retrialDelays if available, else use default
68alpha = 0.1; % Default retrial rate
69if isfield(sn, 'retrialDelays') && ~isempty(sn.retrialDelays)
70 if size(sn.retrialDelays, 1) >= queueIdx && size(sn.retrialDelays, 2) >= classIdx
71 retrialDist = sn.retrialDelays{queueIdx, classIdx};
72 if ~isempty(retrialDist) && iscell(retrialDist)
73 % Extract rate from exponential delay distribution
74 % For Exp(alpha), the rate matrix is -alpha
75 alpha = -retrialDist{2}(1,1);
76 end
77 end
78end
79
80% Extract orbit impatience gamma (default 0)
81gamma = retInfo.gamma;
82if isfield(sn, 'orbitImpatience') && ~isempty(sn.orbitImpatience)
83 if size(sn.orbitImpatience, 1) >= queueIdx && size(sn.orbitImpatience, 2) >= classIdx
84 impatienceDist = sn.orbitImpatience{queueIdx, classIdx};
85 if ~isempty(impatienceDist) && iscell(impatienceDist)
86 gamma = -impatienceDist{2}(1,1);
87 end
88 end
89end
90
91% Extract batch rejection probability p (default 0)
92p = retInfo.p;
93if isfield(sn, 'batchRejectProb') && ~isempty(sn.batchRejectProb)
94 if length(sn.batchRejectProb) >= queueIdx * K + classIdx
95 p = sn.batchRejectProb(queueIdx, classIdx);
96 end
97end
98
99% Extract admission threshold R (from FCR or default N-1)
100R = retInfo.R;
101
102% Solve with options
103maxLevel = 150; % Default
104if isfield(options, 'iter_max') && ~isempty(options.iter_max)
105 maxLevel = options.iter_max;
106end
107
108tol = 1e-10;
109if isfield(options, 'tol') && ~isempty(options.tol)
110 tol = options.tol;
111end
112
113verbose = false;
114if isfield(options, 'verbose') && options.verbose
115 verbose = true;
116end
117
118% Call qsys BMAP/PH/N/N retrial solver
119perf = qsys_bmapphnn_retrial(D, beta, S, N, alpha, gamma, p, R, ...
120 'MaxLevel', maxLevel, 'Tolerance', tol, 'Verbose', verbose);
121
122% Map to LINE output format
123% Queue length includes both orbit and servers
124QN(queueIdx, classIdx) = perf.L_orbit + perf.N_server;
125UN(queueIdx, classIdx) = perf.Utilization;
126TN(queueIdx, classIdx) = perf.Throughput;
127
128% Response time via Little's law
129if perf.Throughput > 0
130 RN(queueIdx, classIdx) = QN(queueIdx, classIdx) / perf.Throughput;
131else
132 RN(queueIdx, classIdx) = Inf;
133end
134
135% System-level metrics
136XN(classIdx) = perf.Throughput;
137CN(classIdx) = RN(queueIdx, classIdx);
138
139% Return iteration count (truncation level used)
140totiter = perf.truncLevel;
141
142end
143
144%% Helper functions
145
146function D = extractBMAPMatrices(proc)
147% Extract BMAP matrices {D0, D1, ...} from process representation
148% LINE stores arrival processes in MAP format: {D0, D1, D2, ...}
149% where D0 is the "hidden" generator and D1, D2, ... are arrival matrices
150
151D = {};
152
153if isempty(proc) || ~iscell(proc)
154 return;
155end
156
157% Check if already in BMAP format (cell of cell arrays - unlikely)
158if iscell(proc{1})
159 D = proc;
160 return;
161end
162
163% For LINE, arrival processes are stored as {D0, D1, D2, ...}
164% D0 is a square matrix with negative diagonal entries (subgenerator)
165% D1, D2, ... are non-negative matrices for arrivals
166
167if length(proc) >= 2 && isnumeric(proc{1}) && isnumeric(proc{2})
168 D0 = proc{1};
169 D1 = proc{2};
170
171 n = size(D0, 1);
172
173 % Check if D0 looks like a generator (negative diagonal)
174 % For scalar Exp(rate), D0 = -rate (negative)
175 if n == 1
176 % Scalar case (exponential)
177 if D0 < 0 && D1 > 0
178 % Already in MAP format {D0, D1}
179 D = proc;
180 return;
181 end
182 else
183 % Matrix case - check if D0 has negative diagonal
184 diagD0 = diag(D0);
185 if all(diagD0 < 0) || all(diagD0 <= 0)
186 % Looks like MAP format {D0, D1, ...}
187 D = proc;
188 return;
189 end
190 end
191
192 % If we get here, try to interpret as PH format {alpha, T}
193 % and convert to MAP
194 alpha = D0; % Initial probability vector
195 T = D1; % Subgenerator
196
197 if isrow(alpha) || (numel(alpha) == size(T, 1) && size(T, 1) == size(T, 2))
198 alpha = alpha(:)';
199 n = length(alpha);
200 if size(T, 1) == n && size(T, 2) == n
201 % Convert PH to MAP: D0 = T, D1 = (-T*e)*alpha
202 D0_new = T;
203 D1_new = (-T * ones(n, 1)) * alpha;
204 D = {D0_new, D1_new};
205 return;
206 end
207 end
208end
209
210% Fallback: assume it's already in MAP format
211D = proc;
212
213end
214
215function [beta, S] = extractPHParams(proc)
216% Extract PH parameters (beta, S) from process representation
217% LINE stores PH as MAP format: {D0, D1} where D0=T (subgenerator), D1=S0*alpha
218
219beta = [];
220S = [];
221
222if isempty(proc) || ~iscell(proc)
223 return;
224end
225
226% LINE stores PH in MAP format: {D0, D1}
227% D0 = T (subgenerator matrix)
228% D1 = S0 * alpha (exit rate times initial prob)
229if length(proc) >= 2 && isnumeric(proc{1}) && isnumeric(proc{2})
230 D0 = proc{1};
231 D1 = proc{2};
232
233 % Validate dimensions
234 n = size(D0, 1);
235 if size(D0, 2) ~= n || size(D1, 1) ~= n || size(D1, 2) ~= n
236 return;
237 end
238
239 % S = D0 (subgenerator)
240 S = D0;
241
242 % S0 = -S * ones (exit rates)
243 S0 = -S * ones(n, 1);
244
245 % Extract alpha from D1 = S0 * alpha
246 % Find a row with non-zero exit rate
247 idx = find(S0 > 1e-10, 1);
248 if isempty(idx)
249 % All rows have zero exit rate - use uniform
250 beta = ones(1, n) / n;
251 else
252 % alpha = D1(idx,:) / S0(idx)
253 beta = D1(idx, :) / S0(idx);
254 end
255
256 % Ensure beta is row vector and sums to 1
257 beta = beta(:)';
258 if abs(sum(beta) - 1) > 1e-6
259 % Normalize
260 if sum(beta) > 0
261 beta = beta / sum(beta);
262 else
263 beta = ones(1, n) / n;
264 end
265 end
266end
267
268end
269
270%% Reneging (MAPMSG) helper functions
271
272function [isReneging, info] = detectRenegingTopology(sn)
273% DETECTRENGINGTOPOLOGY Detect if model is suitable for MAPMSG solver
274%
275% Requirements:
276% - Open model, single class
277% - Single queue station with reneging/patience configured
278% - MAP/BMAP arrival at source
279% - Exponential service at queue (single-phase PH)
280% - FCFS scheduling
281
282info = struct();
283info.sourceIdx = [];
284info.queueIdx = [];
285info.classIdx = [];
286info.nServers = [];
287info.serviceRate = [];
288info.errorMsg = '';
289isReneging = false;
290
291% Check open model
292if ~sn_is_open_model(sn)
293 info.errorMsg = 'MAPMSG requires open queueing model.';
294 return;
295end
296
297% Check single class (current limitation)
298if sn.nclasses > 1
299 info.errorMsg = 'MAPMSG currently supports single class only.';
300 return;
301end
302info.classIdx = 1;
303
304% Find source and queue stations
305sourceIdx = [];
306queueIdx = [];
307for ist = 1:sn.nstations
308 nodeIdx = sn.stationToNode(ist);
309 if sn.nodetype(nodeIdx) == NodeType.Source
310 sourceIdx = ist;
311 elseif sn.nodetype(nodeIdx) == NodeType.Queue
312 if isempty(queueIdx)
313 queueIdx = ist;
314 else
315 % Multiple queues - not supported
316 info.errorMsg = 'MAPMSG requires single queue station.';
317 return;
318 end
319 end
320end
321
322if isempty(sourceIdx)
323 info.errorMsg = 'No Source node found.';
324 return;
325end
326if isempty(queueIdx)
327 info.errorMsg = 'No Queue node found.';
328 return;
329end
330
331info.sourceIdx = sourceIdx;
332info.queueIdx = queueIdx;
333
334% Check for reneging patience configuration
335if ~isfield(sn, 'impatienceClass') || isempty(sn.impatienceClass)
336 info.errorMsg = 'No patience/impatience configuration found.';
337 return;
338end
339
340if sn.impatienceClass(queueIdx, info.classIdx) ~= ImpatienceType.RENEGING
341 info.errorMsg = 'Queue does not have reneging configured.';
342 return;
343end
344
345% Check patience distribution exists
346if ~isfield(sn, 'patienceProc') || isempty(sn.patienceProc)
347 info.errorMsg = 'No patience distribution found.';
348 return;
349end
350if isempty(sn.patienceProc{queueIdx, info.classIdx})
351 info.errorMsg = 'No patience distribution for this class.';
352 return;
353end
354
355% Check FCFS scheduling
356if sn.sched(queueIdx) ~= SchedStrategy.FCFS
357 info.errorMsg = 'MAPMSG requires FCFS scheduling.';
358 return;
359end
360
361% Check exponential service (single-phase)
362serviceProc = sn.proc{queueIdx}{info.classIdx};
363if isempty(serviceProc) || ~iscell(serviceProc) || length(serviceProc) < 2
364 info.errorMsg = 'Invalid service process.';
365 return;
366end
367% For exponential, the service process should be 1x1 matrices
368if size(serviceProc{1}, 1) ~= 1
369 info.errorMsg = 'MAPMSG requires exponential service (single-phase).';
370 return;
371end
372
373% Extract service rate
374info.serviceRate = -serviceProc{1}(1,1);
375info.nServers = sn.nservers(queueIdx);
376
377% Check MAP arrival process
378arrivalProc = sn.proc{sourceIdx}{info.classIdx};
379if isempty(arrivalProc) || ~iscell(arrivalProc) || length(arrivalProc) < 2
380 info.errorMsg = 'Invalid arrival process.';
381 return;
382end
383
384% All checks passed
385isReneging = true;
386
387end
388
389function [QN,UN,RN,TN,CN,XN,totiter] = solveReneging(sn, options, info)
390% SOLVERENEGING Solve MAP/M/s+G queue using MAPMSG library
391%
392% Reference: O. Gursoy, K. A. Mehr, N. Akar, "The MAP/M/s + G Call Center
393% Model with Generally Distributed Patience Times"
394
395M = sn.nstations;
396K = sn.nclasses;
397QN = zeros(M, K);
398UN = zeros(M, K);
399RN = zeros(M, K);
400TN = zeros(M, K);
401CN = zeros(1, K);
402XN = zeros(1, K);
403
404sourceIdx = info.sourceIdx;
405queueIdx = info.queueIdx;
406classIdx = info.classIdx;
407
408% Extract MAP arrival process matrices (C, D in MAPMSG notation)
409arrivalProc = sn.proc{sourceIdx}{classIdx};
410C = arrivalProc{1}; % D0 (subgenerator)
411D = arrivalProc{2}; % D1 (arrival transitions)
412MAPSIZE = size(C, 1);
413
414% Extract service rate and server count
415mu = info.serviceRate;
416SERVERSIZE = info.nServers;
417
418% Extract patience distribution and convert to regimes
419patienceProc = sn.patienceProc{queueIdx, classIdx};
420[BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options);
421
422% Build MRMFQ matrices following MAPMSGCompiler.m logic
423I = eye(MAPSIZE);
424em = ones(MAPSIZE, 1);
425lmap = D * em; % Arrival rate vector
426
427% Initialize Qy0 (boundary generator at level 0)
428Qy0 = zeros((SERVERSIZE+1)*MAPSIZE, (SERVERSIZE+1)*MAPSIZE);
429for row = 1:SERVERSIZE+1
430 if row == 1
431 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE) = C;
432 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, row*MAPSIZE+1:row*MAPSIZE+MAPSIZE) = D;
433 elseif row == SERVERSIZE+1
434 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE) = -(row-1)*mu*I;
435 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-2)*MAPSIZE+1:(row-2)*MAPSIZE+MAPSIZE) = (row-1)*mu*I;
436 else
437 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE) = C - (row-1)*mu*I;
438 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-2)*MAPSIZE+1:(row-2)*MAPSIZE+MAPSIZE) = (row-1)*mu*I;
439 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, row*MAPSIZE+1:row*MAPSIZE+MAPSIZE) = D;
440 end
441end
442
443% Initialize Qy for each regime (with abandonment)
444Qy = zeros((SERVERSIZE+1)*MAPSIZE, (SERVERSIZE+1)*MAPSIZE, QUANTIZATION);
445for regimecount = 1:QUANTIZATION
446 for row = SERVERSIZE:SERVERSIZE+1
447 if row == SERVERSIZE
448 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, regimecount) = ga(regimecount+1)*D + C;
449 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, row*MAPSIZE+1:row*MAPSIZE+MAPSIZE, regimecount) = (1-ga(regimecount+1))*D;
450 elseif row == SERVERSIZE+1
451 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, regimecount) = -(row-1)*mu*I;
452 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-2)*MAPSIZE+1:(row-2)*MAPSIZE+MAPSIZE, regimecount) = (row-1)*mu*I;
453 end
454 end
455end
456
457% Build drift matrices
458Rydiag = -ones(1, size(Qy0, 1));
459Rydiag(size(Qy0,1)-MAPSIZE+1:size(Qy0,1)) = -Rydiag(size(Qy0,1)-MAPSIZE+1:size(Qy0,1));
460Ry = diag(Rydiag);
461
462ydriftregimes = zeros(QUANTIZATION, length(Rydiag));
463Ryregimes = zeros(size(Ry,1), size(Ry,2), QUANTIZATION);
464for regimecount = 1:QUANTIZATION
465 ydriftregimes(regimecount,:) = Rydiag;
466 Ryregimes(:,:,regimecount) = Ry;
467end
468
469% Combine boundaries and regimes
470Qybounds = cat(3, Qy0, Qy);
471ydriftbounds = cat(1, Rydiag, ydriftregimes);
472
473% Prepare boundary levels (remove first, add large value at end)
474B = BoundaryLevels;
475B(1) = [];
476B(end+1) = 10000000;
477
478% Call MRMFQ solver
479[coefficients, boundaries, Lzeromulti, Lnegmulti, Lposmulti, Anegmulti, Aposmulti] = ...
480 MRMFQSolver(Qy, Qybounds, ydriftregimes, ydriftbounds, B);
481
482% Compute steady-state results following MAPMSGCompiler.m
483zeromass = boundaries{1};
484
485% Compute integrals for each regime
486integral = zeros(QUANTIZATION, length(zeromass));
487waitintegral = zeros(QUANTIZATION, length(zeromass));
488abandonintegral = zeros(QUANTIZATION, length(zeromass));
489successfulintegral = zeros(QUANTIZATION, length(zeromass));
490
491for d = 1:QUANTIZATION
492 if d == 1
493 prevB = 0;
494 else
495 prevB = B(d-1);
496 end
497
498 Lz = Lzeromulti{d};
499 Ln = Lnegmulti{d};
500 Lp = Lposmulti{d};
501 An = Anegmulti{d};
502 Ap = Aposmulti{d};
503 coef = coefficients{d};
504
505 deltaB = B(d) - prevB;
506
507 integrand = [Lz * deltaB; ...
508 An \ (expm(An * deltaB) - eye(size(An))) * Ln; ...
509 Ap \ (eye(size(Ap)) - expm(-Ap * deltaB)) * Lp];
510
511 integral(d,:) = coef * integrand;
512 waitintegral(d,:) = ((B(d) + prevB)/2) * (1 - ga(d+1)) * coef * integrand;
513 abandonintegral(d,:) = ga(d+1) * coef * integrand;
514 successfulintegral(d,:) = (1 - ga(d+1)) * coef * integrand;
515end
516
517% Map integrals to arrival rates
518normalization = SERVERSIZE * MAPSIZE;
519IntegralMapped = zeros(size(integral));
520AbandonIntegralMapped = zeros(size(integral));
521WaitIntegralMapped = zeros(size(integral));
522ZeroMassMapped = zeros(1, length(zeromass));
523
524for r = 1:length(zeromass)
525 lmapIdx = mod(r-1, MAPSIZE) + 1;
526 IntegralMapped(:,r) = integral(:,r) * lmap(lmapIdx);
527 AbandonIntegralMapped(:,r) = abandonintegral(:,r) * lmap(lmapIdx);
528 WaitIntegralMapped(:,r) = waitintegral(:,r) * lmap(lmapIdx);
529 ZeroMassMapped(r) = zeromass(r) * lmap(lmapIdx);
530end
531
532% Compute performance metrics
533totalMass = sum(ZeroMassMapped) + sum(sum(IntegralMapped(:,1:normalization)));
534AbandonProb = sum(sum(AbandonIntegralMapped(:,1:normalization))) / totalMass;
535ExpectedWait = sum(sum(WaitIntegralMapped(:,1:normalization))) / (totalMass * (1 - AbandonProb));
536
537% Compute arrival rate (lambda)
538lambda = sum(lmap);
539
540% Map to LINE output format
541% Throughput = arrival rate * (1 - abandonment probability)
542throughput = lambda * (1 - AbandonProb);
543TN(queueIdx, classIdx) = throughput;
544
545% Utilization
546UN(queueIdx, classIdx) = throughput / (mu * SERVERSIZE);
547
548% Response time = expected wait + expected service time
549RN(queueIdx, classIdx) = ExpectedWait + 1/mu;
550
551% Queue length via Little's law
552QN(queueIdx, classIdx) = throughput * RN(queueIdx, classIdx);
553
554% System-level metrics
555XN(classIdx) = throughput;
556CN(classIdx) = RN(queueIdx, classIdx);
557
558% Iteration count
559totiter = QUANTIZATION;
560
561end
562
563function [BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options)
564% CONVERTPATIENCETOREGIMES Convert patience distribution to MAPMSG regimes
565%
566% Converts a patience distribution (in MAP/PH format) to piecewise-constant
567% abandonment function for MAPMSG.
568
569% Default quantization
570QUANTIZATION = 11;
571if isfield(options, 'config') && isfield(options.config, 'mapmsg_quantization')
572 QUANTIZATION = options.config.mapmsg_quantization;
573end
574
575% Extract patience rate from the distribution
576% For exponential patience Exp(gamma): patienceProc = {-gamma, gamma}
577if iscell(patienceProc) && length(patienceProc) >= 2
578 % Extract rate from subgenerator
579 gamma = -patienceProc{1}(1,1);
580else
581 % Default patience rate
582 gamma = 0.1;
583end
584
585% Generate boundary levels and abandonment probabilities
586% For exponential patience with rate gamma:
587% F(t) = 1 - exp(-gamma*t) is the CDF (abandonment probability by time t)
588maxTime = 10; % Maximum time horizon for regimes
589BoundaryLevels = linspace(0, maxTime, QUANTIZATION);
590
591% Compute abandonment probability at each boundary
592% ga(k) = F(BoundaryLevels(k)) for piecewise constant approximation
593ga = zeros(1, QUANTIZATION + 1);
594ga(1) = 0; % No abandonment at time 0
595for k = 1:QUANTIZATION
596 % Abandonment probability at midpoint of regime
597 midpoint = (BoundaryLevels(k) + BoundaryLevels(min(k+1, QUANTIZATION))) / 2;
598 if k == 1
599 midpoint = BoundaryLevels(k) / 2;
600 end
601 ga(k+1) = 1 - exp(-gamma * midpoint);
602end
603
604end