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};
58% Fold joint-dependence handles (sn.jdscaling, non-product-
form eta_i) into the
59% same per-station handle used by the convolution recursion. cd and jd are
60% evaluated identically; the product reproduces the single-mechanism
case when
61% only one
is present (the other
is treated as absent below).
62if ~isempty(sn.jdscaling)
65 if ist <= length(sn.jdscaling) && ~isempty(sn.jdscaling{ist})
66 jdh = sn.jdscaling{ist};
67 if isempty(cdscaling_conv{qi})
68 cdscaling_conv{qi} = jdh;
70 cdh = cdscaling_conv{qi};
71 cdscaling_conv{qi} = @(ni) cdh(ni) .* jdh(ni);
78[G_N, lG] = pfqn_conv(L_conv, NK, Z_conv, cdscaling_conv);
80%% Compute G(N - e_k)
for each class -> throughput
85 NK_minus(k) = NK_minus(k) - 1;
86 [G_Nk, ~] = pfqn_conv(L_conv, NK_minus, Z_conv, cdscaling_conv);
91%% Compute per-station throughput
92TN = V .* repmat(XN, M, 1);
94%% Compute queue lengths
97% Delay stations: Q = L * X
100 QN(ist, k) = Ldemand(ist, k) * XN(k);
104% Queue stations: use marginal distribution
105% P_m(n|N) = X_m(n) * G_{-m}(N-n) / G(N)
106% Q_m_k = sum_{n: n_k>=1} n_k * P_m(n|N)
108% G_{-m}(N-n) is computed by pfqn_conv on all stations except m
109stateSpaceSize = prod(NK + 1);
114 % Build X_m(n) for this station
115 Xm = zeros(stateSpaceSize, 1);
116 Xm(1) = 1; % X_m(0) = 1
117 isCdStation = ~isempty(cdscaling_conv{qi});
121 idx = hashpop(n, NK);
124 % class-dependent: X_m(n) = (L/mu_km(n)) * X_m(n-e_k) via eq. (40)
125 % Pick any k with n_k > 0 (result is path-independent)
128 % beta_{qi,r}(n): DIMENSIONLESS scaling of the demand,
129 % so the effective demand is L/beta. Handle returns a
130 % scalar (shared by all classes) or a length-R vector.
131 bval = cdscaling_conv{qi}(n);
137 % X_m(n) = (|n|/n_r) * (L/beta) * X_m(n-e_r); at beta=1
138 % this is the load-independent multinomial recurrence.
142 idx_prev = hashpop(n, NK);
145 Xm(idx) = (tot / nr) * (L_conv(qi, r) / beta) * Xm(idx_prev);
151 % LI: X_m(n) = Σ_r L(m,r) * X_m(n-e_r) (multinomial form)
155 idx_prev = hashpop(n, NK);
157 Xm(idx) = Xm(idx) + L_conv(qi, r) * Xm(idx_prev);
162 n = pprod_next(n, NK);
165 % Build complement: all stations except qi
166 L_comp = L_conv; L_comp(qi, :) = [];
167 cd_comp = cdscaling_conv; cd_comp(qi) = [];
169 % Compute Q_m_k using marginal
173 idx = hashpop(n, NK);
176 % G_{-m}(N-n) with delay
177 [G_comp, ~] = pfqn_conv(L_comp, nmi, Z_conv, cd_comp);
178 prob = Xm(idx) * G_comp / G_N;
180 QN(ist, k) = QN(ist, k) + n(k) * prob;
184 n = pprod_next(n, NK);
188%% Compute remaining metrics
193% Utilization at a class-dependent station is normalized by the peak service
194% capacity (sn.cdscalingpeak), not T*ST, so U<=1 by construction; see
195% _kb/06-solver-catalog.md (NC section, convolution/beta scaling)
198 if isempty(cdscaling_conv{qi})
202 % Effective peak = product of the class- and joint-dependence peaks
203 % declared at the station (a missing one contributes 1).
206 if ~isempty(sn.cdscaling) && ist <= length(sn.cdscaling) && ~isempty(sn.cdscaling{ist})
207 bmax = bmax * sn.cdscalingpeak(ist,r); haspeak = true;
209 if ~isempty(sn.jdscaling) && ist <= length(sn.jdscaling) && ~isempty(sn.jdscaling{ist})
210 bmax = bmax * sn.jdscalingpeak(ist,r); haspeak = true;
212 if haspeak && bmax > 0
213 UN(ist,r) = UN(ist,r) / bmax;
219CN = CN - sum(Z_conv .* (repmat(1, M, 1) .* isDelay(:)), 1); % subtract delay
228runtime = toc(Tstart);
231%% --- Local helper functions ---
234function idx = hashpop(n, N)
238 idx = idx + prod(N(1:r-1) + 1) * n(r);
242function n = pprod_init(N)
246function n = pprod_next(n, N)
253while s > 0 && n(s) == N(s)