1function [Q,stateSpace,stateSpaceAggr,Dfilt,arvRates,depRates,sn]=solver_ctmc(sn,options)
2% [Q,SS,SSQ,DFILT,ARVRATES,DEPRATES,QN]=SOLVER_CTMC(QN,OPTIONS)
4% Copyright (c) 2012-2026, Imperial College London
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);
12 line_error(mfilename,'SolverCTMC supports only exponential (memoryless) patience for reneging. Use SolverLDES or SolverJMT for phase-type patience.');
15if isfield(sn,'balkingStrategy') && ~isempty(sn.balkingStrategy)
16 badBalk = (sn.balkingStrategy~=0) & (sn.balkingStrategy~=BalkingStrategy.QUEUE_LENGTH);
18 line_error(mfilename,'SolverCTMC supports only QUEUE_LENGTH balking. Use SolverLDES or SolverJMT for wait-time-based balking.');
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);
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.');
28 if any(hasRetrial(:) & (sn.retrialMaxAttempts(:)>=0))
29 line_error(mfilename,'SolverCTMC supports only unlimited retrials (maxAttempts=-1). Use SolverLDES for finite max-attempts.');
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))'
35 for rr = 1:sn.nclasses
36 if ~isempty(sn.proc{ii}{rr}) && ~any(any(isnan(sn.proc{ii}{rr}{1})))
41 line_error(mfilename,
'SolverCTMC supports retrial only for single-class stations. Use SolverLDES for multi-class retrial.');
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;
54% see _kb/06-solver-catalog.md (CTMC section, heterogeneous servers) for rationale
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
65 np = sn.nodeparam{ind};
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>
73 line_error(mfilename,
'SolverCTMC supports heterogeneous servers only for single-class stations. Use SolverJMT or SolverLDES for multi-class heterogeneous servers.');
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>
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));
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));
100%% generate state space
102nstateful = sn.nstateful;
103nclasses = sn.nclasses;
108line_debug('CTMC solver starting: nstateful=%d, nclasses=%d, sync_events=%d', nstateful, nclasses, A);
110if ~isfield(options.config, 'hide_immediate')
111 options.config.hide_immediate = true;
114if ~isfield(options.config, 'state_space_gen')
115 options.config.state_space_gen = 'default';
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);
128line_debug(
'State space generated: %d states', size(stateSpaceHashed,1));
130%% Finite Capacity Region handling
131% see _kb/06-solver-catalog.md (CTMC section)
for rationale
132fcrWaitq = isfield(sn,
'nregions') && sn.nregions > 0;
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
142Q = speye(size(stateSpaceHashed,1)); % the diagonal elements will be removed later
147% see _kb/06-solver-catalog.md (True BAS blocking)
for rationale
149% see _kb/06-solver-catalog.md (Vanishing states)
for rationale
151local = sn.nnodes+1; % passive action
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);
160%%
for all synchronizations
162 stateCell = cell(nstateful,1);
163 %sn.sync{a}.active{1}.print
164 for s=1:size(stateSpaceHashed,1)
166 state = stateSpaceHashed(s,:);
168 % ustate = stateSpace(s,:);
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);
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});
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);
193 %[new_state_a, rate_a,~,trans_a, modes_a] = State.afterEventHashed( qn, node_a, state_a, event_a, class_a);
196 %
if true%options.verbose == 2
197 % line_printf(
'---\n');
198 % sync{a}.active{1}.print,
201 if new_state_a == -1 % hash not found
204 for ia=1:length(new_state_a)
207 %
if rate_a(ia)>0 || modes_a(ia) > 0
208 node_p = sync{a}.passive{1}.node;
210 % Skip
if the active transition hash was not found
211 if new_state_a(ia) == -1
214 state_p = state(sn.nodeToStateful(node_p));
215 class_p = sync{a}.passive{1}.class;
216 event_p = sync{a}.passive{1}.event;
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
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));
229 %prob_sync_p = sync{a}.passive{1}.prob(state_a, state_p)
232 %if options.verbose == 2
233 % line_printf('---\n
');
234 % sync{a}.active{1}.print,
235 % sync{a}.passive{1}.print
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);
241 [new_state_p, ~, outprob_p] = State.afterEventHashed( sn, node_p, state_p, event_p, class_p);
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);
247 % [new_state_p, ~, outprob_p, trans_p, modes_p] = State.afterEventHashed( qn, node_p, state_p, event_p, class_p);
249 for ip=1:size(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
258 prob_sync_p = sync{a}.passive{1}.prob * outprob_p(ip);
264 if ~isempty(new_state_a(ia))
265 if node_p == local % local action
267 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
268 prob_sync_p = outprob_p(ip);
269 elseif ~isempty(new_state_p)
271 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
272 new_state(sn.nodeToStateful(node_p)) = new_state_p(ip);
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)
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);
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);
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;
298 % Dfilt{a}(s,ns(ins)) = rate_a(ia) * prob_sync_p;
307 ns = matchrow(stateSpaceHashed, new_state);
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}));
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;
316 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
324 % see _kb/06-solver-catalog.md (True BAS blocking)
for rationale
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),:);
333 blockedVec = curVecA; blockedVec(end) = 1;
334 blockedIdx = matchrow(sn.space{isfA}, blockedVec);
337 new_state_b(isfA) = blockedIdx;
338 nsb = matchrow(stateSpaceHashed, new_state_b);
340 basBlockQ(s,nsb) = basBlockQ(s,nsb) + rate_a(ia);
345 else % node_p == local
346 if ~isempty(new_state_a(ia))
348 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
350 ns = matchrow(stateSpaceHashed, new_state);
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;
356 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
368% see _kb/06-solver-catalog.md (Vanishing states)
for rationale
369isfjaug = isfield(sn,
'fjsync') && ~isempty(sn.fjsync);
370immAction =
false(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);
378 Qimm = Qimm + Dfilt{a};
381% Fold in
true-BAS become-blocked transitions (not counted as departures).
384%%
for all global synchronizations (SPN support)
385if isfield(sn,
'gsync') && ~isempty(sn.gsync)
386 gsyncEvents = sn.gsync;
387 G = length(gsyncEvents);
389 % Track FIRE completion rates for arvRates/depRates
390 Dfilt_gsync_comp = cell(1, G);
392 Dfilt_gsync_comp{g} = sparse(size(stateSpaceHashed,1), size(stateSpaceHashed,1));
394 immGsync =
false(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
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;
411 for s = 1:size(stateSpaceHashed, 1)
412 state = stateSpaceHashed(s, :);
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), :);
420 % Process event (both ENABLE and FIRE)
421 [outglspace, outrate, outprob, outcomp] = State.afterGlobalEvent(sn, gind, glspace, gsyncEvents{g},
false);
427 for io = 1:length(outrate)
432 % Build
new hashed state
435 % Hash transition
's new state
436 if size(outglspace{isf_transition}, 1) < io
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;
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);
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;
458 % For ENABLE events: only Transition state changes, Places unchanged
460 ns = matchrow(stateSpaceHashed, new_state);
463 if ~isempty(outprob) && io <= length(outprob)
464 prob_val = outprob(io);
466 rate_val = outrate(io) * prob_val;
467 Q(s, ns) = Q(s, ns) + rate_val;
469 Qimm(s, ns) = Qimm(s, ns) + rate_val;
472 Dfilt_gsync_comp{g}(s, ns) = Dfilt_gsync_comp{g}(s, ns) + rate_val;
480%% for all fork firing synchronizations (native fork-join support)
482if isfield(sn,'fjsync
') && ~isempty(sn.fjsync)
483 FJ = length(sn.fjsync);
484 Dfilt_fjsync = cell(1,FJ);
486 Dfilt_fjsync{k} = sparse(size(stateSpaceHashed,1), size(stateSpaceHashed,1));
489 for s=1:size(stateSpaceHashed,1)
490 state = stateSpaceHashed(s,:);
491 glspace = cell(nstateful,1);
493 glspace{isf} = sn.space{isf}(state(isf),:);
495 [fjStates, fjrate, fjprob] = State.afterFJEvent(sn, sn.fjsync{k}, glspace, false);
496 for io=1:length(fjStates)
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];
508 hash_p = matchrow(sn.space{isf}, newrow);
513 new_state(isf) = hash_p;
519 ns = matchrow(stateSpaceHashed, new_state);
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;
531%% vanishing-row purge
532% see _kb/06-solver-catalog.md (Vanishing states) for rationale
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
539 immGap = immRows(full(sum(Qimm(immRows,:),2)) <= 0);
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);
546 Q(immRows,:) = Qimm(immRows,:);
549 Dfilt{a}(immRows,:) = 0;
552 if exist('Dfilt_gsync_comp
','var
')
553 for g=1:numel(Dfilt_gsync_comp)
555 Dfilt_gsync_comp{g}(immRows,:) = 0;
562Q = Q - diag(diag(Q));
563%SolverCTMC.printInfGen(Q,stateSpace)
565arvRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
566depRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
569 node_a = sync{a}.active{1}.node;
570 class_a = sync{a}.active{1}.class;
571 event_a = sync{a}.active{1}.event;
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,:));
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);
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)
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,:));
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,:));
612%% Compute arrival/departure rates for fork firing synchronizations
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,:));
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;
630zero_row = find(sum(Q,2)==0);
631zero_col = find(sum(Q,1)==0);
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
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));
642 Dfilt{a}(:,end+1:end+(size(Dfilt{a},1)-size(Dfilt{a},2)))=0;
645if options.verbose == VerboseLevel.DEBUG || GlobalConstants.Verbose == VerboseLevel.DEBUG
646 SolverCTMC.printInfGen(Q,stateSpace);
648Q = ctmc_makeinfgen(Q);
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'));
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));
666 line_debug('CTMC: %d of %d states unreachable from the initial state, dropped
', ...
667 sum(~reach), numel(reach));
669 immPurged = []; % state indices shift, the predicate must be re-evaluated
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,:,:);
678 Dfilt{a} = Dfilt{a}(keep,keep);
681 Dfilt_fjsync{k} = Dfilt_fjsync{k}(keep,keep);
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);
692%% now remove immediate transitions
693% we first determine states in stateful nodes where there is an immediate
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);
701 imm = immPurged; % already evaluated on this state space by the purge above
703 nonimm = setdiff(1:size(Q,1),imm);
704 stateSpace(imm,:) = [];
705 stateSpaceAggr(imm,:) = [];
707 [Q,~,Q12,~,Q22] = ctmc_stochcomp(Q, nonimm);
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);
715 node_a = sync{a}.active{1}.node;
716 class_a = sync{a}.active{1}.class;
717 event_a = sync{a}.active{1}.event;
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);
728 r_a = full(sum(Dfilt{a}(nonimm,:),2));
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;
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
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)
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;
757 % fork firings: departure of the parent class at the Fork, one
758 % sibling arrival per branch head in the tag's auxiliary classes
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;
771 % stochastic complement for action a
772 Q21a = Dfilt{a}(imm,nonimm);
775 Dfilt{a} = Dfilt{a}(nonimm,nonimm)+Ta;
777 % recompute arvRates and depRates
778 % arvRates = zeros(size(stateSpace,1),nstateful,nclasses);
779 % depRates = zeros(size(stateSpace,1),nstateful,nclasses);
782 % node_a = sync{a}.active{1}.node;
783 % class_a = sync{a}.active{1}.class;
784 % event_a = sync{a}.active{1}.event;
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,:));
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);
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)))];
817 % see _kb/06-solver-catalog.md (Vanishing states)
for rationale
819 for ind = 1:sn.nnodes
820 if sn.nodetype(ind) == NodeType.Join
821 isf = sn.nodeToStateful(ind);
823 origcl = sn.nodeparam{ind}.fj.origclasses;
824 for row=1:size(sn.space{isf},1)
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>
833 if ~isempty(firable_rows)
834 imm = [imm; find(arrayfun(@(a) any(a==firable_rows),stateSpaceHashed(:,isf)))]; %#ok<AGROW>
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)
844 continue; % already marked
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), :);
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
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)
869 imm = [imm; s]; %
#ok<AGROW>
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
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)
888 if mode_im > numel(sn.nodeparam{gind_im}.timing) ...
889 || sn.nodeparam{gind_im}.timing(mode_im) ~= TimingStrategy.IMMEDIATE
892 for s = 1:size(stateSpaceHashed, 1)
894 continue; % already marked
896 glspace_im = cell(nstateful, 1);
897 for isf = 1:nstateful
898 glspace_im{isf} = sn.space{isf}(stateSpaceHashed(s, isf), :);
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>