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