LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_nc_conv.m
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)
3%
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.
7%
8% This solver handles models where some stations have class-dependent scaling
9% (e.g., Flow-Equivalent Servers from aggregateFES).
10
11% Copyright (c) 2012-2026, Imperial College London
12% All rights reserved.
13
14Tstart = tic;
15method = 'conv';
16iter = 1;
17
18M = sn.nstations;
19K = sn.nclasses;
20NK = sn.njobs';
21nservers = sn.nservers;
22
23V = cellsum(sn.visits);
24ST = 1 ./ sn.rates;
25ST(isnan(ST)) = 0;
26
27% Demands: L(ist,k) = V(ist,k) * ST(ist,k)
28Ldemand = V .* ST;
29
30% Separate delay and queue stations
31isDelay = isinf(nservers);
32delayIdx = find(isDelay);
33queueIdx = find(~isDelay);
34nQueues = length(queueIdx);
35
36% Build pfqn_conv inputs
37% Z: total delay demand per class
38Z_conv = zeros(1, K);
39for ist = delayIdx(:)'
40 Z_conv = Z_conv + Ldemand(ist, :);
41end
42
43% L_conv: demands for queue stations only
44L_conv = Ldemand(queueIdx, :);
45
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)
51 for qi = 1:nQueues
52 ist = queueIdx(qi);
53 if ist <= length(sn.cdscaling) && ~isempty(sn.cdscaling{ist})
54 cdscaling_conv{qi} = sn.cdscaling{ist};
55 end
56 end
57end
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)
63 for qi = 1:nQueues
64 ist = queueIdx(qi);
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;
69 else
70 cdh = cdscaling_conv{qi};
71 cdscaling_conv{qi} = @(ni) cdh(ni) .* jdh(ni);
72 end
73 end
74 end
75end
76
77%% Compute G(N)
78[G_N, lG] = pfqn_conv(L_conv, NK, Z_conv, cdscaling_conv);
79
80%% Compute G(N - e_k) for each class -> throughput
81XN = zeros(1, K);
82for k = 1:K
83 if NK(k) > 0
84 NK_minus = NK;
85 NK_minus(k) = NK_minus(k) - 1;
86 [G_Nk, ~] = pfqn_conv(L_conv, NK_minus, Z_conv, cdscaling_conv);
87 XN(k) = G_Nk / G_N;
88 end
89end
90
91%% Compute per-station throughput
92TN = V .* repmat(XN, M, 1);
93
94%% Compute queue lengths
95QN = zeros(M, K);
96
97% Delay stations: Q = L * X
98for ist = delayIdx(:)'
99 for k = 1:K
100 QN(ist, k) = Ldemand(ist, k) * XN(k);
101 end
102end
103
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)
107%
108% G_{-m}(N-n) is computed by pfqn_conv on all stations except m
109stateSpaceSize = prod(NK + 1);
110
111for qi = 1:nQueues
112 ist = queueIdx(qi);
113
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});
118
119 n = pprod_init(NK);
120 while n(1) >= 0
121 idx = hashpop(n, NK);
122 if sum(n) > 0
123 if isCdStation
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)
126 for r = 1:K
127 if n(r) > 0
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);
132 if numel(bval) > 1
133 beta = bval(r);
134 else
135 beta = bval;
136 end
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.
139 tot = sum(n);
140 nr = n(r);
141 n(r) = n(r) - 1;
142 idx_prev = hashpop(n, NK);
143 n(r) = n(r) + 1;
144 if beta > 0
145 Xm(idx) = (tot / nr) * (L_conv(qi, r) / beta) * Xm(idx_prev);
146 end
147 break
148 end
149 end
150 else
151 % LI: X_m(n) = Σ_r L(m,r) * X_m(n-e_r) (multinomial form)
152 for r = 1:K
153 if n(r) > 0
154 n(r) = n(r) - 1;
155 idx_prev = hashpop(n, NK);
156 n(r) = n(r) + 1;
157 Xm(idx) = Xm(idx) + L_conv(qi, r) * Xm(idx_prev);
158 end
159 end
160 end
161 end
162 n = pprod_next(n, NK);
163 end
164
165 % Build complement: all stations except qi
166 L_comp = L_conv; L_comp(qi, :) = [];
167 cd_comp = cdscaling_conv; cd_comp(qi) = [];
168
169 % Compute Q_m_k using marginal
170 n = pprod_init(NK);
171 while n(1) >= 0
172 if any(n > 0)
173 idx = hashpop(n, NK);
174 nmi = NK - n;
175 if all(nmi >= 0)
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;
179 for k = 1:K
180 QN(ist, k) = QN(ist, k) + n(k) * prob;
181 end
182 end
183 end
184 n = pprod_next(n, NK);
185 end
186end
187
188%% Compute remaining metrics
189RN = QN ./ TN;
190RN(TN == 0) = 0;
191UN = TN .* ST;
192
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)
196for qi = 1:nQueues
197 ist = queueIdx(qi);
198 if isempty(cdscaling_conv{qi})
199 continue
200 end
201 for r = 1:K
202 % Effective peak = product of the class- and joint-dependence peaks
203 % declared at the station (a missing one contributes 1).
204 bmax = 1;
205 haspeak = false;
206 if ~isempty(sn.cdscaling) && ist <= length(sn.cdscaling) && ~isempty(sn.cdscaling{ist})
207 bmax = bmax * sn.cdscalingpeak(ist,r); haspeak = true;
208 end
209 if ~isempty(sn.jdscaling) && ist <= length(sn.jdscaling) && ~isempty(sn.jdscaling{ist})
210 bmax = bmax * sn.jdscalingpeak(ist,r); haspeak = true;
211 end
212 if haspeak && bmax > 0
213 UN(ist,r) = UN(ist,r) / bmax;
214 end
215 end
216end
217CN = NK ./ XN;
218CN(XN == 0) = 0;
219CN = CN - sum(Z_conv .* (repmat(1, M, 1) .* isDelay(:)), 1); % subtract delay
220
221% Output
222Q = QN;
223U = UN;
224R = RN;
225T = TN;
226C = CN;
227X = XN;
228runtime = toc(Tstart);
229end
230
231%% --- Local helper functions ---
232
233
234function idx = hashpop(n, N)
235idx = 1;
236R = length(N);
237for r = 1:R
238 idx = idx + prod(N(1:r-1) + 1) * n(r);
239end
240end
241
242function n = pprod_init(N)
243n = zeros(size(N));
244end
245
246function n = pprod_next(n, N)
247R = length(N);
248if all(n == N)
249 n = -1 * ones(1, R);
250 return
251end
252s = R;
253while s > 0 && n(s) == N(s)
254 n(s) = 0;
255 s = s - 1;
256end
257if s > 0
258 n(s) = n(s) + 1;
259end
260end