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% Joint Class Dependence (LJCD) service rates,
using the multichain
6% convolution algorithm of Sauer (1983), Section 5.2.
8% This solver handles models where some stations have LJCD 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% mu_conv: load-dependent rates
for queue stations (ones
for LI, lldscaling otherwise)
48mu_conv = ones(nQueues, Nt);
49if ~isempty(sn.lldscaling)
52 nEntries = min(Nt, size(sn.lldscaling, 2));
53 mu_conv(qi, 1:nEntries) = sn.lldscaling(ist, 1:nEntries);
57% LJCD scaling tables for queue stations
58ljcdscaling_conv = cell(nQueues, 1);
59ljcdcutoffs_conv = cell(nQueues, 1);
60hasLjcd = ~isempty(sn.ljcdscaling);
64 if ist <= length(sn.ljcdscaling) && ~isempty(sn.ljcdscaling{ist})
65 ljcdscaling_conv{qi} = sn.ljcdscaling{ist};
66 ljcdcutoffs_conv{qi} = sn.ljcdcutoffs{ist};
72[G_N, lG] = pfqn_conv(L_conv, NK, Z_conv, mu_conv, ljcdscaling_conv, ljcdcutoffs_conv);
74%% Compute G(N - e_k)
for each class -> throughput
79 NK_minus(k) = NK_minus(k) - 1;
80 [G_Nk, ~] = pfqn_conv(L_conv, NK_minus, Z_conv, mu_conv, ljcdscaling_conv, ljcdcutoffs_conv);
85%% Compute per-station throughput
86TN = V .* repmat(XN, M, 1);
88%% Compute queue lengths
91% Delay stations: Q = L * X
94 QN(ist, k) = Ldemand(ist, k) * XN(k);
98% Queue stations: use marginal distribution
99% P_m(n|N) = X_m(n) * G_{-m}(N-n) / G(N)
100% Q_m_k = sum_{n: n_k>=1} n_k * P_m(n|N)
102% G_{-m}(N-n) is computed by pfqn_conv on all stations except m
103stateSpaceSize = prod(NK + 1);
108 % Build X_m(n) for this station
109 Xm = zeros(stateSpaceSize, 1);
110 Xm(1) = 1; % X_m(0) = 1
111 isLjcdStation = ~isempty(ljcdscaling_conv{qi});
115 idx = hashpop(n, NK);
118 % LJCD: X_m(n) = (L/mu_km(n)) * X_m(n-e_k) via eq. (40)
119 % Pick any k with n_k > 0 (result is path-independent)
122 cutoffs = ljcdcutoffs_conv{qi};
123 nClamped = min(n, cutoffs);
124 lidx = ljd_linearize(nClamped, cutoffs);
125 mu_km = ljcdscaling_conv{qi}{r}(lidx);
127 idx_prev = hashpop(n, NK);
130 Xm(idx) = (L_conv(qi, r) / mu_km) * Xm(idx_prev);
136 % LI: X_m(n) = Σ_r L(m,r) * X_m(n-e_r) (multinomial form)
140 idx_prev = hashpop(n, NK);
142 Xm(idx) = Xm(idx) + L_conv(qi, r) * Xm(idx_prev);
147 n = pprod_next(n, NK);
150 % Build complement: all stations except qi
151 L_comp = L_conv; L_comp(qi, :) = [];
152 mu_comp = mu_conv; mu_comp(qi, :) = [];
153 ljcd_comp = ljcdscaling_conv; ljcd_comp(qi) = [];
154 ljcdc_comp = ljcdcutoffs_conv; ljcdc_comp(qi) = [];
156 % Compute Q_m_k using marginal
160 idx = hashpop(n, NK);
163 % G_{-m}(N-n) with delay
164 [G_comp, ~] = pfqn_conv(L_comp, nmi, Z_conv, mu_comp, ljcd_comp, ljcdc_comp);
165 prob = Xm(idx) * G_comp / G_N;
167 QN(ist, k) = QN(ist, k) + n(k) * prob;
171 n = pprod_next(n, NK);
175%% Compute remaining metrics
181CN = CN - sum(Z_conv .* (repmat(1, M, 1) .* isDelay(:)), 1); % subtract delay
190runtime = toc(Tstart);
193%% --- Local helper functions ---
195function idx = hashpop(n, N)
199 idx = idx + prod(N(1:r-1) + 1) * n(r);
203function n = pprod_init(N)
207function n = pprod_next(n, N)
214while s > 0 && n(s) == N(s)