LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
qsys_bmapphnn_retrial.m
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.
3%
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.
6%
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.
10%
11% Inputs:
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
23%
24% Optional parameters:
25% 'MaxLevel' - Fixed orbit truncation level. When empty or non-positive
26% (default) the level is chosen adaptively: it is doubled
27% until the mass retained at the top level contributes less
28% than 'TailTolerance' of the mean orbit length. A fixed
29% level disables the adaptive refinement.
30% 'Tolerance' - Convergence tolerance (default: 1e-10)
31% 'TailTolerance'- Relative orbit-truncation error target (default: 1e-6)
32% 'MaxDim' - Cap on the total generator dimension explored by the
33% adaptive refinement (default: 2e5)
34% 'MaxBlockSize' - Cap on the per-level block size V*d (default: 5000).
35% Exceeding it is an error: the phase-type service order
36% and the server count make the level block intractable.
37% 'Verbose' - Print progress messages (default: false)
38%
39% Returns a struct with fields:
40% L_orbit - Mean number of customers in orbit
41% N_server - Mean number of busy servers
42% L_system - Mean number in system (orbit + servers)
43% Utilization - Server utilization (N_server / N)
44% Throughput - System throughput
45% P_idle - Probability all servers are idle
46% P_empty_orbit - Probability orbit is empty
47% P_empty_system - Probability system is empty (idle and empty orbit)
48% pi - Stationary distribution (levels x Vd)
49% truncLevel - Truncation level used
50% truncError - Relative orbit-truncation error estimate at truncLevel
51% analyzer - Name of analyzer used
52%
53% Example:
54% % M/M/3/3 retrial queue (exponential arrivals and service)
55% D = {-2.0, 2.0}; % Exp(2) arrivals
56% beta = 1;
57% S = -1; % Exp(1) service
58% N = 3;
59% alpha = 0.5; % Retrial rate
60% gamma = 0; % No impatience
61% p = 0; % No batch rejection
62% R = 2; % Admission threshold
63% result = qsys_bmapphnn_retrial(D, beta, S, N, alpha, gamma, p, R);
64%
65% See also qsys_mapph1, qsys_is_retrial
66%
67% Copyright (c) 2012-2026, Imperial College London
68% All rights reserved.
69
70% Parse optional arguments
71parser = inputParser;
72addParameter(parser, 'MaxLevel', [], @isnumeric);
73addParameter(parser, 'RetrialPolicy', RetrialPolicy.LINEAR, @isnumeric);
74addParameter(parser, 'Tolerance', 1e-10, @isnumeric);
75addParameter(parser, 'TailTolerance', 1e-6, @isnumeric);
76addParameter(parser, 'MaxDim', 2e5, @isnumeric);
77addParameter(parser, 'MaxBlockSize', 5000, @isnumeric);
78addParameter(parser, 'Verbose', false, @islogical);
79parse(parser, varargin{:});
80
81maxLevelParam = parser.Results.MaxLevel;
82retrialPolicy = parser.Results.RetrialPolicy;
83tol = parser.Results.Tolerance;
84tailTol = parser.Results.TailTolerance;
85dimMax = parser.Results.MaxDim;
86blockMax = parser.Results.MaxBlockSize;
87verbose = parser.Results.Verbose;
88
89%% Validate and process inputs
90
91% Reject anything that is not a well-formed Markovian input before any large
92% allocation: a malformed (D0,D1) or a NaN service subgenerator otherwise
93% propagates silently into the generator and corrupts the solve.
94validateRetrialInputs(D, beta, S, N, alpha, gamma, p, R);
95
96% BMAP parameters
97K = length(D) - 1; % Maximum batch size
98V = size(D{1}, 1); % Number of BMAP states
99
100% Compute generator of fundamental process: D^(1) = sum(D_k)
101D1_gen = zeros(size(D{1}));
102for k = 1:length(D)
103 D1_gen = D1_gen + D{k};
104end
105
106% Stationary distribution of fundamental process
107theta = computeStationaryVector(D1_gen);
108
109% Mean arrival rate: lambda = theta * sum(k * D_k) * e
110sumKDk = zeros(size(D{1}));
111for k = 2:length(D)
112 sumKDk = sumKDk + (k-1) * D{k};
113end
114lambda = theta * sumKDk * ones(V, 1);
115
116% PH service parameters
117beta = beta(:)';
118M = size(S, 1);
119S0 = -S * ones(M, 1);
120b1 = beta * (-S \ ones(M, 1)); % Mean service time
121
122% Handle R parameter (threshold)
123if isscalar(R)
124 R = R * ones(1, V);
125else
126 R = R(:)';
127end
128
129% Compute T_n values: T_n = C(n+M-1, M-1) = number of service states with n busy
130T = zeros(1, N+1);
131for n = 0:N
132 T(n+1) = nchoosek(n + M - 1, M - 1);
133end
134d = sum(T); % Total dimension per BMAP state
135
136% Build state mapping
137stateMap = buildStateMap(N, M, T);
138
139%% Determine truncation level
140rho = lambda * b1 / N; % Offered load
141
142if verbose
143 fprintf('Solving BMAP/PH/N/N retrial queue...\n');
144 fprintf(' V=%d, M=%d, N=%d, K=%d\n', V, M, N, K);
145 fprintf(' d=%d, block size Vd=%d\n', d, V*d);
146 fprintf(' lambda=%.4f, mu=%.4f\n', lambda, 1/b1);
147 fprintf(' Offered load rho=%.4f\n', rho);
148end
149
150% Context structure for helper functions
151ctx = struct('D', {D}, 'beta', beta, 'S', S, 'S0', S0, 'M', M, 'N', N, ...
152 'V', V, 'K', K, 'd', d, 'T', T, 'R', R, 'alpha', alpha, ...
153 'gamma', gamma, 'p', p, 'stateMap', {stateMap}, 'retrialPolicy', retrialPolicy);
154
155Vd = V * d;
156
157% A level block of size Vd is dense and is built once per level, so an
158% oversized block (high phase-type service order combined with many servers)
159% must be rejected rather than attempted.
160if Vd > blockMax
161 line_error(mfilename, sprintf(['Per-level block size V*d = %d exceeds MaxBlockSize = %d. ' ...
162 'The service distribution has %d phases and the station has %d servers, which yields ' ...
163 '%d service configurations. Reduce the phase-type order (e.g. fit the service ' ...
164 'distribution with fewer phases), reduce the number of servers, or raise ' ...
165 '''MaxBlockSize'' if the memory cost is acceptable.'], Vd, round(blockMax), M, N, d));
166end
167
168%% Build and solve the system
169% see _kb/03-api-layer.md (qsys/ family) for rationale
170if ~isempty(maxLevelParam) && maxLevelParam > 0
171 truncLevel = round(maxLevelParam);
172 pi = solveAtLevel(ctx, truncLevel, Vd, verbose);
173 truncError = orbitTruncationError(pi, truncLevel);
174else
175 truncLevel = max(100, ceil(50 / (1 - min(rho, 0.99))));
176 truncError = Inf;
177 converged = false;
178 while true
179 pi = solveAtLevel(ctx, truncLevel, Vd, verbose);
180 truncError = orbitTruncationError(pi, truncLevel);
181 if truncError <= tailTol
182 converged = true;
183 break;
184 end
185 nextLevel = 2 * truncLevel;
186 if (nextLevel + 1) * Vd > dimMax
187 break;
188 end
189 if verbose
190 fprintf(' Truncation error %.3e > %.3e, refining to level %d\n', ...
191 truncError, tailTol, nextLevel);
192 end
193 truncLevel = nextLevel;
194 end
195 if ~converged
196 line_warning(mfilename, sprintf(['Orbit truncation did not reach the requested accuracy: ' ...
197 'residual %.3e > TailTolerance %.3e at level %d (dimension cap MaxDim = %d). ' ...
198 'Orbit measures are underestimated; raise ''MaxDim'' or set ''MaxLevel'' explicitly.'], ...
199 truncError, tailTol, truncLevel, round(dimMax)));
200 end
201end
202
203if verbose
204 fprintf(' Truncation level: %d (residual %.3e)\n', truncLevel, truncError);
205end
206
207%% Compute performance measures
208maxLevel = size(pi, 1) - 1;
209
210% Mean number in orbit
211L_orbit = 0;
212for i = 1:maxLevel
213 L_orbit = L_orbit + i * sum(pi(i+1, :));
214end
215
216% Mean number of busy servers
217N_server = 0;
218for i = 0:maxLevel
219 piLevel = pi(i+1, :);
220 for nu = 1:V
221 for n = 0:N
222 offset = (nu-1)*d + getBlockOffset(T, n);
223 for t = 1:T(n+1)
224 idx = offset + t - 1;
225 if idx <= Vd
226 N_server = N_server + n * piLevel(idx);
227 end
228 end
229 end
230 end
231end
232
233% Probability all servers idle
234P_idle = 0;
235for i = 0:maxLevel
236 piLevel = pi(i+1, :);
237 for nu = 1:V
238 offset = (nu-1)*d + 1; % n=0
239 P_idle = P_idle + piLevel(offset);
240 end
241end
242
243% Probability orbit empty
244P_empty_orbit = sum(pi(1, :));
245
246% Probability system empty
247P_empty = 0;
248piLevel = pi(1, :);
249for nu = 1:V
250 offset = (nu-1)*d + 1;
251 P_empty = P_empty + piLevel(offset);
252end
253
254%% Build result struct
255result = 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;
264result.pi = pi;
265result.truncLevel = truncLevel;
266result.truncError = truncError;
267result.analyzer = 'LINE:qsys_bmapphnn_retrial';
268
269if verbose
270 fprintf('Solution complete.\n');
271end
272
273end
274
275%% ========== Helper Functions ==========
276
277function validateRetrialInputs(D, beta, S, N, alpha, gamma, p, R)
278% Reject inputs that are not a well-formed BMAP/PH pair. The generator build
279% is driven entirely by these matrices, so a NaN, an Inf, or a non-square
280% block would otherwise be written into the generator and only surface as a
281% meaningless stationary vector or as an out-of-memory failure.
282
283if ~iscell(D) || isempty(D)
284 line_error(mfilename, 'BMAP arrival representation D must be a non-empty cell array {D0,D1,...}.');
285end
286if numel(D) < 2
287 line_error(mfilename, ['BMAP arrival representation D must contain at least {D0,D1}. ' ...
288 'A single-element representation is a non-Markovian distribution (e.g. Det or a trace) ' ...
289 'and is not admissible in the matrix-analytic retrial engine.']);
290end
291V = size(D{1}, 1);
292for k = 1:numel(D)
293 Dk = D{k};
294 if ~isnumeric(Dk) || ~ismatrix(Dk) || size(Dk,1) ~= size(Dk,2) || size(Dk,1) ~= V
295 line_error(mfilename, sprintf('BMAP matrix D{%d} must be a %dx%d numeric matrix.', k, V, V));
296 end
297 if any(~isfinite(Dk(:)))
298 line_error(mfilename, sprintf(['BMAP matrix D{%d} contains NaN or Inf entries. The arrival ' ...
299 'process is disabled or not phase-type representable.'], k));
300 end
301 if k > 1 && any(Dk(:) < -GlobalConstants.FineTol)
302 line_error(mfilename, sprintf('BMAP arrival matrix D{%d} must be non-negative.', k));
303 end
304end
305if any(diag(D{1}) > GlobalConstants.FineTol)
306 line_error(mfilename, 'BMAP matrix D0 must have non-positive diagonal entries.');
307end
308Dsum = zeros(V);
309for k = 1:numel(D)
310 Dsum = Dsum + D{k};
311end
312if any(abs(Dsum * ones(V,1)) > sqrt(GlobalConstants.FineTol))
313 line_error(mfilename, 'BMAP matrices are inconsistent: sum_k D_k must have zero row sums.');
314end
315
316if ~isnumeric(beta) || isempty(beta) || any(~isfinite(beta(:)))
317 line_error(mfilename, ['Phase-type service vector beta is empty or contains NaN/Inf. The service ' ...
318 'distribution is disabled or not phase-type representable.']);
319end
320M = size(S, 1);
321if ~isnumeric(S) || ~ismatrix(S) || size(S,2) ~= M || numel(beta) ~= M
322 line_error(mfilename, sprintf('Phase-type service subgenerator S must be square and conformant with beta (%d phases).', numel(beta)));
323end
324if any(~isfinite(S(:)))
325 line_error(mfilename, ['Phase-type service subgenerator S contains NaN or Inf entries. The service ' ...
326 'distribution is disabled or not phase-type representable.']);
327end
328if any(diag(S) >= 0)
329 line_error(mfilename, 'Phase-type service subgenerator S must have strictly negative diagonal entries.');
330end
331if any(beta(:) < -GlobalConstants.FineTol) || abs(sum(beta(:)) - 1) > sqrt(GlobalConstants.FineTol)
332 line_error(mfilename, 'Phase-type service vector beta must be non-negative and sum to one.');
333end
334if any(-S * ones(M,1) < -GlobalConstants.FineTol)
335 line_error(mfilename, 'Phase-type service subgenerator S must have non-negative exit rates.');
336end
337
338if ~isscalar(N) || ~isfinite(N) || N < 1 || N ~= round(N)
339 line_error(mfilename, 'Number of servers N must be a positive integer.');
340end
341if ~isscalar(alpha) || ~isfinite(alpha) || alpha < 0
342 line_error(mfilename, 'Retrial rate alpha must be a finite non-negative scalar.');
343end
344if ~isscalar(gamma) || ~isfinite(gamma) || gamma < 0
345 line_error(mfilename, 'Orbit impatience rate gamma must be a finite non-negative scalar.');
346end
347if ~isscalar(p) || ~isfinite(p) || p < 0 || p > 1
348 line_error(mfilename, 'Batch rejection probability p must lie in [0,1].');
349end
350if any(~isfinite(R(:))) || any(R(:) < 0) || any(R(:) > N)
351 line_error(mfilename, sprintf('Admission threshold R must lie in [0,%d].', N));
352end
353end
354
355function err = orbitTruncationError(pi, truncLevel)
356% Relative contribution that the truncated tail would add to the mean orbit
357% length. Truncation reflects the probability flow that would leave the top
358% level back into it, so the mass sitting at the top level bounds the error.
359levelMass = sum(pi, 2);
360L_orbit = (0:truncLevel) * levelMass;
361err = truncLevel * levelMass(end) / max(L_orbit, realmin);
362end
363
364function pi = solveAtLevel(ctx, truncLevel, Vd, verbose)
365% Build the level-truncated generator and solve pi*Q = 0, pi*e = 1.
366%
367% The level blocks are level-homogeneous apart from the orbit terms, which
368% are linear in the level index: the diagonal block is Qdiag0 + i*Qdiag1
369% (retrial and impatience departures from an orbit of size i), the
370% subdiagonal block is i*Qsub1 (one of the i orbiting customers succeeds or
371% abandons) and the k-th superdiagonal block is level-independent. Building
372% those four shapes once and replicating them keeps the assembly linear in
373% the truncation level, which the adaptive refinement relies on.
374totalDim = (truncLevel + 1) * Vd;
375
376if verbose
377 fprintf('Total matrix dimension: %d x %d\n', totalDim, totalDim);
378end
379
380% see _kb/03-api-layer.md (qsys/ family) for rationale
381ctxGamma = ctx; ctxGamma.alpha = 0; % impatience only
382ctxAlpha = ctx; ctxAlpha.gamma = 0; % retrials only
383
384Qdiag0 = buildGeneratorLevel(ctx, 0, 0); % diagonal block, empty orbit
385Qdiag1G = buildGeneratorLevel(ctxGamma, 1, 1) - Qdiag0; % per-customer impatience increment
386Qdiag1A = buildGeneratorLevel(ctxAlpha, 1, 1) - Qdiag0; % one-unit retrial increment
387QsubG = buildGeneratorLevel(ctxGamma, 1, 0); % subdiagonal, impatience part
388QsubA = buildGeneratorLevel(ctxAlpha, 1, 0); % subdiagonal, retrial part
389Qsup = cell(1, ctx.K);
390for k = 1:ctx.K
391 Qsup{k} = buildGeneratorLevel(ctx, 0, k);
392end
393
394levels = (0:truncLevel)';
395
396% Retrial weight per level: the orbit size under LINEAR, one whenever the orbit
397% is non-empty under CONSTANT.
398if ctx.retrialPolicy == RetrialPolicy.CONSTANT
399 retrialWeight = double(levels >= 1);
400else
401 retrialWeight = levels;
402end
403
404[rd0, cd0, vd0] = find(Qdiag0);
405[rdG, cdG, vdG] = find(Qdiag1G);
406[rdA, cdA, vdA] = find(Qdiag1A);
407[rsG, csG, vsG] = find(QsubG);
408[rsA, csA, vsA] = find(QsubA);
409
410% Diagonal blocks, replicated over all levels
411[I, J, X] = replicateBlock(rd0, cd0, vd0, levels, levels, ones(size(levels)), Vd);
412% Orbit terms on the diagonal blocks: impatience scales with the orbit size,
413% retrials with the policy weight
414[I2, J2, X2] = replicateBlock(rdG, cdG, vdG, levels, levels, levels, Vd);
415[I2a, J2a, X2a] = replicateBlock(rdA, cdA, vdA, levels, levels, retrialWeight, Vd);
416% Subdiagonal blocks (levels 1..truncLevel)
417subLevels = levels(levels >= 1);
418subWeight = retrialWeight(levels >= 1);
419[I3, J3, X3] = replicateBlock(rsG, csG, vsG, subLevels, subLevels - 1, subLevels, Vd);
420[I3a, J3a, X3a] = replicateBlock(rsA, csA, vsA, subLevels, subLevels - 1, subWeight, Vd);
421
422I = [I; I2; I2a; I3; I3a];
423J = [J; J2; J2a; J3; J3a];
424X = [X; X2; X2a; X3; X3a];
425
426for k = 1:ctx.K
427 [rk, ck, vk] = find(Qsup{k});
428 supLevels = levels(levels <= truncLevel - k);
429 if isempty(supLevels) || isempty(rk)
430 continue;
431 end
432 [Ik, Jk, Xk] = replicateBlock(rk, ck, vk, supLevels, supLevels + k, ones(size(supLevels)), Vd);
433 I = [I; Ik];
434 J = [J; Jk];
435 X = [X; Xk];
436end
437
438Q = sparse(I, J, X, totalDim, totalDim);
439
440% Ensure rows sum to zero
441Q = Q - spdiags(full(sum(Q, 2)), 0, totalDim, totalDim);
442
443% Solve pi * Q = 0, pi * e = 1
444if verbose
445 fprintf('Solving linear system...\n');
446end
447
448Q(:, end) = ones(totalDim, 1);
449b = zeros(1, totalDim);
450b(end) = 1;
451
452if totalDim > 5000
453 if verbose
454 fprintf('Using sparse representation\n');
455 end
456 pi = (Q' \ b')';
457else
458 pi = b / full(Q);
459end
460
461% Reshape to level structure
462pi = reshape(pi, Vd, truncLevel + 1)';
463
464% Handle numerical issues
465if any(pi(:) < -1e-8)
466 line_warning(mfilename, 'Negative probabilities detected, clipping to zero');
467end
468pi(pi < 0) = 0;
469pi = pi / sum(pi(:)); % Renormalize
470end
471
472function [I, J, X] = replicateBlock(r, c, v, rowLevels, colLevels, scale, Vd)
473% Place a Vd x Vd block pattern (r,c,v) at every (rowLevels, colLevels) pair,
474% scaling the entries of the block at position n by scale(n).
475if isempty(r) || isempty(rowLevels)
476 I = zeros(0,1); J = zeros(0,1); X = zeros(0,1);
477 return;
478end
479r = r(:); c = c(:); v = v(:);
480rowLevels = rowLevels(:)'; colLevels = colLevels(:)'; scale = scale(:)';
481I = reshape(r + rowLevels * Vd, [], 1);
482J = reshape(c + colLevels * Vd, [], 1);
483X = reshape(v * scale, [], 1);
484end
485
486function theta = computeStationaryVector(Q)
487% Solve theta * Q = 0, theta * e = 1
488n = size(Q, 1);
489A = Q';
490A(end, :) = ones(1, n);
491b = zeros(n, 1);
492b(end) = 1;
493theta = (A \ b)';
494end
495
496function stateMap = buildStateMap(N, M, T)
497% Build mapping from (n, service_state_vector) to linear index
498stateMap = cell(N + 1, 1);
499for n = 0:N
500 stateMap{n+1} = generateCompositions(n, M);
501end
502end
503
504function comps = generateCompositions(n, M)
505% Generate all weak compositions of n into M parts (reverse lexicographic)
506if M == 1
507 comps = n;
508 return;
509end
510numComps = nchoosek(n + M - 1, M - 1);
511comps = zeros(numComps, M);
512idx = 1;
513for m1 = n:-1:0
514 subComps = generateCompositions(n - m1, M - 1);
515 numSub = size(subComps, 1);
516 comps(idx:idx+numSub-1, 1) = m1;
517 comps(idx:idx+numSub-1, 2:end) = subComps;
518 idx = idx + numSub;
519end
520end
521
522function offset = getBlockOffset(T, n)
523% Get starting index (1-based) for states with n busy servers
524if n == 0
525 offset = 1;
526else
527 offset = sum(T(1:n)) + 1;
528end
529end
530
531function L = computeL(ctx, n)
532% Matrix L_n: service completion transitions (T_n x T_{n-1})
533if n == 0
534 L = [];
535 return;
536end
537L = zeros(ctx.T(n+1), ctx.T(n));
538compsN = ctx.stateMap{n+1};
539compsNm1 = ctx.stateMap{n};
540
541for i = 1:size(compsN, 1)
542 m = compsN(i, :);
543 for l = 1:ctx.M
544 if m(l) > 0
545 mPrime = m;
546 mPrime(l) = mPrime(l) - 1;
547 for j = 1:size(compsNm1, 1)
548 if all(compsNm1(j,:) == mPrime)
549 L(i, j) = L(i, j) + m(l) * ctx.S0(l);
550 break;
551 end
552 end
553 end
554 end
555end
556end
557
558function A = computeA(ctx, n)
559% Matrix A_n: phase change transitions (T_n x T_n)
560if n == 0
561 A = 0;
562 return;
563end
564A = zeros(ctx.T(n+1), ctx.T(n+1));
565comps = ctx.stateMap{n+1};
566
567for i = 1:size(comps, 1)
568 m = comps(i, :);
569 for l = 1:ctx.M
570 if m(l) > 0
571 for lPrime = 1:ctx.M
572 if lPrime ~= l && ctx.S(l, lPrime) > 0
573 mPrime = m;
574 mPrime(l) = mPrime(l) - 1;
575 mPrime(lPrime) = mPrime(lPrime) + 1;
576 for j = 1:size(comps, 1)
577 if all(comps(j,:) == mPrime)
578 A(i, j) = A(i, j) + m(l) * ctx.S(l, lPrime);
579 break;
580 end
581 end
582 end
583 end
584 end
585 end
586end
587end
588
589function P = computeP(ctx, n)
590% Matrix P_n: new arrival transitions (T_n x T_{n+1})
591if n >= ctx.N
592 P = [];
593 return;
594end
595P = zeros(ctx.T(n+1), ctx.T(n+2));
596compsN = ctx.stateMap{n+1};
597compsNp1 = ctx.stateMap{n+2};
598
599for i = 1:size(compsN, 1)
600 m = compsN(i, :);
601 for l = 1:ctx.M
602 if ctx.beta(l) > 0
603 mPrime = m;
604 mPrime(l) = mPrime(l) + 1;
605 for j = 1:size(compsNp1, 1)
606 if all(compsNp1(j,:) == mPrime)
607 P(i, j) = P(i, j) + ctx.beta(l);
608 break;
609 end
610 end
611 end
612 end
613end
614end
615
616function Delta = computeDelta(ctx, n)
617% Diagonal matrix Delta_n: exit rates (T_n x T_n)
618if n == 0
619 Delta = 0;
620 return;
621end
622comps = ctx.stateMap{n+1};
623diagVals = zeros(ctx.T(n+1), 1);
624for i = 1:size(comps, 1)
625 m = comps(i, :);
626 total = 0;
627 for l = 1:ctx.M
628 total = total + m(l) * (-ctx.S(l, l));
629 end
630 diagVals(i) = total;
631end
632Delta = diag(diagVals);
633end
634
635function Gamma = computeGamma(ctx, nu)
636% Diagonal matrix Gamma^(nu): 0 for n <= R_nu, 1 for n > R_nu
637diagVals = zeros(ctx.d, 1);
638offset = 1;
639for n = 0:ctx.N
640 if n > ctx.R(nu)
641 diagVals(offset:offset + ctx.T(n+1) - 1) = 1;
642 end
643 offset = offset + ctx.T(n+1);
644end
645Gamma = diag(diagVals);
646end
647
648function G = computeG_nn(ctx, n, nu, nuPrime)
649% G_{n,n}^{(nu,nu')} matrix for batch losses
650if n <= ctx.N - ctx.K
651 G = zeros(ctx.T(n+1));
652else
653 total = 0;
654 for k = (ctx.N - n + 1):ctx.K
655 if k >= 1 && k <= ctx.K
656 total = total + ctx.D{k+1}(nu, nuPrime);
657 end
658 end
659 G = ctx.p * total * eye(ctx.T(n+1));
660end
661end
662
663function B = computeB(ctx, nu)
664% Block matrix B^(nu) of size d x d
665B = zeros(ctx.d, ctx.d);
666
667% Precompute matrices
668L = cell(ctx.N + 1, 1);
669A = cell(ctx.N + 1, 1);
670P = cell(ctx.N + 1, 1);
671Delta = cell(ctx.N + 1, 1);
672
673for n = 0:ctx.N
674 L{n+1} = computeL(ctx, n);
675 A{n+1} = computeA(ctx, n);
676 P{n+1} = computeP(ctx, n);
677 Delta{n+1} = computeDelta(ctx, n);
678end
679
680for n = 0:ctx.N
681 rowStart = getBlockOffset(ctx.T, n);
682 rowEnd = rowStart + ctx.T(n+1) - 1;
683
684 % Diagonal block
685 G_nn = computeG_nn(ctx, n, nu, nu);
686 if n == 0
687 B(rowStart, rowStart) = G_nn;
688 else
689 B(rowStart:rowEnd, rowStart:rowEnd) = A{n+1} + Delta{n+1} + G_nn;
690 end
691
692 % Subdiagonal block
693 if n >= 1
694 colStart = getBlockOffset(ctx.T, n-1);
695 colEnd = colStart + ctx.T(n) - 1;
696 B(rowStart:rowEnd, colStart:colEnd) = L{n+1};
697 end
698
699 % Superdiagonal blocks
700 for k = 1:ctx.K
701 if n + k <= ctx.N
702 colStart = getBlockOffset(ctx.T, n+k);
703 colEnd = colStart + ctx.T(n+k+1) - 1;
704 D_k_nu_nu = ctx.D{k+1}(nu, nu);
705
706 Pprod = eye(ctx.T(n+1));
707 for j = n:n+k-1
708 if j < ctx.N
709 Pprod = Pprod * P{j+1};
710 end
711 end
712 B(rowStart:rowEnd, colStart:colEnd) = D_k_nu_nu * Pprod;
713 end
714 end
715end
716end
717
718function Bbar = computeBbar(ctx, nu)
719% B_bar^(nu) matrix for successful retrials
720Bbar = zeros(ctx.d, ctx.d);
721for n = 0:min(ctx.R(nu), ctx.N - 1)
722 rowStart = getBlockOffset(ctx.T, n);
723 rowEnd = rowStart + ctx.T(n+1) - 1;
724 colStart = getBlockOffset(ctx.T, n+1);
725 colEnd = colStart + ctx.T(n+2) - 1;
726 P_n = computeP(ctx, n);
727 Bbar(rowStart:rowEnd, colStart:colEnd) = P_n;
728end
729end
730
731function Btilde = computeBtilde(ctx, nu, nuPrime)
732% B_tilde^(nu, nu') for BMAP state transitions
733Btilde = zeros(ctx.d, ctx.d);
734
735P = cell(ctx.N + 1, 1);
736for n = 0:ctx.N
737 P{n+1} = computeP(ctx, n);
738end
739
740for n = 0:ctx.N
741 rowStart = getBlockOffset(ctx.T, n);
742 rowEnd = rowStart + ctx.T(n+1) - 1;
743
744 % Diagonal block
745 G_nn = computeG_nn(ctx, n, nu, nuPrime);
746 Btilde(rowStart:rowEnd, rowStart:rowEnd) = G_nn;
747
748 % Superdiagonal blocks
749 for k = 1:ctx.K
750 if n + k <= ctx.N
751 colStart = getBlockOffset(ctx.T, n+k);
752 colEnd = colStart + ctx.T(n+k+1) - 1;
753 D_k_nu_nuPrime = ctx.D{k+1}(nu, nuPrime);
754
755 Pprod = eye(ctx.T(n+1));
756 for j = n:n+k-1
757 if j < ctx.N
758 Pprod = Pprod * P{j+1};
759 end
760 end
761 Btilde(rowStart:rowEnd, colStart:colEnd) = D_k_nu_nuPrime * Pprod;
762 end
763 end
764end
765end
766
767function C = computeC(ctx, n, k, nu, nuPrime)
768% C_{n,k}^(nu, nu') for partial batch admission to orbit
769if n < ctx.N - ctx.K + k
770 C = zeros(ctx.T(n+1), 1);
771 if ctx.T(n+1) > 1
772 C = zeros(ctx.T(n+1), 1);
773 end
774elseif n < ctx.N
775 batchSize = ctx.N - n + k;
776 if batchSize >= 1 && batchSize <= ctx.K
777 D_batch = ctx.D{batchSize + 1}(nu, nuPrime);
778
779 P = cell(ctx.N + 1, 1);
780 for nn = 0:ctx.N
781 P{nn+1} = computeP(ctx, nn);
782 end
783
784 Pprod = eye(ctx.T(n+1));
785 for j = n:ctx.N-1
786 Pprod = Pprod * P{j+1};
787 end
788 C = (1 - ctx.p) * D_batch * Pprod;
789 else
790 C = zeros(ctx.T(n+1), ctx.T(ctx.N+1));
791 end
792else % n == N
793 if k >= 1 && k <= ctx.K
794 D_k = ctx.D{k+1}(nu, nuPrime);
795 C = (1 - ctx.p) * D_k * eye(ctx.T(ctx.N+1));
796 else
797 C = zeros(ctx.T(ctx.N+1));
798 end
799end
800end
801
802function Q = buildGeneratorLevel(ctx, i, j)
803% Build generator block Q_{i,j}
804Vd = ctx.V * ctx.d;
805Q = zeros(Vd, Vd);
806
807if j < max(0, i-1) || j > i + ctx.K
808 return;
809end
810
811% Precompute matrices
812B = cell(ctx.V, 1);
813Bbar = cell(ctx.V, 1);
814Gamma = cell(ctx.V, 1);
815
816for nu = 1:ctx.V
817 B{nu} = computeB(ctx, nu);
818 Bbar{nu} = computeBbar(ctx, nu);
819 Gamma{nu} = computeGamma(ctx, nu);
820end
821
822if i == j % Diagonal block
823 for nu = 1:ctx.V
824 rowStart = (nu-1)*ctx.d + 1;
825 rowEnd = nu*ctx.d;
826
827 for nuPrime = 1:ctx.V
828 colStart = (nuPrime-1)*ctx.d + 1;
829 colEnd = nuPrime*ctx.d;
830
831 if nu == nuPrime
832 D0_nu_nu = ctx.D{1}(nu, nu);
833 block = D0_nu_nu * eye(ctx.d) + B{nu} ...
834 - i*(ctx.gamma + ctx.alpha)*eye(ctx.d) ...
835 + i*ctx.alpha*Gamma{nu};
836 Q(rowStart:rowEnd, colStart:colEnd) = block;
837 else
838 Btilde = computeBtilde(ctx, nu, nuPrime);
839 D0_nu_nuPrime = ctx.D{1}(nu, nuPrime);
840 Q(rowStart:rowEnd, colStart:colEnd) = ...
841 Btilde + D0_nu_nuPrime * eye(ctx.d);
842 end
843 end
844 end
845
846elseif j == i - 1 && i >= 1 % Subdiagonal block
847 for nu = 1:ctx.V
848 rowStart = (nu-1)*ctx.d + 1;
849 rowEnd = nu*ctx.d;
850 colStart = rowStart;
851 colEnd = rowEnd;
852
853 block = i*ctx.gamma*eye(ctx.d) + i*ctx.alpha*Bbar{nu};
854 Q(rowStart:rowEnd, colStart:colEnd) = block;
855 end
856
857elseif j > i && j <= i + ctx.K % Superdiagonal blocks
858 k = j - i;
859
860 for nu = 1:ctx.V
861 rowStart = (nu-1)*ctx.d + 1;
862 rowEnd = nu*ctx.d;
863
864 for nuPrime = 1:ctx.V
865 colStart = (nuPrime-1)*ctx.d + 1;
866 colEnd = nuPrime*ctx.d;
867
868 block = zeros(ctx.d, ctx.d);
869 for n = 0:ctx.N
870 C_nk = computeC(ctx, n, k, nu, nuPrime);
871 if ~isempty(C_nk) && any(C_nk(:) ~= 0)
872 nRowStart = getBlockOffset(ctx.T, n);
873 nRowEnd = nRowStart + ctx.T(n+1) - 1;
874 NColStart = getBlockOffset(ctx.T, ctx.N);
875 NColEnd = NColStart + ctx.T(ctx.N+1) - 1;
876
877 if size(C_nk, 2) == ctx.T(ctx.N+1)
878 block(nRowStart:nRowEnd, NColStart:NColEnd) = C_nk;
879 end
880 end
881 end
882 Q(rowStart:rowEnd, colStart:colEnd) = block;
883 end
884 end
885end
886end