LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
afterGlobalEvent.m
1function [outglspace, outrate, outprob, outcomp] = afterGlobalEvent(sn, ind, glspace, glevent, isSimulation)
2% [OUTGLSPACE, OUTRATE, OUTPROB, OUTCOMP] = AFTERGLOBALEVENT(QN, IND, GLSPACE, GLEVENT, ISSIMULATION)
3%
4% OUTCOMP(io) is true when outcome io is a firing completion (a D1 firing of
5% the active mode, which applies the PRE/POST place updates), and false when
6% it is not (e.g. every ENABLE outcome). Callers must not re-derive this from
7% the place markings: a transition whose firing outcome returns exactly what
8% its enabling condition consumed leaves every marking invariant, yet still
9% completes at a nonzero rate.
10%
11% Copyright (c) 2012-2026, Imperial College London
12% All rights reserved.
13
14outspace = []; % state space of ind node
15outrate = []; % rate of synchronization
16outprob = []; % probability of synchronization
17outcomp = []; % true if the outcome is a firing completion (see above)
18outglspace = glspace; % new global state after synchronization
19
20R = sn.nclasses;
21%phasessz = sn.phasessz;
22%phaseshift = sn.phaseshift;
23if sn.nodetype(ind) == NodeType.Transition % same isa(glevent,'ModeEvent')
24 % ModeEvent
25 isf = sn.nodeToStateful(ind);
26 inspace = glspace{isf};
27 V = sum(sn.nvars(ind,:));
28 % in this case service is always immediate so sum(K)=1
29 space_var = inspace(:,(end-V+1):end); % local state variables
30 if sn.nodetype(ind) == NodeType.Transition
31 fK = sn.nodeparam{ind}.firingphases;
32 nmodes = sn.nodeparam{ind}.nmodes;
33 % Handle NaN firingphases (non-phase-type distributions like Pareto)
34 if any(isnan(fK))
35 fK = zeros(1, nmodes);
36 for m = 1:nmodes
37 if iscell(sn.nodeparam{ind}.firingproc) && ~isempty(sn.nodeparam{ind}.firingproc{m})
38 fK(m) = size(sn.nodeparam{ind}.firingproc{m}{1}, 1);
39 else
40 fK(m) = 1;
41 end
42 end
43 end
44 fKs = [0,cumsum(fK,2)];
45 mode = glevent.active{1}.mode;
46 % Transition state format: [idle_counts(nmodes), phase_counts(sum(fK)), fired_counts(nmodes)]
47 space_buf = inspace(:,1:nmodes); % idle servers count put in buf
48 space_srv = inspace(:,(nmodes+1):(nmodes+sum(fK))); % enabled servers' phases
49 % Handle both state formats: with and without fired component
50 expected_len_with_fired = 2*nmodes + sum(fK);
51 expected_len_without_fired = nmodes + sum(fK);
52 if size(inspace, 2) >= expected_len_with_fired
53 space_fired = inspace(:,(nmodes+sum(fK)+1):(2*nmodes+sum(fK))); % servers that just fired
54 elseif size(inspace, 2) == expected_len_without_fired
55 % Legacy format without fired component - initialize to zeros
56 space_fired = zeros(size(inspace,1), nmodes);
57 else
58 line_error(mfilename, 'Unexpected state vector length for Transition node');
59 end
60
61 switch glevent.active{1}.event
62 case EventType.ENABLE
63 enabling_m = sn.nodeparam{ind}.enabling{mode}; % enabling requirement for mode m
64 inhibiting_m = sn.nodeparam{ind}.inhibiting{mode}; % inhibitor thresholds (Inf = no inhibition)
65 ep_space = zeros(sn.nnodes,R);
66 for j=1:length(glevent.passive)
67 ep_linidx = glevent.passive{j}.node; % linear index into (nnodes x nclasses) matrix
68 % Decode linear index to (node, class) - the passive node is a linear index from find() on enabling matrix
69 [ep_ind, ~] = ind2sub([sn.nnodes, R], ep_linidx);
70 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
71 continue; % skip non-stateful nodes
72 end
73 ep_isf = sn.nodeToStateful(ep_ind);
74 K = ones(1,R);
75 Ks = [0,cumsum(K,2)];
76 ep_space_buf = glspace{ep_isf};
77 ep_space_srv = zeros(1,R);
78 ep_space_var = [];
79 [~,ep_space(ep_ind,1:R)] = State.toMarginalAggr(sn,ep_ind, glspace{ep_isf},K,Ks,ep_space_buf,ep_space_srv,ep_space_var);
80 end
81 m = mode;
82 % Inhibitor arcs: mode is disabled while any inhibited input
83 % place has reached its threshold (Inf default => never true).
84 inhibited = any(ep_space(:) >= inhibiting_m(:));
85 if any(ep_space(:) < enabling_m(:)) || inhibited
86 % check if any servers need to be disabled
87 new_space_buf = space_buf;
88 new_space_srv = space_srv;
89 % Cap nmodeservers to MaxInt for state (SSA needs finite states)
90 nmodeservers_m = sn.nodeparam{ind}.nmodeservers(m);
91 if isinf(nmodeservers_m)
92 nmodeservers_m = GlobalConstants.MaxInt();
93 end
94 new_space_buf(m) = nmodeservers_m;
95 new_space_srv((fKs(m)+1):(fKs(m)+fK(m))) = 0;
96 new_state = [new_space_buf, new_space_srv, space_fired, space_var];
97 old_state = [space_buf, space_srv, space_fired, space_var];
98 if ~isequal(new_state, old_state)
99 % State changes - return the disable action
100 outspace = new_state;
101 outrate = GlobalConstants.Immediate;
102 outprob = 1.0;
103 else
104 % Already disabled, no state change
105 outspace = [];
106 outrate = [];
107 outprob = 1.0;
108 end
109 else
110 % find enabling degree, i.e., running servers
111 en_degree_m = 1;
112 while all(ep_space >= en_degree_m * enabling_m)
113 en_degree_m = en_degree_m + 1;
114 end
115 % Cap nmodeservers to MaxInt for enabling degree calculation
116 nmodeservers_m = sn.nodeparam{ind}.nmodeservers(m);
117 if isinf(nmodeservers_m)
118 nmodeservers_m = GlobalConstants.MaxInt();
119 end
120 en_degree_m = min(en_degree_m - 1, nmodeservers_m);
121 running_m = sum(space_srv((fKs(m)+1):(fKs(m)+fK(m))));
122 if running_m == en_degree_m
123 % all running as expected, do nothing
124 outspace = [];
125 outrate = [];
126 outprob = 1.0;
127 elseif running_m < en_degree_m
128 % fewer expected, start en_degree_m - running_m
129 space_buf(m) = space_buf(m) - (en_degree_m - running_m);
130 pentry = sn.nodeparam{ind}.firingpie{mode};
131 nadd = en_degree_m - running_m;
132 combs = sortrows(multichoose(fK(m), nadd),'descend');
133
134 for i = 1:size(combs,1)
135 comb = combs(i,:);
136 space_srv_k = space_srv;
137 for k = 1:fK(m)
138 space_srv_k(fKs(m)+k) = space_srv_k(fKs(m)+k) + comb(k);
139 end
140 outspace = [outspace; space_buf, space_srv_k, space_fired, space_var]; %#ok<AGROW>
141 outrate = [outrate; GlobalConstants.Immediate]; %#ok<AGROW>
142
143 % compute multinomial coefficient
144 % multinomial probability: n! / (k1! * k2! * ...) * p1^k1 * p2^k2 * ...
145 logprob = factln(nadd);
146 for k = 1:fK(m)
147 if pentry(k)>0
148 logprob = logprob + comb(k)*log(pentry(k)) - factln(comb(k));
149 elseif (pentry(k)==0 && comb(k)==0)
150 continue
151 else % (pentry(k)==0 && comb(k)>0)
152 logprob = -Inf; % set zero outprob
153 end
154 end
155 outprob = [outprob; exp(logprob)]; %#ok<AGROW>
156 end
157 %outprob = outprob / sum(outprob);
158 else %running_m > en_degree_m
159 % more than expected, stop running_m - en_degree_m
160 % servers chosen uniformly at random across phases.
161 % We enumerate how many servers to stop per phase (comb),
162 % where 0 <= comb(k) <= srv_vec(k) and sum(comb) = ndiff.
163 % Probability of each comb follows the hypergeometric
164 % mixture: prod_k C(srv_vec(k), comb(k)) / C(running_m, ndiff).
165 ndiff = running_m - en_degree_m; % number of servers to stop
166 srv_vec = space_srv((fKs(m)+1):(fKs(m)+fK(m)));
167 n = numel(srv_vec);
168 % Enumerate all (comb) of length n with sum == ndiff and
169 % comb(k) <= srv_vec(k). multichoose gives sum==ndiff with
170 % entries >= 0 but without the per-phase cap, so filter.
171 all_combs = multichoose(n, ndiff);
172 valid = all(bsxfun(@le, all_combs, srv_vec(:)'), 2);
173 all_combs = all_combs(valid, :);
174 % Compute weights via multivariate hypergeometric
175 logW = zeros(size(all_combs,1),1);
176 for i = 1:size(all_combs,1)
177 for k = 1:n
178 logW(i) = logW(i) + factln(srv_vec(k)) ...
179 - factln(all_combs(i,k)) ...
180 - factln(srv_vec(k) - all_combs(i,k));
181 end
182 end
183 W = exp(logW - max(logW));
184 W = W / sum(W);
185 for i = 1:size(all_combs,1)
186 comb = all_combs(i,:);
187 space_srv_reduced = space_srv;
188 space_srv_reduced((fKs(m)+1):(fKs(m)+fK(m))) = srv_vec - comb;
189 space_buf_reduced = space_buf;
190 space_buf_reduced(m) = space_buf_reduced(m) + ndiff; % return stopped servers to idle pool
191 outspace = [outspace; space_buf_reduced, space_srv_reduced, space_fired, space_var]; %#ok<AGROW>
192 outrate = [outrate; GlobalConstants.Immediate]; %#ok<AGROW>
193 outprob = [outprob; W(i)]; %#ok<AGROW>
194 end
195 end
196 end
197 % update new state space
198 outglspace{isf} = outspace;
199 case EventType.FIRE
200 %% Update transition servers
201 fK = sn.nodeparam{ind}.firingphases;
202 % Handle NaN firingphases (non-phase-type distributions like Pareto)
203 if any(isnan(fK))
204 fK = zeros(1, nmodes);
205 for m = 1:nmodes
206 if iscell(sn.nodeparam{ind}.firingproc) && ~isempty(sn.nodeparam{ind}.firingproc{m})
207 fK(m) = size(sn.nodeparam{ind}.firingproc{m}{1}, 1);
208 else
209 fK(m) = 1;
210 end
211 end
212 end
213 fKs = [0,cumsum(fK,2)];
214 mode = glevent.active{1}.mode;
215 % find transition server counts
216 [~,nim,~,kim] = State.toMarginal(sn,ind,inspace,fK,fKs,space_buf,space_srv,space_var);
217 % state of transition mode
218 enabling_m = sn.nodeparam{ind}.enabling{mode}; % enabling requirement for mode m
219 inhibiting_m = sn.nodeparam{ind}.inhibiting{mode}; % inhibitor thresholds (Inf = no inhibition)
220 firing_m = sn.nodeparam{ind}.firing{mode}; % firing requirement for mode m
221 % find enabling degree, i.e., running servers
222 ep_space = zeros(sn.nnodes,R);
223 for j=1:length(glevent.passive)
224 ep_linidx = glevent.passive{j}.node; % linear index into (nnodes x nclasses) matrix
225 % Decode linear index to (node, class) - the passive node is a linear index from find() on enabling/firing matrix
226 [ep_ind, ~] = ind2sub([sn.nnodes, R], ep_linidx);
227 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
228 continue; % skip non-stateful nodes
229 end
230 ep_isf = sn.nodeToStateful(ep_ind);
231 K = ones(1,R);
232 Ks = [0,cumsum(K,2)];
233 ep_space_buf = outglspace{ep_isf}; % this equals glspace at this point
234 ep_space_srv = zeros(1,R);
235 ep_space_var = [];
236 [~,ep_space(ep_ind,1:R)] = State.toMarginalAggr(sn,ep_ind, glspace{ep_isf},K,Ks,ep_space_buf,ep_space_srv,ep_space_var);
237 end
238 if any(ep_space(:) >= inhibiting_m(:))
239 % Inhibitor arc active: mode cannot fire.
240 en_degree_m = 0;
241 else
242 en_degree_m = 1;
243 while all(ep_space >= en_degree_m * enabling_m)
244 en_degree_m = en_degree_m + 1;
245 end
246 mark_degree_m = en_degree_m - 1; % concurrent firings the marking alone supports
247 %en_degree_m = min(en_degree_m - 1, sn.nodeparam{ind}.nmodeservers(m));
248 en_degree_m = min(mark_degree_m, nim(mode)); % the actual enabling degree depends on servers actually in execution, though this should coincide
249 end
250
251 % Get D0 and D1 matrices for phase transitions and completions
252 D0 = sn.nodeparam{ind}.firingproc{mode}{1}; % Internal phase transitions
253 D1 = sn.nodeparam{ind}.firingproc{mode}{2}; % Completions/firings
254
255 % Track whether this is a completion (with place updates) or just phase transition
256 completion_start_idx = []; % indices in outspace that are completions
257
258 % see _kb/04-networkstruct.md (SPN/GSPN state generation) for rationale
259
260 % Add completions (D1) - these update places
261 % Track which outcomes are completions (vs phase transitions)
262 is_completion = false(size(outspace,1), 1); % mark existing as phase transitions
263 % see _kb/04-networkstruct.md (SPN/GSPN state generation) for rationale
264 isImmediateMode = false;
265 if isfield(sn.nodeparam{ind}, 'timing') && ~isempty(sn.nodeparam{ind}.timing) ...
266 && mode <= numel(sn.nodeparam{ind}.timing)
267 isImmediateMode = sn.nodeparam{ind}.timing(mode) == TimingStrategy.IMMEDIATE;
268 end
269 immediateWeight = 1.0;
270 if isImmediateMode && isfield(sn.nodeparam{ind}, 'fireweight') ...
271 && ~isempty(sn.nodeparam{ind}.fireweight) ...
272 && mode <= numel(sn.nodeparam{ind}.fireweight)
273 immediateWeight = sn.nodeparam{ind}.fireweight(mode);
274 end
275
276 % see _kb/04-networkstruct.md (SPN/GSPN state generation) for rationale
277 if isImmediateMode
278 imm_nservers = sn.nodeparam{ind}.nmodeservers(mode);
279 if isinf(imm_nservers)
280 imm_nservers = GlobalConstants.MaxInt();
281 end
282 imm_servers_m = min(mark_degree_m, imm_nservers);
283 else
284 imm_servers_m = 0;
285 end
286 for k = 1:fK(mode) % source phase
287 if isImmediateMode
288 fires_k = (k == 1) && imm_servers_m >= 1;
289 else
290 fires_k = kim(:,mode,k) > 0 && en_degree_m >= 1; % servers in phase k, and the place can still pay the PRE
291 end
292 if fires_k
293 if isImmediateMode
294 rate_kd = GlobalConstants.Immediate * immediateWeight * imm_servers_m;
295 else
296 rate_kd = sum(D1(k,:)) * kim(:,mode,k);
297 % Marking-dependent firing-rate multiplier g_mode(marking):
298 % ep_space is the node-indexed input-place marking, the
299 % same object the JSON tabulation was built over. Exact
300 % because CTMC evaluates it per enumerated state.
301 if isfield(sn.nodeparam{ind}, 'firingdep') ...
302 && numel(sn.nodeparam{ind}.firingdep) >= mode ...
303 && ~isempty(sn.nodeparam{ind}.firingdep{mode})
304 rate_kd = rate_kd * double(sn.nodeparam{ind}.firingdep{mode}(ep_space));
305 end
306 end
307 if rate_kd > 0
308 space_buf_kd = space_buf;
309 space_srv_kd = space_srv;
310 if kim(:,mode,k) > 0
311 % Decrease by one the firing server
312 space_srv_kd(fKs(mode)+k) = space_srv_kd(fKs(mode)+k) - 1;
313 % Move the server back to the disabled pool
314 space_buf_kd(mode) = space_buf_kd(mode) + 1;
315 end
316 space_fired_kd = space_fired;
317 if isSimulation
318 space_fired_kd(mode) = space_fired_kd(mode) + 1; % increment fired count only for simulation
319 end
320 outspace = [outspace; space_buf_kd, space_srv_kd, space_fired_kd, space_var]; %#ok<AGROW>
321 outrate = [outrate; rate_kd]; %#ok<AGROW>
322 outprob = [outprob; 1.0]; %#ok<AGROW>
323 is_completion = [is_completion; true]; %#ok<AGROW>
324 end
325 end
326 end
327 outglspace{isf} = outspace;
328 outcomp = is_completion;
329
330 % For simulation: select outcome first, then apply PRE/POST only if completion
331 if isSimulation && size(outspace,1) > 1 && ~isempty(outprob)
332 % Use effective rate (rate * prob) for selection
333 eff_rate = outrate .* outprob;
334 tot_rate = sum(eff_rate);
335 if tot_rate > 0
336 cum_rate = cumsum(eff_rate) / tot_rate;
337 firing_ctr = 1 + max([0,find( rand > cum_rate' )]);
338 selected_is_completion = is_completion(firing_ctr);
339 outcomp = selected_is_completion;
340 outspace = outspace(firing_ctr,:);
341 outrate = sum(eff_rate); % return effective rate
342 outprob = 1.0; % probability already incorporated
343 outglspace{isf} = outspace;
344
345 % Only process PRE/POST if this is a completion
346 if selected_is_completion
347 %% Process ID_PRE events, i.e., consume from all input places
348 for j=1:length(glevent.passive)
349 if glevent.passive{j}.event == EventType.PRE
350 ep_linidx = glevent.passive{j}.node;
351 [ep_ind, ep_class] = ind2sub([sn.nnodes, R], ep_linidx);
352 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
353 continue;
354 end
355 ep_isf = sn.nodeToStateful(ep_ind);
356 ep_space_buf = outglspace{ep_isf};
357 ep_ist = sn.nodeToStation(ep_ind);
358 % see _kb/04-networkstruct.md (SPN/GSPN state generation) for rationale
359 consume_count = glevent.passive{j}.weight;
360 % Check scheduling strategy and state format to determine handling
361 % Places with INF scheduling use queue-based state format
362 is_queue_based = sn.nodetype(ep_ind) == NodeType.Place && ...
363 length(ep_space_buf) ~= R && length(ep_space_buf) ~= 2*R;
364 if is_queue_based || sn.sched(ep_ist) == SchedStrategy.FCFS
365 % FCFS queue-based handling
366 idx = find(ep_space_buf == ep_class);
367 if length(idx) >= consume_count
368 ep_space_buf(idx(end - consume_count + 1:end)) = [];
369 end
370 elseif sn.sched(ep_ist) == SchedStrategy.LCFS
371 idx = find(ep_space_buf == ep_class);
372 if length(idx) >= consume_count
373 ep_space_buf(idx(1:consume_count)) = [];
374 end
375 elseif sn.sched(ep_ist) == SchedStrategy.SIRO
376 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
377 elseif sn.nodetype(ep_ind) == NodeType.Place
378 % Place count-based state format: [buffer(R), server_phases(sum(K))]
379 state_len = length(ep_space_buf);
380 if state_len > R
381 buf_jobs = ep_space_buf(ep_class);
382 srv_jobs = ep_space_buf(R + ep_class);
383 total_jobs = buf_jobs + srv_jobs;
384 remaining = total_jobs - consume_count;
385 ep_space_buf(ep_class) = remaining;
386 ep_space_buf(R + ep_class) = 0;
387 else
388 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
389 end
390 else
391 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(ep_ist))));
392 end
393 outglspace{ep_isf} = ep_space_buf;
394 end
395 end
396
397 %% Process ID_POST events, i.e., produce to all output places
398 for j=1:length(glevent.passive)
399 if glevent.passive{j}.event == EventType.POST
400 fp_linidx = glevent.passive{j}.node;
401 [fp_ind, fp_class] = ind2sub([sn.nnodes, R], fp_linidx);
402 if fp_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(fp_ind)) || sn.nodeToStateful(fp_ind) <= 0
403 continue;
404 end
405 fp_isf = sn.nodeToStateful(fp_ind);
406 fp_ist = sn.nodeToStation(fp_ind);
407 fp_space_buf = outglspace{fp_isf};
408 produce_count = glevent.passive{j}.weight;
409 % Check scheduling strategy and state format to determine handling
410 % Use original glspace length for detection (before PRE modified it)
411 orig_len = length(glspace{fp_isf});
412 is_queue_based = sn.nodetype(fp_ind) == NodeType.Place && ...
413 orig_len ~= R && orig_len ~= 2*R;
414 if is_queue_based || sn.sched(fp_ist) == SchedStrategy.FCFS || sn.sched(fp_ist) == SchedStrategy.LCFS
415 % Queue-based handling - prepend to buffer
416 fp_space_buf = [repmat(fp_class, 1, produce_count), fp_space_buf];
417 elseif sn.sched(fp_ist) == SchedStrategy.SIRO
418 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
419 elseif sn.nodetype(fp_ind) == NodeType.Place
420 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
421 else
422 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(fp_ist))));
423 end
424 outglspace{fp_isf} = fp_space_buf;
425 end
426 end
427 end
428 else
429 % All effective rates are zero, pick first option with non-zero prob if any
430 valid_idx = find(outprob > 0, 1);
431 if isempty(valid_idx)
432 valid_idx = 1;
433 end
434 outspace = outspace(valid_idx,:);
435 outrate = 0;
436 outprob = outprob(valid_idx,:);
437 outglspace{isf} = outspace;
438 end
439 elseif ~isSimulation
440 %% Non-simulation mode: process all completions for PRE/POST
441 % For state space analysis, we apply PRE/POST for completion outcomes
442 for j=1:length(glevent.passive)
443 if glevent.passive{j}.event == EventType.PRE
444 ep_linidx = glevent.passive{j}.node;
445 [ep_ind, ep_class] = ind2sub([sn.nnodes, R], ep_linidx);
446 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
447 continue;
448 end
449 ep_isf = sn.nodeToStateful(ep_ind);
450 ep_space_buf = outglspace{ep_isf};
451 ep_ist = sn.nodeToStation(ep_ind);
452 consume_count = glevent.passive{j}.weight; % one completion = one firing (see PRE note above)
453 % Check scheduling strategy and state format to determine handling
454 is_queue_based = sn.nodetype(ep_ind) == NodeType.Place && ...
455 length(ep_space_buf) ~= R && length(ep_space_buf) ~= 2*R;
456 if is_queue_based || sn.sched(ep_ist) == SchedStrategy.FCFS
457 % FCFS queue-based handling
458 idx = find(ep_space_buf == ep_class);
459 if length(idx) >= consume_count
460 ep_space_buf(idx(end - consume_count + 1:end)) = [];
461 end
462 elseif sn.sched(ep_ist) == SchedStrategy.LCFS
463 idx = find(ep_space_buf == ep_class);
464 if length(idx) >= consume_count
465 ep_space_buf(idx(1:consume_count)) = [];
466 end
467 elseif sn.sched(ep_ist) == SchedStrategy.SIRO
468 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
469 elseif sn.nodetype(ep_ind) == NodeType.Place
470 % Place count-based state format
471 state_len = length(ep_space_buf);
472 if state_len > R
473 buf_jobs = ep_space_buf(ep_class);
474 srv_jobs = ep_space_buf(R + ep_class);
475 total_jobs = buf_jobs + srv_jobs;
476 remaining = total_jobs - consume_count;
477 ep_space_buf(ep_class) = remaining;
478 ep_space_buf(R + ep_class) = 0;
479 else
480 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
481 end
482 else
483 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(ep_ist))));
484 end
485 outglspace{ep_isf} = ep_space_buf;
486 end
487 end
488
489 for j=1:length(glevent.passive)
490 if glevent.passive{j}.event == EventType.POST
491 fp_linidx = glevent.passive{j}.node;
492 [fp_ind, fp_class] = ind2sub([sn.nnodes, R], fp_linidx);
493 if fp_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(fp_ind)) || sn.nodeToStateful(fp_ind) <= 0
494 continue;
495 end
496 fp_isf = sn.nodeToStateful(fp_ind);
497 fp_ist = sn.nodeToStation(fp_ind);
498 fp_space_buf = outglspace{fp_isf};
499 produce_count = glevent.passive{j}.weight;
500 % Check scheduling strategy and state format to determine handling
501 % Use original glspace length for detection (before PRE modified it)
502 orig_len = length(glspace{fp_isf});
503 is_queue_based = sn.nodetype(fp_ind) == NodeType.Place && ...
504 orig_len ~= R && orig_len ~= 2*R;
505 if is_queue_based || sn.sched(fp_ist) == SchedStrategy.FCFS || sn.sched(fp_ist) == SchedStrategy.LCFS
506 fp_space_buf = [repmat(fp_class, 1, produce_count), fp_space_buf];
507 elseif sn.sched(fp_ist) == SchedStrategy.SIRO
508 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
509 elseif sn.nodetype(fp_ind) == NodeType.Place
510 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
511 else
512 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(fp_ist))));
513 end
514 outglspace{fp_isf} = fp_space_buf;
515 end
516 end
517 else
518 % Single outcome case in simulation mode
519 if ~isempty(is_completion) && is_completion(1)
520 %% Process ID_PRE events
521 for j=1:length(glevent.passive)
522 if glevent.passive{j}.event == EventType.PRE
523 ep_linidx = glevent.passive{j}.node;
524 [ep_ind, ep_class] = ind2sub([sn.nnodes, R], ep_linidx);
525 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
526 continue;
527 end
528 ep_isf = sn.nodeToStateful(ep_ind);
529 ep_space_buf = outglspace{ep_isf};
530 ep_ist = sn.nodeToStation(ep_ind);
531 consume_count = glevent.passive{j}.weight; % one completion = one firing (see PRE note above)
532 % Check scheduling strategy and state format to determine handling
533 is_queue_based = sn.nodetype(ep_ind) == NodeType.Place && ...
534 length(ep_space_buf) ~= R && length(ep_space_buf) ~= 2*R;
535 if is_queue_based || sn.sched(ep_ist) == SchedStrategy.FCFS
536 % FCFS queue-based handling
537 idx = find(ep_space_buf == ep_class);
538 if length(idx) >= consume_count
539 ep_space_buf(idx(end - consume_count + 1:end)) = [];
540 end
541 elseif sn.sched(ep_ist) == SchedStrategy.LCFS
542 idx = find(ep_space_buf == ep_class);
543 if length(idx) >= consume_count
544 ep_space_buf(idx(1:consume_count)) = [];
545 end
546 elseif sn.sched(ep_ist) == SchedStrategy.SIRO
547 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
548 elseif sn.nodetype(ep_ind) == NodeType.Place
549 % Place count-based state format
550 state_len = length(ep_space_buf);
551 if state_len > R
552 buf_jobs = ep_space_buf(ep_class);
553 srv_jobs = ep_space_buf(R + ep_class);
554 total_jobs = buf_jobs + srv_jobs;
555 remaining = total_jobs - consume_count;
556 ep_space_buf(ep_class) = remaining;
557 ep_space_buf(R + ep_class) = 0;
558 else
559 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
560 end
561 else
562 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(ep_ist))));
563 end
564 outglspace{ep_isf} = ep_space_buf;
565 end
566 end
567
568 %% Process ID_POST events
569 for j=1:length(glevent.passive)
570 if glevent.passive{j}.event == EventType.POST
571 fp_linidx = glevent.passive{j}.node;
572 [fp_ind, fp_class] = ind2sub([sn.nnodes, R], fp_linidx);
573 if fp_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(fp_ind)) || sn.nodeToStateful(fp_ind) <= 0
574 continue;
575 end
576 fp_isf = sn.nodeToStateful(fp_ind);
577 fp_ist = sn.nodeToStation(fp_ind);
578 fp_space_buf = outglspace{fp_isf};
579 produce_count = glevent.passive{j}.weight;
580 % Check scheduling strategy and state format to determine handling
581 % Use original glspace length for detection (before PRE modified it)
582 orig_len = length(glspace{fp_isf});
583 is_queue_based = sn.nodetype(fp_ind) == NodeType.Place && ...
584 orig_len ~= R && orig_len ~= 2*R;
585 if is_queue_based || sn.sched(fp_ist) == SchedStrategy.FCFS || sn.sched(fp_ist) == SchedStrategy.LCFS
586 fp_space_buf = [repmat(fp_class, 1, produce_count), fp_space_buf];
587 elseif sn.sched(fp_ist) == SchedStrategy.SIRO
588 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
589 elseif sn.nodetype(fp_ind) == NodeType.Place
590 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
591 else
592 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(fp_ist))));
593 end
594 outglspace{fp_isf} = fp_space_buf;
595 end
596 end
597 end
598 end
599 end
600 end
601end
602
603if isempty(outcomp) && ~isempty(outrate)
604 outcomp = false(numel(outrate),1); % ENABLE outcomes are never completions
605end
606
607% TODO: if there are FIRE events with Immediate rate it
608% is unclear if the ENABLE event should get priority.
609% The order of choice of what fires could change the system behavior.
610% On the other hand, this could cause perpetual loops if the enabling is
611% applied with priority on the same state with a FIRE?
612if isSimulation
613 if size(outspace,1) > 1 && ~isempty(outprob)
614 % Use effective rate (rate * prob) for selection to avoid selecting zero-prob outcomes
615 eff_rate = outrate .* outprob;
616 tot_rate = sum(eff_rate);
617 if tot_rate > 0
618 cum_rate = cumsum(eff_rate) / tot_rate;
619 firing_ctr = 1 + max([0,find( rand > cum_rate' )]); % select action
620 outspace = outspace(firing_ctr,:);
621 outrate = sum(eff_rate); % return effective rate
622 outprob = 1.0; % probability is already incorporated into rate
623 if numel(outcomp) >= firing_ctr
624 outcomp = outcomp(firing_ctr);
625 end
626 else
627 % All effective rates are zero, pick first option with non-zero prob if any
628 valid_idx = find(outprob > 0, 1);
629 if isempty(valid_idx)
630 valid_idx = 1;
631 end
632 outspace = outspace(valid_idx,:);
633 outrate = 0;
634 outprob = outprob(valid_idx,:);
635 if numel(outcomp) >= valid_idx
636 outcomp = outcomp(valid_idx);
637 end
638 end
639 % Update the global state with the selected outcome for the active node
640 if exist('isf', 'var') && isf > 0 && isf <= length(outglspace)
641 outglspace{isf} = outspace;
642 end
643 end
644end
645end
Definition fjtag.m:161