1function [Q,U,R,T,C,X,lG,runtime,iter,method] = solver_ncld(sn, options)
2% [Q,U,R,T,C,X,LG,RUNTIME,ITER.METHOD] = SOLVER_NCLD(QN, OPTIONS)
4% Copyright (c) 2012-2026, Imperial College London
6M = sn.nstations; %number of stations
9if nservers(isfinite(nservers))>1
10 if isempty(sn.lldscaling) && M==2 && all(isfinite(sn.njobs))
13 sn.lldscaling(ist,1:Nt) = min(1:Nt,sn.nservers(ist));
15 elseif lld_encodes_multiserver(sn, nservers, M)
16 % The caller (SolverNC.runAnalyzer) already expressed every multiserver as
17 % mu(n)=min(n,c), which
is what this guard asks for, so
the model
is
18 % supported. nservers
is deliberately left at c: utilization
is the
19 % fraction of
the c servers busy and c cannot be read back from
20 % lldscaling once
the population
is below it (min(1:Nt,c)
is then 1:Nt).
22 line_error(mfilename,'The load-dependent solver does not support multi-server stations yet. Specify multi-server stations via limited load-dependence.');
26if ~isempty(sn.cdscaling)
27 % Class-dependent service rates
beta_{i,r}(n): route to
the convolution
28 % solver, which builds
the station
factor X_m(n) by Sauer
's chain-dependent
29 % recurrence (Sauer 1983, Sec. 5.2, eq. (40)). This covers both the
30 % chain-independent case (beta_i(n), a scalar-valued handle) and the
31 % chain-specific one (a length-R vector), the latter being what the
32 % flow-equivalent-server aggregation installs.
34 % The routing is unconditional on options.method: the algorithms below read
35 % mu(n) from lldscaling only and have no way to apply beta, so gating this
36 % on method='exact
' made every other method (including the default) return
37 % the UNSCALED network instead of erroring. Convolution is the only exact
38 % algorithm for beta, and it is what the native Python NC already does.
39 [Q,U,R,T,C,X,lG,runtime,iter,method] = solver_nc_conv(sn, options);
43NK = sn.njobs'; % initial population per
class
45% Mixed open/closed load-dependent networks are handled below through
the exact
46% chain-level MVALDMX algorithm (Bruell-Balbo-Afshari effective capacity);
the
47% closed-only convolution path
is retained
for purely closed models.
54V = cellsum(sn.visits);
58lldscaling = sn.lldscaling;
59Nt = sum(NK(isfinite(NK)));
61 lldscaling = ones(M,ceil(Nt));
64[~,~,Vchain,alpha] = sn_get_demands_chain(sn);
68if all(sched~=SchedStrategy.FCFS) options.iter_max=1; end
70while max(abs(1-eta./eta_1)) > options.iter_tol & iter < options.iter_max
73 M = sn.nstations; %number of stations
74 K = sn.nclasses; %number of classes
79 SCVchain = zeros(M,C);
81 refstatchain = zeros(C,1);
83 inchain = sn.inchain{c};
84 isOpenChain = any(isinf(sn.njobs(inchain)));
86 % we assume that
the visits in L(i,inchain) are equal to 1
87 Lchain(ist,c) = Vchain(ist,c) * ST(ist,inchain) * alpha(ist,inchain)
';
88 STchain(ist,c) = ST(ist,inchain) * alpha(ist,inchain)';
89 if isOpenChain && ist == sn.refstat(inchain(1)) %
if this is a source ST = 1 / arrival rates
90 STchain(ist,c) = sumfinite(ST(ist,inchain)); % ignore degenerate classes with zero arrival rates
92 STchain(ist,c) = ST(ist,inchain) * alpha(ist,inchain)
';
94 SCVchain(ist,c) = SCV(ist,inchain) * alpha(ist,inchain)';
96 Nchain(c) = sum(NK(inchain));
97 refstatchain(c) = sn.refstat(inchain(1));
98 if any((sn.refstat(inchain(1))-refstatchain(c))~=0)
99 line_error(mfilename,sprintf('Classes in chain %d have different reference station.',c));
102 STchain(~isfinite(STchain))=0;
103 Lchain(~isfinite(Lchain))=0;
104 % chain-level arrival rates for open chains (source ST = 1/arrival rate)
107 inchain = sn.inchain{c};
108 if any(isinf(sn.njobs(inchain)))
109 rst = sn.refstat(inchain(1));
110 if STchain(rst,c) > 0
111 lambda(c) = 1 / STchain(rst,c);
116 Nt = sum(Nchain(isfinite(Nchain)));
119 mu = zeros(M,ceil(Nt));
123 if isinf(nservers(ist)) % infinite server
124 %mu_chain(i,1:sum(Nchain)) = 1:sum(Nchain);
125 infServers(end+1) = ist;
126 L(ist,:) = Lchain(ist,:);
127 Z(ist,:) = Lchain(ist,:);
130 % Only warn when
the multiserver
is not already represented by
the
131 % load-dependent rates: SolverNC converts mu(n)=min(n,c) upfront, and
132 % that
case is solved exactly, so it must not warn.
133 if strcmpi(options.method,
'exact') && nservers(ist)>1 ...
134 && ~isequal(lldscaling(ist,1:Nt), min(1:Nt,nservers(ist)))
135 %options.method =
'default';
136 line_warning(mfilename,sprintf(
'%s does not support exact multiserver yet. Switching to approximate method.\n',
'SolverNC'));
138 L(ist,:) = Lchain(ist,:);
139 mu(ist,1:Nt) = lldscaling(ist,1:Nt);
142 openChains = find(isinf(Nchain));
143 if ~isempty(openChains)
144 % Mixed limited load-dependent network: exact chain-level MVALDMX. The open
145 % chains enter through arrival rates lambda; their reference (source)
146 % stations carry only
the 1/lambda bookkeeping demand and are excluded from
147 %
the queueing set. Infinite-server (delay) stations fold into
the
148 % think-time vector;
the remaining queueing stations carry
the limited
149 % load-dependent rates lldscaling.
150 sourceStations = unique(refstatchain(openChains))
';
151 delayStations = setdiff(infServers, sourceStations);
152 queueStations = setdiff(1:M, [delayStations, sourceStations]);
153 nq = numel(queueStations);
154 Ncl = sum(Nchain(isfinite(Nchain)));
156 if ~isempty(delayStations)
158 Zvec(c) = sum(Lchain(delayStations,c));
161 Dq = Lchain(queueStations,:);
164 avail = min(ncol, size(lldscaling,2));
167 muq(qi,1:avail) = lldscaling(queueStations(qi),1:avail);
170 [Xchain,QN_mx,~,~,lG] = pfqn_mvaldmx(lambda, Dq, Nchain, Zvec, muq, ones(nq,1));
173 Qchain(queueStations,:) = QN_mx;
174 for di=1:numel(delayStations)
175 ist = delayStations(di);
176 Qchain(ist,:) = Lchain(ist,:) .* Xchain;
181 % Solve original system
182 [lG,~,method] = pfqn_ncld(L, Nchain, 0*Nchain, mu, options);
186 % Solve systems with a job less
189 Nchain_r =oner(Nchain,r);
190 lGr(r) = pfqn_ncld(L,Nchain_r,0*Nchain,mu,options);
192 Xchain(r) = exp(lGr(r) - lG);
196 CQchain_r = zeros(M,1);
198 if M==2 && any(isinf(sn.nservers)) % repairmen model
199 firstDelay = find(isinf(sn.nservers),1);
200 Qchain(firstDelay,r) = real(Lchain(firstDelay,r) * Xchain(r));
201 Qchain(setdiff(1:M,firstDelay),r) = Nchain(r) - real(Lchain(firstDelay,r) * Xchain(r));
203 % Add queue replicas for queue-length
205 Lms_i = L; Lms_i(ist,:) = [];
206 mu_i = mu; mu_i(ist,:) = [];
207 muhati = mu; muhati = pfqn_mushift(mu,ist); %#ok<NASGU>
208 [muhati_f,c] = pfqn_fnc(muhati(ist,:));
210 if isinf(nservers(ist)) % infinite server
211 Qchain(ist,r) = real(Lchain(ist,r) * Xchain(r));
213 if ist==M && sum(isfinite(nservers))==1 % normalize queue-lengths to Nchain(r)
214 Qchain(ist,r) = max(0,real(Nchain(r) - sum(Lchain(isinf(nservers),r)) * Xchain(r)) - sum(Qchain(setdiff(1:(M-1),find(isinf(nservers))),r)));
216 [lGhat_fnci(r)] = pfqn_ncld([L;L(ist,:)],Nchain_r, 0*Nchain, [muhati;muhati_f], options);
217 [lGhatir(r)] = pfqn_ncld(L,Nchain_r, 0*Nchain, muhati, options);
218 [lGr_i(r)] = pfqn_ncld(Lms_i,Nchain_r, 0*Nchain, mu_i, options);
219 [lGhati(r)] = pfqn_ncld(L,Nchain_r, 0*Nchain, muhati, options);
220 dlGa = real(lGhat_fnci(r)) - real(lGhatir(r));
221 dlG_i = real(lGr_i(r)) - real(lGhatir(r));
222 CQchain(ist) = (exp(dlGa) - 1) + c*(exp(dlG_i)-1); % conditional qlen
223 ldDemand(ist,r) = log(L(ist,r)) + real(lGhati(r)) - log(mu(ist,1)) - real(lGr(r));
224 Qchain(ist,r) = exp(ldDemand(ist,r)) * Xchain(r) * (1+CQchain(ist)); % conditional MVA formula
233 % just fill the delay servers
237 if isinf(nservers(ist)) % infinite server
238 Qchain(ist,r) = Lchain(ist,r) * Xchain(r);
246 line_warning(mfilename,'Normalizing constant computations produced a floating-point range exception. Model
is likely too large.\n
');
248 end % close open/closed dispatch
252 Rchain = Qchain ./ repmat(Xchain,M,1) ./ Vchain;
253 Rchain(infServers,:) = Lchain(infServers,:) ./ Vchain(infServers,:);
254 Tchain = repmat(Xchain,M,1) .* Vchain;
255 Uchain = Tchain .* Lchain;
256 Cchain = Nchain ./ Xchain - Z;
263 Xchain(~isfinite(Xchain))=0;
264 Uchain(~isfinite(Uchain))=0;
265 Qchain(~isfinite(Qchain))=0;
266 Rchain(~isfinite(Rchain))=0;
269 Uchain(:,Nchain==0)=0;
270 Qchain(:,Nchain==0)=0;
271 Rchain(:,Nchain==0)=0;
272 Tchain(:,Nchain==0)=0;
274 [Q,U,R,T,C,X] = sn_deaggregate_chain_results(sn, Lchain, ST, STchain, Vchain, alpha, [], [], Rchain, Tchain, [], Xchain);
276 [ST,gamma,~,~,~,~,eta] = npfqn_nonexp_approx(options.config.highvar,sn,ST0,V,SCV,T,U,gamma,nservers);
280[lambda,L]= sn_get_product_form_params(sn);
281runtime = toc(Tstart);
282Q=abs(Q); R=abs(R); X=abs(X); U=abs(U);
284 if sn.nservers(ist)>1 && sn.nservers(ist)<Inf
285 openClasses = find(isinf(NK));
286 closedClasses = setdiff(1:K, openClasses);
288 c = find(sn.chains(:,r));
290 U(ist,r) = X(r) * sn.visits{c}(ist,r) / sn.visits{c}(sn.refstat(r),r) * ST(ist,r)/sn.nservers(ist);
294 c = find(sn.chains(:,r));
296 U(ist,r) = lambda(r) * sn.visits{c}(ist,r) / sn.visits{c}(sn.refstat(r),r) * ST(ist,r)/sn.nservers(ist);
299 elseif isinf(sn.nservers(ist))
300 openClasses = find(isinf(NK));
301 closedClasses = setdiff(1:K, openClasses);
304 c = find(sn.chains(:,r));
305 U(ist,r) = X(r) * sn.visits{c}(ist,r) / sn.visits{c}(sn.refstat(r),r) * ST(ist,r);
310 c = find(sn.chains(:,r));
311 U(ist,r) = lambda(r) * sn.visits{c}(ist,r) / sn.visits{c}(sn.refstat(r),r) * ST(ist,r);
315 U(ist,:) = U(ist,:) / max(lldscaling(ist,:));
317 U(ist,:) = U(ist,:) / sum(U(ist,:),"omitnan");
322X(~isfinite(X))=0; U(~isfinite(U))=0; Q(~isfinite(Q))=0; R(~isfinite(R))=0;
324% renormalize qlen and tput to correct for unforeseen population constraint deviations
326 inchain = sn.inchain{c};
327 Nchain(c) = sum(NK(inchain)); %#ok<FNDSB>
328 if isfinite(Nchain(c))
329 q_den = sum(sum(Q(:,inchain)));
331 ratio = Nchain(c)/ q_den;
335 Q(:,inchain) = ratio * Q(:,inchain);
336 X(inchain) = ratio * X(inchain);
337 T(:,inchain) = ratio * T(:,inchain);
338 U(:,inchain) = ratio * U(:,inchain);
339 R(:,inchain) = Q(:,inchain) ./ T(:,inchain);
344function tf = lld_encodes_multiserver(sn, nservers, M)
345% True if every finite multi-server station already carries mu(n)=min(n,c) in
346% lldscaling, i.e. the multiserver is fully described by the load-dependent
347% rates and needs no further handling.
349Ntot = sum(sn.njobs(isfinite(sn.njobs)));
350if isempty(sn.lldscaling) || ~isfinite(Ntot) || Ntot < 1
353if size(sn.lldscaling,2) < Ntot
357 if isfinite(nservers(ist)) && nservers(ist) > 1
358 if ~isequal(sn.lldscaling(ist,1:Ntot), min(1:Ntot, nservers(ist)))