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 % caller already installed mu(n)=min(n,c); nservers stays at c for U
17 % normalization. see _kb/06-solver-catalog.md (NC section)
19 line_error(mfilename,'The load-dependent solver does not support multi-server stations yet. Specify multi-server stations via limited load-dependence.');
23if ~isempty(sn.cdscaling) || ~isempty(sn.jdscaling)
24 % Class-dependent
beta_{i,r}(n) or joint-dependent eta_i(n) rates ->
25 % convolution solver, unconditionally on options.method (the lld algorithms
26 % cannot apply per-
class scaling; solver_nc_conv folds jd into the same
27 % recursion); see _kb/06-solver-catalog.md (NC section, convolution/beta scaling)
28 [Q,U,R,T,C,X,lG,runtime,iter,method] = solver_nc_conv(sn, options);
32NK = sn.njobs
'; % initial population per class
34% Mixed open/closed load-dependent networks are handled below through the exact
35% chain-level MVALDMX algorithm (Bruell-Balbo-Afshari effective capacity); the
36% closed-only convolution path is retained for purely closed models.
43V = cellsum(sn.visits);
47lldscaling = sn.lldscaling;
48Nt = sum(NK(isfinite(NK)));
50 lldscaling = ones(M,ceil(Nt));
53[~,~,Vchain,alpha] = sn_get_demands_chain(sn);
57if all(sched~=SchedStrategy.FCFS) options.iter_max=1; end
59while max(abs(1-eta./eta_1)) > options.iter_tol & iter < options.iter_max
62 M = sn.nstations; %number of stations
63 K = sn.nclasses; %number of classes
68 SCVchain = zeros(M,C);
70 refstatchain = zeros(C,1);
72 inchain = sn.inchain{c};
73 isOpenChain = any(isinf(sn.njobs(inchain)));
75 % we assume that the visits in L(i,inchain) are equal to 1
76 Lchain(ist,c) = Vchain(ist,c) * ST(ist,inchain) * alpha(ist,inchain)';
77 STchain(ist,c) = ST(ist,inchain) * alpha(ist,inchain)
';
78 if isOpenChain && ist == sn.refstat(inchain(1)) % if this is a source ST = 1 / arrival rates
79 STchain(ist,c) = sumfinite(ST(ist,inchain)); % ignore degenerate classes with zero arrival rates
81 STchain(ist,c) = ST(ist,inchain) * alpha(ist,inchain)';
83 SCVchain(ist,c) = SCV(ist,inchain) * alpha(ist,inchain)
';
85 Nchain(c) = sum(NK(inchain));
86 refstatchain(c) = sn.refstat(inchain(1));
87 if any((sn.refstat(inchain(1))-refstatchain(c))~=0)
88 line_error(mfilename,sprintf('Classes in chain %d have different reference station.
',c));
91 STchain(~isfinite(STchain))=0;
92 Lchain(~isfinite(Lchain))=0;
93 % chain-level arrival rates for open chains (source ST = 1/arrival rate)
96 inchain = sn.inchain{c};
97 if any(isinf(sn.njobs(inchain)))
98 rst = sn.refstat(inchain(1));
100 lambda(c) = 1 / STchain(rst,c);
105 Nt = sum(Nchain(isfinite(Nchain)));
108 mu = zeros(M,ceil(Nt));
112 if isinf(nservers(ist)) % infinite server
113 %mu_chain(i,1:sum(Nchain)) = 1:sum(Nchain);
114 infServers(end+1) = ist;
115 L(ist,:) = Lchain(ist,:);
116 Z(ist,:) = Lchain(ist,:);
119 % Only warn when the multiserver is not already represented by the
120 % load-dependent rates: SolverNC converts mu(n)=min(n,c) upfront, and
121 % that case is solved exactly, so it must not warn.
122 if strcmpi(options.method,'exact
') && nservers(ist)>1 ...
123 && ~isequal(lldscaling(ist,1:Nt), min(1:Nt,nservers(ist)))
124 %options.method = 'default';
125 line_warning(mfilename,sprintf('%s does not support exact multiserver yet. Switching to approximate method.\n
', 'SolverNC
'));
127 L(ist,:) = Lchain(ist,:);
128 mu(ist,1:Nt) = lldscaling(ist,1:Nt);
131 openChains = find(isinf(Nchain));
132 if ~isempty(openChains)
133 % Mixed limited load-dependent network: exact chain-level MVALDMX; see
134 % _kb/06-solver-catalog.md (NC section, convolution/beta scaling)
135 sourceStations = unique(refstatchain(openChains))';
136 delayStations = setdiff(infServers, sourceStations);
137 queueStations = setdiff(1:M, [delayStations, sourceStations]);
138 nq = numel(queueStations);
139 Ncl = sum(Nchain(isfinite(Nchain)));
141 if ~isempty(delayStations)
143 Zvec(c) = sum(Lchain(delayStations,c));
146 Dq = Lchain(queueStations,:);
149 avail = min(ncol, size(lldscaling,2));
152 muq(qi,1:avail) = lldscaling(queueStations(qi),1:avail);
155 [Xchain,QN_mx,~,~,lG] = pfqn_mvaldmx(lambda, Dq, Nchain, Zvec, muq, ones(nq,1));
158 Qchain(queueStations,:) = QN_mx;
159 for di=1:numel(delayStations)
160 ist = delayStations(di);
161 Qchain(ist,:) = Lchain(ist,:) .* Xchain;
166 % Solve original system
167 [lG,~,method] = pfqn_ncld(L, Nchain, 0*Nchain, mu, options);
171 % Solve systems with a job less
174 Nchain_r =oner(Nchain,r);
175 lGr(r) = pfqn_ncld(L,Nchain_r,0*Nchain,mu,options);
177 Xchain(r) = exp(lGr(r) - lG);
181 CQchain_r = zeros(M,1);
183 if M==2 && any(isinf(sn.nservers)) % repairmen model
184 firstDelay = find(isinf(sn.nservers),1);
185 Qchain(firstDelay,r) = real(Lchain(firstDelay,r) * Xchain(r));
186 Qchain(setdiff(1:M,firstDelay),r) = Nchain(r) - real(Lchain(firstDelay,r) * Xchain(r));
188 % Add queue replicas
for queue-length
190 Lms_i = L; Lms_i(ist,:) = [];
191 mu_i = mu; mu_i(ist,:) = [];
192 muhati = mu; muhati = pfqn_mushift(mu,ist); %#ok<NASGU>
193 [muhati_f,c] = pfqn_fnc(muhati(ist,:));
195 if isinf(nservers(ist)) % infinite server
196 Qchain(ist,r) = real(Lchain(ist,r) * Xchain(r));
198 if ist==M && sum(isfinite(nservers))==1 % normalize queue-lengths to Nchain(r)
199 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)));
201 [lGhat_fnci(r)] = pfqn_ncld([L;L(ist,:)],Nchain_r, 0*Nchain, [muhati;muhati_f], options);
202 [lGhatir(r)] = pfqn_ncld(L,Nchain_r, 0*Nchain, muhati, options);
203 [lGr_i(r)] = pfqn_ncld(Lms_i,Nchain_r, 0*Nchain, mu_i, options);
204 [lGhati(r)] = pfqn_ncld(L,Nchain_r, 0*Nchain, muhati, options);
205 dlGa = real(lGhat_fnci(r)) - real(lGhatir(r));
206 dlG_i = real(lGr_i(r)) - real(lGhatir(r));
207 CQchain(ist) = (exp(dlGa) - 1) + c*(exp(dlG_i)-1); % conditional qlen
208 ldDemand(ist,r) = log(L(ist,r)) + real(lGhati(r)) - log(mu(ist,1)) - real(lGr(r));
209 Qchain(ist,r) = exp(ldDemand(ist,r)) * Xchain(r) * (1+CQchain(ist)); % conditional MVA formula
218 % just fill the delay servers
222 if isinf(nservers(ist)) % infinite server
223 Qchain(ist,r) = Lchain(ist,r) * Xchain(r);
231 line_warning(mfilename,
'Normalizing constant computations produced a floating-point range exception. Model is likely too large.\n');
233 end % close open/closed dispatch
237 Rchain = Qchain ./ repmat(Xchain,M,1) ./ Vchain;
238 Rchain(infServers,:) = Lchain(infServers,:) ./ Vchain(infServers,:);
239 Tchain = repmat(Xchain,M,1) .* Vchain;
240 Uchain = Tchain .* Lchain;
241 Cchain = Nchain ./ Xchain - Z;
248 Xchain(~isfinite(Xchain))=0;
249 Uchain(~isfinite(Uchain))=0;
250 Qchain(~isfinite(Qchain))=0;
251 Rchain(~isfinite(Rchain))=0;
254 Uchain(:,Nchain==0)=0;
255 Qchain(:,Nchain==0)=0;
256 Rchain(:,Nchain==0)=0;
257 Tchain(:,Nchain==0)=0;
259 [Q,U,R,T,C,X] = sn_deaggregate_chain_results(sn, Lchain, ST, STchain, Vchain, alpha, [], [], Rchain, Tchain, [], Xchain);
261 [ST,gamma,~,~,~,~,eta] = npfqn_nonexp_approx(options.config.highvar,sn,ST0,V,SCV,T,U,gamma,nservers);
265[lambda,L]= sn_get_product_form_params(sn);
266runtime = toc(Tstart);
267Q=abs(Q); R=abs(R); X=abs(X); U=abs(U);
269 if sn.nservers(ist)>1 && sn.nservers(ist)<Inf
270 openClasses = find(isinf(NK));
271 closedClasses = setdiff(1:K, openClasses);
273 c = find(sn.chains(:,r));
275 U(ist,r) = X(r) * sn.visits{c}(ist,r) / sn.visits{c}(sn.refstat(r),r) * ST(ist,r)/sn.nservers(ist);
279 c = find(sn.chains(:,r));
281 U(ist,r) = lambda(r) * sn.visits{c}(ist,r) / sn.visits{c}(sn.refstat(r),r) * ST(ist,r)/sn.nservers(ist);
284 elseif isinf(sn.nservers(ist))
285 openClasses = find(isinf(NK));
286 closedClasses = setdiff(1:K, openClasses);
289 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);
295 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);
300 U(ist,:) = U(ist,:) / max(lldscaling(ist,:));
302 U(ist,:) = U(ist,:) / sum(U(ist,:),
"omitnan");
307X(~isfinite(X))=0; U(~isfinite(U))=0; Q(~isfinite(Q))=0; R(~isfinite(R))=0;
309% renormalize qlen and tput to correct
for unforeseen population constraint deviations
311 inchain = sn.inchain{c};
312 Nchain(c) = sum(NK(inchain)); %#ok<FNDSB>
313 if isfinite(Nchain(c))
314 q_den = sum(sum(Q(:,inchain)));
316 ratio = Nchain(c)/ q_den;
320 Q(:,inchain) = ratio * Q(:,inchain);
321 X(inchain) = ratio * X(inchain);
322 T(:,inchain) = ratio * T(:,inchain);
323 U(:,inchain) = ratio * U(:,inchain);
324 R(:,inchain) = Q(:,inchain) ./ T(:,inchain);
329function tf = lld_encodes_multiserver(sn, nservers, M)
330% True
if every finite multi-server station already carries mu(n)=min(n,c) in
331% lldscaling, i.e. the multiserver
is fully described by the load-dependent
332% rates and needs no further handling.
334Ntot = sum(sn.njobs(isfinite(sn.njobs)));
335if isempty(sn.lldscaling) || ~isfinite(Ntot) || Ntot < 1
338if size(sn.lldscaling,2) < Ntot
342 if isfinite(nservers(ist)) && nservers(ist) > 1
343 if ~isequal(sn.lldscaling(ist,1:Ntot), min(1:Ntot, nservers(ist)))