1function [fjmodel, fjsn, fjclassmap, fjforkmap, fjjoinmap, fjbranchmap, fjtagmap] = fjtag(model)
2% [FJMODEL, FJSN, FJCLASSMAP, FJFORKMAP, FJJOINMAP, FJBRANCHMAP, FJTAGMAP] = FJTAG(MODEL)
4% Build a tag-augmented copy of a closed fork-join model
for exact native
5% analysis by SolverCTMC/SolverSSA. For each (fork f,
class r) pair with
6% matched join j, branch b=1..B and tag t=1..njobs(r), an auxiliary
7% transient closed
class A(f,r,b,t) with population 0
is created. The tag
8% identifies
the origin job:
the fork firing (State.afterFJEvent) emits
9% one sibling per branch in classes A(f,r,1..B,t) using
the lowest free
10% tag, and
the join fires only when all siblings OF THE SAME TAG are
11% buffered, releasing one class-r job (State.afterEventJoin). Identity
12% matching
is therefore exact even when siblings overtake each other
16% fjmodel - augmented Network copy (forkStateful/isFJAugmented set)
17% fjsn - augmented NetworkStruct with sn.fjsync and
nodeparam fj
18% blocks; run
the CTMC/SSA algorithms on this struct
19% fjclassmap(a) - original class of auxiliary class a (0 for originals)
20% fjforkmap(a) - fork node of auxiliary class a (0 for originals)
21% fjjoinmap(a) - join node of auxiliary class a (0 for originals)
22% fjbranchmap(a)- branch index of auxiliary class a (0 for originals)
23% fjtagmap(a) - tag (origin-job slot) of auxiliary class a (0 for originals)
25% Copyright (c) 2012-2026, Imperial College London
35fjmodel = model.copy();
36fjmodel.allowReplace = true;
37P = fjmodel.getLinkedRoutingMatrix;
39 line_error(mfilename,'The native CTMC/SSA fork-join implementation requires
the routing topology to be generated using Network.link.');
41fjmodel.resetNetwork(true);
43fjmodel.forkStateful = true;
44fjmodel.isFJAugmented = true;
45%
the linked routing matrix
is indexed over user
nodes only (resetNetwork
46% deletes auto-inserted ClassSwitch
nodes, relink re-creates them), while
47% sn indices include them: keep
the two spaces distinct
50fjclassmap = zeros(1,K);
51fjforkmap = zeros(1,K);
52fjjoinmap = zeros(1,K);
53fjbranchmap = zeros(1,K);
56forkIndexes = find(sn.nodetype == NodeType.Fork)
';
57% per-fork bookkeeping for the sn post-edits
58forkinfo = {}; % rows: {f, j, r, branchheads, branchsets, auxmatrix (BxT)}
62 % tasks emitted per output link at each fork firing
64 if isfield(sn.nodeparam{f},'fanOut
') && ~isempty(sn.nodeparam{f}.fanOut)
65 w = round(sn.nodeparam{f}.fanOut(1));
67 for r=find(Vnodes(f,:)>0)
68 % branch heads: nodes receiving class r directly from the fork
71 if sn.rtnodes((f-1)*K+r,(jnd-1)*K+r) > 0
72 branchheads(end+1) = jnd; %#ok<AGROW>
75 B = length(branchheads);
77 line_error(mfilename,'Degenerate forks with a single output link are not supported by
the native CTMC/SSA fork-join implementation.
');
79 % branch discovery: class-r BFS closure from each head up to the join
80 branchsets = cell(1,B);
82 visitset = branchheads(b);
83 frontier = branchheads(b);
84 while ~isempty(frontier)
85 cn = frontier(1); frontier(1) = [];
86 if sn.nodetype(cn) == NodeType.Fork
87 line_error(mfilename,'Nested fork-join
is not supported by
the native CTMC/SSA fork-join implementation.
');
89 if sn.nodetype(cn) == NodeType.Join && cn ~= j
90 line_error(mfilename,'Overlapping fork-join pairs are not supported by
the native CTMC/SSA fork-join implementation.
');
94 if sn.rtnodes((cn-1)*K+r,(jnd-1)*K+s) > 0
96 line_error(mfilename,'Class switching between fork and join
is not supported by
the native CTMC/SSA fork-join implementation.
');
98 if jnd ~= j && ~ismember(jnd, visitset)
99 visitset(end+1) = jnd; %#ok<AGROW>
100 frontier(end+1) = jnd; %#ok<AGROW>
106 % trap check: every branch node must reach the join within the branch
112 if ~ismember(cn, canreach)
114 if sn.rtnodes((cn-1)*K+r,(jnd-1)*K+r) > 0
115 canreach(end+1) = cn; %#ok<AGROW>
123 if ~all(ismember(visitset, canreach))
124 line_error(mfilename,'Fork branches from which
the Join
is unreachable are not supported by
the native CTMC/SSA fork-join implementation.
');
126 branchsets{b} = visitset;
128 % tag pool size = maximum number of concurrently outstanding forked
129 % jobs of class r = population of its chain (class switching outside
130 % the fork-join section can concentrate the whole chain population
131 % in class r, e.g. a class switch on the edge into the fork)
132 c = find(sn.chains(:,r), 1);
133 T = round(sum(sn.njobs(sn.chains(c,:))));
135 line_error(mfilename,'Chains with infinite population routed through a Fork are not supported by
the native CTMC/SSA fork-join implementation.
');
137 auxmatrix = zeros(B,T);
140 auxname = sprintf('%s_f%d_b%d_t%d
', sn.classnames{r}, f, b, t);
141 auxclass = ClosedClass(fjmodel, auxname, 0, fjmodel.stations{sn.refstat(r)}, sn.classprio(r));
149 % sibling service on the branch copies the original class
151 if sn.isstation(cn) && sn.nodetype(cn) ~= NodeType.Join
152 svc = model.nodes{cn}.getService(model.classes{r});
153 fjmodel.nodes{cn}.setService(auxclass, svc.copy());
156 % register the auxiliary class at the join input section
157 fjmodel.nodes{j}.setStrategy(auxclass, JoinStrategy.STD);
158 fjmodel.nodes{j}.setRequired(auxclass, -1);
159 % sibling routing: copy the class-r branch routing; the
160 % auxiliary class terminates at the join (no outgoing row)
161 P{auxclass,auxclass} = zeros(Ip);
163 P{auxclass,auxclass}(cn,:) = P{r,r}(cn,:);
167 forkinfo(end+1,:) = {f, j, r, branchheads, branchsets, auxmatrix, w}; %#ok<AGROW>
171% complete the routing cell array over all class pairs
172Kaug = length(fjmodel.classes);
175 if size(P,1)<a || size(P,2)<b || isempty(P{a,b})
180% re-initialize output dispatchers of non-station nodes over the enlarged
181% class set (resetNetwork only re-initializes station dispatchers); the
182% fork out-links are restored by relink from the unchanged P{r,r} rows
183for nd = 1:length(fjmodel.nodes)
184 if ~isa(fjmodel.nodes{nd}, 'Station') && isprop(fjmodel.nodes{nd}, 'output
') ...
185 && ~isempty(fjmodel.nodes{nd}.output) && ismethod(fjmodel.nodes{nd}.output, 'initDispatcherJobClasses
')
186 fjmodel.nodes{nd}.output.initDispatcherJobClasses(fjmodel.classes);
191% couple each auxiliary class to its original class in the class-switching
192% mask so that chain analysis places them in the same chain: the actual
193% r->aux switch happens inside the fork firing (State.afterFJEvent) and
194% aux->r inside the join firing (State.afterEventJoin), outside the
196csm = fjmodel.csMatrix;
198 csm = logical(eye(Kaug));
201 csm(Kaug,Kaug) = false;
206 csm(fjclassmap(a),a) = true;
207 csm(a,fjclassmap(a)) = true;
210fjmodel.csMatrix = logical(csm);
212% re-initialize the default state: the copy inherits the original model's
213% initialization flag and states, which have pre-augmentation
class widths
214fjmodel.initDefault();
216fjsn = fjmodel.getStruct();
218% ---- sn post-edits ----
220% original-
class visits on
the augmented struct are polluted by
the fork
221% row (which sums to
the number of branches): copy
the corrected
visits
222% from
the original struct, whose refreshStruct applied
the MMT correction.
223% Stateful indices differ (Fork
is stateful only on
the augmented copy),
226 corig = find(sn.chains(:,r), 1);
227 cnew = find(fjsn.chains(:,r), 1);
228 for isfnew=1:fjsn.nstateful
229 ind = fjsn.statefulToNode(isfnew);
230 if sn.isstateful(ind)
231 fjsn.visits{cnew}(isfnew,r) = sn.visits{corig}(sn.nodeToStateful(ind),r);
232 else % stateful Fork: nonzero marker
for capacity gating
233 fjsn.visits{cnew}(isfnew,r) =
double(Vnodes(ind,r) > 0);
236 nshared = min(size(fjsn.nodevisits{cnew},1), size(sn.nodevisits{corig},1));
237 fjsn.nodevisits{cnew}(1:nshared,r) = sn.nodevisits{corig}(1:nshared,r);
240% auxiliary-
class visits: engines use them only as a zero-versus-nonzero
241% capacity gate, so set 1 on
the branch support set and 0 elsewhere
242for row=1:size(forkinfo,1)
243 [f, j, r, branchheads, branchsets, auxmatrix, w] = forkinfo{row,:};
244 B = size(auxmatrix,1);
245 T = size(auxmatrix,2);
246 cnew = find(fjsn.chains(:,r), 1);
248 support = [branchsets{b}, j];
251 fjsn.visits{cnew}(:,a) = 0;
252 fjsn.nodevisits{cnew}(:,a) = 0;
254 fjsn.nodevisits{cnew}(cn,a) = 1;
255 if fjsn.isstateful(cn)
256 fjsn.visits{cnew}(fjsn.nodeToStateful(cn),a) = 1;
259 % capacity: each auxiliary
class holds at most tasksPerLink
260 % siblings network-wide (STD join, one tag at a time)
261 fjsn.classcap(:,a) = 0;
263 if fjsn.isstation(cn)
264 fjsn.classcap(fjsn.nodeToStation(cn),a) = w;
269 %
nodeparam fj blocks read by State.afterEventJoin/afterFJEvent
270 if ~isstruct(fjsn.nodeparam{f})
273 if ~isfield(fjsn.nodeparam{f},
'fj')
274 fjsn.
nodeparam{f}.fj =
struct(
'classes',[],
'joins',[],
'auxmatrix',{cell(1,Kaug)},
'branchheads',{cell(1,Kaug)});
276 fjsn.nodeparam{f}.fj.classes(end+1) = r; % original classes forked here
277 fjsn.nodeparam{f}.fj.joins(end+1) = j;
278 fjsn.nodeparam{f}.fj.auxmatrix{r} = auxmatrix;
279 fjsn.nodeparam{f}.fj.branchheads{r} = branchheads;
280 if ~isstruct(fjsn.nodeparam{j})
283 if ~isfield(fjsn.nodeparam{j},
'fj')
284 fjsn.
nodeparam{j}.fj =
struct(
'fork',f,
'origclasses',[],
'auxmatrix',{cell(1,Kaug)},
'required',{cell(1,Kaug)});
286 fjsn.nodeparam{j}.fj.origclasses(end+1) = r;
287 fjsn.nodeparam{j}.fj.auxmatrix{r} = auxmatrix;
288 fjsn.nodeparam{j}.fj.required{r} = w*ones(B,1); % STD, tasksPerLink siblings per branch; slot
for PARTIAL/fanIn
291% fork firing synchronizations: one entry per (fork,
class, tag)
293for row=1:size(forkinfo,1)
294 [f, j, r, branchheads, ~, auxmatrix, w] = forkinfo{row,:};
295 T = size(auxmatrix,2);
298 entry.active{1} = Event(EventType.FIRE, f, r);
303 entry.branchheads = branchheads;
304 entry.auxclasses = auxmatrix(:,t)
';
305 entry.auxall = auxmatrix; % B x T, for the tag-occupancy scan
306 entry.weight = w; % tasksPerLink: siblings emitted per branch
308 fjsn.fjsync{end+1,1} = entry;
312% auxiliary class map, consumed by capacity gating (e.g. the SSA preamble
313% must not cap sibling multiplicities by the chain job population)
314fjsn.fjclassmap = fjclassmap;