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% Reneging is supported only for exponential (memoryless) patience: PH/MAP
9% patience would require a per-waiting-job phase dimension. Balking is
10% supported only for the QUEUE_LENGTH strategy (a pure state function).
11% Reject other cases explicitly rather than silently ignoring them.
12if isfield(sn,'impatienceClass') && ~isempty(sn.impatienceClass)
13 badRenege = (sn.impatienceClass==ImpatienceType.RENEGING) & (sn.impatienceType~=ProcessType.EXP);
14 if any(badRenege(:))
15 line_error(mfilename,'SolverCTMC supports only exponential (memoryless) patience for reneging. Use SolverLDES or SolverJMT for phase-type patience.');
16 end
17end
18if isfield(sn,'balkingStrategy') && ~isempty(sn.balkingStrategy)
19 badBalk = (sn.balkingStrategy~=0) & (sn.balkingStrategy~=BalkingStrategy.QUEUE_LENGTH);
20 if any(badBalk(:))
21 line_error(mfilename,'SolverCTMC supports only QUEUE_LENGTH balking. Use SolverLDES or SolverJMT for wait-time-based balking.');
22 end
23end
24% Retrial orbit is supported only for exponential (memoryless) retrial delay
25% with unlimited attempts; phase-type delay or finite max-attempts would need
26% a per-orbiting-job phase/attempt dimension.
27if isfield(sn,'retrialProc') && ~isempty(sn.retrialProc)
28 hasRetrial = ~cellfun(@isempty, sn.retrialProc);
29 if any(hasRetrial(:))
30 if any(hasRetrial(:) & (sn.retrialType(:)~=ProcessType.EXP))
31 line_error(mfilename,'SolverCTMC supports only exponential (memoryless) retrial delay. Use SolverLDES or SolverMAM for phase-type retrials.');
32 end
33 if any(hasRetrial(:) & (sn.retrialMaxAttempts(:)>=0))
34 line_error(mfilename,'SolverCTMC supports only unlimited retrials (maxAttempts=-1). Use SolverLDES for finite max-attempts.');
35 end
36 % A retrial orbit is enumerated per single populated class; reject a
37 % retrial station that serves more than one class.
38 for ii = find(any(hasRetrial,2))'
39 served = 0;
40 for rr = 1:sn.nclasses
41 if ~isempty(sn.proc{ii}{rr}) && ~any(any(isnan(sn.proc{ii}{rr}{1})))
42 served = served + 1;
43 end
44 end
45 if served > 1
46 line_error(mfilename,'SolverCTMC supports retrial only for single-class stations. Use SolverLDES for multi-class retrial.');
47 end
48 end
49 end
50end
51% Signal (G-network negative-customer) classes are annihilated on arrival and
52% never occupy a station; cap their per-station capacity at 0 (except at the
53% Source/EXT node, whose infinite buffer must stay intact) so the state space
54% does not enumerate unreachable states holding signal jobs.
55if isfield(sn,'issignal') && ~isempty(sn.issignal) && any(sn.issignal)
56 for ii = 1:sn.nstations
57 if sn.sched(ii) ~= SchedStrategy.EXT
58 sn.classcap(ii, sn.issignal(:)') = 0;
59 end
60 end
61end
62% Heterogeneous servers (single-class, ORDER policy): a station whose server
63% types have different per-type rates is mapped to a load-dependent service
64% rate mu(n) = sum of the rates of the first min(n,c) servers (types filled in
65% definition order). For a single served class this reproduces the exact
66% QLen / RespT / Tput because the birth-death dynamics depend only on mu(n).
67% Multi-class heterogeneous servers (per-type-per-class rates + compatibility)
68% need a per-server-type in-service state and are rejected here.
69for ind = 1:sn.nnodes
70 if sn.isstation(ind) && isfield(sn,'nodeparam') && numel(sn.nodeparam) >= ind ...
71 && ~isempty(sn.nodeparam{ind}) && isstruct(sn.nodeparam{ind}) ...
72 && isfield(sn.nodeparam{ind},'nservertypes') && sn.nodeparam{ind}.nservertypes > 0
73 ist = sn.nodeToStation(ind);
74 % PAS/OI stations model heterogeneous compatible servers through the OI
75 % rank rate (svcRateFun), not a single-class load-dependent scaling.
76 if sn.sched(ist) == SchedStrategy.PAS || sn.sched(ist) == SchedStrategy.OI
77 continue
78 end
79 np = sn.nodeparam{ind};
80 served = [];
81 for r = 1:sn.nclasses
82 if ~isempty(sn.proc{ist}{r}) && ~any(any(isnan(sn.proc{ist}{r}{1}))) && sn.rates(ist,r) > 0
83 served(end+1) = r; %#ok<AGROW>
84 end
85 end
86 if numel(served) > 1
87 line_error(mfilename,'SolverCTMC supports heterogeneous servers only for single-class stations. Use SolverJMT or SolverLDES for multi-class heterogeneous servers.');
88 end
89 if numel(served) == 1
90 r = served;
91 srvrates = [];
92 for t = 1:np.nservertypes
93 if np.servercompat(t,r) && np.heterorates(t,r) > 0
94 srvrates = [srvrates, repmat(np.heterorates(t,r), 1, np.serverspertype(t))]; %#ok<AGROW>
95 end
96 end
97 c = numel(srvrates);
98 mu_base = sn.rates(ist,r);
99 if c > 0 && mu_base > 0
100 if isempty(sn.lldscaling)
101 sn.lldscaling = ones(sn.nstations, max([c, sum(sn.njobs(isfinite(sn.njobs))), 1]));
102 elseif size(sn.lldscaling,2) < c
103 sn.lldscaling(:, (size(sn.lldscaling,2)+1):c) = repmat(sn.lldscaling(:,end), 1, c-size(sn.lldscaling,2));
104 end
105 for n = 1:size(sn.lldscaling,2)
106 mun = sum(srvrates(1:min(n,c)));
107 sn.lldscaling(ist,n) = mun / (mu_base * min(n,c));
108 end
109 end
110 end
111 end
112end
113
114%% generate state space
115%nnodes = sn.nnodes;
116nstateful = sn.nstateful;
117nclasses = sn.nclasses;
118sync = sn.sync;
119A = length(sync);
120csmask = sn.csmask;
121
122line_debug('CTMC solver starting: nstateful=%d, nclasses=%d, sync_events=%d', nstateful, nclasses, A);
123
124if ~isfield(options.config, 'hide_immediate')
125 options.config.hide_immediate = true;
126end
127
128if ~isfield(options.config, 'state_space_gen')
129 options.config.state_space_gen = 'default';
130end
131
132%% generate state spaces, detailed and aggregate
133switch options.config.state_space_gen
134 case 'reachable' % does not handle open models yet (no cutoff)
135 line_debug('Using reachable state space generation, calling ctmc_ssg_reachability');
136 [stateSpace, stateSpaceAggr, stateSpaceHashed,~,sn] = ctmc_ssg_reachability(sn,options);
137 case {'default','full'}
138 line_debug('Using full state space generation, calling ctmc_ssg');
139 [stateSpace, stateSpaceAggr, stateSpaceHashed,~,sn] = ctmc_ssg(sn,options);
140end
141
142line_debug('State space generated: %d states', size(stateSpaceHashed,1));
143
144%% Finite Capacity Region handling
145% Both rules need the reachability-based augmented generator
146% (solver_ctmc_fcr_waitq): WAITQ parks refused jobs in a per-region FIFO
147% released head-of-line; DROP destroys them (JMT semantics: the job vanishes;
148% for closed chains the stationary regime is the absorbed surviving
149% population).
150fcrWaitq = isfield(sn,'nregions') && sn.nregions > 0;
151
152%%
153if fcrWaitq
154 % WAITQ finite capacity region: reachability-based augmented state space
155 % (station states + per-region FIFO of blocked jobs) with exact JMT
156 % semantics; returns the per-action rate filters directly
157 [stateSpace,stateSpaceAggr,stateSpaceHashed,Dfilt,sn] = solver_ctmc_fcr_waitq(sn,options);
158 Q = speye(size(stateSpaceHashed,1)); % the diagonal elements will be removed later
159 % This path resolves blocking inside its own augmented state space, so it has
160 % no separate true-BAS become-blocked transitions to fold in. basBlockQ is
161 % still added to Q below on both branches, so it must be defined here too.
162 basBlockQ = 0*Q;
163 % See the Qimm comment on the else branch.
164 Qimm = 0*Q;
165else
166Q = speye(size(stateSpaceHashed,1)); % the diagonal elements will be removed later
167Dfilt = cell(1,A);
168for a=1:A
169 Dfilt{a} = 0*Q;
170end
171% True BAS become-blocked transitions (a completed job held at its server when the
172% downstream is full): change the chain but are NOT station departures (the job stays).
173% Kept out of the departure filters Dfilt so throughput is not double-counted; added
174% straight into Q below.
175basBlockQ = 0*Q;
176% Immediate-provenance accumulator, filled in parallel with Q by every arc
177% emitted on the GlobalConstants.Immediate scale (Router/Fork/Join pass-through,
178% fork firings, SPN ENABLE phase moves and TimingStrategy.IMMEDIATE firings). In
179% a vanishing marking the exact GSPN semantics give the timed arcs probability
180% zero, but a finite Immediate rate lets a timed arc win the race with
181% probability mu/(mu+Immediate), which survives stochastic complementation as an
182% O(mu/Immediate) ~ 1e-8 relative bias on every marginal. Overwriting each
183% vanishing row with its immediate-only counterpart below removes that bias
184% exactly rather than merely shrinking it.
185Qimm = 0*Q;
186local = sn.nnodes+1; % passive action
187
188% SPN code
189% Adj_t = zeros(size(SSh,1),size(SSh,1));
190% Adj_m = zeros(size(SSh,1),size(SSh,1));
191% if ~isempty(Adj) && ~isempty(ST)
192% edges = adj_to_mat(Adj);
193% end
194
195%% for all synchronizations
196for a=1:A
197 stateCell = cell(nstateful,1);
198 %sn.sync{a}.active{1}.print
199 for s=1:size(stateSpaceHashed,1)
200 %[a,s]
201 state = stateSpaceHashed(s,:);
202 % SPN code
203 % ustate = stateSpace(s,:);
204 % state_pn = [];
205 % for st=1:length(ustate)
206 % if ~isempty(sn.varsparam{st}) && isfield(sn.varsparam{st}, 'nodeToPlace')
207 % state_pn(sn.varsparam{st}.nodeToPlace) = ustate(st);
208 % end
209 % end
210
211 % update state cell array and SSq
212 for ind = 1:sn.nnodes
213 if sn.isstateful(ind)
214 isf = sn.nodeToStateful(ind);
215 stateCell{isf} = sn.space{isf}(state(isf),:);
216 % if sn.isstation(ind)
217 % ist = sn.nodeToStation(ind);
218 % [~,nir] = State.toMarginal(sn,ind,stateCell{isf});
219 % end
220 end
221 end
222 node_a = sync{a}.active{1}.node;
223 state_a = state(sn.nodeToStateful(node_a));
224 class_a = sync{a}.active{1}.class;
225 event_a = sync{a}.active{1}.event;
226 [new_state_a, rate_a] = State.afterEventHashed( sn, node_a, state_a, event_a, class_a);
227 % SPN code:
228 %[new_state_a, rate_a,~,trans_a, modes_a] = State.afterEventHashed( qn, node_a, state_a, event_a, class_a);
229
230 %% debugging block
231 % if true%options.verbose == 2
232 % line_printf('---\n');
233 % sync{a}.active{1}.print,
234 % end
235 %%
236 if new_state_a == -1 % hash not found
237 continue
238 end
239 for ia=1:length(new_state_a)
240 if rate_a(ia)>0
241 % SPN code:
242 %if rate_a(ia)>0 || modes_a(ia) > 0
243 node_p = sync{a}.passive{1}.node;
244 if node_p ~= local
245 % Skip if the active transition hash was not found
246 if new_state_a(ia) == -1
247 continue
248 end
249 state_p = state(sn.nodeToStateful(node_p));
250 class_p = sync{a}.passive{1}.class;
251 event_p = sync{a}.passive{1}.event;
252
253 % SPN code:
254 % enabled = 0;
255 % if ia <= length(trans_a)
256 % % check if other input places of the transition contains as many token as the multiplicity of the input arcs
257 % tr = trans_a(ia);
258 % mode = modes_a(ia);
259 % bmatrix = sn.varsparam{tr}.back(:,mode);
260 % inmatrix = sn.varsparam{tr}.inh(:,mode);
261 % enabled = all(state_pn >= bmatrix' & ~any(inmatrix'>0 & inmatrix' <= state_pn));
262 % end
263
264 %prob_sync_p = sync{a}.passive{1}.prob(state_a, state_p)
265 %if prob_sync_p > 0
266 %% debugging block
267 %if options.verbose == 2
268 % line_printf('---\n');
269 % sync{a}.active{1}.print,
270 % sync{a}.passive{1}.print
271 %end
272 %%
273 if node_p == node_a %self-loop
274 [new_state_p, ~, outprob_p] = State.afterEventHashed( sn, node_p, new_state_a(ia), event_p, class_p);
275 else % departure
276 [new_state_p, ~, outprob_p] = State.afterEventHashed( sn, node_p, state_p, event_p, class_p);
277 end
278 % SPN code:
279 % if node_p == node_a %self-loop
280 % [new_state_p, ~, outprob_p, trans_p, modes_p] = State.afterEventHashed( qn, node_p, new_state_a(ia), event_p, class_p);
281 % else % departure
282 % [new_state_p, ~, outprob_p, trans_p, modes_p] = State.afterEventHashed( qn, node_p, state_p, event_p, class_p);
283 % end
284 for ip=1:size(new_state_p,1)
285 if node_p ~= local
286 if new_state_p ~= -1
287 if sn.isstatedep(node_a,3)
288 newStateCell = stateCell;
289 newStateCell{sn.nodeToStateful(node_a)} = sn.space{sn.nodeToStateful(node_a)}(new_state_a(ia),:);
290 newStateCell{sn.nodeToStateful(node_p)} = sn.space{sn.nodeToStateful(node_p)}(new_state_p(ip),:);
291 prob_sync_p = sync{a}.passive{1}.prob(stateCell, newStateCell) * outprob_p(ip); %state-dependent
292 else
293 prob_sync_p = sync{a}.passive{1}.prob * outprob_p(ip);
294 end
295 else
296 prob_sync_p = 0;
297 end
298 end
299 if ~isempty(new_state_a(ia))
300 if node_p == local % local action
301 new_state = state;
302 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
303 prob_sync_p = outprob_p(ip);
304 elseif ~isempty(new_state_p)
305 new_state = state;
306 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
307 new_state(sn.nodeToStateful(node_p)) = new_state_p(ip);
308 end
309 % SPN code:
310 % if enabled
311 % ns = find(ismember(SSh(:,[sn.nodeToStateful(node_a),sn.nodeToStateful(node_p)]),[new_state_a(ia),new_state_p(ip)],'rows'));
312 % for ins=1:length(ns)
313 % if ns(ins) > 0 && ~isempty(trans_p)
314 % tr = trans_p(ip);
315 % mode = modes_p(ip);
316 % bmatrix = sn.varsparam{tr}.back(:,mode);
317 % fmatrix = sn.varsparam{tr}.forw(:,mode);
318 % cmatrix = fmatrix - bmatrix;
319 % if isequal(state_pn + cmatrix',SS(ns(ins),3:end))
320 % [ex_a,seq_a] = ST.search(state_pn');
321 % [ex_p,seq_p] = ST.search(SS(ns(ins),3:end)');
322 % if ex_a && ex_p && edges(seq_a, seq_p)
323 % Adj_m(s, ns(ins)) = modes_p(ip);
324 % Adj_t(s, ns(ins)) = trans_p(ip);
325 % % s,ns(ins)
326 % if ~isnan(rate_a(ia))
327 % if node_p < local && ~csmask(class_a, class_p) && rate_a(ia) * prob_sync_p >0 && (sn.nodetype(node_p)~=NodeType.Source)
328 % 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);
329 % end
330 % if size(Dfilt{a}) >= [s,ns(ins)] % check needed as D{a} is a sparse matrix
331 % Dfilt{a}(s,ns(ins)) = Dfilt{a}(s,ns(ins)) + rate_a(ia) * prob_sync_p;
332 % else
333 % Dfilt{a}(s,ns(ins)) = rate_a(ia) * prob_sync_p;
334 % end
335 % end
336 % end
337 % end
338 % end
339 % end
340 % else
341
342 ns = matchrow(stateSpaceHashed, new_state);
343 if ns>0
344 if ~isnan(rate_a)
345 if node_p < local && ~csmask(class_a, class_p) && rate_a(ia) * prob_sync_p >0 && (sn.nodetype(node_p)~=NodeType.Source)
346 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}));
347 end
348 if size(Dfilt{a}) >= [s,ns] % check needed as D{a} is a sparse matrix
349 Dfilt{a}(s,ns) = Dfilt{a}(s,ns) + rate_a(ia) * prob_sync_p;
350 else
351 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
352 end
353 end
354 end
355 % SPN code:
356 % end
357 end
358 end
359 % True BAS: a service completion (DEP, b=0) at a finite-capacity BAS
360 % station whose destination is full (passive ARV yielded no admissible
361 % state). Rather than voiding the departure (= repetitive service), hold
362 % the completed job at the server: transition to node_a's SAME local state
363 % with the blocked marker set (b:0->1), at rate mu_i. The held job later
364 % transfers to the destination at rate 1e7 (afterEventStation). Added to
365 % basBlockQ (Q only), NOT Dfilt, so it is not counted as a departure.
366 R2 = sn.nclasses;
367 if event_a == EventType.DEP && rate_a(ia) > 0 ...
368 && ~isempty(sn.isbasblocking) && numel(sn.isbasblocking) >= node_a ...
369 && sn.isbasblocking(node_a) == 1 ...
370 && all(new_state_p(:) == -1)
371 isfA = sn.nodeToStateful(node_a);
372 curVecA = sn.space{isfA}(state(isfA),:);
373 if curVecA(end) == 0
374 blockedVec = curVecA; blockedVec(end) = 1;
375 blockedIdx = matchrow(sn.space{isfA}, blockedVec);
376 if blockedIdx > 0
377 new_state_b = state;
378 new_state_b(isfA) = blockedIdx;
379 nsb = matchrow(stateSpaceHashed, new_state_b);
380 if nsb > 0
381 basBlockQ(s,nsb) = basBlockQ(s,nsb) + rate_a(ia);
382 end
383 end
384 end
385 end
386 else % node_p == local
387 if ~isempty(new_state_a(ia))
388 new_state = state;
389 new_state(sn.nodeToStateful(node_a)) = new_state_a(ia);
390 prob_sync_p = 1;
391 ns = matchrow(stateSpaceHashed, new_state);
392 if ns>0
393 if ~isnan(rate_a)
394 if size(Dfilt{a}) >= [s,ns] % needed for sparse matrix
395 Dfilt{a}(s,ns) = Dfilt{a}(s,ns) + rate_a(ia) * prob_sync_p;
396 else
397 Dfilt{a}(s,ns) = rate_a(ia) * prob_sync_p;
398 end
399 end
400 end
401 end
402 end
403 end
404 end
405 end
406end
407end % if fcrWaitq
408
409% An action emits on the Immediate scale when its active node is one of the
410% pass-through node types whose occupancy ctmc_find_vanishing_states marks as
411% vanishing (State.afterEventRouter / State.afterEventJoin return
412% GlobalConstants.Immediate). Keep the two predicates in step: a node marked
413% vanishing but not recognised here would have its whole row zeroed.
414isfjaug = isfield(sn,'fjsync') && ~isempty(sn.fjsync);
415immAction = false(1,A);
416for a=1:A
417 nt_a = sn.nodetype(sync{a}.active{1}.node);
418 immAction(a) = (nt_a == NodeType.Router) || (nt_a == NodeType.Fork) || (isfjaug && nt_a == NodeType.Join);
419end
420for a=1:A
421 Q = Q + Dfilt{a};
422 if immAction(a)
423 Qimm = Qimm + Dfilt{a};
424 end
425end
426% Fold in true-BAS become-blocked transitions (not counted as departures).
427Q = Q + basBlockQ;
428
429%% for all global synchronizations (SPN support)
430if isfield(sn, 'gsync') && ~isempty(sn.gsync)
431 gsyncEvents = sn.gsync;
432 G = length(gsyncEvents);
433
434 % Track FIRE completion rates for arvRates/depRates
435 Dfilt_gsync_comp = cell(1, G);
436 for g = 1:G
437 Dfilt_gsync_comp{g} = sparse(size(stateSpaceHashed,1), size(stateSpaceHashed,1));
438 end
439 immGsync = false(1,G);
440
441 for g = 1:G
442 gind = gsyncEvents{g}.active{1}.node;
443 isf_transition = sn.nodeToStateful(gind);
444 nmodes_g = sn.nodeparam{gind}.nmodes;
445 % ENABLE phase moves and firings of a TimingStrategy.IMMEDIATE mode are
446 % the two gsync sources emitted at the GlobalConstants.Immediate scale.
447 if gsyncEvents{g}.active{1}.event == EventType.ENABLE
448 immGsync(g) = true;
449 elseif gsyncEvents{g}.active{1}.event == EventType.FIRE
450 mode_g = gsyncEvents{g}.active{1}.mode;
451 immGsync(g) = isfield(sn.nodeparam{gind},'timing') && ~isempty(sn.nodeparam{gind}.timing) ...
452 && mode_g <= numel(sn.nodeparam{gind}.timing) ...
453 && sn.nodeparam{gind}.timing(mode_g) == TimingStrategy.IMMEDIATE;
454 end
455
456 for s = 1:size(stateSpaceHashed, 1)
457 state = stateSpaceHashed(s, :);
458
459 % Build glspace cell array from hashed state
460 glspace = cell(nstateful, 1);
461 for isf = 1:nstateful
462 glspace{isf} = sn.space{isf}(state(isf), :);
463 end
464
465 % Process event (both ENABLE and FIRE)
466 [outglspace, outrate, outprob, outcomp] = State.afterGlobalEvent(sn, gind, glspace, gsyncEvents{g}, false);
467
468 if isempty(outrate)
469 continue;
470 end
471
472 for io = 1:length(outrate)
473 if outrate(io) == 0
474 continue;
475 end
476
477 % Build new hashed state
478 new_state = state;
479
480 % Hash transition's new state
481 if size(outglspace{isf_transition}, 1) < io
482 continue;
483 end
484 trans_state = outglspace{isf_transition}(io, :);
485 hash_t = matchrow(sn.space{isf_transition}, trans_state);
486 if hash_t <= 0, continue; end
487 new_state(isf_transition) = hash_t;
488
489 is_comp = false;
490 % For FIRE events: apply the place updates that afterGlobalEvent
491 % already staged in outglspace (PRE consumption / POST production
492 % on completion). Take the completion flag from afterGlobalEvent
493 % rather than re-deriving it here. Two heuristics have been wrong:
494 % the transition idle-count columns are interleaved per mode
495 % ([idle_m, phases_m]) and so are not contiguous over 1:nmodes,
496 % and "some place marking changed" is false for a firing whose
497 % outcome returns exactly what its enabling condition consumed
498 % (e.g. P1 -> T1 -> P1), which is marking-invariant yet fires at
499 % a nonzero rate -- that reported Tput = 0 for such nets.
500 if gsyncEvents{g}.active{1}.event == EventType.FIRE
501 if ~isempty(outcomp) && io <= numel(outcomp)
502 is_comp = outcomp(io);
503 end
504 for isf = 1:nstateful
505 if isf ~= isf_transition && ~isequal(glspace{isf}, outglspace{isf})
506 hash_p = matchrow(sn.space{isf}, outglspace{isf});
507 if hash_p <= 0, continue; end
508 new_state(isf) = hash_p;
509 end
510 end
511 end
512 % For ENABLE events: only Transition state changes, Places unchanged
513
514 ns = matchrow(stateSpaceHashed, new_state);
515 if ns > 0
516 prob_val = 1;
517 if ~isempty(outprob) && io <= length(outprob)
518 prob_val = outprob(io);
519 end
520 rate_val = outrate(io) * prob_val;
521 Q(s, ns) = Q(s, ns) + rate_val;
522 if immGsync(g)
523 Qimm(s, ns) = Qimm(s, ns) + rate_val;
524 end
525 if is_comp
526 Dfilt_gsync_comp{g}(s, ns) = Dfilt_gsync_comp{g}(s, ns) + rate_val;
527 end
528 end
529 end
530 end
531 end
532end
533
534%% for all fork firing synchronizations (native fork-join support)
535FJ = 0;
536if isfield(sn,'fjsync') && ~isempty(sn.fjsync)
537 FJ = length(sn.fjsync);
538 Dfilt_fjsync = cell(1,FJ);
539 for k=1:FJ
540 Dfilt_fjsync{k} = sparse(size(stateSpaceHashed,1), size(stateSpaceHashed,1));
541 end
542 for k=1:FJ
543 for s=1:size(stateSpaceHashed,1)
544 state = stateSpaceHashed(s,:);
545 glspace = cell(nstateful,1);
546 for isf=1:nstateful
547 glspace{isf} = sn.space{isf}(state(isf),:);
548 end
549 [fjStates, fjrate, fjprob] = State.afterFJEvent(sn, sn.fjsync{k}, glspace, false);
550 for io=1:length(fjStates)
551 if fjprob(io) <= 0
552 continue
553 end
554 new_state = state;
555 skip = false;
556 for isf=1:nstateful
557 if ~isequal(glspace{isf}, fjStates{io}{isf})
558 newrow = fjStates{io}{isf};
559 if length(newrow) < size(sn.space{isf},2)
560 newrow = [zeros(1,size(sn.space{isf},2)-length(newrow)), newrow];
561 end
562 hash_p = matchrow(sn.space{isf}, newrow);
563 if hash_p <= 0
564 skip = true;
565 break
566 end
567 new_state(isf) = hash_p;
568 end
569 end
570 if skip
571 continue
572 end
573 ns = matchrow(stateSpaceHashed, new_state);
574 if ns > 0
575 rate_val = fjrate(io) * fjprob(io);
576 Q(s,ns) = Q(s,ns) + rate_val;
577 Qimm(s,ns) = Qimm(s,ns) + rate_val;
578 Dfilt_fjsync{k}(s,ns) = Dfilt_fjsync{k}(s,ns) + rate_val;
579 end
580 end
581 end
582 end
583end
584
585%% vanishing-row purge
586% Replace every vanishing row by its immediate-only counterpart so that the
587% embedded chain extracted by stochastic complementation below is the exact GSPN
588% one, rather than one perturbed at O(rate/GlobalConstants.Immediate). The same
589% masking is applied to the rate filters, so an event that the exact semantics
590% never let fire in a vanishing marking is not counted in arvRates/depRates
591% either. immPurged records that the predicate has already been evaluated on
592% this state space, so it is not recomputed below when no state is dropped.
593immPurged = [];
594if options.config.hide_immediate
595 immPurged = ctmc_find_vanishing_states(sn, stateSpaceHashed, nclasses, nstateful, FJ);
596 % A state marked vanishing must have at least one immediate outgoing arc. A
597 % zero row in Qimm therefore means the vanishing predicate and the
598 % immediate-arc tagging have drifted apart; purging it would manufacture an
599 % absorbing state and make -Q22 singular. Report the gap and leave those
600 % rows alone rather than corrupting the chain. They still take part in the
601 % stochastic complementation below, exactly as before this fix.
602 immRows = immPurged;
603 if ~isempty(immRows)
604 immGap = immRows(full(sum(Qimm(immRows,:),2)) <= 0);
605 if ~isempty(immGap)
606 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));
607 immRows = setdiff(immRows, immGap);
608 end
609 end
610 if ~isempty(immRows)
611 Q(immRows,:) = Qimm(immRows,:);
612 for a=1:A
613 if ~immAction(a)
614 Dfilt{a}(immRows,:) = 0;
615 end
616 end
617 if exist('Dfilt_gsync_comp','var')
618 for g=1:numel(Dfilt_gsync_comp)
619 if ~immGsync(g)
620 Dfilt_gsync_comp{g}(immRows,:) = 0;
621 end
622 end
623 end
624 end
625end
626
627Q = Q - diag(diag(Q));
628%SolverCTMC.printInfGen(Q,stateSpace)
629%%
630arvRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
631depRates = zeros(size(stateSpaceHashed,1),nstateful,nclasses);
632for a=1:A
633 % active
634 node_a = sync{a}.active{1}.node;
635 class_a = sync{a}.active{1}.class;
636 event_a = sync{a}.active{1}.event;
637 % passive
638 node_p = sync{a}.passive{1}.node;
639 class_p = sync{a}.passive{1}.class;
640 if event_a == EventType.DEP
641 node_a_sf = sn.nodeToStateful(node_a);
642 node_p_sf = sn.nodeToStateful(node_p);
643 for s=1:size(stateSpaceHashed,1)
644 depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
645 arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
646 end
647 end
648end
649
650%% Compute arrival/departure rates for gsync FIRE completion events
651if isfield(sn, 'gsync') && ~isempty(sn.gsync)
652 gsyncEvents = sn.gsync;
653 G = length(gsyncEvents);
654 for g = 1:G
655 if gsyncEvents{g}.active{1}.event == EventType.FIRE
656 for j = 1:length(gsyncEvents{g}.passive)
657 pev = gsyncEvents{g}.passive{j};
658 [pev_node, pev_class] = ind2sub([sn.nnodes, nclasses], pev.node);
659 if pev_node > sn.nnodes || ~sn.isstateful(pev_node)
660 continue;
661 end
662 pev_isf = sn.nodeToStateful(pev_node);
663 if pev.event == EventType.PRE
664 for s = 1:size(stateSpaceHashed, 1)
665 depRates(s, pev_isf, pev_class) = depRates(s, pev_isf, pev_class) + sum(Dfilt_gsync_comp{g}(s,:));
666 end
667 elseif pev.event == EventType.POST
668 for s = 1:size(stateSpaceHashed, 1)
669 arvRates(s, pev_isf, pev_class) = arvRates(s, pev_isf, pev_class) + sum(Dfilt_gsync_comp{g}(s,:));
670 end
671 end
672 end
673 end
674 end
675end
676
677%% Compute arrival/departure rates for fork firing synchronizations
678if FJ > 0
679 for k=1:FJ
680 fjentry = sn.fjsync{k};
681 isf_fork = sn.nodeToStateful(fjentry.fork);
682 for s=1:size(stateSpaceHashed,1)
683 rowsum = sum(Dfilt_fjsync{k}(s,:));
684 if rowsum > 0
685 depRates(s, isf_fork, fjentry.class) = depRates(s, isf_fork, fjentry.class) + rowsum;
686 for b=1:length(fjentry.branchheads)
687 isf_bh = sn.nodeToStateful(fjentry.branchheads(b));
688 arvRates(s, isf_bh, fjentry.auxclasses(b)) = arvRates(s, isf_bh, fjentry.auxclasses(b)) + rowsum;
689 end
690 end
691 end
692 end
693end
694
695zero_row = find(sum(Q,2)==0);
696zero_col = find(sum(Q,1)==0);
697
698%%
699% in case the last column of Q represent a state for a transient class, it
700% is possible that no transitions go back to it, although it is valid for
701% the system to be initialized in that state. So we need to fill-in the
702% zeros at the end.
703Q(:,end+1:end+(size(Q,1)-size(Q,2)))=0;
704Q(zero_row,zero_row) = -eye(length(zero_row)); % can this be replaced by []?
705Q(zero_col,zero_col) = -eye(length(zero_col));
706for a=1:A
707 Dfilt{a}(:,end+1:end+(size(Dfilt{a},1)-size(Dfilt{a},2)))=0;
708end
709
710if options.verbose == VerboseLevel.DEBUG || GlobalConstants.Verbose == VerboseLevel.DEBUG
711 SolverCTMC.printInfGen(Q,stateSpace);
712end
713Q = ctmc_makeinfgen(Q);
714
715%% drop states unreachable from the initial state
716% The default state space generator enumerates the whole population lattice, so
717% for a model whose reachable set is constrained -- a Petri net with P-invariants
718% is the clearest case -- it also produces markings that cannot be reached. Some
719% of those enable nothing at all and are therefore absorbing, which leaves the
720% generator with several recurrent classes and no unique stationary distribution:
721% ctmc_solve then rejects it as malformed. Such states carry zero probability by
722% definition, and no reachable state has an arc into them, so restricting every
723% quantity to the reachable set is exact rather than an approximation.
724% This runs before the immediate-state removal below, since the initial state may
725% itself be vanishing and is then absent from the complemented chain.
726if ~isempty(sn.state) && all(~cellfun(@isempty, sn.state))
727 initState = matchrow(stateSpace, cell2mat(sn.state'));
728 if initState > 0
729 adj = (Q - diag(diag(Q))) > 0;
730 reach = false(size(Q,1),1);
731 reach(initState) = true;
732 frontier = initState;
733 while ~isempty(frontier)
734 nxt = find(any(adj(frontier,:),1))';
735 nxt = nxt(~reach(nxt));
736 reach(nxt) = true;
737 frontier = nxt;
738 end
739 if ~all(reach)
740 line_debug('CTMC: %d of %d states unreachable from the initial state, dropped', ...
741 sum(~reach), numel(reach));
742 keep = find(reach);
743 immPurged = []; % state indices shift, the predicate must be re-evaluated
744 Q = Q(keep,keep);
745 Q = ctmc_makeinfgen(Q);
746 stateSpace = stateSpace(keep,:);
747 stateSpaceAggr = stateSpaceAggr(keep,:);
748 stateSpaceHashed = stateSpaceHashed(keep,:);
749 arvRates = arvRates(keep,:,:);
750 depRates = depRates(keep,:,:);
751 for a=1:A
752 Dfilt{a} = Dfilt{a}(keep,keep);
753 end
754 for k=1:FJ
755 Dfilt_fjsync{k} = Dfilt_fjsync{k}(keep,keep);
756 end
757 if exist('Dfilt_gsync_comp','var')
758 for g=1:numel(Dfilt_gsync_comp)
759 Dfilt_gsync_comp{g} = Dfilt_gsync_comp{g}(keep,keep);
760 end
761 end
762 end
763 end
764end
765
766%% now remove immediate transitions
767% we first determine states in stateful nodes where there is an immediate
768% job in the node
769
770if options.config.hide_immediate % if want to remove immediate transitions
771 % Design Y — Router-as-immediate-pass-through.
772 %
773 % Non-station stateful nodes whose presence-of-jobs signals an immediate
774 % transition are listed explicitly here (positive list, not "everything
775 % except Cache/Transition"), so a future node type added to the language
776 % defaults to NOT being silently eliminated by stochcomp — the developer
777 % must opt in. Currently only Router qualifies:
778 % - Router : pure zero-time pass-through. RROBIN/WRROBIN pointers are
779 % kept in the per-node state vector, but Router-occupied
780 % global states are folded into their downstream successors
781 % via stochastic complementation.
782 % - Cache : immediate read/write transitions kept visible so hit/miss
783 % rates can be computed (NOT in this list).
784 % - Transition (SPN): timed D1 firings + ENABLE-driven phase moves,
785 % handled by the gsync block below (NOT in this list).
786 % - Fork : (FJ-augmented structs only) holds the parent job for one
787 % vanishing state before the fork firing (sn.fjsync).
788 if isempty(immPurged)
789 imm = ctmc_find_vanishing_states(sn, stateSpaceHashed, nclasses, nstateful, FJ);
790 else
791 imm = immPurged; % already evaluated on this state space by the purge above
792 end
793 nonimm = setdiff(1:size(Q,1),imm);
794 stateSpace(imm,:) = [];
795 stateSpaceAggr(imm,:) = [];
796 % full(Q)
797 [Q,~,Q12,~,Q22] = ctmc_stochcomp(Q, nonimm);
798 % full(Q)
799 if FJ > 0 || ~isempty(imm)
800 % Actions that occur in vanishing states (fork firings, join
801 % departures, and the firing of immediate SPN modes) would be lost by
802 % simply dropping the vanishing rows: the place they take tokens from
803 % then reports zero throughput while still showing a nonzero arrival
804 % rate. Per-action rates are therefore recomputed here, before the
805 % Dfilt complement below overwrites the raw filters. The exact
806 % long-run rate of action a as seen from timed state s is the
807 % direct exit rate via a (to any destination, including vanishing
808 % ones) plus the expected number of a-firings along the vanishing
809 % chain entered from s: r_a = Dfilt{a}(nonimm,:)*1
810 % + Q12*(-Q22)^(-1)*(Dfilt{a}(imm,:)*1).
811 arvRates = zeros(length(nonimm),nstateful,nclasses);
812 depRates = zeros(length(nonimm),nstateful,nclasses);
813 for a=1:A
814 % active
815 node_a = sync{a}.active{1}.node;
816 class_a = sync{a}.active{1}.class;
817 event_a = sync{a}.active{1}.event;
818 % passive
819 node_p = sync{a}.passive{1}.node;
820 class_p = sync{a}.passive{1}.class;
821 if event_a == EventType.DEP
822 node_a_sf = sn.nodeToStateful(node_a);
823 node_p_sf = sn.nodeToStateful(node_p);
824 % A DEP is a TIMED service completion, which fires only from a
825 % tangible marking: in exact SPN/GSPN semantics a timed transition
826 % is disabled while an immediate transition is enabled, so it never
827 % fires during the zero-time vanishing sojourn. Complementing the
828 % vanishing rows (as the immediate SPN-FIRE and fork/join firings
829 % below legitimately do) would credit firings that physically cannot
830 % occur, inflating the departure rate at any station that shares a
831 % marking with an immediate node (e.g. an INF Delay downstream of an
832 % immediate Router in a class-switching cache model, whose RespT then
833 % drifts off the exact service time). Use the direct tangible rate.
834 r_a = full(sum(Dfilt{a}(nonimm,:),2));
835 depRates(:,node_a_sf,class_a) = depRates(:,node_a_sf,class_a) + r_a;
836 arvRates(:,node_p_sf,class_p) = arvRates(:,node_p_sf,class_p) + r_a;
837 end
838 end
839 % SPN firings: a mode declared IMMEDIATE fires only in vanishing
840 % states, so its PRE/POST token moves survive solely through the
841 % chain term above.
842 if isfield(sn, 'gsync') && ~isempty(sn.gsync) && exist('Dfilt_gsync_comp','var')
843 gsyncEvents_rc = sn.gsync;
844 for g = 1:length(gsyncEvents_rc)
845 if gsyncEvents_rc{g}.active{1}.event ~= EventType.FIRE
846 continue;
847 end
848 r_g = solver_ctmc_ratecomplement(Dfilt_gsync_comp{g}, nonimm, imm, Q12, Q22);
849 for j = 1:length(gsyncEvents_rc{g}.passive)
850 pev = gsyncEvents_rc{g}.passive{j};
851 [pev_node, pev_class] = ind2sub([sn.nnodes, nclasses], pev.node);
852 if pev_node > sn.nnodes || ~sn.isstateful(pev_node)
853 continue;
854 end
855 pev_isf = sn.nodeToStateful(pev_node);
856 if pev.event == EventType.PRE
857 depRates(:, pev_isf, pev_class) = depRates(:, pev_isf, pev_class) + r_g;
858 elseif pev.event == EventType.POST
859 arvRates(:, pev_isf, pev_class) = arvRates(:, pev_isf, pev_class) + r_g;
860 end
861 end
862 end
863 end
864 % fork firings: departure of the parent class at the Fork, one
865 % sibling arrival per branch head in the tag's auxiliary classes
866 for k=1:FJ
867 fjentry = sn.fjsync{k};
868 isf_fork = sn.nodeToStateful(fjentry.fork);
869 r_k = solver_ctmc_ratecomplement(Dfilt_fjsync{k}, nonimm, imm, Q12, Q22);
870 depRates(:,isf_fork,fjentry.class) = depRates(:,isf_fork,fjentry.class) + r_k;
871 for b=1:length(fjentry.branchheads)
872 isf_bh = sn.nodeToStateful(fjentry.branchheads(b));
873 arvRates(:,isf_bh,fjentry.auxclasses(b)) = arvRates(:,isf_bh,fjentry.auxclasses(b)) + r_k;
874 end
875 end
876 end
877 for a=1:A
878 % stochastic complement for action a
879 Q21a = Dfilt{a}(imm,nonimm);
880 Ta = (-Q22) \ Q21a;
881 Ta = Q12*Ta;
882 Dfilt{a} = Dfilt{a}(nonimm,nonimm)+Ta;
883 end
884 % recompute arvRates and depRates
885 % arvRates = zeros(size(stateSpace,1),nstateful,nclasses);
886 % depRates = zeros(size(stateSpace,1),nstateful,nclasses);
887 % for a=1:A
888 % % active
889 % node_a = sync{a}.active{1}.node;
890 % class_a = sync{a}.active{1}.class;
891 % event_a = sync{a}.active{1}.event;
892 % % passive
893 % node_p = sync{a}.passive{1}.node;
894 % class_p = sync{a}.passive{1}.class;
895 % if event_a == EventType.DEP
896 % node_a_sf = sn.nodeToStateful(node_a);
897 % node_p_sf = sn.nodeToStateful(node_p);
898 % for s=1:size(stateSpace,1)
899 % depRates(s,node_a_sf,class_a) = depRates(s,node_a_sf,class_a) + sum(Dfilt{a}(s,:));
900 % arvRates(s,node_p_sf,class_p) = arvRates(s,node_p_sf,class_p) + sum(Dfilt{a}(s,:));
901 % end
902 % end
903 % end
904end
905%SolverCTMC.printInfGen(Q,stateSpace)
906%
907% Draft SPN:
908% if ~isempty(Adj) && ~isempty(ST)
909% imm = [];
910% for s=1:size(SSh, 1)
911% % check for immediate transitions
912% enabled_t = find(Adj_t(s,:));
913% imm_t = [];
914% for t=1:length(enabled_t)
915% if sn.varsparam{Adj_t(s,enabled_t(t))}.timingstrategies(Adj_m(s,enabled_t(t)))
916% imm_t = [imm_t, enabled_t(t)];
917% end
918% end
919% % Immediate transitions exists, the marking needs to be removed.
920% if ~isempty(imm_t)
921% imm = [imm, s];
922% non_imm = setdiff(enabled_t,imm_t);
923% inM = find(Adj_t(:,s));
924% for in=1:length(inM)
925% depRates(inM(in),:,:) = depRates(inM(in),:,:) + depRates(s,:,:);
926% for nim=1:length(non_imm)
927% Q(inM(in), non_imm(nim)) = Q(inM(in), non_imm(nim)) + Q(inM(in), s) + Q(s, non_imm(nim));
928% if Adj_t(inM(in), non_imm(nim))==0
929% Adj_t(inM(in), non_imm(nim)) = Adj_t(inM(in), s);
930% Adj_m(inM(in), non_imm(nim)) = Adj_m(inM(in), s);
931% end
932% arvRates(non_imm(nim),:,:) = arvRates(non_imm(nim),:,:) + arvRates(s,:,:);
933% end
934% totalWeight = 0;
935% for im=1:length(imm_t)
936% weight = sn.varsparam{Adj_t(s,imm_t(im))}.firingweights(Adj_m(s,imm_t(im)));
937% totalWeight = totalWeight + weight;
938% if Adj_t(inM(in), imm_t(im))==0
939% Adj_t(inM(in), imm_t(im)) = Adj_t(inM(in), s);
940% Adj_m(inM(in), imm_t(im)) = Adj_m(inM(in), s);
941% end
942% end
943% for im=1:length(imm_t)
944% weight = sn.varsparam{Adj_t(s,imm_t(im))}.firingweights(Adj_m(s,imm_t(im)));
945% Q(inM(in), imm_t(im)) = Q(inM(in), imm_t(im)) + Q(inM(in), s) * (weight/totalWeight);
946% end
947% end
948% Adj_t(s,:) = 0;
949% Adj_m(s,:) = 0;
950% Adj_t(:,s) = 0;
951% Adj_m(:,s) = 0;
952% end
953% end
954% Q(imm,:) = [];
955% SS(imm,:) = [];
956% SSq(imm,:) = [];
957% arvRates(imm,:,:) = [];
958% depRates(imm,:,:) = [];
959% Q(:,imm) = [];
960% end
961%%
962%Q = ctmc_makeinfgen(Q);
963end
964
965%% Local functions
966function imm = ctmc_find_vanishing_states(sn, stateSpaceHashed, nclasses, nstateful, FJ)
967% Indices of the vanishing (zero-sojourn) global states: Router/Fork
968% pass-through occupancy, firable Join sibling sets, SPN markings from
969% which an ENABLE event moves the Transition row, and markings enabling a
970% TimingStrategy.IMMEDIATE mode. Extracted so the same predicate drives
971% both the vanishing-row purge and the stochastic complementation below.
972 isImmediatePassThrough = @(nt) (nt == NodeType.Router || nt == NodeType.Fork);
973
974 imm = [];
975 for ind = 1:sn.nnodes
976 if sn.isstateful(ind) && ~sn.isstation(ind) && isImmediatePassThrough(sn.nodetype(ind))
977 isf = sn.nodeToStateful(ind);
978 imm_st = find(sum(sn.space{isf}(:,1:nclasses),2)>0);
979 imm = [imm; find(arrayfun(@(a) any(a==imm_st),stateSpaceHashed(:,isf)))];
980 end
981 end
982 % Join-firable states (native fork-join): a Join holding a complete
983 % sibling set (or a plain job) fires immediately, so such global
984 % states are vanishing; incomplete sibling sets are NOT immediate
985 % (they are the genuine synchronization-delay states)
986 if FJ > 0
987 for ind = 1:sn.nnodes
988 if sn.nodetype(ind) == NodeType.Join
989 isf = sn.nodeToStateful(ind);
990 firable_rows = [];
991 origcl = sn.nodeparam{ind}.fj.origclasses;
992 for row=1:size(sn.space{isf},1)
993 for r=origcl(:)'
994 [ospace_j] = State.afterEventJoin(sn, ind, sn.space{isf}(row,:), EventType.DEP, r, false, [], NaN);
995 if ~isempty(ospace_j)
996 firable_rows(end+1) = row; %#ok<AGROW>
997 break
998 end
999 end
1000 end
1001 if ~isempty(firable_rows)
1002 imm = [imm; find(arrayfun(@(a) any(a==firable_rows),stateSpaceHashed(:,isf)))]; %#ok<AGROW>
1003 end
1004 end
1005 end
1006 end
1007 % Transition immediate states: states where any ENABLE event would change the state
1008 if isfield(sn, 'gsync') && ~isempty(sn.gsync)
1009 gsyncEvents_sc = sn.gsync;
1010 for s = 1:size(stateSpaceHashed, 1)
1011 if any(s == imm)
1012 continue; % already marked
1013 end
1014 state_sc = stateSpaceHashed(s, :);
1015 glspace_sc = cell(nstateful, 1);
1016 for isf = 1:nstateful
1017 glspace_sc{isf} = sn.space{isf}(state_sc(isf), :);
1018 end
1019 for g = 1:length(gsyncEvents_sc)
1020 if gsyncEvents_sc{g}.active{1}.event == EventType.ENABLE
1021 gind_sc = gsyncEvents_sc{g}.active{1}.node;
1022 isf_t_sc = sn.nodeToStateful(gind_sc);
1023 orig_row_sc = glspace_sc{isf_t_sc};
1024 [outgl_sc, outrate_sc, ~] = State.afterGlobalEvent(sn, gind_sc, glspace_sc, gsyncEvents_sc{g}, false);
1025 % A state is vanishing only if the ENABLE event actually
1026 % changes the transition's own row (mirrors the native-Python
1027 % detection). Marking on a non-empty outrate alone over-censors
1028 % multi-mode transitions: for a mode whose servers already
1029 % match the enabling degree the event returns a no-op outcome
1030 % equal to the current row, which must NOT be treated as
1031 % immediate (doing so collapsed the tangible state space).
1032 rowchanged = false;
1033 if ~isempty(outrate_sc)
1034 og_sc = outgl_sc{isf_t_sc};
1035 for io=1:size(og_sc,1)
1036 if outrate_sc(io) > 0 && ~isequal(og_sc(io,:), orig_row_sc)
1037 rowchanged = true;
1038 break;
1039 end
1040 end
1041 end
1042 if rowchanged
1043 imm = [imm; s]; %#ok<AGROW>
1044 break;
1045 end
1046 end
1047 end
1048 end
1049 end
1050 % Immediate-firing states (SPN): a marking that enables a mode declared
1051 % TimingStrategy.IMMEDIATE is vanishing, since that mode fires in zero time.
1052 % afterGlobalEvent emits such firings at GlobalConstants.Immediate scaled by
1053 % the firing weight, so the branching among competing immediate modes is
1054 % their weight ratio and any timed mode enabled in the same marking loses the
1055 % race. Complementing these states out below leaves the embedded chain over
1056 % the tangible markings, which is what a GSPN steady-state solution is.
1057 if isfield(sn, 'gsync') && ~isempty(sn.gsync)
1058 gsyncEvents_im = sn.gsync;
1059 for g = 1:length(gsyncEvents_im)
1060 if gsyncEvents_im{g}.active{1}.event ~= EventType.FIRE
1061 continue;
1062 end
1063 gind_im = gsyncEvents_im{g}.active{1}.node;
1064 mode_im = gsyncEvents_im{g}.active{1}.mode;
1065 if ~isfield(sn.nodeparam{gind_im}, 'timing') || isempty(sn.nodeparam{gind_im}.timing)
1066 continue;
1067 end
1068 if mode_im > numel(sn.nodeparam{gind_im}.timing) ...
1069 || sn.nodeparam{gind_im}.timing(mode_im) ~= TimingStrategy.IMMEDIATE
1070 continue;
1071 end
1072 for s = 1:size(stateSpaceHashed, 1)
1073 if any(s == imm)
1074 continue; % already marked
1075 end
1076 glspace_im = cell(nstateful, 1);
1077 for isf = 1:nstateful
1078 glspace_im{isf} = sn.space{isf}(stateSpaceHashed(s, isf), :);
1079 end
1080 [~, outrate_im, ~] = State.afterGlobalEvent(sn, gind_im, glspace_im, gsyncEvents_im{g}, false);
1081 if ~isempty(outrate_im) && any(outrate_im > 0)
1082 imm = [imm; s]; %#ok<AGROW>
1083 end
1084 end
1085 end
1086 end
1087
1088 imm = unique(imm);
1089end
Definition Station.m:245