1function [outGlobalStates, outrate, outprob] = afterFJEvent(sn, fjentry, glspace, isSimulation)
2% [OUTGLOBALSTATES, OUTRATE, OUTPROB] = AFTERFJEVENT(SN, FJENTRY, GLSPACE, ISSIMULATION)
4% Fire a fork synchronization (one entry of sn.fjsync, built by
5% ModelAdapter.fjtag) on
the global state GLSPACE, a cell array with
the
6% local state of every stateful node. The firing atomically consumes one
7% parent job of
class r held at
the (stateful) Fork node and emits one
8% sibling per branch, in
the auxiliary classes of
the entry
's tag, at the
11% Enabling condition (evaluated on the global state):
12% 1. the Fork holds at least one class-r parent job;
13% 2. the entry's tag
is the LOWEST free tag
for this (fork,
class):
14% a tag
is free iff its auxiliary classes have zero occupancy
15% network-wide. Canonical lowest-free-tag allocation ensures exactly
16% one fjsync entry per (fork, class)
is enabled in any state.
18% Outputs: OUTGLOBALSTATES
is a cell array of successor global states
19% (full glspace cells); OUTRATE
the firing rate (immediate) and OUTPROB
20%
the probability of each outcome (phase-entry mixtures of
the sibling
21% service processes at
the branch heads). When ISSIMULATION
is true a
22% single outcome
is returned, sampled internally.
24% Copyright (c) 2012-2026, Imperial College London
34isf_f = sn.nodeToStateful(f);
36% 1. parent job held at
the fork
37forkstate = glspace{isf_f};
38if forkstate(end-R+r) < 1
42% 2. lowest-free-tag test: occupancy of each tag
's auxiliary classes,
43% scanned across all stateful nodes
44auxall = fjentry.auxall; % B x T auxiliary class indices
49 [~, nir] = State.toMarginalAggr(sn, sn.statefulToNode(isf), glspace{isf});
50 nglobal = nglobal + nir(1,1:R);
52occ = sum(reshape(nglobal(auxall), size(auxall,1), T), 1);
56if any(occ(1:t-1) == 0)
57 return % a lower tag is free: that entry fires instead
60% consume the parent job at the fork
62newgl{isf_f}(end-R+r) = newgl{isf_f}(end-R+r) - 1;
64% emit fjentry.weight (tasksPerLink) siblings per branch: sequential
65% application over the partial outcome list handles branches sharing the
66% same head node, repeated emissions on the same branch, and expands
67% phase-entry mixtures of non-exponential sibling services
70B = length(fjentry.branchheads);
71emissions = repmat(1:B, 1, fjentry.weight);
73 bh = fjentry.branchheads(b);
74 isf_b = sn.nodeToStateful(bh);
75 a = fjentry.auxclasses(b);
78 for pp=1:length(partials)
80 [arvspace, ~, arvprob] = State.afterEvent(sn, bh, curgl{isf_b}, EventType.ARV, a, isSimulation, []);
82 % sibling arrival blocked (cannot occur under the per-tag
83 % auxiliary class capacity invariant); disable the firing
89 for io=1:size(arvspace,1)
91 nextgl{isf_b} = arvspace(io,:);
92 newpartials{end+1,1} = nextgl; %#ok<AGROW>
93 if length(arvprob) >= io
94 newpartprob(end+1,1) = partprob(pp)*arvprob(io); %#ok<AGROW>
96 newpartprob(end+1,1) = partprob(pp); %#ok<AGROW>
100 partials = newpartials;
101 partprob = newpartprob;
104outGlobalStates = partials;
105outprob = partprob(:) * fjentry.prob;
106outrate = GlobalConstants.Immediate * ones(length(partials),1);
109 if length(partials) > 1
110 % sample one outcome (afterEvent already samples per-node mixtures
111 % when isSimulation is true, so this is defensive)
112 cum_prob = cumsum(outprob) / sum(outprob);
113 firing_ctr = 1 + max([0,find( rand > cum_prob' )]);
114 outGlobalStates = outGlobalStates(firing_ctr);
115 outrate = outrate(firing_ctr);
117 %
the phase-entry choice has already been sampled inside afterEvent,
118 % so
the firing competes at
the full immediate rate