1function [G,lG] = pfqn_conv(L, N, Z, cdscaling, options)
2% [G,LG] = PFQN_CONV(L, N, Z, CDSCALING, OPTIONS)
4% Multichain convolution algorithm
for closed queueing networks with
5%
class-dependent service rates.
7% Implements the convolution algorithm of Sauer (1983), Section 5.2,
8%
"Computational Algorithms for State-Dependent Queueing Networks",
9% ACM TOCS, Vol. 1, No. 1, pp. 67-92.
11% The algorithm computes G(N) = (X_1 * X_2 * ... * X_M)(N) where X_m(n)
12%
is the station
factor at population vector n, and * denotes the
13% multivariate discrete convolution:
14% A(n) =
sum_{i: 0<=i<=n} B(i) * C(n-i)
16% For
class-dependent stations, X_m(n)
is computed recursively via Sauer
18% X_m(n) = (u_km / mu_km(n)) * X_m(n - e_k)
19% where mu_km(n) = (n_k/|n|) *
beta_{m,k}(n) and beta
is the DIMENSIONLESS
20% class-dependent scaling of the service demand supplied by CDSCALING{m}: a
21% handle of the per-class population vector n at station m, returning either a
22% scalar (shared by every class) or a length-R vector. Equivalently
23% X_m(n) = (|n|/n_k) * (L(m,k)/
beta_{m,k}(n)) * X_m(n - e_k),
24% which at beta = 1
is exactly the load-independent multinomial
form, so a unit
25% scaling means
"no correction". This
is the same convention AMVA and CTMC use
26% (effective service time ST/beta). Any saturation/cutoff
is applied inside the
29% For standard (load-independent) stations, X_m(n) reduces to the
30% multinomial
form and the convolution uses the efficient recurrence:
31% G_m(n) = G_{m-1}(n) + sum_r L(m,r) * G_m(n - e_r)
34% L - Service demand matrix (M x R)
35% N - Population vector (1 x R), must be finite (closed network)
36% Z - Think time vector (1 x R), or empty
37% cdscaling - Cell array {M,1} of class-dependence handles beta_m(n);
38% empty entries denote load-independent stations
39% options - Solver options (optional)
42% G - Normalizing constant G(N)
45% Copyright (c) 2012-2026, Imperial College London
50if nargin < 3 || isempty(Z)
53if nargin < 4 || isempty(cdscaling)
54 cdscaling = cell(M, 1);
58 line_error(mfilename,
'Convolution algorithm requires finite (closed) populations.');
61% Total state space size
62stateSpaceSize = prod(N + 1);
64% Identify which stations carry a
class-dependence function
67 isCd(ist) = ~isempty(cdscaling{ist});
70% --- Precompute X_m(n) tables
for class-dependent stations ---
71% see _kb/03-api-layer.md (pfqn/ family: scaling, log-domain switches, dispatch gates)
76 Xm{ist} = zeros(stateSpaceSize, 1);
77 Xm{ist}(1) = 1; % X_m(0) = 1
79 % Enumerate all population vectors and build X_m(n) recursively
86 % Use eq. (40): X_m(n) = (u_km / mu_km(n)) * X_m(n-e_k)
87 % Pick first class k with n(k) > 0
90 % Get service rate mu_km(n) from the class-dependence handle
91 % see _kb/03-api-layer.md (pfqn/ family: scaling, log-domain switches, dispatch gates)
92 bval = cdscaling{ist}(n);
99 % see _kb/03-api-layer.md (pfqn/ family: scaling, log-domain switches, dispatch gates)
103 idx_prev = hashpop(n, N);
106 Xm{ist}(idx) = (tot / nr) * (L(ist, r) / beta) * Xm{ist}(idx_prev);
120% G_0(n) = F_Z(n) (delay contribution)
121% G_m(n) = (G_{m-1} * X_m)(n)
for class-dependent stations
122% G_m(n) = G_{m-1}(n) + sum_r L(m,r) * G_m(n-e_r)
for LI stations
124G_curr = zeros(stateSpaceSize, 1);
126% Initialize G_0(n) = F_Z(n): delay server contribution
130 G_curr(idx) = Fz(Z, n);
134% Convolve one station at a time
137 %
class-dependent station: direct convolution sum
138 % G_new(n) =
sum_{i: 0<=i<=n} X_m(i) * G_old(n-i)
140 G_curr = zeros(stateSpaceSize, 1);
144 idx_n = hashpop(n, N);
147 % Inner loop: enumerate all i from 0 to n
150 idx_i = hashpop(i, N);
151 nmi = n - i; % n - i (component-wise)
152 idx_nmi = hashpop(nmi, N);
153 conv_sum = conv_sum + Xm{ist}(idx_i) * G_old(idx_nmi);
157 G_curr(idx_n) = conv_sum;
161 % Load-independent station: efficient recurrence
162 % G_m(n) = G_{m-1}(n) + sum_r L(m,r) * G_m(n - e_r)
165 idx_n = hashpop(n, N);
166 % G_curr(idx_n) already has G_{m-1}(n) from previous iteration
170 idx_n1r = hashpop(n, N);
172 G_curr(idx_n) = G_curr(idx_n) + L(ist, r) * G_curr(idx_n1r);
180G = G_curr(end); % G(N)
is at the last index (hashpop(N,N) = prod(N+1))
184%% --- Local functions ---
186function idx = hashpop(n, N)
187% HASHPOP Map population vector to linear index (1-based)
188% idx = 1 + n(1) + n(2)*(N(1)+1) + n(3)*(N(1)+1)*(N(2)+1) + ...
192 idx = idx + prod(N(1:r-1) + 1) * n(r);
196function [n] = pprod(n, N)
197% PPROD Sequentially generate all vectors n: 0 <= n <= N
198% n = pprod(N) - initialize to zeros
199% n = pprod(n, N) - advance to next vector, returns n(1)=-1 when done
213while s > 0 && n(s) == N(s)
223% FZ Delay server unnormalized probability
factor
224% F = (Z(1)^n(1) / n(1)!) * ... * (Z(R)^n(R) / n(R)!)
233 f = f + log(Z(r)) * n(r);
234 f = f - gammaln(1 + n(r));