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)
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);
21 perf =
struct(
'analyzer',
'LINE:solver_mam_reneging');
25% Check
for retrial topology
26[isRetrial, retInfo] = qsys_is_retrial(sn);
28 line_error(mfilename,
'No valid impatience configuration detected (retrial or reneging).');
31% Initialize output arrays
42sourceIdx = retInfo.sourceIdx;
43queueIdx = retInfo.stationIdx;
44classIdx = retInfo.classIdx;
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);
54% Extract arrival process (BMAP) from source
55% sn.proc{sourceIdx}{classIdx} contains
the arrival process
56arrivalProc = sn.proc{sourceIdx}{classIdx};
58% Convert arrival process to BMAP matrices D = {D0, D1, ...}
59D = extractBMAPMatrices(arrivalProc);
62 line_error(mfilename,
'Could not extract BMAP matrices from arrival process.');
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);
70if isempty(beta) || isempty(S)
71 line_error(mfilename,
'Could not extract PH parameters from service process.');
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);
86% Extract orbit impatience gamma (
default 0)
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);
98% Extract batch rejection probability p (
default 0)
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);
106% Extract admission threshold R (from FCR or default N-1)
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.
115if isfield(options, 'config') && isstruct(options.config) ...
116 && isfield(options.config, 'orbit_maxlevel') && ~isempty(options.config.orbit_maxlevel)
117 maxLevel = options.config.orbit_maxlevel;
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);
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);
138if isfield(options, 'config') && isstruct(options.config) ...
139 && isfield(options.config, 'orbit_tailtol') && ~isempty(options.config.orbit_tailtol)
140 tailTol = options.config.orbit_tailtol;
144if isfield(options, 'tol') && ~isempty(options.tol)
149if isfield(options, 'verbose') && options.verbose
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);
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;
164% Response time via Little's law
165if perf.Throughput > 0
166 RN(queueIdx, classIdx) = QN(queueIdx, classIdx) / perf.Throughput;
168 RN(queueIdx, classIdx) = Inf;
171% System-level metrics
172XN(classIdx) = perf.Throughput;
173CN(classIdx) = RN(queueIdx, classIdx);
175% Return iteration
count (truncation level used)
176totiter = perf.truncLevel;
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};
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));
198n0 = size(proc{1}, 1);
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));
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));
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)
220if size(sn.procid, 1) < ist || size(sn.procid, 2) < r
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)));
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
242if isempty(proc) || ~iscell(proc)
246% Check
if already in BMAP format (cell of cell arrays - unlikely)
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
256if length(proc) >= 2 && isnumeric(proc{1}) && isnumeric(proc{2})
262 % Check
if D0 looks like a generator (negative diagonal)
263 % For scalar Exp(rate), D0 = -rate (negative)
265 % Scalar
case (exponential)
267 % Already in MAP format {D0, D1}
272 % Matrix
case - check
if D0 has negative diagonal
274 if all(diagD0 < 0) || all(diagD0 <= 0)
275 % Looks like MAP format {D0, D1, ...}
281 % If we get here,
try to interpret as PH format {alpha, T}
283 alpha = D0; % Initial probability vector
284 T = D1; % Subgenerator
286 if isrow(alpha) || (numel(alpha) == size(T, 1) && size(T, 1) == size(T, 2))
289 if size(T, 1) == n && size(T, 2) == n
290 % Convert PH to MAP: D0 = T, D1 = (-T*e)*alpha
292 D1_new = (-T * ones(n, 1)) * alpha;
293 D = {D0_new, D1_new};
299% Fallback: assume it's already in MAP format
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
311if isempty(proc) || ~iscell(proc)
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})
322 % Validate dimensions
324 if size(D0, 2) ~= n || size(D1, 1) ~= n || size(D1, 2) ~= n
328 % S = D0 (subgenerator)
331 % S0 = -S * ones (exit rates)
332 S0 = -S * ones(n, 1);
334 % Extract alpha from D1 = S0 * alpha
335 % Find a row with non-zero exit rate
336 idx = find(S0 > 1e-10, 1);
338 % All rows have zero exit rate - use uniform
339 beta = ones(1, n) / n;
341 % alpha = D1(idx,:) / S0(idx)
342 beta = D1(idx, :) / S0(idx);
345 % Ensure beta
is row vector and sums to 1
347 if abs(sum(beta) - 1) > 1e-6
350 beta = beta / sum(beta);
352 beta = ones(1, n) / n;
359%% Reneging (MAPMsG) helper functions
361function [isReneging, info] = detectRenegingTopology(sn)
362% DETECTRENGINGTOPOLOGY Detect if model is suitable for MAPMsG solver
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)
376info.serviceRate = [];
381if ~sn_is_open_model(sn)
382 info.errorMsg = 'MAPMsG requires open queueing model.
';
386% Check single class (current limitation)
388 info.errorMsg = 'MAPMsG currently supports single class only.
';
393% Find source and queue stations
396for ist = 1:sn.nstations
397 nodeIdx = sn.stationToNode(ist);
398 if sn.nodetype(nodeIdx) == NodeType.Source
400 elseif sn.nodetype(nodeIdx) == NodeType.Queue
404 % Multiple queues - not supported
405 info.errorMsg = 'MAPMsG requires single queue station.
';
412 info.errorMsg = 'No Source node found.
';
416 info.errorMsg = 'No Queue node found.
';
420info.sourceIdx = sourceIdx;
421info.queueIdx = queueIdx;
423% Check for reneging patience configuration
424if ~isfield(sn, 'impatienceClass
') || isempty(sn.impatienceClass)
425 info.errorMsg = 'No patience/impatience configuration found.
';
429if sn.impatienceClass(queueIdx, info.classIdx) ~= ImpatienceType.RENEGING
430 info.errorMsg = 'Queue does not have reneging configured.
';
434% Check patience distribution exists
435if ~isfield(sn, 'patienceProc
') || isempty(sn.patienceProc)
436 info.errorMsg = 'No patience distribution found.
';
439if isempty(sn.patienceProc{queueIdx, info.classIdx})
440 info.errorMsg = 'No patience distribution for this class.
';
444% Check FCFS scheduling
445if sn.sched(queueIdx) ~= SchedStrategy.FCFS
446 info.errorMsg = 'MAPMsG requires FCFS scheduling.
';
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.
';
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).
';
462% Extract service rate
463info.serviceRate = -serviceProc{1}(1,1);
464info.nServers = sn.nservers(queueIdx);
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.
';
478function [QN,UN,RN,TN,CN,XN,totiter] = solveReneging(sn, options, info)
479% SOLVERENEGING Solve MAP/M/s+G queue using MAPMsG library
481% Reference: O. Gursoy, K. A. Mehr, N. Akar, "The MAP/M/s + G Call Center
482% Model with Generally Distributed Patience Times"
493sourceIdx = info.sourceIdx;
494queueIdx = info.queueIdx;
495classIdx = info.classIdx;
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)
503% Extract service rate and server count
504mu = info.serviceRate;
505SERVERSIZE = info.nServers;
507% Extract patience distribution and convert to regimes
508patienceProc = sn.patienceProc{queueIdx, classIdx};
509[BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options);
511% Build MRMFQ matrices following MAPMsGCompiler.m logic
513em = ones(MAPSIZE, 1);
514lmap = D * em; % Arrival rate vector
516% Initialize Qy0 (boundary generator at level 0)
517Qy0 = zeros((SERVERSIZE+1)*MAPSIZE, (SERVERSIZE+1)*MAPSIZE);
518for row = 1:SERVERSIZE+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;
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;
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
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;
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));
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;
558% Combine boundaries and regimes
559Qybounds = cat(3, Qy0, Qy);
560ydriftbounds = cat(1, Rydiag, ydriftregimes);
562% Prepare boundary levels (remove first, add large value at end)
568[coefficients, boundaries, Lzeromulti, Lnegmulti, Lposmulti, Anegmulti, Aposmulti] = ...
569 MRMFQSolver(Qy, Qybounds, ydriftregimes, ydriftbounds, B);
571% Compute steady-state results following MAPMsGCompiler.m
572zeromass = boundaries{1};
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));
580for d = 1:QUANTIZATION
592 coef = coefficients{d};
594 deltaB = B(d) - prevB;
596 integrand = [Lz * deltaB; ...
597 An \ (expm(An * deltaB) - eye(size(An))) * Ln; ...
598 Ap \ (eye(size(Ap)) - expm(-Ap * deltaB)) * Lp];
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;
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));
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);
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));
626% Compute arrival rate (lambda)
629% Map to LINE output format
630% Throughput = arrival rate * (1 - abandonment probability)
631throughput = lambda * (1 - AbandonProb);
632TN(queueIdx, classIdx) = throughput;
635UN(queueIdx, classIdx) = throughput / (mu * SERVERSIZE);
637% Response time = expected wait + expected service time
638RN(queueIdx, classIdx) = ExpectedWait + 1/mu;
640% Queue length via Little's law
641QN(queueIdx, classIdx) = throughput * RN(queueIdx, classIdx);
643% System-level metrics
644XN(classIdx) = throughput;
645CN(classIdx) = RN(queueIdx, classIdx);
648totiter = QUANTIZATION;
652function [BoundaryLevels, ga, QUANTIZATION] = convertPatienceToRegimes(patienceProc, options)
653% CONVERTPATIENCETOREGIMES Convert patience distribution to MAPMsG regimes
655% Converts a patience distribution (in MAP/PH format) to piecewise-constant
656% abandonment function
for MAPMsG.
658% Default quantization
660if isfield(options,
'config') && isfield(options.config,
'mapmsg_quantization')
661 QUANTIZATION = options.config.mapmsg_quantization;
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);
670 % Default patience rate
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);
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;
688 midpoint = BoundaryLevels(k) / 2;
690 ga(k+1) = 1 - exp(-gamma * midpoint);