1function runtime = runAnalyzer(self, options)
5 options = self.getOptions;
7% Wall-clock time-budget launch marker (see options.timeout / lineTimeoutExceeded)
8options.timeout_tic = tic;
10if strcmp(options.lang,
'python')
11 line_debug(options, 'NC: using lang=python, delegating to native line_solver');
12 [QN,UN,RN,TN,AN,WN,runtime] = PYLINE.getAvg(self.name, self.model, options);
13 self.setAvgResults(QN,UN,RN,TN,AN,WN,[],[],runtime,options.method,NaN);
18self.runAnalyzerChecks(options);
21if isfield(sn,'immfeed') && ~isempty(sn.immfeed) && any(sn.immfeed(:))
22 line_warning(mfilename,'SolverNC does not handle immediate feedback (immfeed);
the solver will treat self-loops as class-switching with re-queueing.\n');
25Solver.resetRandomGeneratorSeed(options.seed);
27% Show library attribution if verbose and not yet shown
28if options.verbose ~= VerboseLevel.SILENT && ~GlobalConstants.isLibraryAttributionShown()
29 sn = self.getStruct();
30 libs = SolverNC.getLibrariesUsed(sn, options);
32 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
33 GlobalConstants.setLibraryAttributionShown(true);
37origmethod = options.method;
39% Warn (debug level) when
the requested method
is unrecognized. With solver
40% checks disabled (e.g. when SolverNC
is used as a SolverLN layer backend,
41% where enableChecks=false bypasses runAnalyzerChecks) an unknown method name
42% silently falls through to
the default normalizing-constant analyzer instead
43% of raising an error, which masks typos such as 'adaptive'.
44validMethods = [self.listValidMethods(), {
'comomld'}]; % comomld
is auto-selected internally from
'default'
45if ~any(strcmpi(origmethod, validMethods))
46 line_debug(options,
'NC: unrecognized method ''%s'', falling back to the default normalizing-constant analyzer (nc_analyzer/comom).', origmethod);
49%options.lang =
'java';
53 line_debug(options,
'NC: using lang=java, delegating to JLINE');
55 jmodel = LINE2JLINE(self.model);
56 %M = jmodel.getNumberOfStatefulNodes;
57 M = jmodel.getNumberOfStations;
58 R = jmodel.getNumberOfClasses;
59 jsolver = JLINE.SolverNC(jmodel, options);
60 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(jsolver.getAvgTable);
61 runtime = jsolver.result.runtime;
64 QN = reshape(QN
',R,M)';
65 UN = reshape(UN
',R,M)';
66 RN = reshape(RN
',R,M)';
67 TN = reshape(TN
',R,M)';
68 WN = reshape(WN
',R,M)';
69 AN = reshape(AN
',R,M)';
73 if sn.nodetype(ind) == NodeType.Cache
74 jnode = jmodel.getNodeByIndex(ind-1);
75 self.model.nodes{ind}.setResultHitProb(JLINE.from_jline_matrix(jnode.getHitRatio()));
76 self.model.nodes{ind}.setResultMissProb(JLINE.from_jline_matrix(jnode.getMissRatio()));
77 % Retrieval-cache extras (delayed-hit ratio, per-list hit ratio
78 % and expected latency) so getAvgCacheTable matches
the native path.
79 self.model.nodes{ind}.setResultDelayedHitProb(JLINE.from_jline_matrix(jnode.getDelayedHitRatio()));
80 self.model.nodes{ind}.setResultHitProbList(JLINE.from_jline_matrix(jnode.getHitRatioByList()));
81 self.model.nodes{ind}.setResultItemProb(JLINE.from_jline_matrix(jnode.getItemProb()));
82 self.model.nodes{ind}.setResultResidT(JLINE.from_jline_matrix(jnode.getResidT()));
85 %self.model.refreshChains();
86 self.model.refreshStruct(
true);
87 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,options.method,lastiter);
88 self.result.Prob.logNormConstAggr = lG;
91 line_debug(options,
'NC: using lang=matlab');
92 sn = getStruct(self); % doesn
't need initial state
94 % Order-independent (OI) closed network: solved exactly by the
95 % balanced-fairness normalizing constant (pfqn_oi_nc) with the OI
96 % functional-server (pfqn_oi_fnc) identity for the mean queue lengths.
97 % Intercept before the multiserver->lldscaling conversion below, since
98 % an OI station is multiserver but is not a plain min(n,c) load-
100 if nc_is_oi_model(sn) && any(strcmpi(options.method,{'default','exact
'}))
101 line_debug(options, 'NC: order-independent closed network, routing to solver_nc_oi_analyzer
');
102 [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_nc_oi_analyzer(sn, options);
103 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
104 if strcmp(origmethod,'default') && ~strcmp(actualmethod,'default')
105 self.setAvgResults(QN,UN,RN,TN,AN,[],CN,XN,runtime,['default/
' actualmethod],iter);
107 self.setAvgResults(QN,UN,RN,TN,AN,[],CN,XN,runtime,actualmethod,iter);
109 self.result.Prob.logNormConstAggr = real(lG);
113 switch options.method
115 if sn.nstations == 2 && ~any(sn.nodetype == NodeType.Cache) && any(sn.nodetype == NodeType.Delay) && any(sn.nservers(isfinite(sn.nservers))>1)
116 % 2-station Delay+multiserver (the topology of every SolverLN
117 % layer submodel). When the model is product-form the
118 % multiserver can be solved exactly via load-dependent CoMoM
119 % (convert to mu(n)=min(n,c) and route to the ncld analyzer),
120 % which matches CTMC/MVA exactly. Non-product-form cases (e.g.
121 % LN layers with heterogeneous per-class FCFS rates) have no
122 % exact CoMoM-LD solution and fall back to Seidmann's
123 % approximation (comom).
124 if self.model.hasProductFormSolution && isempty(sn.lldscaling)
127 sn.lldscaling = ones(sn.nstations,Nt);
129 if sn.nservers(i) > 1 && isfinite(sn.nservers(i))
130 % The queueing solve reads mu(n) from lldscaling, so
the
131 % server
count must be kept: utilization
is the fraction
132 % of
the c servers busy, and c
is not recoverable from
133 % lldscaling once Nt<c (min(1:Nt,c)
is then just 1:Nt).
134 % Zeroing it to 1 made solver_ncld normalize U by
135 % max(lldscaling)=min(Nt,c) instead of c, overstating U
136 % by c/Nt whenever
the population
is below
the servers.
137 sn.lldscaling(i,:) = min(1:Nt,sn.nservers(i));
141 line_debug(options, 'Default method: 2-station Delay+multiserver product-form network, using exact load-dependent comomld');
143 options.method = 'comom'; % non-product-form (e.g. LN layer submodels): Seidmann approximation
144 line_debug(options, 'Default method: 2-station Delay+multiserver non-product-form network, using comom');
148 %
'is' (importance sampling) on an ORDINARY product-form model
is
149 % a normalizing-constant estimator and needs
the same model as
150 %
'exact': convert multiserver stations to load-dependent here
151 % too, so it routes to solver_ncld_analyzer -> pfqn_ncld ->
152 % pfqn_ld_is rather than falling through to Seidmann
's
153 % approximation, which would faithfully estimate a DIFFERENT
154 % (approximated) model.
156 % OI / pass-and-swap models are the exception: an OI station is
157 % multiserver but is not a plain min(n,c) load-dependent station,
158 % and a P&S tandem has only per-communicating-class product form
159 % (so hasProductFormSolution is false). Leave them untouched for
160 % solver_nc_analyzer to route to pfqn_pas_is / pfqn_oi_is.
161 if strcmpi(options.method,'is') && nc_is_pas_model(sn)
162 % no-op: handled by solver_nc_analyzer (pfqn_pas_is)
163 elseif ~self.model.hasProductFormSolution
164 line_error(mfilename,'The %s method requires
the model to have a product-form solution. This model does not have one. You can use Network.hasProductFormSolution() to check before running
the solver.
', options.method);
165 elseif isempty(sn.lldscaling) && any(sn.nservers(isfinite(sn.nservers)) > 1)
166 % if exact/is is requested and does not override a lldscaling assigment
167 % ONLY when a genuine multiserver station is present. Setting
168 % lldscaling=ones on an all-single-server model is a semantic
169 % no-op but forces the load-dependent path (solver_ncld_analyzer),
170 % whose recovery mishandles a single-station-confined
171 % (self-looping) closed chain -- it returned QLen=[1,0,0] for a
172 % Delay+2Queue model with self-looping classes where the exact
173 % answer (CTMC / solver_nc_analyzer) is [0.71,0.14,0.14]. With
174 % no multiserver there is nothing to convert, so leave
175 % lldscaling empty and route to the standard normalizing-constant
179 % trasform multi-server nodes into lld nodes; the server count is
180 % kept so that utilization stays normalized by c (see the
181 % 'default
' branch above)
182 sn.lldscaling = ones(sn.nstations,Nt);
184 if sn.nservers(i) > 1 && isfinite(sn.nservers(i))
185 sn.lldscaling(i,:) = min(1:Nt,sn.nservers(i));
189 line_debug(options, 'Exact method: converted multiserver stations to load-dependent (Nt=%d)
', Nt);
193 % The feature gate ran in runAnalyzerChecks above, through
194 % supportsModelMethod, and reports which features are unsupported. A
195 % coarse supports(self.model) repeat here would only re-reject the same
196 % models with a message that names nothing.
198 Solver.resetRandomGeneratorSeed(options.seed);
200 ci_cache = find(sn.nodetype == NodeType.Cache, 1);
201 hasRetrieval = ~isempty(ci_cache) && isfield(sn.nodeparam{ci_cache}, 'retrievalSystemCapacity
') ...
202 && sn.nodeparam{ci_cache}.retrievalSystemCapacity > 0;
203 if hasRetrieval % delayed-hit (retrieval-system) cache
204 if any(sn.nodetype == NodeType.Source)
205 line_debug(options, 'Open delayed-hit retrieval cache, routing to nc_retrieval_analyzer
');
206 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,delayedprob,hitproblist,itemprob,latency,runtime,actualmethod] = solver_nc_retrieval_analyzer(sn, options);
208 line_debug(options, 'Closed integrated delayed-hit retrieval cache, routing to nc_cacheqn_retrieval_analyzer
');
209 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,delayedprob,hitproblist,itemprob,latency,runtime,actualmethod] = solver_nc_cacheqn_retrieval_analyzer(sn, options);
212 for ind = 1:sn.nnodes
213 if sn.nodetype(ind) == NodeType.Cache
214 self.model.nodes{ind}.setResultHitProb(hitprob);
215 self.model.nodes{ind}.setResultMissProb(missprob);
216 self.model.nodes{ind}.setResultDelayedHitProb(delayedprob);
217 self.model.nodes{ind}.setResultHitProbList(hitproblist);
218 self.model.nodes{ind}.setResultItemProb(itemprob);
219 self.model.nodes{ind}.setResultResidT(latency);
222 self.model.refreshStruct(true);
223 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Cache,NodeType.Sink])) %
is a non-rentrant cache
224 line_debug(options,
'Non-reentrant cache (Source-Cache-Sink), routing to nc_cache_analyzer');
225 % random initialization
226 for ind = 1:sn.nnodes
227 if sn.nodetype(ind) == NodeType.Cache
228 prob = self.model.nodes{ind}.server.hitClass;
230 self.model.nodes{ind}.setResultHitProb(prob);
231 self.model.nodes{ind}.setResultMissProb(1-prob);
234 self.model.refreshChains();
236 [QN,UN,RN,TN,CN,XN,lG,pij,runtime,actualmethod,hitproblist,itemprob] = solver_nc_cache_analyzer(sn, options);
237 self.result.Prob.itemProb = pij;
238 for ind = 1:sn.nnodes
239 if sn.nodetype(ind) == NodeType.Cache
240 self.model.nodes{ind}.setResultHitProbList(hitproblist);
241 self.model.nodes{ind}.setResultItemProb(itemprob);
242 %prob = self.model.nodes{ind}.server.hitClass;
244 hitClass = self.model.nodes{ind}.getHitClass;
245 missClass = self.model.nodes{ind}.getMissClass;
246 hitprob = zeros(1,length(hitClass));
247 for k=1:length(self.model.nodes{ind}.getHitClass)
248 %
for k=1:length(self.model.nodes{ind}.server.hitClass)
249 chain_k = sn.chains(:,k)>0;
250 inchain = sn.chains(chain_k,:)>0;
254 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan");
257 self.model.
nodes{ind}.setResultHitProb(hitprob);
258 self.model.nodes{ind}.setResultMissProb(1-hitprob);
261 self.model.refreshChains;
262 else % queueing network
263 if any(sn.nodetype == NodeType.Cache) %
if integrated caching-queueing
264 line_debug(options,
'Integrated caching-queueing network, routing to nc_cacheqn_analyzer');
265 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,runtime,iter,actualmethod] = solver_nc_cacheqn_analyzer(self, options);
266 for ind = 1:sn.nnodes
267 if sn.nodetype(ind) == NodeType.Cache
268 self.model.nodes{ind}.setResultHitProb(hitprob(ind,:));
269 self.model.nodes{ind}.setResultMissProb(missprob(ind,:));
272 self.model.refreshStruct(
true); % Force refresh to get updated actualhitprob/actualmissprob
274 else % ordinary queueing network
275 % Check
for open model with single FCR containing single Delay (loss network)
276 if ~sn_has_closed_classes(sn) && sn.nregions == 1
277 regionMatrix = sn.region{1};
278 % Find stations in FCR (those with non-negative constraints)
279 stationsInFCR = find(any(regionMatrix(:,1:end-1) >= 0, 2) | regionMatrix(:,end) >= 0);
280 if length(stationsInFCR) == 1 && isinf(sn.nservers(stationsInFCR(1)))
281 % Single delay node in FCR - check drop rule
282 if all(sn.regionrule(1,:) == DropStrategy.DROP)
283 % Use loss network solver
284 line_debug(options,
'Open model with single FCR + Delay node (DROP), routing to nc_lossn_analyzer');
285 [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_nc_lossn_analyzer(sn, options);
286 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
287 if strcmp(origmethod,
'default') && exist(
'actualmethod',
'var') && ~strcmp(actualmethod,
'default')
288 self.setAvgResults(QN,UN,RN,TN,AN,[],CN,XN,runtime,['default/' actualmethod],iter);
290 self.setAvgResults(QN,UN,RN,TN,AN,[],CN,XN,runtime,actualmethod,iter);
292 self.result.Prob.logNormConstAggr = real(lG);
295 % WAITQ (blocking) not supported - error and stop
296 line_error(mfilename, 'SolverNC does not support finite capacity regions with WAITQ (blocking) policy. Use DROP policy instead.');
300 % Residual FCR (not
the single-Delay loss-network case dispatched
301 % above): NC does not enforce
the aggregate region limit; reject
302 % instead of silently returning
the unconstrained answer.
303 if isfield(sn,'nregions') && sn.nregions > 0
304 line_error(mfilename,'This model uses a Finite Capacity Region (addRegion) on queueing stations, which
is not supported by SolverNC. Use SolverJMT, or setCapacity for a single-station limit.');
306 if ~isempty(sn.lldscaling) || ~isempty(sn.cdscaling)
307 line_debug(options, 'Load-dependent scaling detected, routing to ncld_analyzer');
308 [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_ncld_analyzer(sn, options);
310 switch options.method
312 if ~sn_has_open_classes(sn)
313 % This branch
is reached only with EMPTY lldscaling
314 % (
the outer test routes any load-dependent model to
315 % ncld). Genuine multiserver stations were converted
316 % to lldscaling above, so an empty lldscaling here
317 % means an all-single-server closed model, which
the
318 % standard normalizing-constant path solves exactly.
319 % Routing it to solver_ncld_analyzer instead
320 % mishandles a single-station-confined (self-looping)
321 % closed chain -- returning QLen=[1,0,0] for a
322 % Delay+2Queue model with self-looping classes where
323 %
the exact answer
is [0.71,0.14,0.14].
324 line_debug(options, 'NC method=exact, single-server closed model, routing to nc_analyzer');
325 [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_nc_analyzer(sn, options);
326 else%if ~snHasClosedClasses(sn)
327 line_debug(options, 'NC method=exact, open model, routing to nc_analyzer');
328 [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_nc_analyzer(sn, options);
330 case {
'rd',
'nrp',
'nrl',
'comomld'}
331 line_debug(options,
'NC method=%s, routing to ncld_analyzer', options.method);
332 [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_ncld_analyzer(sn, options);
334 line_debug(options,
'NC method=%s, routing to nc_analyzer', options.method);
335 [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_nc_analyzer(sn, options);
340 % Compute average arrival rate at steady-state
341 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
343if strcmp(origmethod,
'default') && exist(
'actualmethod',
'var') && ~strcmp(actualmethod,
'default')
344 self.setAvgResults(QN,UN,RN,TN,AN,[],CN,XN,runtime,['default/' actualmethod],iter);
346 self.setAvgResults(QN,UN,RN,TN,AN,[],CN,XN,runtime,actualmethod,iter);
349self.result.Prob.logNormConstAggr = real(lG);
350if lineTimeoutExceeded(options)
351 self.result.Avg.timedOut = true;
352 line_warning(mfilename,'Solver exceeded
the wall-clock time budget (options.timeout=%gs).\n', options.timeout);