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
9nstateful = sn.nstateful;
10nclasses = sn.nclasses;
15line_debug(
'CTMC solver starting: nstateful=%d, nclasses=%d, sync_events=%d', nstateful, nclasses, A);
17if ~isfield(options.config,
'hide_immediate')
18 options.config.hide_immediate = true;
21if ~isfield(options.config, 'state_space_gen')
22 options.config.state_space_gen = 'default';
25%% generate state spaces, detailed and aggregate
26switch options.config.state_space_gen
27 case 'reachable' % does not handle open models yet (no cutoff)
28 line_debug('Using reachable state space generation, calling ctmc_ssg_reachability');
29 [stateSpace, stateSpaceAggr, stateSpaceHashed,~,sn] = ctmc_ssg_reachability(sn,options);
30 case {
'default',
'full'}
31 line_debug(
'Using full state space generation, calling ctmc_ssg');
32 [stateSpace, stateSpaceAggr, stateSpaceHashed,~,sn] = ctmc_ssg(sn,options);
35line_debug(
'State space generated: %d states', size(stateSpaceHashed,1));
38Q = sparse(eye(size(stateSpaceHashed,1))); % the diagonal elements will be removed later
43local = sn.nnodes+1; % passive action
46% Adj_t = zeros(size(SSh,1),size(SSh,1));
47% Adj_m = zeros(size(SSh,1),size(SSh,1));
48%
if ~isempty(Adj) && ~isempty(ST)
49% edges = adj_to_mat(Adj);
52%%
for all synchronizations
54 stateCell = cell(nstateful,1);
55 %sn.sync{a}.active{1}.print
56 for s=1:size(stateSpaceHashed,1)
58 state = stateSpaceHashed(s,:);
60 % ustate = stateSpace(s,:);
62 %
for st=1:length(ustate)
63 %
if ~isempty(sn.varsparam{st}) && isfield(sn.varsparam{st},
'nodeToPlace')
64 % state_pn(sn.varsparam{st}.nodeToPlace) = ustate(st);
68 % update state cell
array and SSq
71 isf = sn.nodeToStateful(ind);
72 stateCell{isf} = sn.space{isf}(state(isf),:);
73 %
if sn.isstation(ind)
74 % ist = sn.nodeToStation(ind);
75 % [~,nir] = State.toMarginal(sn,ind,stateCell{isf});
79 node_a = sync{a}.active{1}.node;
80 state_a = state(sn.nodeToStateful(node_a));
81 class_a = sync{a}.active{1}.class;
82 event_a = sync{a}.active{1}.event;
83 [new_state_a, rate_a] = State.afterEventHashed( sn, node_a, state_a, event_a, class_a);
85 %[new_state_a, rate_a,~,trans_a, modes_a] = State.afterEventHashed( qn, node_a, state_a, event_a, class_a);
88 %
if true%options.verbose == 2
89 % line_printf(
'---\n');
90 % sync{a}.active{1}.print,
93 if new_state_a == -1 % hash not found
96 for ia=1:length(new_state_a)
99 %
if rate_a(ia)>0 || modes_a(ia) > 0
100 node_p = sync{a}.passive{1}.node;
102 % Skip
if the active transition hash was not found
103 if new_state_a(ia) == -1
106 state_p = state(sn.nodeToStateful(node_p));
107 class_p = sync{a}.passive{1}.class;
108 event_p = sync{a}.passive{1}.event;
112 %
if ia <= length(trans_a)
113 % % check
if other input places of the transition contains as many token as the multiplicity of the input arcs
115 % mode = modes_a(ia);
116 % bmatrix = sn.varsparam{tr}.back(:,mode);
117 % inmatrix = sn.varsparam{tr}.inh(:,mode);
118 % enabled = all(state_pn >= bmatrix
' & ~any(inmatrix'>0 & inmatrix
' <= state_pn));
121 %prob_sync_p = sync{a}.passive{1}.prob(state_a, state_p)
124 %if options.verbose == 2
125 % line_printf('---\n
');
126 % sync{a}.active{1}.print,
127 % sync{a}.passive{1}.print
130 if node_p == node_a %self-loop
131 [new_state_p, ~, outprob_p] = State.afterEventHashed( sn, node_p, new_state_a(ia), event_p, class_p);
133 [new_state_p, ~, outprob_p] = State.afterEventHashed( sn, node_p, state_p, event_p, class_p);
136 % if node_p == node_a %self-loop
137 % [new_state_p, ~, outprob_p, trans_p, modes_p] = State.afterEventHashed( qn, node_p, new_state_a(ia), event_p, class_p);
139 % [new_state_p, ~, outprob_p, trans_p, modes_p] = State.afterEventHashed( qn, node_p, state_p, event_p, class_p);
141 for ip=1:size(new_state_p,1)
144 if sn.isstatedep(node_a,3)
145 newStateCell = stateCell;
146 newStateCell{sn.nodeToStateful(node_a)} = sn.space{sn.nodeToStateful(node_a)}(new_state_a(ia),:);
147 newStateCell{sn.nodeToStateful(node_p)} = sn.space{sn.nodeToStateful(node_p)}(new_state_p(ip),:);
148 prob_sync_p = sync{a}.passive{1}.prob(stateCell, newStateCell) * outprob_p(ip); %state-dependent
150 prob_sync_p = sync{a}.passive{1}.prob * outprob_p(ip);
156 if ~isempty(new_state_a(ia))
157 if node_p == local % local action
159 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
160 prob_sync_p = outprob_p(ip);
161 elseif ~isempty(new_state_p)
163 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
164 new_state(sn.nodeToStateful(node_p)) = new_state_p(ip);
168 % ns = find(ismember(SSh(:,[sn.nodeToStateful(node_a),sn.nodeToStateful(node_p)]),[new_state_a(ia),new_state_p(ip)],'rows
'));
169 % for ins=1:length(ns)
170 % if ns(ins) > 0 && ~isempty(trans_p)
172 % mode = modes_p(ip);
173 % bmatrix = sn.varsparam{tr}.back(:,mode);
174 % fmatrix = sn.varsparam{tr}.forw(:,mode);
175 % cmatrix = fmatrix - bmatrix;
176 % if isequal(state_pn + cmatrix',SS(ns(ins),3:end))
177 % [ex_a,seq_a] = ST.search(state_pn
');
178 % [ex_p,seq_p] = ST.search(SS(ns(ins),3:end)');
179 %
if ex_a && ex_p && edges(seq_a, seq_p)
180 % Adj_m(s, ns(ins)) = modes_p(ip);
181 % Adj_t(s, ns(ins)) = trans_p(ip);
183 %
if ~isnan(rate_a(ia))
184 %
if node_p < local && ~csmask(class_a, class_p) && rate_a(ia) * prob_sync_p >0 && (sn.nodetype(node_p)~=NodeType.Source)
185 % 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);
187 %
if size(Dfilt{a}) >= [s,ns(ins)] % check needed as D{a}
is a sparse matrix
188 % Dfilt{a}(s,ns(ins)) = Dfilt{a}(s,ns(ins)) + rate_a(ia) * prob_sync_p;
190 % Dfilt{a}(s,ns(ins)) = rate_a(ia) * prob_sync_p;
199 ns = matchrow(stateSpaceHashed, new_state);
202 if node_p < local && ~csmask(class_a, class_p) && rate_a(ia) * prob_sync_p >0 && (sn.nodetype(node_p)~=NodeType.Source)
203 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}));
205 if size(Dfilt{a}) >= [s,ns] % check needed as D{a}
is a sparse matrix
206 Dfilt{a}(s,ns) = Dfilt{a}(s,ns) + rate_a(ia) * prob_sync_p;
208 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
216 else % node_p == local
217 if ~isempty(new_state_a(ia))
219 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
221 ns = matchrow(stateSpaceHashed, new_state);
224 if size(Dfilt{a}) >= [s,ns] % needed
for sparse matrix
225 Dfilt{a}(s,ns) = Dfilt{a}(s,ns) + rate_a(ia) * prob_sync_p;
227 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
242%%
for all global synchronizations (SPN support)
243if isfield(sn,
'gsync') && ~isempty(sn.gsync)
244 gsyncEvents = sn.gsync;
245 G = length(gsyncEvents);
247 % Track FIRE completion rates for arvRates/depRates
248 Dfilt_gsync_comp = cell(1, G);
250 Dfilt_gsync_comp{g} = sparse(size(stateSpaceHashed,1), size(stateSpaceHashed,1));
254 gind = gsyncEvents{g}.active{1}.node;
255 isf_transition = sn.nodeToStateful(gind);
256 nmodes_g = sn.nodeparam{gind}.nmodes;
258 for s = 1:size(stateSpaceHashed, 1)
259 state = stateSpaceHashed(s, :);
261 % Build glspace cell
array from hashed state
262 glspace = cell(nstateful, 1);
263 for isf = 1:nstateful
264 glspace{isf} = sn.space{isf}(state(isf), :);
267 % Process event (both ENABLE and FIRE)
268 [outglspace, outrate, outprob] = State.afterGlobalEvent(sn, gind, glspace, gsyncEvents{g},
false);
274 for io = 1:length(outrate)
279 % Build
new hashed state
282 % Hash transition
's new state
283 if size(outglspace{isf_transition}, 1) < io
286 trans_state = outglspace{isf_transition}(io, :);
287 hash_t = matchrow(sn.space{isf_transition}, trans_state);
288 if hash_t <= 0, continue; end
289 new_state(isf_transition) = hash_t;
292 % For FIRE events: detect completion and update Places
293 if gsyncEvents{g}.active{1}.event == EventType.FIRE
294 orig_buf = glspace{isf_transition}(1:nmodes_g);
295 new_buf = trans_state(1:nmodes_g);
296 is_comp = any(new_buf > orig_buf);
299 for isf = 1:nstateful
300 if isf ~= isf_transition && ~isequal(glspace{isf}, outglspace{isf})
301 hash_p = matchrow(sn.space{isf}, outglspace{isf});
302 if hash_p <= 0, continue; end
303 new_state(isf) = hash_p;
308 % For ENABLE events: only Transition state changes, Places unchanged
310 ns = matchrow(stateSpaceHashed, new_state);
313 if ~isempty(outprob) && io <= length(outprob)
314 prob_val = outprob(io);
316 rate_val = outrate(io) * prob_val;
317 Q(s, ns) = Q(s, ns) + rate_val;
319 Dfilt_gsync_comp{g}(s, ns) = Dfilt_gsync_comp{g}(s, ns) + rate_val;
327Q = Q - diag(diag(Q));
328%SolverCTMC.printInfGen(Q,stateSpace)
330arvRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
331depRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
334 node_a = sync{a}.active{1}.node;
335 class_a = sync{a}.active{1}.class;
336 event_a = sync{a}.active{1}.event;
338 node_p = sync{a}.passive{1}.node;
339 class_p = sync{a}.passive{1}.class;
340 if event_a == EventType.DEP
341 node_a_sf = sn.nodeToStateful(node_a);
342 node_p_sf = sn.nodeToStateful(node_p);
343 for s=1:size(stateSpaceHashed,1)
344 depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
345 arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
350%% Compute arrival/departure rates for gsync FIRE completion events
351if isfield(sn, 'gsync
') && ~isempty(sn.gsync)
352 gsyncEvents = sn.gsync;
353 G = length(gsyncEvents);
355 if gsyncEvents{g}.active{1}.event == EventType.FIRE
356 for j = 1:length(gsyncEvents{g}.passive)
357 pev = gsyncEvents{g}.passive{j};
358 [pev_node, pev_class] = ind2sub([sn.nnodes, nclasses], pev.node);
359 if pev_node > sn.nnodes || ~sn.isstateful(pev_node)
362 pev_isf = sn.nodeToStateful(pev_node);
363 if pev.event == EventType.PRE
364 for s = 1:size(stateSpaceHashed, 1)
365 depRates(s, pev_isf, pev_class) = depRates(s, pev_isf, pev_class) + sum(Dfilt_gsync_comp{g}(s,:));
367 elseif pev.event == EventType.POST
368 for s = 1:size(stateSpaceHashed, 1)
369 arvRates(s, pev_isf, pev_class) = arvRates(s, pev_isf, pev_class) + sum(Dfilt_gsync_comp{g}(s,:));
377zero_row = find(sum(Q,2)==0);
378zero_col = find(sum(Q,1)==0);
381% in case the last column of Q represent a state for a transient class, it
382% is possible that no transitions go back to it, although it is valid for
383% the system to be initialized in that state. So we need to fill-in the
385Q(:,end+1:end+(size(Q,1)-size(Q,2)))=0;
386Q(zero_row,zero_row) = -eye(length(zero_row)); % can this be replaced by []?
387Q(zero_col,zero_col) = -eye(length(zero_col));
389 Dfilt{a}(:,end+1:end+(size(Dfilt{a},1)-size(Dfilt{a},2)))=0;
392if options.verbose == VerboseLevel.DEBUG || GlobalConstants.Verbose == VerboseLevel.DEBUG
393 SolverCTMC.printInfGen(Q,stateSpace);
395Q = ctmc_makeinfgen(Q);
396%% now remove immediate transitions
397% we first determine states in stateful nodes where there is an immediate
400if options.config.hide_immediate % if want to remove immediate transitions
402 for ind = 1:sn.nnodes
403 % Only consider non-station stateful nodes that are not Caches
404 % Cache nodes need their immediate transitions to compute hit/miss rates
405 if sn.isstateful(ind) && ~sn.isstation(ind) && sn.nodetype(ind) ~= NodeType.Cache && sn.nodetype(ind) ~= NodeType.Transition
406 isf = sn.nodeToStateful(ind);
407 imm_st = find(sum(sn.space{isf}(:,1:nclasses),2)>0);
408 imm = [imm; find(arrayfun(@(a) any(a==imm_st),stateSpaceHashed(:,isf)))];
411 % Transition immediate states: states where any ENABLE event would change the state
412 if isfield(sn, 'gsync
') && ~isempty(sn.gsync)
413 gsyncEvents_sc = sn.gsync;
414 for s = 1:size(stateSpaceHashed, 1)
416 continue; % already marked
418 state_sc = stateSpaceHashed(s, :);
419 glspace_sc = cell(nstateful, 1);
420 for isf = 1:nstateful
421 glspace_sc{isf} = sn.space{isf}(state_sc(isf), :);
423 for g = 1:length(gsyncEvents_sc)
424 if gsyncEvents_sc{g}.active{1}.event == EventType.ENABLE
425 gind_sc = gsyncEvents_sc{g}.active{1}.node;
426 [~, outrate_sc, ~] = State.afterGlobalEvent(sn, gind_sc, glspace_sc, gsyncEvents_sc{g}, false);
427 if ~isempty(outrate_sc)
428 imm = [imm; s]; %#ok<AGROW>
436 nonimm = setdiff(1:size(Q,1),imm);
437 stateSpace(imm,:) = [];
438 stateSpaceAggr(imm,:) = [];
440 [Q,~,Q12,~,Q22] = ctmc_stochcomp(Q, nonimm);
443 % stochastic complement for action a
444 Q21a = Dfilt{a}(imm,nonimm);
447 Dfilt{a} = Dfilt{a}(nonimm,nonimm)+Ta;
450 depRates(imm,:,:) = [];
451 arvRates(imm,:,:) = [];
452 % recompute arvRates and depRates
453 % arvRates = zeros(size(stateSpace,1),nstateful,nclasses);
454 % depRates = zeros(size(stateSpace,1),nstateful,nclasses);
457 % node_a = sync{a}.active{1}.node;
458 % class_a = sync{a}.active{1}.class;
459 % event_a = sync{a}.active{1}.event;
461 % node_p = sync{a}.passive{1}.node;
462 % class_p = sync{a}.passive{1}.class;
463 % if event_a == EventType.DEP
464 % node_a_sf = sn.nodeToStateful(node_a);
465 % node_p_sf = sn.nodeToStateful(node_p);
466 % for s=1:size(stateSpace,1)
467 % depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
468 % arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
473%SolverCTMC.printInfGen(Q,stateSpace)
476% if ~isempty(Adj) && ~isempty(ST)
478% for s=1:size(SSh, 1)
479% % check for immediate transitions
480% enabled_t = find(Adj_t(s,:));
482% for t=1:length(enabled_t)
483% if sn.varsparam{Adj_t(s,enabled_t(t))}.timingstrategies(Adj_m(s,enabled_t(t)))
484% imm_t = [imm_t, enabled_t(t)];
487% % Immediate transitions exists, the marking needs to be removed.
490% non_imm = setdiff(enabled_t,imm_t);
491% inM = find(Adj_t(:,s));
492% for in=1:length(inM)
493% depRates(inM(in),:,:) = depRates(inM(in),:,:) + depRates(s,:,:);
494% for nim=1:length(non_imm)
495% Q(inM(in), non_imm(nim)) = Q(inM(in), non_imm(nim)) + Q(inM(in), s) + Q(s, non_imm(nim));
496% if Adj_t(inM(in), non_imm(nim))==0
497% Adj_t(inM(in), non_imm(nim)) = Adj_t(inM(in), s);
498% Adj_m(inM(in), non_imm(nim)) = Adj_m(inM(in), s);
500% arvRates(non_imm(nim),:,:) = arvRates(non_imm(nim),:,:) + arvRates(s,:,:);
503% for im=1:length(imm_t)
504% weight = sn.varsparam{Adj_t(s,imm_t(im))}.firingweights(Adj_m(s,imm_t(im)));
505% totalWeight = totalWeight + weight;
506% if Adj_t(inM(in), imm_t(im))==0
507% Adj_t(inM(in), imm_t(im)) = Adj_t(inM(in), s);
508% Adj_m(inM(in), imm_t(im)) = Adj_m(inM(in), s);
511% for im=1:length(imm_t)
512% weight = sn.varsparam{Adj_t(s,imm_t(im))}.firingweights(Adj_m(s,imm_t(im)));
513% Q(inM(in), imm_t(im)) = Q(inM(in), imm_t(im)) + Q(inM(in), s) * (weight/totalWeight);
525% arvRates(imm,:,:) = [];
526% depRates(imm,:,:) = [];
530%Q = ctmc_makeinfgen(Q);