1function [Q,U,R,T,C,X,lG,runtime,iter,method] = solver_nc_conv(sn, options)
2% [Q,U,R,T,C,X,LG,RUNTIME,ITER,METHOD] = SOLVER_NC_CONV(SN, OPTIONS)
4% Exact normalizing constant solver
for closed networks with Limited
5%
class-dependent (cdscaling) service rates,
using the multichain
6% convolution algorithm of Sauer (1983), Section 5.2.
8% This solver handles models where some stations have
class-dependent scaling
9% (e.g., Flow-Equivalent Servers from aggregateFES).
11% Copyright (c) 2012-2026, Imperial College London
21nservers = sn.nservers;
23V = cellsum(sn.visits);
27% Demands: L(ist,k) = V(ist,k) * ST(ist,k)
30% Separate delay and queue stations
31isDelay = isinf(nservers);
32delayIdx = find(isDelay);
33queueIdx = find(~isDelay);
34nQueues = length(queueIdx);
36% Build pfqn_conv inputs
37% Z: total delay demand per class
40 Z_conv = Z_conv + Ldemand(ist, :);
43% L_conv: demands
for queue stations only
44L_conv = Ldemand(queueIdx, :);
46% Class-dependence handles
beta_{i,r}(n)
for the queue stations. Each handle
47% takes
the per-
class population vector at its station and returns either a
48% scalar (chain-independent) or a length-R vector of per-
class rates.
49cdscaling_conv = cell(nQueues, 1);
50if ~isempty(sn.cdscaling)
53 if ist <= length(sn.cdscaling) && ~isempty(sn.cdscaling{ist})
54 cdscaling_conv{qi} = sn.cdscaling{ist};
60[G_N, lG] = pfqn_conv(L_conv, NK, Z_conv, cdscaling_conv);
62%% Compute G(N - e_k)
for each class -> throughput
67 NK_minus(k) = NK_minus(k) - 1;
68 [G_Nk, ~] = pfqn_conv(L_conv, NK_minus, Z_conv, cdscaling_conv);
73%% Compute per-station throughput
74TN = V .* repmat(XN, M, 1);
76%% Compute queue lengths
79% Delay stations: Q = L * X
82 QN(ist, k) = Ldemand(ist, k) * XN(k);
86% Queue stations: use marginal distribution
87% P_m(n|N) = X_m(n) * G_{-m}(N-n) / G(N)
88% Q_m_k = sum_{n: n_k>=1} n_k * P_m(n|N)
90% G_{-m}(N-n) is computed by pfqn_conv on all stations except m
91stateSpaceSize = prod(NK + 1);
96 % Build X_m(n) for this station
97 Xm = zeros(stateSpaceSize, 1);
98 Xm(1) = 1; % X_m(0) = 1
99 isCdStation = ~isempty(cdscaling_conv{qi});
103 idx = hashpop(n, NK);
106 % class-dependent: X_m(n) = (L/mu_km(n)) * X_m(n-e_k) via eq. (40)
107 % Pick any k with n_k > 0 (result is path-independent)
110 % beta_{qi,r}(n): DIMENSIONLESS scaling of the demand,
111 % so the effective demand is L/beta. Handle returns a
112 % scalar (shared by all classes) or a length-R vector.
113 bval = cdscaling_conv{qi}(n);
119 % X_m(n) = (|n|/n_r) * (L/beta) * X_m(n-e_r); at beta=1
120 % this is the load-independent multinomial recurrence.
124 idx_prev = hashpop(n, NK);
127 Xm(idx) = (tot / nr) * (L_conv(qi, r) / beta) * Xm(idx_prev);
133 % LI: X_m(n) = Σ_r L(m,r) * X_m(n-e_r) (multinomial form)
137 idx_prev = hashpop(n, NK);
139 Xm(idx) = Xm(idx) + L_conv(qi, r) * Xm(idx_prev);
144 n = pprod_next(n, NK);
147 % Build complement: all stations except qi
148 L_comp = L_conv; L_comp(qi, :) = [];
149 cd_comp = cdscaling_conv; cd_comp(qi) = [];
151 % Compute Q_m_k using marginal
155 idx = hashpop(n, NK);
158 % G_{-m}(N-n) with delay
159 [G_comp, ~] = pfqn_conv(L_comp, nmi, Z_conv, cd_comp);
160 prob = Xm(idx) * G_comp / G_N;
162 QN(ist, k) = QN(ist, k) + n(k) * prob;
166 n = pprod_next(n, NK);
170%% Compute remaining metrics
175% Utilization at a class-dependent station is the fraction of the station's
176% PEAK service capacity in use, not T*ST:
the scaling
beta_{i,r}(n) multiplies
177%
the nominal rate, so T*ST measures capacity used in units of
the nominal
178% rate and reaches max_n beta(n), not 1, at saturation. Normalize by that peak,
179% exactly as SOLVER_NCLD does with max(lldscaling(ist,:)) for
the load-dependent
180% case. On a beta emulating c servers this returns E[busy]/c, which agrees to
181% machine precision with
the true c-server station; without it a 2-server beta
182% reports U = 2*(
true utilization) and exceeds 1.
184% No cap
is needed: sum_r U(ist,r) = E[sum_r (n_r/|n|) beta_r(n)] / max_n beta,
185% and
the inner sum
is a convex combination of
the beta_r(n), hence <= max_n
186% beta. So U <= 1 holds by construction for
the exact convolution.
187% Utilization at class-dependent stations
is normalized by
the user-declared
188% peak rate scaling per class (sn.cdscalingpeak), giving
the T*S/peak
189% convention of ordinary multiserver stations.
192 if isempty(cdscaling_conv{qi})
196 bmax = sn.cdscalingpeak(ist,r);
198 UN(ist,r) = UN(ist,r) / bmax;
204CN = CN - sum(Z_conv .* (repmat(1, M, 1) .* isDelay(:)), 1); % subtract delay
213runtime = toc(Tstart);
216%% --- Local helper functions ---
219function idx = hashpop(n, N)
223 idx = idx + prod(N(1:r-1) + 1) * n(r);
227function n = pprod_init(N)
231function n = pprod_next(n, N)
238while s > 0 && n(s) == N(s)