1function result = qsys_bmapphnn_retrial(D, beta, S, N, alpha, gamma, p, R, varargin)
2% QSYS_BMAPPHNN_RETRIAL Analyzes a BMAP/PH/N/N bufferless retrial queue.
4% RESULT = QSYS_BMAPPHNN_RETRIAL(D, BETA, S, N, ALPHA, GAMMA,
P, R) analyzes
5% a BMAP/PH/N/N bufferless retrial queueing system with admission control.
7% This implements the algorithm from:
8% Dudin et al.,
"Analysis of BMAP/PH/N-Type Queueing System with Flexible
9% Retrials Admission Control", Mathematics 2025, 13(9), 1434.
12% D - Cell array {D0, D1, ..., DK} of BMAP matrices
13% D0: hidden transition matrix (V x V)
14% D1, ..., DK: arrival matrices for batches of size 1, ..., K
15% BETA - PH service initial probability vector (1 x M)
16% S - PH service subgenerator matrix (M x M)
17% N - Number of servers (also capacity, hence bufferless)
18% ALPHA - Retrial rate per customer in orbit
19% GAMMA - Impatience (abandonment) rate per customer in orbit
20%
P - Probability of batch rejection when not enough servers
21% R - Admission threshold (scalar or 1 x V vector per BMAP state)
22% When n > R(nu), arriving customers go to orbit
25%
'MaxLevel' - Maximum orbit level for truncation (default: auto)
26%
'Tolerance' - Convergence tolerance (default: 1e-10)
27%
'Verbose' - Print progress messages (default: false)
29% Returns a struct with fields:
30% L_orbit - Mean number of customers in orbit
31% N_server - Mean number of busy servers
32% L_system - Mean number in system (orbit + servers)
33% Utilization - Server utilization (N_server / N)
34% Throughput - System throughput
35% P_idle - Probability all servers are idle
36% P_empty_orbit - Probability orbit
is empty
37% P_empty_system - Probability system
is empty (idle and empty orbit)
38% pi - Stationary distribution (levels x Vd)
39% truncLevel - Truncation level used
40% analyzer - Name of analyzer used
43% % M/M/3/3 retrial queue (exponential arrivals and service)
44% D = {-2.0, 2.0}; % Exp(2) arrivals
46% S = -1; % Exp(1) service
48% alpha = 0.5; % Retrial rate
49% gamma = 0; % No impatience
50% p = 0; % No batch rejection
51% R = 2; % Admission threshold
52% result = qsys_bmapphnn_retrial(D, beta, S, N, alpha, gamma, p, R);
54% See also qsys_mapph1, qsys_is_retrial
56% Copyright (c) 2012-2026, Imperial College London
59% Parse optional arguments
61addParameter(parser, 'MaxLevel', [], @isnumeric);
62addParameter(parser, 'Tolerance', 1e-10, @isnumeric);
63addParameter(parser, 'Verbose', false, @islogical);
64parse(parser, varargin{:});
66maxLevelParam = parser.Results.MaxLevel;
67tol = parser.Results.Tolerance;
68verbose = parser.Results.Verbose;
70%% Validate and process inputs
73K = length(D) - 1; % Maximum batch size
74V = size(D{1}, 1); % Number of BMAP states
76% Compute generator of fundamental process: D^(1) = sum(D_k)
77D1_gen = zeros(size(D{1}));
79 D1_gen = D1_gen + D{k};
82% Stationary distribution of fundamental process
83theta = computeStationaryVector(D1_gen);
85% Mean arrival rate: lambda = theta * sum(k * D_k) * e
86sumKDk = zeros(size(D{1}));
88 sumKDk = sumKDk + (k-1) * D{k};
90lambda = theta * sumKDk * ones(V, 1);
92% PH service parameters
96b1 = beta * (-S \ ones(M, 1)); % Mean service time
98% Handle R parameter (threshold)
105% Compute T_n values: T_n = C(n+M-1, M-1) = number of service states with n busy
108 T(n+1) = nchoosek(n + M - 1, M - 1);
110d = sum(T); % Total dimension per BMAP state
113stateMap = buildStateMap(N, M, T);
115%% Determine truncation level
116rho = lambda * b1 / N; % Offered load
119 fprintf(
'Solving BMAP/PH/N/N retrial queue...\n');
120 fprintf(
' V=%d, M=%d, N=%d, K=%d\n', V, M, N, K);
121 fprintf(
' d=%d, block size Vd=%d\n', d, V*d);
122 fprintf(
' lambda=%.4f, mu=%.4f\n', lambda, 1/b1);
123 fprintf(
' Offered load rho=%.4f\n', rho);
126if isempty(maxLevelParam)
127 truncLevel = max(100, ceil(50 / (1 - min(rho, 0.99))));
129 truncLevel = maxLevelParam;
133 fprintf(
' Truncation level: %d\n', truncLevel);
136%% Build and solve the system
138totalDim = (truncLevel + 1) * Vd;
141 fprintf(
'Total matrix dimension: %d x %d\n', totalDim, totalDim);
144% Use sparse
for large systems
147 fprintf(
'Using sparse representation\n');
149 Q = sparse(totalDim, totalDim);
151 Q = zeros(totalDim, totalDim);
154% Context structure
for helper functions
155ctx =
struct(
'D', {D},
'beta', beta,
'S', S,
'S0', S0,
'M', M,
'N', N, ...
156 'V', V,
'K', K,
'd', d,
'T', T,
'R', R,
'alpha', alpha, ...
157 'gamma', gamma,
'p', p,
'stateMap', {stateMap});
159% Fill generator blocks
161 if verbose && mod(i, 20) == 0
162 fprintf(
' Level %d/%d\n', i, truncLevel);
168 for j = max(0, i-1):min(truncLevel, i+K)
172 Qij = buildGeneratorLevel(ctx, i, j);
173 Q(rowStart:rowEnd, colStart:colEnd) = Qij;
177% Ensure rows sum to zero
179 Q(i,i) = Q(i,i) - sum(Q(i,:));
182% Solve pi * Q = 0, pi * e = 1
184 fprintf(
'Solving linear system...\n');
187Q(:, end) = ones(totalDim, 1);
188b = zeros(1, totalDim);
197% Reshape to level structure
198pi = reshape(pi, Vd, truncLevel + 1)';
200% Handle numerical issues
202 line_warning(mfilename,
'Negative probabilities detected, clipping to zero');
205pi = pi / sum(pi(:)); % Renormalize
207%% Compute performance measures
208maxLevel = size(pi, 1) - 1;
210% Mean number in orbit
213 L_orbit = L_orbit + i * sum(pi(i+1, :));
216% Mean number of busy servers
219 piLevel = pi(i+1, :);
222 offset = (nu-1)*d + getBlockOffset(T, n);
224 idx = offset + t - 1;
226 N_server = N_server + n * piLevel(idx);
233% Probability all servers idle
236 piLevel = pi(i+1, :);
238 offset = (nu-1)*d + 1; % n=0
239 P_idle = P_idle + piLevel(offset);
243% Probability orbit empty
244P_empty_orbit = sum(pi(1, :));
246% Probability system empty
250 offset = (nu-1)*d + 1;
251 P_empty = P_empty + piLevel(offset);
254%% Build result struct
256result.L_orbit = L_orbit;
257result.N_server = N_server;
258result.L_system = L_orbit + N_server;
259result.Utilization = N_server / N;
260result.Throughput = N_server / b1;
261result.P_idle = P_idle;
262result.P_empty_orbit = P_empty_orbit;
263result.P_empty_system = P_empty;
265result.truncLevel = truncLevel;
266result.analyzer = 'LINE:qsys_bmapphnn_retrial';
269 fprintf('Solution complete.\n');
274%% ========== Helper Functions ==========
276function theta = computeStationaryVector(Q)
277% Solve theta * Q = 0, theta * e = 1
280A(end, :) = ones(1, n);
286function stateMap = buildStateMap(N, M, T)
287% Build mapping from (n, service_state_vector) to linear index
288stateMap = cell(N + 1, 1);
290 stateMap{n+1} = generateCompositions(n, M);
294function comps = generateCompositions(n, M)
295% Generate all weak compositions of n into M parts (reverse lexicographic)
300numComps = nchoosek(n + M - 1, M - 1);
301comps = zeros(numComps, M);
304 subComps = generateCompositions(n - m1, M - 1);
305 numSub = size(subComps, 1);
306 comps(idx:idx+numSub-1, 1) = m1;
307 comps(idx:idx+numSub-1, 2:end) = subComps;
312function offset = getBlockOffset(T, n)
313% Get starting index (1-based)
for states with n busy servers
317 offset = sum(T(1:n)) + 1;
321function L = computeL(ctx, n)
322% Matrix L_n: service completion transitions (T_n x T_{n-1})
327L = zeros(ctx.T(n+1), ctx.T(n));
328compsN = ctx.stateMap{n+1};
329compsNm1 = ctx.stateMap{n};
331for i = 1:size(compsN, 1)
336 mPrime(l) = mPrime(l) - 1;
337 for j = 1:size(compsNm1, 1)
338 if all(compsNm1(j,:) == mPrime)
339 L(i, j) = L(i, j) + m(l) * ctx.S0(l);
348function A = computeA(ctx, n)
349% Matrix A_n: phase change transitions (T_n x T_n)
354A = zeros(ctx.T(n+1), ctx.T(n+1));
355comps = ctx.stateMap{n+1};
357for i = 1:size(comps, 1)
362 if lPrime ~= l && ctx.S(l, lPrime) > 0
364 mPrime(l) = mPrime(l) - 1;
365 mPrime(lPrime) = mPrime(lPrime) + 1;
366 for j = 1:size(comps, 1)
367 if all(comps(j,:) == mPrime)
368 A(i, j) = A(i, j) + m(l) * ctx.S(l, lPrime);
379function
P = computeP(ctx, n)
380% Matrix P_n: new arrival transitions (T_n x T_{n+1})
385P = zeros(ctx.T(n+1), ctx.T(n+2));
386compsN = ctx.stateMap{n+1};
387compsNp1 = ctx.stateMap{n+2};
389for i = 1:size(compsN, 1)
394 mPrime(l) = mPrime(l) + 1;
395 for j = 1:size(compsNp1, 1)
396 if all(compsNp1(j,:) == mPrime)
397 P(i, j) =
P(i, j) + ctx.beta(l);
406function Delta = computeDelta(ctx, n)
407% Diagonal matrix Delta_n: exit rates (T_n x T_n)
412comps = ctx.stateMap{n+1};
413diagVals = zeros(ctx.T(n+1), 1);
414for i = 1:size(comps, 1)
418 total = total + m(l) * (-ctx.S(l, l));
422Delta = diag(diagVals);
425function Gamma = computeGamma(ctx, nu)
426% Diagonal matrix Gamma^(nu): 0 for n <= R_nu, 1 for n > R_nu
427diagVals = zeros(ctx.d, 1);
431 diagVals(offset:offset + ctx.T(n+1) - 1) = 1;
433 offset = offset + ctx.T(n+1);
435Gamma = diag(diagVals);
438function G = computeG_nn(ctx, n, nu, nuPrime)
439% G_{n,n}^{(nu,nu
')} matrix for batch losses
441 G = zeros(ctx.T(n+1));
444 for k = (ctx.N - n + 1):ctx.K
445 if k >= 1 && k <= ctx.K
446 total = total + ctx.D{k+1}(nu, nuPrime);
449 G = ctx.p * total * eye(ctx.T(n+1));
453function B = computeB(ctx, nu)
454% Block matrix B^(nu) of size d x d
455B = zeros(ctx.d, ctx.d);
458L = cell(ctx.N + 1, 1);
459A = cell(ctx.N + 1, 1);
460P = cell(ctx.N + 1, 1);
461Delta = cell(ctx.N + 1, 1);
464 L{n+1} = computeL(ctx, n);
465 A{n+1} = computeA(ctx, n);
466 P{n+1} = computeP(ctx, n);
467 Delta{n+1} = computeDelta(ctx, n);
471 rowStart = getBlockOffset(ctx.T, n);
472 rowEnd = rowStart + ctx.T(n+1) - 1;
475 G_nn = computeG_nn(ctx, n, nu, nu);
477 B(rowStart, rowStart) = G_nn;
479 B(rowStart:rowEnd, rowStart:rowEnd) = A{n+1} + Delta{n+1} + G_nn;
484 colStart = getBlockOffset(ctx.T, n-1);
485 colEnd = colStart + ctx.T(n) - 1;
486 B(rowStart:rowEnd, colStart:colEnd) = L{n+1};
489 % Superdiagonal blocks
492 colStart = getBlockOffset(ctx.T, n+k);
493 colEnd = colStart + ctx.T(n+k+1) - 1;
494 D_k_nu_nu = ctx.D{k+1}(nu, nu);
496 Pprod = eye(ctx.T(n+1));
499 Pprod = Pprod * P{j+1};
502 B(rowStart:rowEnd, colStart:colEnd) = D_k_nu_nu * Pprod;
508function Bbar = computeBbar(ctx, nu)
509% B_bar^(nu) matrix for successful retrials
510Bbar = zeros(ctx.d, ctx.d);
511for n = 0:min(ctx.R(nu), ctx.N - 1)
512 rowStart = getBlockOffset(ctx.T, n);
513 rowEnd = rowStart + ctx.T(n+1) - 1;
514 colStart = getBlockOffset(ctx.T, n+1);
515 colEnd = colStart + ctx.T(n+2) - 1;
516 P_n = computeP(ctx, n);
517 Bbar(rowStart:rowEnd, colStart:colEnd) = P_n;
521function Btilde = computeBtilde(ctx, nu, nuPrime)
522% B_tilde^(nu, nu')
for BMAP state transitions
523Btilde = zeros(ctx.d, ctx.d);
525P = cell(ctx.N + 1, 1);
527 P{n+1} = computeP(ctx, n);
531 rowStart = getBlockOffset(ctx.T, n);
532 rowEnd = rowStart + ctx.T(n+1) - 1;
535 G_nn = computeG_nn(ctx, n, nu, nuPrime);
536 Btilde(rowStart:rowEnd, rowStart:rowEnd) = G_nn;
538 % Superdiagonal blocks
541 colStart = getBlockOffset(ctx.T, n+k);
542 colEnd = colStart + ctx.T(n+k+1) - 1;
543 D_k_nu_nuPrime = ctx.D{k+1}(nu, nuPrime);
545 Pprod = eye(ctx.T(n+1));
548 Pprod = Pprod *
P{j+1};
551 Btilde(rowStart:rowEnd, colStart:colEnd) = D_k_nu_nuPrime * Pprod;
557function C = computeC(ctx, n, k, nu, nuPrime)
558% C_{n,k}^(nu, nu
') for partial batch admission to orbit
559if n < ctx.N - ctx.K + k
560 C = zeros(ctx.T(n+1), 1);
562 C = zeros(ctx.T(n+1), 1);
565 batchSize = ctx.N - n + k;
566 if batchSize >= 1 && batchSize <= ctx.K
567 D_batch = ctx.D{batchSize + 1}(nu, nuPrime);
569 P = cell(ctx.N + 1, 1);
571 P{nn+1} = computeP(ctx, nn);
574 Pprod = eye(ctx.T(n+1));
576 Pprod = Pprod * P{j+1};
578 C = (1 - ctx.p) * D_batch * Pprod;
580 C = zeros(ctx.T(n+1), ctx.T(ctx.N+1));
583 if k >= 1 && k <= ctx.K
584 D_k = ctx.D{k+1}(nu, nuPrime);
585 C = (1 - ctx.p) * D_k * eye(ctx.T(ctx.N+1));
587 C = zeros(ctx.T(ctx.N+1));
592function Q = buildGeneratorLevel(ctx, i, j)
593% Build generator block Q_{i,j}
597if j < max(0, i-1) || j > i + ctx.K
603Bbar = cell(ctx.V, 1);
604Gamma = cell(ctx.V, 1);
607 B{nu} = computeB(ctx, nu);
608 Bbar{nu} = computeBbar(ctx, nu);
609 Gamma{nu} = computeGamma(ctx, nu);
612if i == j % Diagonal block
614 rowStart = (nu-1)*ctx.d + 1;
617 for nuPrime = 1:ctx.V
618 colStart = (nuPrime-1)*ctx.d + 1;
619 colEnd = nuPrime*ctx.d;
622 D0_nu_nu = ctx.D{1}(nu, nu);
623 block = D0_nu_nu * eye(ctx.d) + B{nu} ...
624 - i*(ctx.gamma + ctx.alpha)*eye(ctx.d) ...
625 + i*ctx.alpha*Gamma{nu};
626 Q(rowStart:rowEnd, colStart:colEnd) = block;
628 Btilde = computeBtilde(ctx, nu, nuPrime);
629 D0_nu_nuPrime = ctx.D{1}(nu, nuPrime);
630 Q(rowStart:rowEnd, colStart:colEnd) = ...
631 Btilde + D0_nu_nuPrime * eye(ctx.d);
636elseif j == i - 1 && i >= 1 % Subdiagonal block
638 rowStart = (nu-1)*ctx.d + 1;
643 block = i*ctx.gamma*eye(ctx.d) + i*ctx.alpha*Bbar{nu};
644 Q(rowStart:rowEnd, colStart:colEnd) = block;
647elseif j > i && j <= i + ctx.K % Superdiagonal blocks
651 rowStart = (nu-1)*ctx.d + 1;
654 for nuPrime = 1:ctx.V
655 colStart = (nuPrime-1)*ctx.d + 1;
656 colEnd = nuPrime*ctx.d;
658 block = zeros(ctx.d, ctx.d);
660 C_nk = computeC(ctx, n, k, nu, nuPrime);
661 if ~isempty(C_nk) && any(C_nk(:) ~= 0)
662 nRowStart = getBlockOffset(ctx.T, n);
663 nRowEnd = nRowStart + ctx.T(n+1) - 1;
664 NColStart = getBlockOffset(ctx.T, ctx.N);
665 NColEnd = NColStart + ctx.T(ctx.N+1) - 1;
667 if size(C_nk, 2) == ctx.T(ctx.N+1)
668 block(nRowStart:nRowEnd, NColStart:NColEnd) = C_nk;
672 Q(rowStart:rowEnd, colStart:colEnd) = block;