1function runtime = runAnalyzer(self, options)
8 options = self.getOptions;
10% Wall-clock time-budget launch marker (see options.timeout / lineTimeoutExceeded)
11options.timeout_tic = T0;
12% Session-level deadline
for budget checkpoints in deep utilities that have no
13% options argument (e.g. multichoose during state-space generation). Cleared
14% automatically when
this analyzer returns or errors.
15if isfield(options,
'timeout') && ~isempty(options.timeout) && isfinite(options.timeout) && options.timeout > 0
16 setappdata(0,
'LINEtimeoutDeadline',
struct(
'tic', T0,
'budget', options.timeout));
17 timeoutDeadlineCleanup = onCleanup(@() setappdata(0,
'LINEtimeoutDeadline', [])); %#ok<NASGU>
20[pyHandled, options, pyRuntime] = self.runAnalyzerPreamble(options,
'CTMC');
26% QRF (Quadratic/Linear Reduction) LP-based bounds moved to SolverBA.
27if startsWith(options.method,
'qrf')
28 line_error(mfilename, ['The QRF reduction bounds (method ''%s'') moved to SolverBA. ' ...
29 'Use SolverBA(model,''method'',''%s'') (or the ''qr''/''lr'' aliases).'], ...
30 options.method, options.method);
33if ~isinf(options.timespan(1)) && (options.timespan(1) == options.timespan(2))
34 line_warning(mfilename,'%s: timespan
is a single point, spacing by options.tol (%e).\n',mfilename, options.tol);
35 options.timespan(2) = options.timespan(1) + options.tol;
39self.runAnalyzerChecks(options);
40% Finite Capacity Region: enforced in solver_ctmc.m by filtering the state space
41% to states within the aggregate per-region job/memory/linear caps (blocking-
42% before-entry). Per-station setCapacity
is also honored.
43Solver.resetRandomGeneratorSeed(options.seed);
46% see _kb/06-solver-catalog.md (CTMC section, feature gate and LN redirect) for rationale
48% Inform user about reducible routing handling
50 [isErg, ergInfo] = self.model.isRoutingErgodic();
51 if ~isErg && ~isempty(ergInfo.absorbingStations)
52 absNames = strjoin(ergInfo.absorbingStations, ', ');
54 'Note: Model has reducible routing with absorbing stations: %s\n' ...
55 ' Results represent limiting/absorption probabilities.\n' ...
56 ' Use model.getReducibilityInfo() for detailed analysis.\n'], ...
62line_debug(options, 'CTMC: using lang=matlab');
64% Native fork-join support: solve the tag-augmented copy exactly and fold
65% the auxiliary sibling classes back into the original classes at the end
66isFJ = any(sn.nodetype == NodeType.Fork) || any(sn.nodetype == NodeType.Join);
68 if ~isinf(options.timespan(1))
69 line_error(mfilename,'Transient analysis of fork-join models
is not supported by SolverCTMC.\n');
73 [~, fjsn, fjclassmap] = ModelAdapter.fjtag(self.model);
75 options.config.state_space_gen = 'reachable';
76 line_debug(options, 'CTMC: fork-join tag augmentation, %d classes (%d auxiliary), %d fork firings', sn.nclasses, sn.nclasses-Korig, length(sn.fjsync));
79% Convert non-Markovian distributions to PH
80sn = sn_nonmarkov_toph(sn, options);
81line_debug(options, 'CTMC: converted non-Markovian distributions to PH (nstations=%d, nclasses=%d)', sn.nstations, sn.nclasses);
88 sizeEstimator = sizeEstimator + gammaln(1+NK(k)+M-1) - gammaln(1+M-1) - gammaln(1+NK(k)); % worst-case estimate of the state space
91if any(isinf(sn.njobs))
92 if isinf(options.cutoff)
93 line_warning(mfilename,sprintf('The model has open chains, it
is recommended to specify a finite cutoff value, e.g., SolverCTMC(model,''cutoff'',1).\n'));
94 self.options.cutoff= ceil(6000^(1/(M*K)));
95 options.cutoff= ceil(6000^(1/(M*K)));
96 line_debug(options, 'Open/mixed model: auto-setting cutoff=%d for %d stations, %d classes', options.cutoff, M, K);
97 line_warning(mfilename,sprintf('Setting cutoff=%d.\n',self.options.cutoff));
99 % Mandatory truncation warning for open/mixed models
100 line_printf('CTMC solver using state space cutoff = %d for open/mixed model.\n', options.cutoff);
101 line_warning(mfilename,'State space truncation may cause inaccurate results. Consider varying cutoff to assess sensitivity.\n');
104% see _kb/06-solver-catalog.md (CTMC section, memory pre-gate) for rationale
110 if numel(nk) > 1; nk = max(nk(:)); end
115 logNstates = logNstates + gammaln(1+nk+M-1) - gammaln(1+M-1) - gammaln(1+nk);
117% see _kb/06-solver-catalog.md (CTMC section, memory pre-gate) for rationale
118if isfield(sn,'phasessz') && ~isempty(sn.phasessz)
119 shareSched = [SchedStrategy.INF, SchedStrategy.PS, SchedStrategy.DPS, ...
120 SchedStrategy.GPS, SchedStrategy.PSPRIO, SchedStrategy.DPSPRIO, ...
121 SchedStrategy.GPSPRIO, SchedStrategy.LPS];
122 for i = 1:min(M, size(sn.phasessz,1))
123 for k = 1:min(K, size(sn.phasessz,2))
124 p = sn.phasessz(i,k);
125 if ~isfinite(p) || p <= 1
128 if sn.sched(i) == SchedStrategy.EXT
130 elseif any(sn.sched(i) == shareSched)
133 m = min(nkEff(k), sn.nservers(i));
138 logNstates = logNstates + gammaln(1+m+p-1) - gammaln(1+p-1) - gammaln(1+m);
142% Routing
factor: each (node,class) doing RROBIN/WRROBIN adds a pointer over
143% that node's outgoing links.
144if isfield(sn,'routing') && ~isempty(sn.routing) && isfield(sn,'connmatrix') && ~isempty(sn.connmatrix)
145 for ind = 1:min(size(sn.routing,1), size(sn.connmatrix,1))
146 nout = nnz(sn.connmatrix(ind,:));
150 nrr = sum(sn.routing(ind,:) == RoutingStrategy.RROBIN | ...
151 sn.routing(ind,:) == RoutingStrategy.WRROBIN);
153 logNstates = logNstates + nrr * log(nout);
157forceFlag = isfield(options,'force') && ~isempty(options.force) && options.force;
158if isfield(options,'memorySafetyFraction') && ~isempty(options.memorySafetyFraction)
159 safetyFraction = options.memorySafetyFraction;
161 safetyFraction = 0.6;
163line_debug(options, 'State space size estimate: exp(%f)', logNstates);
164gateVerbose = isfield(options,'verbose') && options.verbose == VerboseLevel.DEBUG;
165[gateOk, gateMsg] = ctmc_memory_gate(logNstates, forceFlag, gateVerbose, safetyFraction);
167 line_error(mfilename, sprintf('%s Stopping SolverCTMC.\n', gateMsg));
171% we compute all metrics anyway because CTMC has essentially
173if isinf(options.timespan(1))
174 line_debug(options, 'Using standard CTMC method for steady-state analysis');
176 s0prior = sn.stateprior;
178 if sn.isstateful(ind)
179 isf = sn.nodeToStateful(ind);
180 sn.state{isf} = s0{isf}(maxpos(s0prior{1}),:); % pick one particular initial state
183 [QN,UN,RN,TN,CN,XN,Q,SS,SSq,Dfilt,~,~,sn] = solver_ctmc_analyzer(sn, options);
185 % update initial state
if this has been corrected by the state space
186 % generator (skipped on fork-join models: the analyzed struct
is the
187 % tag-augmented copy, whose states do not fit the original model)
188 for isf=1:sn.nstateful
189 ind = sn.statefulToNode(isf);
190 self.model.nodes{ind}.setState(sn.state{isf});
191 switch class(self.model.nodes{sn.statefulToNode(isf)})
193 self.model.nodes{sn.statefulToNode(isf)}.setResultHitProb(sn.nodeparam{ind}.actualhitprob);
194 self.model.nodes{sn.statefulToNode(isf)}.setResultMissProb(sn.nodeparam{ind}.actualmissprob);
195 if isfield(sn.nodeparam{ind},
'actualresidt')
196 self.model.
nodes{sn.statefulToNode(isf)}.setResultResidT(sn.nodeparam{ind}.actualresidt);
198 self.model.refreshChains();
203 self.result.infGen = Q;
204 self.result.space = SS;
205 self.result.spaceAggr = SSq;
206 self.result.nodeSpace = sn.space;
207 self.result.eventFilt = Dfilt;
210 T = getAvgTputHandles(self);
212 [QN,UN,RN,TN,CN,XN] = sn_fj_foldback(QN,UN,RN,TN,CN,XN,fjclassmap,Korig);
213 % A Place counts tokens, not firings: rescale before the arrival rates
214 % are derived, so that everything downstream sees one convention.
215 [TN,~,RN] = sn_pn_avg_rates(sn_orig, QN, TN, [], RN);
216 AN=sn_get_arvr_from_tput(sn_orig, TN, T);
217 % Join stations report the per-sibling waiting time (JMT convention):
218 % QLen over the sibling arrival rate rather than the join firing rate
219 for ist=1:sn_orig.nstations
220 if sn_orig.nodetype(sn_orig.stationToNode(ist)) == NodeType.Join
223 RN(ist,r) = QN(ist,r)/AN(ist,r);
228 self.result.fjclassmap = fjclassmap;
230 [TN,~,RN] = sn_pn_avg_rates(sn, QN, TN, [], RN);
231 AN=sn_get_arvr_from_tput(sn, TN, T);
233 self.setAvgResults(QN,UN,RN,TN,AN,[],CN,XN,runtime,options.method);
234 if lineTimeoutExceeded(options)
235 self.result.Avg.timedOut =
true;
236 line_warning(mfilename,
'Solver exceeded the wall-clock time budget (options.timeout=%gs).\n', options.timeout);
239 line_debug(options,
'Transient analysis: timespan=[%f,%f]', options.timespan(1), options.timespan(2));
242 s0prior = sn.stateprior;
244 s0_sz = cellfun(@(x) size(x,1), s0)
';
245 s0_id = pprod(s0_sz-1);
246 cur_state = sn.state;
247 while s0_id>=0 % for all possible initial states
250 if sn.isstateful(ind)
251 isf = sn.nodeToStateful(ind);
252 s0prior_val = s0prior_val * s0prior{isf}(1+s0_id(isf)); % update prior
253 sn.state{isf} = s0{isf}(1+s0_id(isf),:); % assign initial state to network
257 [t,pit,QNt,UNt,~,TNt,~,~,Q,SS,SSq,Dfilt,runtime_t] = solver_ctmc_transient_analyzer(sn, options);
258 self.result.space = SS;
259 self.result.spaceAggr = SSq;
260 self.result.infGen = Q;
261 self.result.eventFilt = Dfilt;
263 setTranProb(self,t,pit,SS,runtime_t);
264 if isempty(self.result) || ~isfield(self.result,'Tran
') || ~isfield(self.result.Tran,'Avg
') || ~isfield(self.result.Tran.Avg,'Q
')
265 self.result.Tran.Avg.Q = cell(M,K);
266 self.result.Tran.Avg.U = cell(M,K);
267 self.result.Tran.Avg.T = cell(M,K);
270 self.result.Tran.Avg.Q{ist,r} = [QNt{ist,r} * s0prior_val,t];
271 self.result.Tran.Avg.U{ist,r} = [UNt{ist,r} * s0prior_val,t];
272 self.result.Tran.Avg.T{ist,r} = [TNt{ist,r} * s0prior_val,t];
278 tunion = union(self.result.Tran.Avg.Q{ist,r}(:,2), t);
279 dataOld = interp1(self.result.Tran.Avg.Q{ist,r}(:,2),self.result.Tran.Avg.Q{ist,r}(:,1),tunion);
280 dataNew = interp1(t,QNt{ist,r},tunion);
281 self.result.Tran.Avg.Q{ist,r} = [dataOld+s0prior_val*dataNew,tunion];
282 dataOld = interp1(self.result.Tran.Avg.U{ist,r}(:,2),self.result.Tran.Avg.U{ist,r}(:,1),tunion);
283 dataNew = interp1(t,UNt{ist,r},tunion);
284 self.result.Tran.Avg.U{ist,r} = [dataOld+s0prior_val*dataNew,tunion];
286 dataOld = interp1(self.result.Tran.Avg.T{ist,r}(:,2),self.result.Tran.Avg.T{ist,r}(:,1),tunion);
287 dataNew = interp1(t,TNt{ist,r},tunion);
288 self.result.Tran.Avg.T{ist,r} = [dataOld+s0prior_val*dataNew,tunion];
293 s0_id=pprod(s0_id,s0_sz-1); % update initial state
295 % Now we restore the original state
297 if sn.isstateful(ind)
298 isf = sn.nodeToStateful(ind);
299 self.model.nodes{ind}.setState(cur_state{isf});
305 self.result.('solver
') = getName(self);
306 self.result.runtime = runtime;
307 self.result.solverSpecific = lastSol;