LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_ctmc.m
1function [Q,stateSpace,stateSpaceAggr,Dfilt,arvRates,depRates,sn]=solver_ctmc(sn,options)
2% [Q,SS,SSQ,DFILT,ARVRATES,DEPRATES,QN]=SOLVER_CTMC(QN,OPTIONS)
3%
4% Copyright (c) 2012-2026, Imperial College London
5% All rights reserved.
6
7%% impatience support checks
8% see _kb/06-solver-catalog.md (CTMC section, support gates) for rationale
9if isfield(sn,'impatienceClass') && ~isempty(sn.impatienceClass)
10 badRenege = (sn.impatienceClass==ImpatienceType.RENEGING) & (sn.impatienceType~=ProcessType.EXP);
11 if any(badRenege(:))
12 line_error(mfilename,'SolverCTMC supports only exponential (memoryless) patience for reneging. Use SolverLDES or SolverJMT for phase-type patience.');
13 end
14end
15if isfield(sn,'balkingStrategy') && ~isempty(sn.balkingStrategy)
16 badBalk = (sn.balkingStrategy~=0) & (sn.balkingStrategy~=BalkingStrategy.QUEUE_LENGTH);
17 if any(badBalk(:))
18 line_error(mfilename,'SolverCTMC supports only QUEUE_LENGTH balking. Use SolverLDES or SolverJMT for wait-time-based balking.');
19 end
20end
21% see _kb/06-solver-catalog.md (CTMC section, support gates) for rationale
22if isfield(sn,'retrialProc') && ~isempty(sn.retrialProc)
23 hasRetrial = ~cellfun(@isempty, sn.retrialProc);
24 if any(hasRetrial(:))
25 if any(hasRetrial(:) & (sn.retrialType(:)~=ProcessType.EXP))
26 line_error(mfilename,'SolverCTMC supports only exponential (memoryless) retrial delay. Use SolverLDES or SolverMAM for phase-type retrials.');
27 end
28 if any(hasRetrial(:) & (sn.retrialMaxAttempts(:)>=0))
29 line_error(mfilename,'SolverCTMC supports only unlimited retrials (maxAttempts=-1). Use SolverLDES for finite max-attempts.');
30 end
31 % A retrial orbit is enumerated per single populated class; reject a
32 % retrial station that serves more than one class.
33 for ii = find(any(hasRetrial,2))'
34 served = 0;
35 for rr = 1:sn.nclasses
36 if ~isempty(sn.proc{ii}{rr}) && ~any(any(isnan(sn.proc{ii}{rr}{1})))
37 served = served + 1;
38 end
39 end
40 if served > 1
41 line_error(mfilename,'SolverCTMC supports retrial only for single-class stations. Use SolverLDES for multi-class retrial.');
42 end
43 end
44 end
45end
46% see _kb/06-solver-catalog.md (CTMC section, support gates) for rationale
47if isfield(sn,'issignal') && ~isempty(sn.issignal) && any(sn.issignal)
48 for ii = 1:sn.nstations
49 if sn.sched(ii) ~= SchedStrategy.EXT
50 sn.classcap(ii, sn.issignal(:)') = 0;
51 end
52 end
53end
54% see _kb/06-solver-catalog.md (CTMC section, heterogeneous servers) for rationale
55for ind = 1:sn.nnodes
56 if sn.isstation(ind) && isfield(sn,'nodeparam') && numel(sn.nodeparam) >= ind ...
57 && ~isempty(sn.nodeparam{ind}) && isstruct(sn.nodeparam{ind}) ...
58 && isfield(sn.nodeparam{ind},'nservertypes') && sn.nodeparam{ind}.nservertypes > 0
59 ist = sn.nodeToStation(ind);
60 % PAS/OI stations model heterogeneous compatible servers through the OI
61 % rank rate (svcRateFun), not a single-class load-dependent scaling.
62 if sn.sched(ist) == SchedStrategy.PAS || sn.sched(ist) == SchedStrategy.OI
63 continue
64 end
65 np = sn.nodeparam{ind};
66 served = [];
67 for r = 1:sn.nclasses
68 if ~isempty(sn.proc{ist}{r}) && ~any(any(isnan(sn.proc{ist}{r}{1}))) && sn.rates(ist,r) > 0
69 served(end+1) = r; %#ok<AGROW>
70 end
71 end
72 if numel(served) > 1
73 line_error(mfilename,'SolverCTMC supports heterogeneous servers only for single-class stations. Use SolverJMT or SolverLDES for multi-class heterogeneous servers.');
74 end
75 if numel(served) == 1
76 r = served;
77 srvrates = [];
78 for t = 1:np.nservertypes
79 if np.servercompat(t,r) && np.heterorates(t,r) > 0
80 srvrates = [srvrates, repmat(np.heterorates(t,r), 1, np.serverspertype(t))]; %#ok<AGROW>
81 end
82 end
83 c = numel(srvrates);
84 mu_base = sn.rates(ist,r);
85 if c > 0 && mu_base > 0
86 if isempty(sn.lldscaling)
87 sn.lldscaling = ones(sn.nstations, max([c, sum(sn.njobs(isfinite(sn.njobs))), 1]));
88 elseif size(sn.lldscaling,2) < c
89 sn.lldscaling(:, (size(sn.lldscaling,2)+1):c) = repmat(sn.lldscaling(:,end), 1, c-size(sn.lldscaling,2));
90 end
91 for n = 1:size(sn.lldscaling,2)
92 mun = sum(srvrates(1:min(n,c)));
93 sn.lldscaling(ist,n) = mun / (mu_base * min(n,c));
94 end
95 end
96 end
97 end
98end
99
100%% generate state space
101%nnodes = sn.nnodes;
102nstateful = sn.nstateful;
103nclasses = sn.nclasses;
104sync = sn.sync;
105A = length(sync);
106csmask = sn.csmask;
107
108line_debug('CTMC solver starting: nstateful=%d, nclasses=%d, sync_events=%d', nstateful, nclasses, A);
109
110if ~isfield(options.config, 'hide_immediate')
111 options.config.hide_immediate = true;
112end
113
114if ~isfield(options.config, 'state_space_gen')
115 options.config.state_space_gen = 'default';
116end
117
118%% generate state spaces, detailed and aggregate
119switch options.config.state_space_gen
120 case 'reachable' % does not handle open models yet (no cutoff)
121 line_debug('Using reachable state space generation, calling ctmc_ssg_reachability');
122 [stateSpace, stateSpaceAggr, stateSpaceHashed,~,sn] = ctmc_ssg_reachability(sn,options);
123 case {'default','full'}
124 line_debug('Using full state space generation, calling ctmc_ssg');
125 [stateSpace, stateSpaceAggr, stateSpaceHashed,~,sn] = ctmc_ssg(sn,options);
126end
127
128line_debug('State space generated: %d states', size(stateSpaceHashed,1));
129
130%% Finite Capacity Region handling
131% see _kb/06-solver-catalog.md (CTMC section) for rationale
132fcrWaitq = isfield(sn,'nregions') && sn.nregions > 0;
133
134%%
135if fcrWaitq
136 % see _kb/06-solver-catalog.md (CTMC section) for rationale
137 [stateSpace,stateSpaceAggr,stateSpaceHashed,Dfilt,sn,basBlockQ] = solver_ctmc_fcr_waitq(sn,options);
138 Q = speye(size(stateSpaceHashed,1)); % the diagonal elements will be removed later
139 % see _kb/06-solver-catalog.md (True BAS blocking) for rationale
140 Qimm = 0*Q;
141else
142Q = speye(size(stateSpaceHashed,1)); % the diagonal elements will be removed later
143Dfilt = cell(1,A);
144for a=1:A
145 Dfilt{a} = 0*Q;
146end
147% see _kb/06-solver-catalog.md (True BAS blocking) for rationale
148basBlockQ = 0*Q;
149% see _kb/06-solver-catalog.md (Vanishing states) for rationale
150Qimm = 0*Q;
151local = sn.nnodes+1; % passive action
152
153% SPN code
154% Adj_t = zeros(size(SSh,1),size(SSh,1));
155% Adj_m = zeros(size(SSh,1),size(SSh,1));
156% if ~isempty(Adj) && ~isempty(ST)
157% edges = adj_to_mat(Adj);
158% end
159
160%% for all synchronizations
161for a=1:A
162 stateCell = cell(nstateful,1);
163 %sn.sync{a}.active{1}.print
164 for s=1:size(stateSpaceHashed,1)
165 %[a,s]
166 state = stateSpaceHashed(s,:);
167 % SPN code
168 % ustate = stateSpace(s,:);
169 % state_pn = [];
170 % for st=1:length(ustate)
171 % if ~isempty(sn.varsparam{st}) && isfield(sn.varsparam{st}, 'nodeToPlace')
172 % state_pn(sn.varsparam{st}.nodeToPlace) = ustate(st);
173 % end
174 % end
175
176 % update state cell array and SSq
177 for ind = 1:sn.nnodes
178 if sn.isstateful(ind)
179 isf = sn.nodeToStateful(ind);
180 stateCell{isf} = sn.space{isf}(state(isf),:);
181 % if sn.isstation(ind)
182 % ist = sn.nodeToStation(ind);
183 % [~,nir] = State.toMarginal(sn,ind,stateCell{isf});
184 % end
185 end
186 end
187 node_a = sync{a}.active{1}.node;
188 state_a = state(sn.nodeToStateful(node_a));
189 class_a = sync{a}.active{1}.class;
190 event_a = sync{a}.active{1}.event;
191 [new_state_a, rate_a] = State.afterEventHashed( sn, node_a, state_a, event_a, class_a);
192 % SPN code:
193 %[new_state_a, rate_a,~,trans_a, modes_a] = State.afterEventHashed( qn, node_a, state_a, event_a, class_a);
194
195 %% debugging block
196 % if true%options.verbose == 2
197 % line_printf('---\n');
198 % sync{a}.active{1}.print,
199 % end
200 %%
201 if new_state_a == -1 % hash not found
202 continue
203 end
204 for ia=1:length(new_state_a)
205 if rate_a(ia)>0
206 % SPN code:
207 %if rate_a(ia)>0 || modes_a(ia) > 0
208 node_p = sync{a}.passive{1}.node;
209 if node_p ~= local
210 % Skip if the active transition hash was not found
211 if new_state_a(ia) == -1
212 continue
213 end
214 state_p = state(sn.nodeToStateful(node_p));
215 class_p = sync{a}.passive{1}.class;
216 event_p = sync{a}.passive{1}.event;
217
218 % SPN code:
219 % enabled = 0;
220 % if ia <= length(trans_a)
221 % % check if other input places of the transition contains as many token as the multiplicity of the input arcs
222 % tr = trans_a(ia);
223 % mode = modes_a(ia);
224 % bmatrix = sn.varsparam{tr}.back(:,mode);
225 % inmatrix = sn.varsparam{tr}.inh(:,mode);
226 % enabled = all(state_pn >= bmatrix' & ~any(inmatrix'>0 & inmatrix' <= state_pn));
227 % end
228
229 %prob_sync_p = sync{a}.passive{1}.prob(state_a, state_p)
230 %if prob_sync_p > 0
231 %% debugging block
232 %if options.verbose == 2
233 % line_printf('---\n');
234 % sync{a}.active{1}.print,
235 % sync{a}.passive{1}.print
236 %end
237 %%
238 if node_p == node_a %self-loop
239 [new_state_p, ~, outprob_p] = State.afterEventHashed( sn, node_p, new_state_a(ia), event_p, class_p);
240 else % departure
241 [new_state_p, ~, outprob_p] = State.afterEventHashed( sn, node_p, state_p, event_p, class_p);
242 end
243 % SPN code:
244 % if node_p == node_a %self-loop
245 % [new_state_p, ~, outprob_p, trans_p, modes_p] = State.afterEventHashed( qn, node_p, new_state_a(ia), event_p, class_p);
246 % else % departure
247 % [new_state_p, ~, outprob_p, trans_p, modes_p] = State.afterEventHashed( qn, node_p, state_p, event_p, class_p);
248 % end
249 for ip=1:size(new_state_p,1)
250 if node_p ~= local
251 if new_state_p ~= -1
252 if sn.isstatedep(node_a,3)
253 newStateCell = stateCell;
254 newStateCell{sn.nodeToStateful(node_a)} = sn.space{sn.nodeToStateful(node_a)}(new_state_a(ia),:);
255 newStateCell{sn.nodeToStateful(node_p)} = sn.space{sn.nodeToStateful(node_p)}(new_state_p(ip),:);
256 prob_sync_p = sync{a}.passive{1}.prob(stateCell, newStateCell) * outprob_p(ip); %state-dependent
257 else
258 prob_sync_p = sync{a}.passive{1}.prob * outprob_p(ip);
259 end
260 else
261 prob_sync_p = 0;
262 end
263 end
264 if ~isempty(new_state_a(ia))
265 if node_p == local % local action
266 new_state = state;
267 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
268 prob_sync_p = outprob_p(ip);
269 elseif ~isempty(new_state_p)
270 new_state = state;
271 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
272 new_state(sn.nodeToStateful(node_p)) = new_state_p(ip);
273 end
274 % SPN code:
275 % if enabled
276 % ns = find(ismember(SSh(:,[sn.nodeToStateful(node_a),sn.nodeToStateful(node_p)]),[new_state_a(ia),new_state_p(ip)],'rows'));
277 % for ins=1:length(ns)
278 % if ns(ins) > 0 && ~isempty(trans_p)
279 % tr = trans_p(ip);
280 % mode = modes_p(ip);
281 % bmatrix = sn.varsparam{tr}.back(:,mode);
282 % fmatrix = sn.varsparam{tr}.forw(:,mode);
283 % cmatrix = fmatrix - bmatrix;
284 % if isequal(state_pn + cmatrix',SS(ns(ins),3:end))
285 % [ex_a,seq_a] = ST.search(state_pn');
286 % [ex_p,seq_p] = ST.search(SS(ns(ins),3:end)');
287 % if ex_a && ex_p && edges(seq_a, seq_p)
288 % Adj_m(s, ns(ins)) = modes_p(ip);
289 % Adj_t(s, ns(ins)) = trans_p(ip);
290 % % s,ns(ins)
291 % if ~isnan(rate_a(ia))
292 % if node_p < local && ~csmask(class_a, class_p) && rate_a(ia) * prob_sync_p >0 && (sn.nodetype(node_p)~=NodeType.Source)
293 % error('Error: state-dependent routing at node %d (%s) violates the class switching mask (node %d -> node %d, class %d -> class %d).', node_a, sn.nodenames{node_a}, node_a, node_p, class_a, class_p);
294 % end
295 % if size(Dfilt{a}) >= [s,ns(ins)] % check needed as D{a} is a sparse matrix
296 % Dfilt{a}(s,ns(ins)) = Dfilt{a}(s,ns(ins)) + rate_a(ia) * prob_sync_p;
297 % else
298 % Dfilt{a}(s,ns(ins)) = rate_a(ia) * prob_sync_p;
299 % end
300 % end
301 % end
302 % end
303 % end
304 % end
305 % else
306
307 ns = matchrow(stateSpaceHashed, new_state);
308 if ns>0
309 if ~isnan(rate_a)
310 if node_p < local && ~csmask(class_a, class_p) && rate_a(ia) * prob_sync_p >0 && (sn.nodetype(node_p)~=NodeType.Source)
311 line_error(mfilename,sprintf('Error: state-dependent routing at node %d (%s) violates the class switching mask (node %s -> node %s, class %s -> class %s).', node_a, sn.nodenames{node_a}, sn.nodenames{node_a}, sn.nodenames{node_p}, sn.classnames{class_a}, sn.classnames{class_p}));
312 end
313 if size(Dfilt{a}) >= [s,ns] % check needed as D{a} is a sparse matrix
314 Dfilt{a}(s,ns) = Dfilt{a}(s,ns) + rate_a(ia) * prob_sync_p;
315 else
316 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
317 end
318 end
319 end
320 % SPN code:
321 % end
322 end
323 end
324 % see _kb/06-solver-catalog.md (True BAS blocking) for rationale
325 R2 = sn.nclasses;
326 if event_a == EventType.DEP && rate_a(ia) > 0 ...
327 && ~isempty(sn.isbasblocking) && numel(sn.isbasblocking) >= node_a ...
328 && sn.isbasblocking(node_a) == 1 ...
329 && all(new_state_p(:) == -1)
330 isfA = sn.nodeToStateful(node_a);
331 curVecA = sn.space{isfA}(state(isfA),:);
332 if curVecA(end) == 0
333 blockedVec = curVecA; blockedVec(end) = 1;
334 blockedIdx = matchrow(sn.space{isfA}, blockedVec);
335 if blockedIdx > 0
336 new_state_b = state;
337 new_state_b(isfA) = blockedIdx;
338 nsb = matchrow(stateSpaceHashed, new_state_b);
339 if nsb > 0
340 basBlockQ(s,nsb) = basBlockQ(s,nsb) + rate_a(ia);
341 end
342 end
343 end
344 end
345 else % node_p == local
346 if ~isempty(new_state_a(ia))
347 new_state = state;
348 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
349 prob_sync_p = 1;
350 ns = matchrow(stateSpaceHashed, new_state);
351 if ns>0
352 if ~isnan(rate_a)
353 if size(Dfilt{a}) >= [s,ns] % needed for sparse matrix
354 Dfilt{a}(s,ns) = Dfilt{a}(s,ns) + rate_a(ia) * prob_sync_p;
355 else
356 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
357 end
358 end
359 end
360 end
361 end
362 end
363 end
364 end
365end
366end % if fcrWaitq
367
368% see _kb/06-solver-catalog.md (Vanishing states) for rationale
369isfjaug = isfield(sn,'fjsync') && ~isempty(sn.fjsync);
370immAction = false(1,A);
371for a=1:A
372 nt_a = sn.nodetype(sync{a}.active{1}.node);
373 immAction(a) = (nt_a == NodeType.Router) || (nt_a == NodeType.Fork) || (isfjaug && nt_a == NodeType.Join);
374end
375for a=1:A
376 Q = Q + Dfilt{a};
377 if immAction(a)
378 Qimm = Qimm + Dfilt{a};
379 end
380end
381% Fold in true-BAS become-blocked transitions (not counted as departures).
382Q = Q + basBlockQ;
383
384%% for all global synchronizations (SPN support)
385if isfield(sn, 'gsync') && ~isempty(sn.gsync)
386 gsyncEvents = sn.gsync;
387 G = length(gsyncEvents);
388
389 % Track FIRE completion rates for arvRates/depRates
390 Dfilt_gsync_comp = cell(1, G);
391 for g = 1:G
392 Dfilt_gsync_comp{g} = sparse(size(stateSpaceHashed,1), size(stateSpaceHashed,1));
393 end
394 immGsync = false(1,G);
395
396 for g = 1:G
397 gind = gsyncEvents{g}.active{1}.node;
398 isf_transition = sn.nodeToStateful(gind);
399 nmodes_g = sn.nodeparam{gind}.nmodes;
400 % ENABLE phase moves and firings of a TimingStrategy.IMMEDIATE mode are
401 % the two gsync sources emitted at the GlobalConstants.Immediate scale.
402 if gsyncEvents{g}.active{1}.event == EventType.ENABLE
403 immGsync(g) = true;
404 elseif gsyncEvents{g}.active{1}.event == EventType.FIRE
405 mode_g = gsyncEvents{g}.active{1}.mode;
406 immGsync(g) = isfield(sn.nodeparam{gind},'timing') && ~isempty(sn.nodeparam{gind}.timing) ...
407 && mode_g <= numel(sn.nodeparam{gind}.timing) ...
408 && sn.nodeparam{gind}.timing(mode_g) == TimingStrategy.IMMEDIATE;
409 end
410
411 for s = 1:size(stateSpaceHashed, 1)
412 state = stateSpaceHashed(s, :);
413
414 % Build glspace cell array from hashed state
415 glspace = cell(nstateful, 1);
416 for isf = 1:nstateful
417 glspace{isf} = sn.space{isf}(state(isf), :);
418 end
419
420 % Process event (both ENABLE and FIRE)
421 [outglspace, outrate, outprob, outcomp] = State.afterGlobalEvent(sn, gind, glspace, gsyncEvents{g}, false);
422
423 if isempty(outrate)
424 continue;
425 end
426
427 for io = 1:length(outrate)
428 if outrate(io) == 0
429 continue;
430 end
431
432 % Build new hashed state
433 new_state = state;
434
435 % Hash transition's new state
436 if size(outglspace{isf_transition}, 1) < io
437 continue;
438 end
439 trans_state = outglspace{isf_transition}(io, :);
440 hash_t = matchrow(sn.space{isf_transition}, trans_state);
441 if hash_t <= 0, continue; end
442 new_state(isf_transition) = hash_t;
443
444 is_comp = false;
445 % see _kb/06-solver-catalog.md (CTMC section, SPN FIRE completion flag) for rationale
446 if gsyncEvents{g}.active{1}.event == EventType.FIRE
447 if ~isempty(outcomp) && io <= numel(outcomp)
448 is_comp = outcomp(io);
449 end
450 for isf = 1:nstateful
451 if isf ~= isf_transition && ~isequal(glspace{isf}, outglspace{isf})
452 hash_p = matchrow(sn.space{isf}, outglspace{isf});
453 if hash_p <= 0, continue; end
454 new_state(isf) = hash_p;
455 end
456 end
457 end
458 % For ENABLE events: only Transition state changes, Places unchanged
459
460 ns = matchrow(stateSpaceHashed, new_state);
461 if ns > 0
462 prob_val = 1;
463 if ~isempty(outprob) && io <= length(outprob)
464 prob_val = outprob(io);
465 end
466 rate_val = outrate(io) * prob_val;
467 Q(s, ns) = Q(s, ns) + rate_val;
468 if immGsync(g)
469 Qimm(s, ns) = Qimm(s, ns) + rate_val;
470 end
471 if is_comp
472 Dfilt_gsync_comp{g}(s, ns) = Dfilt_gsync_comp{g}(s, ns) + rate_val;
473 end
474 end
475 end
476 end
477 end
478end
479
480%% for all fork firing synchronizations (native fork-join support)
481FJ = 0;
482if isfield(sn,'fjsync') && ~isempty(sn.fjsync)
483 FJ = length(sn.fjsync);
484 Dfilt_fjsync = cell(1,FJ);
485 for k=1:FJ
486 Dfilt_fjsync{k} = sparse(size(stateSpaceHashed,1), size(stateSpaceHashed,1));
487 end
488 for k=1:FJ
489 for s=1:size(stateSpaceHashed,1)
490 state = stateSpaceHashed(s,:);
491 glspace = cell(nstateful,1);
492 for isf=1:nstateful
493 glspace{isf} = sn.space{isf}(state(isf),:);
494 end
495 [fjStates, fjrate, fjprob] = State.afterFJEvent(sn, sn.fjsync{k}, glspace, false);
496 for io=1:length(fjStates)
497 if fjprob(io) <= 0
498 continue
499 end
500 new_state = state;
501 skip = false;
502 for isf=1:nstateful
503 if ~isequal(glspace{isf}, fjStates{io}{isf})
504 newrow = fjStates{io}{isf};
505 if length(newrow) < size(sn.space{isf},2)
506 newrow = [zeros(1,size(sn.space{isf},2)-length(newrow)), newrow];
507 end
508 hash_p = matchrow(sn.space{isf}, newrow);
509 if hash_p <= 0
510 skip = true;
511 break
512 end
513 new_state(isf) = hash_p;
514 end
515 end
516 if skip
517 continue
518 end
519 ns = matchrow(stateSpaceHashed, new_state);
520 if ns > 0
521 rate_val = fjrate(io) * fjprob(io);
522 Q(s,ns) = Q(s,ns) + rate_val;
523 Qimm(s,ns) = Qimm(s,ns) + rate_val;
524 Dfilt_fjsync{k}(s,ns) = Dfilt_fjsync{k}(s,ns) + rate_val;
525 end
526 end
527 end
528 end
529end
530
531%% vanishing-row purge
532% see _kb/06-solver-catalog.md (Vanishing states) for rationale
533immPurged = [];
534if options.config.hide_immediate
535 immPurged = ctmc_find_vanishing_states(sn, stateSpaceHashed, nclasses, nstateful, FJ);
536 % see _kb/06-solver-catalog.md (Vanishing states) for rationale
537 immRows = immPurged;
538 if ~isempty(immRows)
539 immGap = immRows(full(sum(Qimm(immRows,:),2)) <= 0);
540 if ~isempty(immGap)
541 line_warning_always(mfilename, 'CTMC: %d vanishing state(s) have no immediate outgoing arc; the vanishing predicate and the immediate-arc tagging disagree, so those rows keep their timed arcs.', numel(immGap));
542 immRows = setdiff(immRows, immGap);
543 end
544 end
545 if ~isempty(immRows)
546 Q(immRows,:) = Qimm(immRows,:);
547 for a=1:A
548 if ~immAction(a)
549 Dfilt{a}(immRows,:) = 0;
550 end
551 end
552 if exist('Dfilt_gsync_comp','var')
553 for g=1:numel(Dfilt_gsync_comp)
554 if ~immGsync(g)
555 Dfilt_gsync_comp{g}(immRows,:) = 0;
556 end
557 end
558 end
559 end
560end
561
562Q = Q - diag(diag(Q));
563%SolverCTMC.printInfGen(Q,stateSpace)
564%%
565arvRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
566depRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
567for a=1:A
568 % active
569 node_a = sync{a}.active{1}.node;
570 class_a = sync{a}.active{1}.class;
571 event_a = sync{a}.active{1}.event;
572 % passive
573 node_p = sync{a}.passive{1}.node;
574 class_p = sync{a}.passive{1}.class;
575 if event_a == EventType.DEP
576 node_a_sf = sn.nodeToStateful(node_a);
577 node_p_sf = sn.nodeToStateful(node_p);
578 for s=1:size(stateSpaceHashed,1)
579 depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
580 arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
581 end
582 end
583end
584
585%% Compute arrival/departure rates for gsync FIRE completion events
586if isfield(sn, 'gsync') && ~isempty(sn.gsync)
587 gsyncEvents = sn.gsync;
588 G = length(gsyncEvents);
589 for g = 1:G
590 if gsyncEvents{g}.active{1}.event == EventType.FIRE
591 for j = 1:length(gsyncEvents{g}.passive)
592 pev = gsyncEvents{g}.passive{j};
593 [pev_node, pev_class] = ind2sub([sn.nnodes, nclasses], pev.node);
594 if pev_node > sn.nnodes || ~sn.isstateful(pev_node)
595 continue;
596 end
597 pev_isf = sn.nodeToStateful(pev_node);
598 if pev.event == EventType.PRE
599 for s = 1:size(stateSpaceHashed, 1)
600 depRates(s, pev_isf, pev_class) = depRates(s, pev_isf, pev_class) + sum(Dfilt_gsync_comp{g}(s,:));
601 end
602 elseif pev.event == EventType.POST
603 for s = 1:size(stateSpaceHashed, 1)
604 arvRates(s, pev_isf, pev_class) = arvRates(s, pev_isf, pev_class) + sum(Dfilt_gsync_comp{g}(s,:));
605 end
606 end
607 end
608 end
609 end
610end
611
612%% Compute arrival/departure rates for fork firing synchronizations
613if FJ > 0
614 for k=1:FJ
615 fjentry = sn.fjsync{k};
616 isf_fork = sn.nodeToStateful(fjentry.fork);
617 for s=1:size(stateSpaceHashed,1)
618 rowsum = sum(Dfilt_fjsync{k}(s,:));
619 if rowsum > 0
620 depRates(s, isf_fork, fjentry.class) = depRates(s, isf_fork, fjentry.class) + rowsum;
621 for b=1:length(fjentry.branchheads)
622 isf_bh = sn.nodeToStateful(fjentry.branchheads(b));
623 arvRates(s, isf_bh, fjentry.auxclasses(b)) = arvRates(s, isf_bh, fjentry.auxclasses(b)) + rowsum;
624 end
625 end
626 end
627 end
628end
629
630zero_row = find(sum(Q,2)==0);
631zero_col = find(sum(Q,1)==0);
632
633%%
634% in case the last column of Q represent a state for a transient class, it
635% is possible that no transitions go back to it, although it is valid for
636% the system to be initialized in that state. So we need to fill-in the
637% zeros at the end.
638Q(:,end+1:end+(size(Q,1)-size(Q,2)))=0;
639Q(zero_row,zero_row) = -eye(length(zero_row)); % can this be replaced by []?
640Q(zero_col,zero_col) = -eye(length(zero_col));
641for a=1:A
642 Dfilt{a}(:,end+1:end+(size(Dfilt{a},1)-size(Dfilt{a},2)))=0;
643end
644
645if options.verbose == VerboseLevel.DEBUG || GlobalConstants.Verbose == VerboseLevel.DEBUG
646 SolverCTMC.printInfGen(Q,stateSpace);
647end
648Q = ctmc_makeinfgen(Q);
649
650%% drop states unreachable from the initial state
651% see _kb/06-solver-catalog.md (CTMC section, unreachable-state pruning) for rationale
652if ~isempty(sn.state) && all(~cellfun(@isempty, sn.state))
653 initState = matchrow(stateSpace, cell2mat(sn.state'));
654 if initState > 0
655 adj = (Q - diag(diag(Q))) > 0;
656 reach = false(size(Q,1),1);
657 reach(initState) = true;
658 frontier = initState;
659 while ~isempty(frontier)
660 nxt = find(any(adj(frontier,:),1))';
661 nxt = nxt(~reach(nxt));
662 reach(nxt) = true;
663 frontier = nxt;
664 end
665 if ~all(reach)
666 line_debug('CTMC: %d of %d states unreachable from the initial state, dropped', ...
667 sum(~reach), numel(reach));
668 keep = find(reach);
669 immPurged = []; % state indices shift, the predicate must be re-evaluated
670 Q = Q(keep,keep);
671 Q = ctmc_makeinfgen(Q);
672 stateSpace = stateSpace(keep,:);
673 stateSpaceAggr = stateSpaceAggr(keep,:);
674 stateSpaceHashed = stateSpaceHashed(keep,:);
675 arvRates = arvRates(keep,:,:);
676 depRates = depRates(keep,:,:);
677 for a=1:A
678 Dfilt{a} = Dfilt{a}(keep,keep);
679 end
680 for k=1:FJ
681 Dfilt_fjsync{k} = Dfilt_fjsync{k}(keep,keep);
682 end
683 if exist('Dfilt_gsync_comp','var')
684 for g=1:numel(Dfilt_gsync_comp)
685 Dfilt_gsync_comp{g} = Dfilt_gsync_comp{g}(keep,keep);
686 end
687 end
688 end
689 end
690end
691
692%% now remove immediate transitions
693% we first determine states in stateful nodes where there is an immediate
694% job in the node
695
696if options.config.hide_immediate % if want to remove immediate transitions
697 % see _kb/06-solver-catalog.md (Vanishing states, Design Y positive list) for rationale
698 if isempty(immPurged)
699 imm = ctmc_find_vanishing_states(sn, stateSpaceHashed, nclasses, nstateful, FJ);
700 else
701 imm = immPurged; % already evaluated on this state space by the purge above
702 end
703 nonimm = setdiff(1:size(Q,1),imm);
704 stateSpace(imm,:) = [];
705 stateSpaceAggr(imm,:) = [];
706 % full(Q)
707 [Q,~,Q12,~,Q22] = ctmc_stochcomp(Q, nonimm);
708 % full(Q)
709 if FJ > 0 || ~isempty(imm)
710 % see _kb/06-solver-catalog.md (Vanishing states, rate complement) for rationale
711 arvRates = zeros(length(nonimm),nstateful,nclasses);
712 depRates = zeros(length(nonimm),nstateful,nclasses);
713 for a=1:A
714 % active
715 node_a = sync{a}.active{1}.node;
716 class_a = sync{a}.active{1}.class;
717 event_a = sync{a}.active{1}.event;
718 % passive
719 node_p = sync{a}.passive{1}.node;
720 class_p = sync{a}.passive{1}.class;
721 if event_a == EventType.DEP
722 node_a_sf = sn.nodeToStateful(node_a);
723 node_p_sf = sn.nodeToStateful(node_p);
724 % see _kb/06-solver-catalog.md (Vanishing states, rate complement) for rationale
725 if immAction(a) && isfjaug && sn.nodetype(node_a) == NodeType.Join
726 r_a = solver_ctmc_ratecomplement(Dfilt{a}, nonimm, imm, Q12, Q22);
727 else
728 r_a = full(sum(Dfilt{a}(nonimm,:),2));
729 end
730 depRates(:,node_a_sf,class_a) = depRates(:,node_a_sf,class_a) + r_a;
731 arvRates(:,node_p_sf,class_p) = arvRates(:,node_p_sf,class_p) + r_a;
732 end
733 end
734 % see _kb/06-solver-catalog.md (Vanishing states, rate complement) for rationale
735 if isfield(sn, 'gsync') && ~isempty(sn.gsync) && exist('Dfilt_gsync_comp','var')
736 gsyncEvents_rc = sn.gsync;
737 for g = 1:length(gsyncEvents_rc)
738 if gsyncEvents_rc{g}.active{1}.event ~= EventType.FIRE
739 continue;
740 end
741 r_g = solver_ctmc_ratecomplement(Dfilt_gsync_comp{g}, nonimm, imm, Q12, Q22);
742 for j = 1:length(gsyncEvents_rc{g}.passive)
743 pev = gsyncEvents_rc{g}.passive{j};
744 [pev_node, pev_class] = ind2sub([sn.nnodes, nclasses], pev.node);
745 if pev_node > sn.nnodes || ~sn.isstateful(pev_node)
746 continue;
747 end
748 pev_isf = sn.nodeToStateful(pev_node);
749 if pev.event == EventType.PRE
750 depRates(:, pev_isf, pev_class) = depRates(:, pev_isf, pev_class) + r_g;
751 elseif pev.event == EventType.POST
752 arvRates(:, pev_isf, pev_class) = arvRates(:, pev_isf, pev_class) + r_g;
753 end
754 end
755 end
756 end
757 % fork firings: departure of the parent class at the Fork, one
758 % sibling arrival per branch head in the tag's auxiliary classes
759 for k=1:FJ
760 fjentry = sn.fjsync{k};
761 isf_fork = sn.nodeToStateful(fjentry.fork);
762 r_k = solver_ctmc_ratecomplement(Dfilt_fjsync{k}, nonimm, imm, Q12, Q22);
763 depRates(:,isf_fork,fjentry.class) = depRates(:,isf_fork,fjentry.class) + r_k;
764 for b=1:length(fjentry.branchheads)
765 isf_bh = sn.nodeToStateful(fjentry.branchheads(b));
766 arvRates(:,isf_bh,fjentry.auxclasses(b)) = arvRates(:,isf_bh,fjentry.auxclasses(b)) + r_k;
767 end
768 end
769 end
770 for a=1:A
771 % stochastic complement for action a
772 Q21a = Dfilt{a}(imm,nonimm);
773 Ta = (-Q22) \ Q21a;
774 Ta = Q12*Ta;
775 Dfilt{a} = Dfilt{a}(nonimm,nonimm)+Ta;
776 end
777 % recompute arvRates and depRates
778 % arvRates = zeros(size(stateSpace,1),nstateful,nclasses);
779 % depRates = zeros(size(stateSpace,1),nstateful,nclasses);
780 % for a=1:A
781 % % active
782 % node_a = sync{a}.active{1}.node;
783 % class_a = sync{a}.active{1}.class;
784 % event_a = sync{a}.active{1}.event;
785 % % passive
786 % node_p = sync{a}.passive{1}.node;
787 % class_p = sync{a}.passive{1}.class;
788 % if event_a == EventType.DEP
789 % node_a_sf = sn.nodeToStateful(node_a);
790 % node_p_sf = sn.nodeToStateful(node_p);
791 % for s=1:size(stateSpace,1)
792 % depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
793 % arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
794 % end
795 % end
796 % end
797end
798end
799
800%% Local functions
801function imm = ctmc_find_vanishing_states(sn, stateSpaceHashed, nclasses, nstateful, FJ)
802% Indices of the vanishing (zero-sojourn) global states: Router/Fork
803% pass-through occupancy, firable Join sibling sets, SPN markings from
804% which an ENABLE event moves the Transition row, and markings enabling a
805% TimingStrategy.IMMEDIATE mode. Extracted so the same predicate drives
806% both the vanishing-row purge and the stochastic complementation below.
807 isImmediatePassThrough = @(nt) (nt == NodeType.Router || nt == NodeType.Fork);
808
809 imm = [];
810 for ind = 1:sn.nnodes
811 if sn.isstateful(ind) && ~sn.isstation(ind) && isImmediatePassThrough(sn.nodetype(ind))
812 isf = sn.nodeToStateful(ind);
813 imm_st = find(sum(sn.space{isf}(:,1:nclasses),2)>0);
814 imm = [imm; find(arrayfun(@(a) any(a==imm_st),stateSpaceHashed(:,isf)))];
815 end
816 end
817 % see _kb/06-solver-catalog.md (Vanishing states) for rationale
818 if FJ > 0
819 for ind = 1:sn.nnodes
820 if sn.nodetype(ind) == NodeType.Join
821 isf = sn.nodeToStateful(ind);
822 firable_rows = [];
823 origcl = sn.nodeparam{ind}.fj.origclasses;
824 for row=1:size(sn.space{isf},1)
825 for r=origcl(:)'
826 [ospace_j] = State.afterEventJoin(sn, ind, sn.space{isf}(row,:), EventType.DEP, r, false, [], NaN);
827 if ~isempty(ospace_j)
828 firable_rows(end+1) = row; %#ok<AGROW>
829 break
830 end
831 end
832 end
833 if ~isempty(firable_rows)
834 imm = [imm; find(arrayfun(@(a) any(a==firable_rows),stateSpaceHashed(:,isf)))]; %#ok<AGROW>
835 end
836 end
837 end
838 end
839 % Transition immediate states: states where any ENABLE event would change the state
840 if isfield(sn, 'gsync') && ~isempty(sn.gsync)
841 gsyncEvents_sc = sn.gsync;
842 for s = 1:size(stateSpaceHashed, 1)
843 if any(s == imm)
844 continue; % already marked
845 end
846 state_sc = stateSpaceHashed(s, :);
847 glspace_sc = cell(nstateful, 1);
848 for isf = 1:nstateful
849 glspace_sc{isf} = sn.space{isf}(state_sc(isf), :);
850 end
851 for g = 1:length(gsyncEvents_sc)
852 if gsyncEvents_sc{g}.active{1}.event == EventType.ENABLE
853 gind_sc = gsyncEvents_sc{g}.active{1}.node;
854 isf_t_sc = sn.nodeToStateful(gind_sc);
855 orig_row_sc = glspace_sc{isf_t_sc};
856 [outgl_sc, outrate_sc, ~] = State.afterGlobalEvent(sn, gind_sc, glspace_sc, gsyncEvents_sc{g}, false);
857 % see _kb/06-solver-catalog.md (CTMC section, SPN FIRE completion flag) for rationale
858 rowchanged = false;
859 if ~isempty(outrate_sc)
860 og_sc = outgl_sc{isf_t_sc};
861 for io=1:size(og_sc,1)
862 if outrate_sc(io) > 0 && ~isequal(og_sc(io,:), orig_row_sc)
863 rowchanged = true;
864 break;
865 end
866 end
867 end
868 if rowchanged
869 imm = [imm; s]; %#ok<AGROW>
870 break;
871 end
872 end
873 end
874 end
875 end
876 % see _kb/06-solver-catalog.md (Vanishing states) for rationale
877 if isfield(sn, 'gsync') && ~isempty(sn.gsync)
878 gsyncEvents_im = sn.gsync;
879 for g = 1:length(gsyncEvents_im)
880 if gsyncEvents_im{g}.active{1}.event ~= EventType.FIRE
881 continue;
882 end
883 gind_im = gsyncEvents_im{g}.active{1}.node;
884 mode_im = gsyncEvents_im{g}.active{1}.mode;
885 if ~isfield(sn.nodeparam{gind_im}, 'timing') || isempty(sn.nodeparam{gind_im}.timing)
886 continue;
887 end
888 if mode_im > numel(sn.nodeparam{gind_im}.timing) ...
889 || sn.nodeparam{gind_im}.timing(mode_im) ~= TimingStrategy.IMMEDIATE
890 continue;
891 end
892 for s = 1:size(stateSpaceHashed, 1)
893 if any(s == imm)
894 continue; % already marked
895 end
896 glspace_im = cell(nstateful, 1);
897 for isf = 1:nstateful
898 glspace_im{isf} = sn.space{isf}(stateSpaceHashed(s, isf), :);
899 end
900 [~, outrate_im, ~] = State.afterGlobalEvent(sn, gind_im, glspace_im, gsyncEvents_im{g}, false);
901 if ~isempty(outrate_im) && any(outrate_im > 0)
902 imm = [imm; s]; %#ok<AGROW>
903 end
904 end
905 end
906 end
907
908 imm = unique(imm);
909end