LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_mam_retrial.m
1function [QN,UN,RN,TN,CN,XN,totiter,perf] = 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 perf = struct('analyzer','LINE:solver_mam_reneging');
22 return;
23end
24
25% Check for retrial topology
26[isRetrial, retInfo] = qsys_is_retrial(sn);
27if ~isRetrial
28 line_error(mfilename, 'No valid impatience configuration detected (retrial or reneging).');
29end
30
31% Initialize output arrays
32M = sn.nstations;
33K = sn.nclasses;
34QN = zeros(M, K);
35UN = zeros(M, K);
36RN = zeros(M, K);
37TN = zeros(M, K);
38CN = zeros(1, K);
39XN = zeros(1, K);
40
41% Extract indices
42sourceIdx = retInfo.sourceIdx;
43queueIdx = retInfo.stationIdx;
44classIdx = retInfo.classIdx;
45N = retInfo.N;
46
47% The matrix-analytic retrial engine is defined only for Markovian arrival
48% and phase-type service. Reject anything else here, naming the station and
49% class, rather than letting a malformed representation reach the generator.
50assertMarkovian(sn, sourceIdx, classIdx, 'arrival');
51assertMarkovian(sn, queueIdx, classIdx, 'service');
52warnIfApproximated(sn, queueIdx, classIdx);
53
54% Extract arrival process (BMAP) from source
55% sn.proc{sourceIdx}{classIdx} contains the arrival process
56arrivalProc = sn.proc{sourceIdx}{classIdx};
57
58% Convert arrival process to BMAP matrices D = {D0, D1, ...}
59D = extractBMAPMatrices(arrivalProc);
60
61if isempty(D)
62 line_error(mfilename, 'Could not extract BMAP matrices from arrival process.');
63end
64
65% Extract PH service distribution from queue
66% sn.proc{queueIdx}{classIdx} contains {alpha, A} for PH
67serviceProc = sn.proc{queueIdx}{classIdx};
68[beta, S] = extractPHParams(serviceProc);
69
70if isempty(beta) || isempty(S)
71 line_error(mfilename, 'Could not extract PH parameters from service process.');
72end
73
74% Extract retrial rate alpha from the canonical sn.retrialProc {D0,D1} field
75alpha = 0.1; % Default retrial rate
76if isfield(sn, 'retrialProc') && ~isempty(sn.retrialProc)
77 if size(sn.retrialProc, 1) >= queueIdx && size(sn.retrialProc, 2) >= classIdx
78 retrialDist = sn.retrialProc{queueIdx, classIdx};
79 if ~isempty(retrialDist) && iscell(retrialDist)
80 % For Exp(alpha), D0 = -alpha
81 alpha = -retrialDist{1}(1,1);
82 end
83 end
84end
85
86% Extract orbit impatience gamma (default 0)
87gamma = retInfo.gamma;
88if isfield(sn, 'orbitImpatience') && ~isempty(sn.orbitImpatience)
89 if size(sn.orbitImpatience, 1) >= queueIdx && size(sn.orbitImpatience, 2) >= classIdx
90 impatienceDist = sn.orbitImpatience{queueIdx, classIdx};
91 if ~isempty(impatienceDist) && iscell(impatienceDist)
92 % For Exp(gamma), D0 = -gamma
93 gamma = -impatienceDist{1}(1,1);
94 end
95 end
96end
97
98% Extract batch rejection probability p (default 0)
99p = retInfo.p;
100if isfield(sn, 'batchRejectProb') && ~isempty(sn.batchRejectProb)
101 if size(sn.batchRejectProb, 1) >= queueIdx && size(sn.batchRejectProb, 2) >= classIdx
102 p = sn.batchRejectProb(queueIdx, classIdx);
103 end
104end
105
106% Extract admission threshold R (from FCR or default N-1)
107R = retInfo.R;
108
109% Orbit truncation. By default the level is chosen adaptively by the engine
110% from the residual tail mass; options.iter_max is a generic iteration budget
111% and must not be reinterpreted as an accuracy setting, since a level pinned
112% to it underestimates heavy-tailed orbits without any warning. An explicit
113% level is taken from options.config.orbit_maxlevel.
114maxLevel = [];
115if isfield(options, 'config') && isstruct(options.config) ...
116 && isfield(options.config, 'orbit_maxlevel') && ~isempty(options.config.orbit_maxlevel)
117 maxLevel = options.config.orbit_maxlevel;
118end
119% A finite orbit declared by setOrbit is a MODEL property, not an accuracy
120% setting: the level process really stops there and a job that finds the orbit
121% full is lost. The level-truncated generator returns the dropped up-rate to the
122% diagonal, which is exactly that loss, so a declared orbit cap is passed
123% straight through as the truncation level and carries no truncation error.
124if isfield(sn, 'orbitMaxJobs') && ~isempty(sn.orbitMaxJobs) ...
125 && size(sn.orbitMaxJobs,1) >= queueIdx && size(sn.orbitMaxJobs,2) >= classIdx ...
126 && sn.orbitMaxJobs(queueIdx, classIdx) >= 0
127 maxLevel = sn.orbitMaxJobs(queueIdx, classIdx);
128end
129
130retrialPolicy = RetrialPolicy.LINEAR;
131if isfield(sn, 'retrialPolicy') && ~isempty(sn.retrialPolicy) ...
132 && size(sn.retrialPolicy,1) >= queueIdx && size(sn.retrialPolicy,2) >= classIdx ...
133 && sn.retrialPolicy(queueIdx, classIdx) > 0
134 retrialPolicy = sn.retrialPolicy(queueIdx, classIdx);
135end
136
137tailTol = 1e-6;
138if isfield(options, 'config') && isstruct(options.config) ...
139 && isfield(options.config, 'orbit_tailtol') && ~isempty(options.config.orbit_tailtol)
140 tailTol = options.config.orbit_tailtol;
141end
142
143tol = 1e-10;
144if isfield(options, 'tol') && ~isempty(options.tol)
145 tol = options.tol;
146end
147
148verbose = false;
149if isfield(options, 'verbose') && options.verbose
150 verbose = true;
151end
152
153% Call qsys BMAP/PH/N/N retrial solver
154perf = qsys_bmapphnn_retrial(D, beta, S, N, alpha, gamma, p, R, ...
155 'MaxLevel', maxLevel, 'Tolerance', tol, 'TailTolerance', tailTol, ...
156 'RetrialPolicy', retrialPolicy, 'Verbose', verbose);
157
158% Map to LINE output format
159% Queue length includes both orbit and servers
160QN(queueIdx, classIdx) = perf.L_orbit + perf.N_server;
161UN(queueIdx, classIdx) = perf.Utilization;
162TN(queueIdx, classIdx) = perf.Throughput;
163
164% Response time via Little's law
165if perf.Throughput > 0
166 RN(queueIdx, classIdx) = QN(queueIdx, classIdx) / perf.Throughput;
167else
168 RN(queueIdx, classIdx) = Inf;
169end
170
171% System-level metrics
172XN(classIdx) = perf.Throughput;
173CN(classIdx) = RN(queueIdx, classIdx);
174
175% Return iteration count (truncation level used)
176totiter = perf.truncLevel;
177
178end
179
180%% Helper functions
181
182function assertMarkovian(sn, ist, r, role)
183% Reject a station-class process that is not a valid Markovian (D0,D1,...)
184% representation. Non-Markovian distributions (Det, traces, NHPP rate
185% schedules) and disabled classes reach here as scalars or as NaN-filled
186% matrices; they have no place in a matrix-analytic generator.
187stationName = sn.nodenames{sn.stationToNode(ist)};
188className = sn.classnames{r};
189proc = sn.proc{ist}{r};
190
191if isempty(proc) || ~iscell(proc) || numel(proc) < 2
192 line_error(mfilename, sprintf(['The %s process of class ''%s'' at station ''%s'' is not a Markovian ' ...
193 '(D0,D1) representation. The matrix-analytic retrial solver requires phase-type or ' ...
194 'Markovian-arrival distributions; use SolverCTMC, SolverSSA or SolverLDES instead.'], ...
195 role, className, stationName));
196end
197
198n0 = size(proc{1}, 1);
199for e = 1:numel(proc)
200 De = proc{e};
201 if ~isnumeric(De) || ~ismatrix(De) || size(De,1) ~= size(De,2) || size(De,1) ~= n0
202 line_error(mfilename, sprintf(['The %s process of class ''%s'' at station ''%s'' has ' ...
203 'inconsistent (D0,D1) block dimensions.'], role, className, stationName));
204 end
205 if any(~isfinite(De(:)))
206 line_error(mfilename, sprintf(['The %s process of class ''%s'' at station ''%s'' contains ' ...
207 'NaN or Inf entries: the class is disabled at this station or the distribution has no ' ...
208 'Markovian representation.'], role, className, stationName));
209 end
210end
211end
212
213function warnIfApproximated(sn, ist, r)
214% Non-Markovian service distributions are replaced by an Erlang MAP of
215% matching mean (see MNetwork.refreshProcessRepresentations). That is a
216% documented approximation, not the requested distribution, so say so.
217if ~isfield(sn, 'procid') || isempty(sn.procid)
218 return;
219end
220if size(sn.procid, 1) < ist || size(sn.procid, 2) < r
221 return;
222end
223switch sn.procid(ist, r)
224 case {ProcessType.DET, ProcessType.REPLAYER, ProcessType.UNIFORM, ...
225 ProcessType.GAMMA, ProcessType.PARETO, ProcessType.WEIBULL, ...
226 ProcessType.LOGNORMAL}
227 stationName = sn.nodenames{sn.stationToNode(ist)};
228 line_warning(mfilename, sprintf(['Service distribution %s at station ''%s'' is not phase-type. ' ...
229 'The matrix-analytic retrial solver uses an Erlang approximation matching its mean and ' ...
230 'squared coefficient of variation (%d phases); results are approximate.'], ...
231 ProcessType.toText(sn.procid(ist, r)), stationName, sn.phases(ist, r)));
232end
233end
234
235function D = extractBMAPMatrices(proc)
236% Extract BMAP matrices {D0, D1, ...} from process representation
237% LINE stores arrival processes in MAP format: {D0, D1, D2, ...}
238% where D0 is the "hidden" generator and D1, D2, ... are arrival matrices
239
240D = {};
241
242if isempty(proc) || ~iscell(proc)
243 return;
244end
245
246% Check if already in BMAP format (cell of cell arrays - unlikely)
247if iscell(proc{1})
248 D = proc;
249 return;
250end
251
252% For LINE, arrival processes are stored as {D0, D1, D2, ...}
253% D0 is a square matrix with negative diagonal entries (subgenerator)
254% D1, D2, ... are non-negative matrices for arrivals
255
256if length(proc) >= 2 && isnumeric(proc{1}) && isnumeric(proc{2})
257 D0 = proc{1};
258 D1 = proc{2};
259
260 n = size(D0, 1);
261
262 % Check if D0 looks like a generator (negative diagonal)
263 % For scalar Exp(rate), D0 = -rate (negative)
264 if n == 1
265 % Scalar case (exponential)
266 if D0 < 0 && D1 > 0
267 % Already in MAP format {D0, D1}
268 D = proc;
269 return;
270 end
271 else
272 % Matrix case - check if D0 has negative diagonal
273 diagD0 = diag(D0);
274 if all(diagD0 < 0) || all(diagD0 <= 0)
275 % Looks like MAP format {D0, D1, ...}
276 D = proc;
277 return;
278 end
279 end
280
281 % If we get here, try to interpret as PH format {alpha, T}
282 % and convert to MAP
283 alpha = D0; % Initial probability vector
284 T = D1; % Subgenerator
285
286 if isrow(alpha) || (numel(alpha) == size(T, 1) && size(T, 1) == size(T, 2))
287 alpha = alpha(:)';
288 n = length(alpha);
289 if size(T, 1) == n && size(T, 2) == n
290 % Convert PH to MAP: D0 = T, D1 = (-T*e)*alpha
291 D0_new = T;
292 D1_new = (-T * ones(n, 1)) * alpha;
293 D = {D0_new, D1_new};
294 return;
295 end
296 end
297end
298
299% Fallback: assume it's already in MAP format
300D = proc;
301
302end
303
304function [beta, S] = extractPHParams(proc)
305% Extract PH parameters (beta, S) from process representation
306% LINE stores PH as MAP format: {D0, D1} where D0=T (subgenerator), D1=S0*alpha
307
308beta = [];
309S = [];
310
311if isempty(proc) || ~iscell(proc)
312 return;
313end
314
315% LINE stores PH in MAP format: {D0, D1}
316% D0 = T (subgenerator matrix)
317% D1 = S0 * alpha (exit rate times initial prob)
318if length(proc) >= 2 && isnumeric(proc{1}) && isnumeric(proc{2})
319 D0 = proc{1};
320 D1 = proc{2};
321
322 % Validate dimensions
323 n = size(D0, 1);
324 if size(D0, 2) ~= n || size(D1, 1) ~= n || size(D1, 2) ~= n
325 return;
326 end
327
328 % S = D0 (subgenerator)
329 S = D0;
330
331 % S0 = -S * ones (exit rates)
332 S0 = -S * ones(n, 1);
333
334 % Extract alpha from D1 = S0 * alpha
335 % Find a row with non-zero exit rate
336 idx = find(S0 > 1e-10, 1);
337 if isempty(idx)
338 % All rows have zero exit rate - use uniform
339 beta = ones(1, n) / n;
340 else
341 % alpha = D1(idx,:) / S0(idx)
342 beta = D1(idx, :) / S0(idx);
343 end
344
345 % Ensure beta is row vector and sums to 1
346 beta = beta(:)';
347 if abs(sum(beta) - 1) > 1e-6
348 % Normalize
349 if sum(beta) > 0
350 beta = beta / sum(beta);
351 else
352 beta = ones(1, n) / n;
353 end
354 end
355end
356
357end
358
359%% Reneging (MAPMsG) helper functions
360
361function [isReneging, info] = detectRenegingTopology(sn)
362% DETECTRENGINGTOPOLOGY Detect if model is suitable for MAPMsG solver
363%
364% Requirements:
365% - Open model, single class
366% - Single queue station with reneging/patience configured
367% - MAP/BMAP arrival at source
368% - Exponential service at queue (single-phase PH)
369% - FCFS scheduling
370
371info = struct();
372info.sourceIdx = [];
373info.queueIdx = [];
374info.classIdx = [];
375info.nServers = [];
376info.serviceRate = [];
377info.errorMsg = '';
378isReneging = false;
379
380% Check open model
381if ~sn_is_open_model(sn)
382 info.errorMsg = 'MAPMsG requires open queueing model.';
383 return;
384end
385
386% Check single class (current limitation)
387if sn.nclasses > 1
388 info.errorMsg = 'MAPMsG currently supports single class only.';
389 return;
390end
391info.classIdx = 1;
392
393% Find source and queue stations
394sourceIdx = [];
395queueIdx = [];
396for ist = 1:sn.nstations
397 nodeIdx = sn.stationToNode(ist);
398 if sn.nodetype(nodeIdx) == NodeType.Source
399 sourceIdx = ist;
400 elseif sn.nodetype(nodeIdx) == NodeType.Queue
401 if isempty(queueIdx)
402 queueIdx = ist;
403 else
404 % Multiple queues - not supported
405 info.errorMsg = 'MAPMsG requires single queue station.';
406 return;
407 end
408 end
409end
410
411if isempty(sourceIdx)
412 info.errorMsg = 'No Source node found.';
413 return;
414end
415if isempty(queueIdx)
416 info.errorMsg = 'No Queue node found.';
417 return;
418end
419
420info.sourceIdx = sourceIdx;
421info.queueIdx = queueIdx;
422
423% Check for reneging patience configuration
424if ~isfield(sn, 'impatienceClass') || isempty(sn.impatienceClass)
425 info.errorMsg = 'No patience/impatience configuration found.';
426 return;
427end
428
429if sn.impatienceClass(queueIdx, info.classIdx) ~= ImpatienceType.RENEGING
430 info.errorMsg = 'Queue does not have reneging configured.';
431 return;
432end
433
434% Check patience distribution exists
435if ~isfield(sn, 'patienceProc') || isempty(sn.patienceProc)
436 info.errorMsg = 'No patience distribution found.';
437 return;
438end
439if isempty(sn.patienceProc{queueIdx, info.classIdx})
440 info.errorMsg = 'No patience distribution for this class.';
441 return;
442end
443
444% Check FCFS scheduling
445if sn.sched(queueIdx) ~= SchedStrategy.FCFS
446 info.errorMsg = 'MAPMsG requires FCFS scheduling.';
447 return;
448end
449
450% Check exponential service (single-phase)
451serviceProc = sn.proc{queueIdx}{info.classIdx};
452if isempty(serviceProc) || ~iscell(serviceProc) || length(serviceProc) < 2
453 info.errorMsg = 'Invalid service process.';
454 return;
455end
456% For exponential, the service process should be 1x1 matrices
457if size(serviceProc{1}, 1) ~= 1
458 info.errorMsg = 'MAPMsG requires exponential service (single-phase).';
459 return;
460end
461
462% Extract service rate
463info.serviceRate = -serviceProc{1}(1,1);
464info.nServers = sn.nservers(queueIdx);
465
466% Check MAP arrival process
467arrivalProc = sn.proc{sourceIdx}{info.classIdx};
468if isempty(arrivalProc) || ~iscell(arrivalProc) || length(arrivalProc) < 2
469 info.errorMsg = 'Invalid arrival process.';
470 return;
471end
472
473% All checks passed
474isReneging = true;
475
476end
477
478function [QN,UN,RN,TN,CN,XN,totiter] = solveReneging(sn, options, info)
479% SOLVERENEGING Solve MAP/M/s+G queue using MAPMsG library
480%
481% Reference: O. Gursoy, K. A. Mehr, N. Akar, "The MAP/M/s + G Call Center
482% Model with Generally Distributed Patience Times"
483
484M = sn.nstations;
485K = sn.nclasses;
486QN = zeros(M, K);
487UN = zeros(M, K);
488RN = zeros(M, K);
489TN = zeros(M, K);
490CN = zeros(1, K);
491XN = zeros(1, K);
492
493sourceIdx = info.sourceIdx;
494queueIdx = info.queueIdx;
495classIdx = info.classIdx;
496
497% Extract MAP arrival process matrices (C, D in MAPMsG notation)
498arrivalProc = sn.proc{sourceIdx}{classIdx};
499C = arrivalProc{1}; % D0 (subgenerator)
500D = arrivalProc{2}; % D1 (arrival transitions)
501MAPSIZE = size(C, 1);
502
503% Extract service rate and server count
504mu = info.serviceRate;
505SERVERSIZE = info.nServers;
506
507% Extract patience distribution and convert to regimes
508patienceProc = sn.patienceProc{queueIdx, classIdx};
509[BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options);
510
511% Build MRMFQ matrices following MAPMsGCompiler.m logic
512I = eye(MAPSIZE);
513em = ones(MAPSIZE, 1);
514lmap = D * em; % Arrival rate vector
515
516% Initialize Qy0 (boundary generator at level 0)
517Qy0 = zeros((SERVERSIZE+1)*MAPSIZE, (SERVERSIZE+1)*MAPSIZE);
518for row = 1:SERVERSIZE+1
519 if row == 1
520 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE) = C;
521 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, row*MAPSIZE+1:row*MAPSIZE+MAPSIZE) = D;
522 elseif row == SERVERSIZE+1
523 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE) = -(row-1)*mu*I;
524 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-2)*MAPSIZE+1:(row-2)*MAPSIZE+MAPSIZE) = (row-1)*mu*I;
525 else
526 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE) = C - (row-1)*mu*I;
527 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-2)*MAPSIZE+1:(row-2)*MAPSIZE+MAPSIZE) = (row-1)*mu*I;
528 Qy0((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, row*MAPSIZE+1:row*MAPSIZE+MAPSIZE) = D;
529 end
530end
531
532% Initialize Qy for each regime (with abandonment)
533Qy = zeros((SERVERSIZE+1)*MAPSIZE, (SERVERSIZE+1)*MAPSIZE, QUANTIZATION);
534for regimecount = 1:QUANTIZATION
535 for row = SERVERSIZE:SERVERSIZE+1
536 if row == SERVERSIZE
537 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, regimecount) = ga(regimecount+1)*D + C;
538 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, row*MAPSIZE+1:row*MAPSIZE+MAPSIZE, regimecount) = (1-ga(regimecount+1))*D;
539 elseif row == SERVERSIZE+1
540 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, regimecount) = -(row-1)*mu*I;
541 Qy((row-1)*MAPSIZE+1:(row-1)*MAPSIZE+MAPSIZE, (row-2)*MAPSIZE+1:(row-2)*MAPSIZE+MAPSIZE, regimecount) = (row-1)*mu*I;
542 end
543 end
544end
545
546% Build drift matrices
547Rydiag = -ones(1, size(Qy0, 1));
548Rydiag(size(Qy0,1)-MAPSIZE+1:size(Qy0,1)) = -Rydiag(size(Qy0,1)-MAPSIZE+1:size(Qy0,1));
549Ry = diag(Rydiag);
550
551ydriftregimes = zeros(QUANTIZATION, length(Rydiag));
552Ryregimes = zeros(size(Ry,1), size(Ry,2), QUANTIZATION);
553for regimecount = 1:QUANTIZATION
554 ydriftregimes(regimecount,:) = Rydiag;
555 Ryregimes(:,:,regimecount) = Ry;
556end
557
558% Combine boundaries and regimes
559Qybounds = cat(3, Qy0, Qy);
560ydriftbounds = cat(1, Rydiag, ydriftregimes);
561
562% Prepare boundary levels (remove first, add large value at end)
563B = BoundaryLevels;
564B(1) = [];
565B(end+1) = 10000000;
566
567% Call MRMFQ solver
568[coefficients, boundaries, Lzeromulti, Lnegmulti, Lposmulti, Anegmulti, Aposmulti] = ...
569 MRMFQSolver(Qy, Qybounds, ydriftregimes, ydriftbounds, B);
570
571% Compute steady-state results following MAPMsGCompiler.m
572zeromass = boundaries{1};
573
574% Compute integrals for each regime
575integral = zeros(QUANTIZATION, length(zeromass));
576waitintegral = zeros(QUANTIZATION, length(zeromass));
577abandonintegral = zeros(QUANTIZATION, length(zeromass));
578successfulintegral = zeros(QUANTIZATION, length(zeromass));
579
580for d = 1:QUANTIZATION
581 if d == 1
582 prevB = 0;
583 else
584 prevB = B(d-1);
585 end
586
587 Lz = Lzeromulti{d};
588 Ln = Lnegmulti{d};
589 Lp = Lposmulti{d};
590 An = Anegmulti{d};
591 Ap = Aposmulti{d};
592 coef = coefficients{d};
593
594 deltaB = B(d) - prevB;
595
596 integrand = [Lz * deltaB; ...
597 An \ (expm(An * deltaB) - eye(size(An))) * Ln; ...
598 Ap \ (eye(size(Ap)) - expm(-Ap * deltaB)) * Lp];
599
600 integral(d,:) = coef * integrand;
601 waitintegral(d,:) = ((B(d) + prevB)/2) * (1 - ga(d+1)) * coef * integrand;
602 abandonintegral(d,:) = ga(d+1) * coef * integrand;
603 successfulintegral(d,:) = (1 - ga(d+1)) * coef * integrand;
604end
605
606% Map integrals to arrival rates
607normalization = SERVERSIZE * MAPSIZE;
608IntegralMapped = zeros(size(integral));
609AbandonIntegralMapped = zeros(size(integral));
610WaitIntegralMapped = zeros(size(integral));
611ZeroMassMapped = zeros(1, length(zeromass));
612
613for r = 1:length(zeromass)
614 lmapIdx = mod(r-1, MAPSIZE) + 1;
615 IntegralMapped(:,r) = integral(:,r) * lmap(lmapIdx);
616 AbandonIntegralMapped(:,r) = abandonintegral(:,r) * lmap(lmapIdx);
617 WaitIntegralMapped(:,r) = waitintegral(:,r) * lmap(lmapIdx);
618 ZeroMassMapped(r) = zeromass(r) * lmap(lmapIdx);
619end
620
621% Compute performance metrics
622totalMass = sum(ZeroMassMapped) + sum(sum(IntegralMapped(:,1:normalization)));
623AbandonProb = sum(sum(AbandonIntegralMapped(:,1:normalization))) / totalMass;
624ExpectedWait = sum(sum(WaitIntegralMapped(:,1:normalization))) / (totalMass * (1 - AbandonProb));
625
626% Compute arrival rate (lambda)
627lambda = sum(lmap);
628
629% Map to LINE output format
630% Throughput = arrival rate * (1 - abandonment probability)
631throughput = lambda * (1 - AbandonProb);
632TN(queueIdx, classIdx) = throughput;
633
634% Utilization
635UN(queueIdx, classIdx) = throughput / (mu * SERVERSIZE);
636
637% Response time = expected wait + expected service time
638RN(queueIdx, classIdx) = ExpectedWait + 1/mu;
639
640% Queue length via Little's law
641QN(queueIdx, classIdx) = throughput * RN(queueIdx, classIdx);
642
643% System-level metrics
644XN(classIdx) = throughput;
645CN(classIdx) = RN(queueIdx, classIdx);
646
647% Iteration count
648totiter = QUANTIZATION;
649
650end
651
652function [BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options)
653% CONVERTPATIENCETOREGIMES Convert patience distribution to MAPMsG regimes
654%
655% Converts a patience distribution (in MAP/PH format) to piecewise-constant
656% abandonment function for MAPMsG.
657
658% Default quantization
659QUANTIZATION = 11;
660if isfield(options, 'config') && isfield(options.config, 'mapmsg_quantization')
661 QUANTIZATION = options.config.mapmsg_quantization;
662end
663
664% Extract patience rate from the distribution
665% For exponential patience Exp(gamma): patienceProc = {-gamma, gamma}
666if iscell(patienceProc) && length(patienceProc) >= 2
667 % Extract rate from subgenerator
668 gamma = -patienceProc{1}(1,1);
669else
670 % Default patience rate
671 gamma = 0.1;
672end
673
674% Generate boundary levels and abandonment probabilities
675% For exponential patience with rate gamma:
676% F(t) = 1 - exp(-gamma*t) is the CDF (abandonment probability by time t)
677maxTime = 10; % Maximum time horizon for regimes
678BoundaryLevels = linspace(0, maxTime, QUANTIZATION);
679
680% Compute abandonment probability at each boundary
681% ga(k) = F(BoundaryLevels(k)) for piecewise constant approximation
682ga = zeros(1, QUANTIZATION + 1);
683ga(1) = 0; % No abandonment at time 0
684for k = 1:QUANTIZATION
685 % Abandonment probability at midpoint of regime
686 midpoint = (BoundaryLevels(k) + BoundaryLevels(min(k+1, QUANTIZATION))) / 2;
687 if k == 1
688 midpoint = BoundaryLevels(k) / 2;
689 end
690 ga(k+1) = 1 - exp(-gamma * midpoint);
691end
692
693end
Definition Station.m:265