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 % NOTE: D0 off-diagonal phase transitions are emitted by the
259 % PHASE sync action (refreshSync produces a PHASE event per
260 % Transition mode, which dispatches to afterEventTransition
261 % to enumerate the same D0 moves). Including them here would
262 % double-count: e.g., for Erlang(k) closed loop with N=1, the
263 % observed Tput would be 2k/(k+1) instead of the correct 1.0.
264
265 % Add completions (D1) - these update places
266 % Track which outcomes are completions (vs phase transitions)
267 is_completion = false(size(outspace,1), 1); % mark existing as phase transitions
268 % An IMMEDIATE mode fires in zero time, so its firing process is
269 % not the rate: the mode's distribution is ignored and the firing
270 % is emitted at GlobalConstants.Immediate scaled by the mode's
271 % firing weight. Only the ratio of these rates matters, since it
272 % sets the branching probabilities among the immediate modes that
273 % are enabled together, and the states in which they fire are
274 % vanishing and removed by stochastic complementation in
275 % solver_ctmc. The Immediate scale makes any timed mode enabled in
276 % the same marking lose the race, which is the priority of
277 % immediate over timed transitions required by GSPN semantics.
278 isImmediateMode = false;
279 if isfield(sn.nodeparam{ind}, 'timing') && ~isempty(sn.nodeparam{ind}.timing) ...
280 && mode <= numel(sn.nodeparam{ind}.timing)
281 isImmediateMode = sn.nodeparam{ind}.timing(mode) == TimingStrategy.IMMEDIATE;
282 end
283 immediateWeight = 1.0;
284 if isImmediateMode && isfield(sn.nodeparam{ind}, 'fireweight') ...
285 && ~isempty(sn.nodeparam{ind}.fireweight) ...
286 && mode <= numel(sn.nodeparam{ind}.fireweight)
287 immediateWeight = sn.nodeparam{ind}.fireweight(mode);
288 end
289
290 % A completion consumes enabling_m tokens from the input places
291 % (the PRE below), so it can only fire while those places still
292 % hold them. en_degree_m is exactly that count -- how many
293 % concurrent firings the current place marking can support -- and
294 % is zero when a place is short. A server can be left latched in a
295 % marking that no longer enables it: two immediate modes contend
296 % for one token, the first consumes it, and the second stays in
297 % execution with kim > 0 though its input place is now empty.
298 % Gating on kim alone then fired it and drove the place negative.
299 % Requiring en_degree_m >= 1 is the enabling condition the firing
300 % must satisfy, and it prevents the invalid successor at the
301 % source rather than filtering it downstream.
302 % An immediate mode is gated on the marking rather than on the
303 % servers latched by an earlier ENABLE event.
304 %
305 % Latching costs no time for a timed mode, whose ENABLE runs at
306 % GlobalConstants.Immediate while its firing runs at an ordinary
307 % rate. For an immediate mode the two are the same scale, so a
308 % marking that enables two immediate modes can fire the one that
309 % happens to be latched before the other is latched at all, and
310 % the branching then follows the latching order instead of the
311 % firing weights: with weights 1 and 3 the split came out at
312 % 0.344 rather than 0.25. Gating on the marking makes every
313 % latch variant of a marking offer the same firing alternatives,
314 % which is the weight ratio GSPN semantics require.
315 if isImmediateMode
316 imm_nservers = sn.nodeparam{ind}.nmodeservers(mode);
317 if isinf(imm_nservers)
318 imm_nservers = GlobalConstants.MaxInt();
319 end
320 imm_servers_m = min(mark_degree_m, imm_nservers);
321 else
322 imm_servers_m = 0;
323 end
324 for k = 1:fK(mode) % source phase
325 if isImmediateMode
326 fires_k = (k == 1) && imm_servers_m >= 1;
327 else
328 fires_k = kim(:,mode,k) > 0 && en_degree_m >= 1; % servers in phase k, and the place can still pay the PRE
329 end
330 if fires_k
331 if isImmediateMode
332 rate_kd = GlobalConstants.Immediate * immediateWeight * imm_servers_m;
333 else
334 rate_kd = sum(D1(k,:)) * kim(:,mode,k);
335 end
336 if rate_kd > 0
337 space_buf_kd = space_buf;
338 space_srv_kd = space_srv;
339 if kim(:,mode,k) > 0
340 % Decrease by one the firing server
341 space_srv_kd(fKs(mode)+k) = space_srv_kd(fKs(mode)+k) - 1;
342 % Move the server back to the disabled pool
343 space_buf_kd(mode) = space_buf_kd(mode) + 1;
344 end
345 space_fired_kd = space_fired;
346 if isSimulation
347 space_fired_kd(mode) = space_fired_kd(mode) + 1; % increment fired count only for simulation
348 end
349 outspace = [outspace; space_buf_kd, space_srv_kd, space_fired_kd, space_var]; %#ok<AGROW>
350 outrate = [outrate; rate_kd]; %#ok<AGROW>
351 outprob = [outprob; 1.0]; %#ok<AGROW>
352 is_completion = [is_completion; true]; %#ok<AGROW>
353 end
354 end
355 end
356 outglspace{isf} = outspace;
357 outcomp = is_completion;
358
359 % For simulation: select outcome first, then apply PRE/POST only if completion
360 if isSimulation && size(outspace,1) > 1 && ~isempty(outprob)
361 % Use effective rate (rate * prob) for selection
362 eff_rate = outrate .* outprob;
363 tot_rate = sum(eff_rate);
364 if tot_rate > 0
365 cum_rate = cumsum(eff_rate) / tot_rate;
366 firing_ctr = 1 + max([0,find( rand > cum_rate' )]);
367 selected_is_completion = is_completion(firing_ctr);
368 outcomp = selected_is_completion;
369 outspace = outspace(firing_ctr,:);
370 outrate = sum(eff_rate); % return effective rate
371 outprob = 1.0; % probability already incorporated
372 outglspace{isf} = outspace;
373
374 % Only process PRE/POST if this is a completion
375 if selected_is_completion
376 %% Process ID_PRE events, i.e., consume from all input places
377 for j=1:length(glevent.passive)
378 if glevent.passive{j}.event == EventType.PRE
379 ep_linidx = glevent.passive{j}.node;
380 [ep_ind, ep_class] = ind2sub([sn.nnodes, R], ep_linidx);
381 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
382 continue;
383 end
384 ep_isf = sn.nodeToStateful(ep_ind);
385 ep_space_buf = outglspace{ep_isf};
386 ep_ist = sn.nodeToStation(ep_ind);
387 % One D1 completion is ONE firing, so it consumes
388 % the arc weight, exactly as the matching POST
389 % produces it. The concurrency of the other
390 % running servers is already carried by the
391 % completion rate (scaled by the servers in the
392 % firing phase); multiplying the weight by
393 % en_degree_m here too would let a single firing
394 % consume every enabled token and would not
395 % conserve tokens against POST.
396 consume_count = glevent.passive{j}.weight;
397 % Check scheduling strategy and state format to determine handling
398 % Places with INF scheduling use queue-based state format
399 is_queue_based = sn.nodetype(ep_ind) == NodeType.Place && ...
400 length(ep_space_buf) ~= R && length(ep_space_buf) ~= 2*R;
401 if is_queue_based || sn.sched(ep_ist) == SchedStrategy.FCFS
402 % FCFS queue-based handling
403 idx = find(ep_space_buf == ep_class);
404 if length(idx) >= consume_count
405 ep_space_buf(idx(end - consume_count + 1:end)) = [];
406 end
407 elseif sn.sched(ep_ist) == SchedStrategy.LCFS
408 idx = find(ep_space_buf == ep_class);
409 if length(idx) >= consume_count
410 ep_space_buf(idx(1:consume_count)) = [];
411 end
412 elseif sn.sched(ep_ist) == SchedStrategy.SIRO
413 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
414 elseif sn.nodetype(ep_ind) == NodeType.Place
415 % Place count-based state format: [buffer(R), server_phases(sum(K))]
416 state_len = length(ep_space_buf);
417 if state_len > R
418 buf_jobs = ep_space_buf(ep_class);
419 srv_jobs = ep_space_buf(R + ep_class);
420 total_jobs = buf_jobs + srv_jobs;
421 remaining = total_jobs - consume_count;
422 ep_space_buf(ep_class) = remaining;
423 ep_space_buf(R + ep_class) = 0;
424 else
425 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
426 end
427 else
428 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(ep_ist))));
429 end
430 outglspace{ep_isf} = ep_space_buf;
431 end
432 end
433
434 %% Process ID_POST events, i.e., produce to all output places
435 for j=1:length(glevent.passive)
436 if glevent.passive{j}.event == EventType.POST
437 fp_linidx = glevent.passive{j}.node;
438 [fp_ind, fp_class] = ind2sub([sn.nnodes, R], fp_linidx);
439 if fp_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(fp_ind)) || sn.nodeToStateful(fp_ind) <= 0
440 continue;
441 end
442 fp_isf = sn.nodeToStateful(fp_ind);
443 fp_ist = sn.nodeToStation(fp_ind);
444 fp_space_buf = outglspace{fp_isf};
445 produce_count = glevent.passive{j}.weight;
446 % Check scheduling strategy and state format to determine handling
447 % Use original glspace length for detection (before PRE modified it)
448 orig_len = length(glspace{fp_isf});
449 is_queue_based = sn.nodetype(fp_ind) == NodeType.Place && ...
450 orig_len ~= R && orig_len ~= 2*R;
451 if is_queue_based || sn.sched(fp_ist) == SchedStrategy.FCFS || sn.sched(fp_ist) == SchedStrategy.LCFS
452 % Queue-based handling - prepend to buffer
453 fp_space_buf = [repmat(fp_class, 1, produce_count), fp_space_buf];
454 elseif sn.sched(fp_ist) == SchedStrategy.SIRO
455 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
456 elseif sn.nodetype(fp_ind) == NodeType.Place
457 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
458 else
459 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(fp_ist))));
460 end
461 outglspace{fp_isf} = fp_space_buf;
462 end
463 end
464 end
465 else
466 % All effective rates are zero, pick first option with non-zero prob if any
467 valid_idx = find(outprob > 0, 1);
468 if isempty(valid_idx)
469 valid_idx = 1;
470 end
471 outspace = outspace(valid_idx,:);
472 outrate = 0;
473 outprob = outprob(valid_idx,:);
474 outglspace{isf} = outspace;
475 end
476 elseif ~isSimulation
477 %% Non-simulation mode: process all completions for PRE/POST
478 % For state space analysis, we apply PRE/POST for completion outcomes
479 for j=1:length(glevent.passive)
480 if glevent.passive{j}.event == EventType.PRE
481 ep_linidx = glevent.passive{j}.node;
482 [ep_ind, ep_class] = ind2sub([sn.nnodes, R], ep_linidx);
483 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
484 continue;
485 end
486 ep_isf = sn.nodeToStateful(ep_ind);
487 ep_space_buf = outglspace{ep_isf};
488 ep_ist = sn.nodeToStation(ep_ind);
489 consume_count = glevent.passive{j}.weight; % one completion = one firing (see PRE note above)
490 % Check scheduling strategy and state format to determine handling
491 is_queue_based = sn.nodetype(ep_ind) == NodeType.Place && ...
492 length(ep_space_buf) ~= R && length(ep_space_buf) ~= 2*R;
493 if is_queue_based || sn.sched(ep_ist) == SchedStrategy.FCFS
494 % FCFS queue-based handling
495 idx = find(ep_space_buf == ep_class);
496 if length(idx) >= consume_count
497 ep_space_buf(idx(end - consume_count + 1:end)) = [];
498 end
499 elseif sn.sched(ep_ist) == SchedStrategy.LCFS
500 idx = find(ep_space_buf == ep_class);
501 if length(idx) >= consume_count
502 ep_space_buf(idx(1:consume_count)) = [];
503 end
504 elseif sn.sched(ep_ist) == SchedStrategy.SIRO
505 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
506 elseif sn.nodetype(ep_ind) == NodeType.Place
507 % Place count-based state format
508 state_len = length(ep_space_buf);
509 if state_len > R
510 buf_jobs = ep_space_buf(ep_class);
511 srv_jobs = ep_space_buf(R + ep_class);
512 total_jobs = buf_jobs + srv_jobs;
513 remaining = total_jobs - consume_count;
514 ep_space_buf(ep_class) = remaining;
515 ep_space_buf(R + ep_class) = 0;
516 else
517 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
518 end
519 else
520 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(ep_ist))));
521 end
522 outglspace{ep_isf} = ep_space_buf;
523 end
524 end
525
526 for j=1:length(glevent.passive)
527 if glevent.passive{j}.event == EventType.POST
528 fp_linidx = glevent.passive{j}.node;
529 [fp_ind, fp_class] = ind2sub([sn.nnodes, R], fp_linidx);
530 if fp_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(fp_ind)) || sn.nodeToStateful(fp_ind) <= 0
531 continue;
532 end
533 fp_isf = sn.nodeToStateful(fp_ind);
534 fp_ist = sn.nodeToStation(fp_ind);
535 fp_space_buf = outglspace{fp_isf};
536 produce_count = glevent.passive{j}.weight;
537 % Check scheduling strategy and state format to determine handling
538 % Use original glspace length for detection (before PRE modified it)
539 orig_len = length(glspace{fp_isf});
540 is_queue_based = sn.nodetype(fp_ind) == NodeType.Place && ...
541 orig_len ~= R && orig_len ~= 2*R;
542 if is_queue_based || sn.sched(fp_ist) == SchedStrategy.FCFS || sn.sched(fp_ist) == SchedStrategy.LCFS
543 fp_space_buf = [repmat(fp_class, 1, produce_count), fp_space_buf];
544 elseif sn.sched(fp_ist) == SchedStrategy.SIRO
545 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
546 elseif sn.nodetype(fp_ind) == NodeType.Place
547 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
548 else
549 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(fp_ist))));
550 end
551 outglspace{fp_isf} = fp_space_buf;
552 end
553 end
554 else
555 % Single outcome case in simulation mode
556 if ~isempty(is_completion) && is_completion(1)
557 %% Process ID_PRE events
558 for j=1:length(glevent.passive)
559 if glevent.passive{j}.event == EventType.PRE
560 ep_linidx = glevent.passive{j}.node;
561 [ep_ind, ep_class] = ind2sub([sn.nnodes, R], ep_linidx);
562 if ep_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(ep_ind)) || sn.nodeToStateful(ep_ind) <= 0
563 continue;
564 end
565 ep_isf = sn.nodeToStateful(ep_ind);
566 ep_space_buf = outglspace{ep_isf};
567 ep_ist = sn.nodeToStation(ep_ind);
568 consume_count = glevent.passive{j}.weight; % one completion = one firing (see PRE note above)
569 % Check scheduling strategy and state format to determine handling
570 is_queue_based = sn.nodetype(ep_ind) == NodeType.Place && ...
571 length(ep_space_buf) ~= R && length(ep_space_buf) ~= 2*R;
572 if is_queue_based || sn.sched(ep_ist) == SchedStrategy.FCFS
573 % FCFS queue-based handling
574 idx = find(ep_space_buf == ep_class);
575 if length(idx) >= consume_count
576 ep_space_buf(idx(end - consume_count + 1:end)) = [];
577 end
578 elseif sn.sched(ep_ist) == SchedStrategy.LCFS
579 idx = find(ep_space_buf == ep_class);
580 if length(idx) >= consume_count
581 ep_space_buf(idx(1:consume_count)) = [];
582 end
583 elseif sn.sched(ep_ist) == SchedStrategy.SIRO
584 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
585 elseif sn.nodetype(ep_ind) == NodeType.Place
586 % Place count-based state format
587 state_len = length(ep_space_buf);
588 if state_len > R
589 buf_jobs = ep_space_buf(ep_class);
590 srv_jobs = ep_space_buf(R + ep_class);
591 total_jobs = buf_jobs + srv_jobs;
592 remaining = total_jobs - consume_count;
593 ep_space_buf(ep_class) = remaining;
594 ep_space_buf(R + ep_class) = 0;
595 else
596 ep_space_buf(ep_class) = ep_space_buf(ep_class) - consume_count;
597 end
598 else
599 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(ep_ist))));
600 end
601 outglspace{ep_isf} = ep_space_buf;
602 end
603 end
604
605 %% Process ID_POST events
606 for j=1:length(glevent.passive)
607 if glevent.passive{j}.event == EventType.POST
608 fp_linidx = glevent.passive{j}.node;
609 [fp_ind, fp_class] = ind2sub([sn.nnodes, R], fp_linidx);
610 if fp_ind > length(sn.nodeToStateful) || isnan(sn.nodeToStateful(fp_ind)) || sn.nodeToStateful(fp_ind) <= 0
611 continue;
612 end
613 fp_isf = sn.nodeToStateful(fp_ind);
614 fp_ist = sn.nodeToStation(fp_ind);
615 fp_space_buf = outglspace{fp_isf};
616 produce_count = glevent.passive{j}.weight;
617 % Check scheduling strategy and state format to determine handling
618 % Use original glspace length for detection (before PRE modified it)
619 orig_len = length(glspace{fp_isf});
620 is_queue_based = sn.nodetype(fp_ind) == NodeType.Place && ...
621 orig_len ~= R && orig_len ~= 2*R;
622 if is_queue_based || sn.sched(fp_ist) == SchedStrategy.FCFS || sn.sched(fp_ist) == SchedStrategy.LCFS
623 fp_space_buf = [repmat(fp_class, 1, produce_count), fp_space_buf];
624 elseif sn.sched(fp_ist) == SchedStrategy.SIRO
625 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
626 elseif sn.nodetype(fp_ind) == NodeType.Place
627 fp_space_buf(fp_class) = fp_space_buf(fp_class) + produce_count;
628 else
629 line_error(mfilename,sprintf('Scheduling strategy %s is unsupported at places.', SchedStrategy.toText(sn.sched(fp_ist))));
630 end
631 outglspace{fp_isf} = fp_space_buf;
632 end
633 end
634 end
635 end
636 end
637 end
638end
639
640if isempty(outcomp) && ~isempty(outrate)
641 outcomp = false(numel(outrate),1); % ENABLE outcomes are never completions
642end
643
644% TODO: if there are FIRE events with Immediate rate it
645% is unclear if the ENABLE event should get priority.
646% The order of choice of what fires could change the system behavior.
647% On the other hand, this could cause perpetual loops if the enabling is
648% applied with priority on the same state with a FIRE?
649if isSimulation
650 if size(outspace,1) > 1 && ~isempty(outprob)
651 % Use effective rate (rate * prob) for selection to avoid selecting zero-prob outcomes
652 eff_rate = outrate .* outprob;
653 tot_rate = sum(eff_rate);
654 if tot_rate > 0
655 cum_rate = cumsum(eff_rate) / tot_rate;
656 firing_ctr = 1 + max([0,find( rand > cum_rate' )]); % select action
657 outspace = outspace(firing_ctr,:);
658 outrate = sum(eff_rate); % return effective rate
659 outprob = 1.0; % probability is already incorporated into rate
660 if numel(outcomp) >= firing_ctr
661 outcomp = outcomp(firing_ctr);
662 end
663 else
664 % All effective rates are zero, pick first option with non-zero prob if any
665 valid_idx = find(outprob > 0, 1);
666 if isempty(valid_idx)
667 valid_idx = 1;
668 end
669 outspace = outspace(valid_idx,:);
670 outrate = 0;
671 outprob = outprob(valid_idx,:);
672 if numel(outcomp) >= valid_idx
673 outcomp = outcomp(valid_idx);
674 end
675 end
676 % Update the global state with the selected outcome for the active node
677 if exist('isf', 'var') && isf > 0 && isf <= length(outglspace)
678 outglspace{isf} = outspace;
679 end
680 end
681end
682end
Definition fjtag.m:157
Definition Station.m:245