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;
241Q = Q - diag(diag(Q));
242%SolverCTMC.printInfGen(Q,stateSpace)
244arvRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
245depRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
248 node_a = sync{a}.active{1}.node;
249 class_a = sync{a}.active{1}.class;
250 event_a = sync{a}.active{1}.event;
252 node_p = sync{a}.passive{1}.node;
253 class_p = sync{a}.passive{1}.class;
254 if event_a == EventType.DEP
255 node_a_sf = sn.nodeToStateful(node_a);
256 node_p_sf = sn.nodeToStateful(node_p);
257 for s=1:size(stateSpaceHashed,1)
258 depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
259 arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
263zero_row = find(sum(Q,2)==0);
264zero_col = find(sum(Q,1)==0);
267% in
case the last column of Q represent a state
for a transient
class, it
268%
is possible that no transitions go back to it, although it
is valid
for
269% the system to be initialized in that state. So we need to fill-in the
271Q(:,end+1:end+(size(Q,1)-size(Q,2)))=0;
272Q(zero_row,zero_row) = -eye(length(zero_row)); % can
this be replaced by []?
273Q(zero_col,zero_col) = -eye(length(zero_col));
275 Dfilt{a}(:,end+1:end+(size(Dfilt{a},1)-size(Dfilt{a},2)))=0;
278if options.verbose == VerboseLevel.DEBUG || GlobalConstants.Verbose == VerboseLevel.DEBUG
279 SolverCTMC.printInfGen(Q,stateSpace);
281Q = ctmc_makeinfgen(Q);
282%% now remove immediate transitions
283% we first determine states in stateful
nodes where there
is an immediate
286if options.config.hide_immediate %
if want to remove immediate transitions
288 for ind = 1:sn.nnodes
289 % Only consider non-station stateful
nodes that are not Caches
290 % Cache
nodes need their immediate transitions to compute hit/miss rates
291 if sn.isstateful(ind) && ~sn.isstation(ind) && sn.nodetype(ind) ~= NodeType.Cache
292 isf = sn.nodeToStateful(ind);
293 imm_st = find(sum(sn.space{isf}(:,1:nclasses),2)>0);
294 imm = [imm; find(arrayfun(@(a) any(a==imm_st),stateSpaceHashed(:,isf)))];
298 nonimm = setdiff(1:size(Q,1),imm);
299 stateSpace(imm,:) = [];
300 stateSpaceAggr(imm,:) = [];
302 [Q,~,Q12,~,Q22] = ctmc_stochcomp(Q, nonimm);
305 % stochastic complement
for action a
306 Q21a = Dfilt{a}(imm,nonimm);
309 Dfilt{a} = Dfilt{a}(nonimm,nonimm)+Ta;
312 depRates(imm,:,:) = [];
313 arvRates(imm,:,:) = [];
314 % recompute arvRates and depRates
315 % arvRates = zeros(size(stateSpace,1),nstateful,nclasses);
316 % depRates = zeros(size(stateSpace,1),nstateful,nclasses);
319 % node_a = sync{a}.active{1}.node;
320 % class_a = sync{a}.active{1}.class;
321 % event_a = sync{a}.active{1}.event;
323 % node_p = sync{a}.passive{1}.node;
324 % class_p = sync{a}.passive{1}.class;
325 %
if event_a == EventType.DEP
326 % node_a_sf = sn.nodeToStateful(node_a);
327 % node_p_sf = sn.nodeToStateful(node_p);
328 %
for s=1:size(stateSpace,1)
329 % depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
330 % arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
335%SolverCTMC.printInfGen(Q,stateSpace)
338%
if ~isempty(Adj) && ~isempty(ST)
340%
for s=1:size(SSh, 1)
341% % check
for immediate transitions
342% enabled_t = find(Adj_t(s,:));
344%
for t=1:length(enabled_t)
345%
if sn.varsparam{Adj_t(s,enabled_t(t))}.timingstrategies(Adj_m(s,enabled_t(t)))
346% imm_t = [imm_t, enabled_t(t)];
349% % Immediate transitions exists, the marking needs to be removed.
352% non_imm = setdiff(enabled_t,imm_t);
353% inM = find(Adj_t(:,s));
354%
for in=1:length(inM)
355% depRates(inM(in),:,:) = depRates(inM(in),:,:) + depRates(s,:,:);
356%
for nim=1:length(non_imm)
357% Q(inM(in), non_imm(nim)) = Q(inM(in), non_imm(nim)) + Q(inM(in), s) + Q(s, non_imm(nim));
358%
if Adj_t(inM(in), non_imm(nim))==0
359% Adj_t(inM(in), non_imm(nim)) = Adj_t(inM(in), s);
360% Adj_m(inM(in), non_imm(nim)) = Adj_m(inM(in), s);
362% arvRates(non_imm(nim),:,:) = arvRates(non_imm(nim),:,:) + arvRates(s,:,:);
365%
for im=1:length(imm_t)
366% weight = sn.varsparam{Adj_t(s,imm_t(im))}.firingweights(Adj_m(s,imm_t(im)));
367% totalWeight = totalWeight + weight;
368%
if Adj_t(inM(in), imm_t(im))==0
369% Adj_t(inM(in), imm_t(im)) = Adj_t(inM(in), s);
370% Adj_m(inM(in), imm_t(im)) = Adj_m(inM(in), s);
373%
for im=1:length(imm_t)
374% weight = sn.varsparam{Adj_t(s,imm_t(im))}.firingweights(Adj_m(s,imm_t(im)));
375% Q(inM(in), imm_t(im)) = Q(inM(in), imm_t(im)) + Q(inM(in), s) * (weight/totalWeight);
387% arvRates(imm,:,:) = [];
388% depRates(imm,:,:) = [];
392%Q = ctmc_makeinfgen(Q);