LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
runAnalyzer.m
1function runtime = runAnalyzer(self, options)
2% RUNTIME = RUN()
3% Run the solver
4
5T0=tic;
6
7if nargin<2
8 options = self.getOptions;
9end
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>
18end
19
20[pyHandled, options, pyRuntime] = self.runAnalyzerPreamble(options, 'CTMC');
21if pyHandled
22 runtime = pyRuntime;
23 return
24end
25
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);
31end
32
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;
36end
37
38
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);
44
45
46% see _kb/06-solver-catalog.md (CTMC section, feature gate and LN redirect) for rationale
47
48% Inform user about reducible routing handling
49if self.enableChecks
50 [isErg, ergInfo] = self.model.isRoutingErgodic();
51 if ~isErg && ~isempty(ergInfo.absorbingStations)
52 absNames = strjoin(ergInfo.absorbingStations, ', ');
53 line_printf([...
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'], ...
57 absNames);
58 end
59end
60
61sn = getStruct(self);
62line_debug(options, 'CTMC: using lang=matlab');
63
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);
67if isFJ
68 if ~isinf(options.timespan(1))
69 line_error(mfilename,'Transient analysis of fork-join models is not supported by SolverCTMC.\n');
70 end
71 sn_orig = sn;
72 Korig = sn.nclasses;
73 [~, fjsn, fjclassmap] = ModelAdapter.fjtag(self.model);
74 sn = fjsn;
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));
77end
78
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);
82
83M = sn.nstations;
84K = sn.nclasses;
85NK = sn.njobs;
86sizeEstimator = 0;
87for k=1:K
88 sizeEstimator = sizeEstimator + gammaln(1+NK(k)+M-1) - gammaln(1+M-1) - gammaln(1+NK(k)); % worst-case estimate of the state space
89end
90
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));
98 end
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');
102end
103
104% see _kb/06-solver-catalog.md (CTMC section, memory pre-gate) for rationale
105logNstates = 0;
106nkEff = zeros(1,K);
107for k = 1:K
108 if isinf(NK(k))
109 nk = options.cutoff;
110 if numel(nk) > 1; nk = max(nk(:)); end
111 else
112 nk = NK(k);
113 end
114 nkEff(k) = nk;
115 logNstates = logNstates + gammaln(1+nk+M-1) - gammaln(1+M-1) - gammaln(1+nk);
116end
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
126 continue
127 end
128 if sn.sched(i) == SchedStrategy.EXT
129 m = 1;
130 elseif any(sn.sched(i) == shareSched)
131 m = nkEff(k);
132 else
133 m = min(nkEff(k), sn.nservers(i));
134 end
135 if ~isfinite(m)
136 m = nkEff(k);
137 end
138 logNstates = logNstates + gammaln(1+m+p-1) - gammaln(1+p-1) - gammaln(1+m);
139 end
140 end
141end
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,:));
147 if nout <= 1
148 continue
149 end
150 nrr = sum(sn.routing(ind,:) == RoutingStrategy.RROBIN | ...
151 sn.routing(ind,:) == RoutingStrategy.WRROBIN);
152 if nrr > 0
153 logNstates = logNstates + nrr * log(nout);
154 end
155 end
156end
157forceFlag = isfield(options,'force') && ~isempty(options.force) && options.force;
158if isfield(options,'memorySafetyFraction') && ~isempty(options.memorySafetyFraction)
159 safetyFraction = options.memorySafetyFraction;
160else
161 safetyFraction = 0.6;
162end
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);
166if ~gateOk
167 line_error(mfilename, sprintf('%s Stopping SolverCTMC.\n', gateMsg));
168 return
169end
170
171% we compute all metrics anyway because CTMC has essentially
172% the same cost
173if isinf(options.timespan(1))
174 line_debug(options, 'Using standard CTMC method for steady-state analysis');
175 s0 = sn.state;
176 s0prior = sn.stateprior;
177 for ind=1:sn.nnodes
178 if sn.isstateful(ind)
179 isf = sn.nodeToStateful(ind);
180 sn.state{isf} = s0{isf}(maxpos(s0prior{1}),:); % pick one particular initial state
181 end
182 end
183 [QN,UN,RN,TN,CN,XN,Q,SS,SSq,Dfilt,~,~,sn] = solver_ctmc_analyzer(sn, options);
184 if ~isFJ
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)})
192 case 'Cache'
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);
197 end
198 self.model.refreshChains();
199 end
200 end
201 end
202 %sn.space = SS;
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;
208 runtime = toc(T0);
209 sn.space = {};
210 T = getAvgTputHandles(self);
211 if isFJ
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
221 for r=1:Korig
222 if AN(ist,r) > 0
223 RN(ist,r) = QN(ist,r)/AN(ist,r);
224 end
225 end
226 end
227 end
228 self.result.fjclassmap = fjclassmap;
229 else
230 [TN,~,RN] = sn_pn_avg_rates(sn, QN, TN, [], RN);
231 AN=sn_get_arvr_from_tput(sn, TN, T);
232 end
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);
237 end
238else
239 line_debug(options, 'Transient analysis: timespan=[%f,%f]', options.timespan(1), options.timespan(2));
240 lastSol= [];
241 s0 = sn.space;
242 s0prior = sn.stateprior;
243
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
248 s0prior_val = 1;
249 for ind=1:sn.nnodes
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
254 end
255 end
256 if s0prior_val > 0
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;
262 %sn.space = SS;
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);
268 for ist=1:M
269 for r=1: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];
273 end
274 end
275 else
276 for ist=1:M
277 for r=1:K
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];
285
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];
289 end
290 end
291 end
292 end
293 s0_id=pprod(s0_id,s0_sz-1); % update initial state
294 end
295 % Now we restore the original state
296 for ind=1:sn.nnodes
297 if sn.isstateful(ind)
298 isf = sn.nodeToStateful(ind);
299 self.model.nodes{ind}.setState(cur_state{isf});
300 end
301 end
302
303 runtime = toc(T0);
304 sn.space = {};
305 self.result.('solver') = getName(self);
306 self.result.runtime = runtime;
307 self.result.solverSpecific = lastSol;
308end
309end
Definition fjtag.m:161