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)
4% Solves queueing models with customer impatience:
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.
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"
14% Copyright (c) 2012-2026, Imperial College London
17% Check for reneging (queue abandonment) first
18[isReneging, renegingInfo] = detectRenegingTopology(sn);
20 [QN,UN,RN,TN,CN,XN,totiter] = solveReneging(sn, options, renegingInfo);
24% Check
for retrial topology
25[isRetrial, retInfo] = qsys_is_retrial(sn);
27 line_error(mfilename,
'No valid impatience configuration detected (retrial or reneging).');
30% Initialize output arrays
41sourceIdx = retInfo.sourceIdx;
42queueIdx = retInfo.stationIdx;
43classIdx = retInfo.classIdx;
46% Extract arrival process (BMAP) from source
47% sn.proc{sourceIdx}{classIdx} contains the arrival process
48arrivalProc = sn.proc{sourceIdx}{classIdx};
50% Convert arrival process to BMAP matrices D = {D0, D1, ...}
51D = extractBMAPMatrices(arrivalProc);
54 line_error(mfilename,
'Could not extract BMAP matrices from arrival process.');
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);
62if isempty(beta) || isempty(S)
63 line_error(mfilename,
'Could not extract PH parameters from service process.');
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);
80% Extract orbit impatience gamma (
default 0)
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);
91% Extract batch rejection probability p (
default 0)
93if isfield(sn,
'batchRejectProb') && ~isempty(sn.batchRejectProb)
94 if length(sn.batchRejectProb) >= queueIdx * K + classIdx
95 p = sn.batchRejectProb(queueIdx, classIdx);
99% Extract admission threshold R (from FCR or default N-1)
103maxLevel = 150; % Default
104if isfield(options, 'iter_max') && ~isempty(options.iter_max)
105 maxLevel = options.iter_max;
109if isfield(options, 'tol') && ~isempty(options.tol)
114if isfield(options, 'verbose') && options.verbose
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);
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;
128% Response time via Little's law
129if perf.Throughput > 0
130 RN(queueIdx, classIdx) = QN(queueIdx, classIdx) / perf.Throughput;
132 RN(queueIdx, classIdx) = Inf;
135% System-level metrics
136XN(classIdx) = perf.Throughput;
137CN(classIdx) = RN(queueIdx, classIdx);
139% Return iteration count (truncation level used)
140totiter = perf.truncLevel;
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
153if isempty(proc) || ~iscell(proc)
157% Check
if already in BMAP format (cell of cell arrays - unlikely)
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
167if length(proc) >= 2 && isnumeric(proc{1}) && isnumeric(proc{2})
173 % Check
if D0 looks like a generator (negative diagonal)
174 % For scalar Exp(rate), D0 = -rate (negative)
176 % Scalar
case (exponential)
178 % Already in MAP format {D0, D1}
183 % Matrix
case - check
if D0 has negative diagonal
185 if all(diagD0 < 0) || all(diagD0 <= 0)
186 % Looks like MAP format {D0, D1, ...}
192 % If we get here,
try to interpret as PH format {alpha, T}
194 alpha = D0; % Initial probability vector
195 T = D1; % Subgenerator
197 if isrow(alpha) || (numel(alpha) == size(T, 1) && size(T, 1) == size(T, 2))
200 if size(T, 1) == n && size(T, 2) == n
201 % Convert PH to MAP: D0 = T, D1 = (-T*e)*alpha
203 D1_new = (-T * ones(n, 1)) * alpha;
204 D = {D0_new, D1_new};
210% Fallback: assume it's already in MAP format
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
222if isempty(proc) || ~iscell(proc)
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})
233 % Validate dimensions
235 if size(D0, 2) ~= n || size(D1, 1) ~= n || size(D1, 2) ~= n
239 % S = D0 (subgenerator)
242 % S0 = -S * ones (exit rates)
243 S0 = -S * ones(n, 1);
245 % Extract alpha from D1 = S0 * alpha
246 % Find a row with non-zero exit rate
247 idx = find(S0 > 1e-10, 1);
249 % All rows have zero exit rate - use uniform
250 beta = ones(1, n) / n;
252 % alpha = D1(idx,:) / S0(idx)
253 beta = D1(idx, :) / S0(idx);
256 % Ensure beta
is row vector and sums to 1
258 if abs(sum(beta) - 1) > 1e-6
261 beta = beta / sum(beta);
263 beta = ones(1, n) / n;
270%% Reneging (MAPMSG) helper functions
272function [isReneging, info] = detectRenegingTopology(sn)
273% DETECTRENGINGTOPOLOGY Detect if model is suitable for MAPMSG solver
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)
287info.serviceRate = [];
292if ~sn_is_open_model(sn)
293 info.errorMsg = 'MAPMSG requires open queueing model.
';
297% Check single class (current limitation)
299 info.errorMsg = 'MAPMSG currently supports single class only.
';
304% Find source and queue stations
307for ist = 1:sn.nstations
308 nodeIdx = sn.stationToNode(ist);
309 if sn.nodetype(nodeIdx) == NodeType.Source
311 elseif sn.nodetype(nodeIdx) == NodeType.Queue
315 % Multiple queues - not supported
316 info.errorMsg = 'MAPMSG requires single queue station.
';
323 info.errorMsg = 'No Source node found.
';
327 info.errorMsg = 'No Queue node found.
';
331info.sourceIdx = sourceIdx;
332info.queueIdx = queueIdx;
334% Check for reneging patience configuration
335if ~isfield(sn, 'impatienceClass
') || isempty(sn.impatienceClass)
336 info.errorMsg = 'No patience/impatience configuration found.
';
340if sn.impatienceClass(queueIdx, info.classIdx) ~= ImpatienceType.RENEGING
341 info.errorMsg = 'Queue does not have reneging configured.
';
345% Check patience distribution exists
346if ~isfield(sn, 'patienceProc
') || isempty(sn.patienceProc)
347 info.errorMsg = 'No patience distribution found.
';
350if isempty(sn.patienceProc{queueIdx, info.classIdx})
351 info.errorMsg = 'No patience distribution for this class.
';
355% Check FCFS scheduling
356if sn.sched(queueIdx) ~= SchedStrategy.FCFS
357 info.errorMsg = 'MAPMSG requires FCFS scheduling.
';
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.
';
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).
';
373% Extract service rate
374info.serviceRate = -serviceProc{1}(1,1);
375info.nServers = sn.nservers(queueIdx);
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.
';
389function [QN,UN,RN,TN,CN,XN,totiter] = solveReneging(sn, options, info)
390% SOLVERENEGING Solve MAP/M/s+G queue using MAPMSG library
392% Reference: O. Gursoy, K. A. Mehr, N. Akar, "The MAP/M/s + G Call Center
393% Model with Generally Distributed Patience Times"
404sourceIdx = info.sourceIdx;
405queueIdx = info.queueIdx;
406classIdx = info.classIdx;
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)
414% Extract service rate and server count
415mu = info.serviceRate;
416SERVERSIZE = info.nServers;
418% Extract patience distribution and convert to regimes
419patienceProc = sn.patienceProc{queueIdx, classIdx};
420[BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options);
422% Build MRMFQ matrices following MAPMSGCompiler.m logic
424em = ones(MAPSIZE, 1);
425lmap = D * em; % Arrival rate vector
427% Initialize Qy0 (boundary generator at level 0)
428Qy0 = zeros((SERVERSIZE+1)*MAPSIZE, (SERVERSIZE+1)*MAPSIZE);
429for row = 1:SERVERSIZE+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;
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;
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
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;
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));
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;
469% Combine boundaries and regimes
470Qybounds = cat(3, Qy0, Qy);
471ydriftbounds = cat(1, Rydiag, ydriftregimes);
473% Prepare boundary levels (remove first, add large value at end)
479[coefficients, boundaries, Lzeromulti, Lnegmulti, Lposmulti, Anegmulti, Aposmulti] = ...
480 MRMFQSolver(Qy, Qybounds, ydriftregimes, ydriftbounds, B);
482% Compute steady-state results following MAPMSGCompiler.m
483zeromass = boundaries{1};
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));
491for d = 1:QUANTIZATION
503 coef = coefficients{d};
505 deltaB = B(d) - prevB;
507 integrand = [Lz * deltaB; ...
508 An \ (expm(An * deltaB) - eye(size(An))) * Ln; ...
509 Ap \ (eye(size(Ap)) - expm(-Ap * deltaB)) * Lp];
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;
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));
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);
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));
537% Compute arrival rate (lambda)
540% Map to LINE output format
541% Throughput = arrival rate * (1 - abandonment probability)
542throughput = lambda * (1 - AbandonProb);
543TN(queueIdx, classIdx) = throughput;
546UN(queueIdx, classIdx) = throughput / (mu * SERVERSIZE);
548% Response time = expected wait + expected service time
549RN(queueIdx, classIdx) = ExpectedWait + 1/mu;
551% Queue length via Little's law
552QN(queueIdx, classIdx) = throughput * RN(queueIdx, classIdx);
554% System-level metrics
555XN(classIdx) = throughput;
556CN(classIdx) = RN(queueIdx, classIdx);
559totiter = QUANTIZATION;
563function [BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options)
564% CONVERTPATIENCETOREGIMES Convert patience distribution to MAPMSG regimes
566% Converts a patience distribution (in MAP/PH format) to piecewise-constant
567% abandonment function
for MAPMSG.
569% Default quantization
571if isfield(options,
'config') && isfield(options.config,
'mapmsg_quantization')
572 QUANTIZATION = options.config.mapmsg_quantization;
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);
581 % Default patience rate
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);
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;
599 midpoint = BoundaryLevels(k) / 2;
601 ga(k+1) = 1 - exp(-gamma * midpoint);