LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
fjtag.m
1function [fjmodel, fjsn, fjclassmap, fjforkmap, fjjoinmap, fjbranchmap, fjtagmap] = fjtag(model)
2% [FJMODEL, FJSN, FJCLASSMAP, FJFORKMAP, FJJOINMAP, FJBRANCHMAP, FJTAGMAP] = FJTAG(MODEL)
3%
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
13% across branches.
14%
15% Outputs:
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)
24
25% Copyright (c) 2012-2026, Imperial College London
26% All rights reserved.
27
28sn = model.getStruct;
29sn_fj_validate(sn);
30
31K = sn.nclasses;
32I = sn.nnodes;
33Vnodes = cellsum(sn.nodevisits);
34
35fjmodel = model.copy();
36fjmodel.allowReplace = true;
37P = fjmodel.getLinkedRoutingMatrix;
38if isempty(P)
39 line_error(mfilename,'The native CTMC/SSA fork-join implementation requires the routing topology to be generated using Network.link.');
40end
41fjmodel.resetNetwork(true);
42fjmodel.resetStruct();
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
48Ip = size(P{1,1},1);
49
50fjclassmap = zeros(1,K);
51fjforkmap = zeros(1,K);
52fjjoinmap = zeros(1,K);
53fjbranchmap = zeros(1,K);
54fjtagmap = zeros(1,K);
55
56forkIndexes = find(sn.nodetype == NodeType.Fork)';
57% per-fork bookkeeping for the sn post-edits
58forkinfo = {}; % rows: {f, j, r, branchheads, branchsets, auxmatrix (BxT)}
59
60for f=forkIndexes
61 j = find(sn.fj(f,:));
62 % tasks emitted per output link at each fork firing
63 w = 1;
64 if isfield(sn.nodeparam{f},'fanOut') && ~isempty(sn.nodeparam{f}.fanOut)
65 w = round(sn.nodeparam{f}.fanOut(1));
66 end
67 for r=find(Vnodes(f,:)>0)
68 % branch heads: nodes receiving class r directly from the fork
69 branchheads = [];
70 for jnd=1:I
71 if sn.rtnodes((f-1)*K+r,(jnd-1)*K+r) > 0
72 branchheads(end+1) = jnd; %#ok<AGROW>
73 end
74 end
75 B = length(branchheads);
76 if B < 2
77 line_error(mfilename,'Degenerate forks with a single output link are not supported by the native CTMC/SSA fork-join implementation.');
78 end
79 % branch discovery: class-r BFS closure from each head up to the join
80 branchsets = cell(1,B);
81 for b=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.');
88 end
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.');
91 end
92 for jnd=1:I
93 for s=1:K
94 if sn.rtnodes((cn-1)*K+r,(jnd-1)*K+s) > 0
95 if s ~= r
96 line_error(mfilename,'Class switching between fork and join is not supported by the native CTMC/SSA fork-join implementation.');
97 end
98 if jnd ~= j && ~ismember(jnd, visitset)
99 visitset(end+1) = jnd; %#ok<AGROW>
100 frontier(end+1) = jnd; %#ok<AGROW>
101 end
102 end
103 end
104 end
105 end
106 % trap check: every branch node must reach the join within the branch
107 canreach = j;
108 changed = true;
109 while changed
110 changed = false;
111 for cn=visitset
112 if ~ismember(cn, canreach)
113 for jnd=canreach
114 if sn.rtnodes((cn-1)*K+r,(jnd-1)*K+r) > 0
115 canreach(end+1) = cn; %#ok<AGROW>
116 changed = true;
117 break
118 end
119 end
120 end
121 end
122 end
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.');
125 end
126 branchsets{b} = visitset;
127 end
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,:))));
134 if ~isfinite(T)
135 line_error(mfilename,'Chains with infinite population routed through a Fork are not supported by the native CTMC/SSA fork-join implementation.');
136 end
137 auxmatrix = zeros(B,T);
138 for t=1:T
139 for b=1:B
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));
142 a = auxclass.index;
143 auxmatrix(b,t) = a;
144 fjclassmap(a) = r;
145 fjforkmap(a) = f;
146 fjjoinmap(a) = j;
147 fjbranchmap(a) = b;
148 fjtagmap(a) = t;
149 % sibling service on the branch copies the original class
150 for cn=branchsets{b}
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());
154 end
155 end
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);
162 for cn=branchsets{b}
163 P{auxclass,auxclass}(cn,:) = P{r,r}(cn,:);
164 end
165 end
166 end
167 forkinfo(end+1,:) = {f, j, r, branchheads, branchsets, auxmatrix, w}; %#ok<AGROW>
168 end
169end
170
171% complete the routing cell array over all class pairs
172Kaug = length(fjmodel.classes);
173for a=1:Kaug
174 for b=1:Kaug
175 if size(P,1)<a || size(P,2)<b || isempty(P{a,b})
176 P{a,b} = zeros(Ip);
177 end
178 end
179end
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);
187 end
188end
189fjmodel.relink(P);
190
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
195% routing matrix
196csm = fjmodel.csMatrix;
197if isempty(csm)
198 csm = logical(eye(Kaug));
199end
200if size(csm,1) < Kaug
201 csm(Kaug,Kaug) = false;
202end
203for a=1:Kaug
204 csm(a,a) = true;
205 if fjclassmap(a) > 0
206 csm(fjclassmap(a),a) = true;
207 csm(a,fjclassmap(a)) = true;
208 end
209end
210fjmodel.csMatrix = logical(csm);
211
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();
215
216fjsn = fjmodel.getStruct();
217
218% ---- sn post-edits ----
219
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),
224% so visits rows are remapped node by node; nodevisits rows align directly.
225for r=1:K
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);
234 end
235 end
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);
238end
239
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);
247 for b=1:B
248 support = [branchsets{b}, j];
249 for t=1:T
250 a = auxmatrix(b,t);
251 fjsn.visits{cnew}(:,a) = 0;
252 fjsn.nodevisits{cnew}(:,a) = 0;
253 for cn=support
254 fjsn.nodevisits{cnew}(cn,a) = 1;
255 if fjsn.isstateful(cn)
256 fjsn.visits{cnew}(fjsn.nodeToStateful(cn),a) = 1;
257 end
258 end
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;
262 for cn=support
263 if fjsn.isstation(cn)
264 fjsn.classcap(fjsn.nodeToStation(cn),a) = w;
265 end
266 end
267 end
268 end
269 % nodeparam fj blocks read by State.afterEventJoin/afterFJEvent
270 if ~isstruct(fjsn.nodeparam{f})
271 fjsn.nodeparam{f} = struct();
272 end
273 if ~isfield(fjsn.nodeparam{f},'fj')
274 fjsn.nodeparam{f}.fj = struct('classes',[],'joins',[],'auxmatrix',{cell(1,Kaug)},'branchheads',{cell(1,Kaug)});
275 end
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})
281 fjsn.nodeparam{j} = struct();
282 end
283 if ~isfield(fjsn.nodeparam{j},'fj')
284 fjsn.nodeparam{j}.fj = struct('fork',f,'origclasses',[],'auxmatrix',{cell(1,Kaug)},'required',{cell(1,Kaug)});
285 end
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
289end
290
291% fork firing synchronizations: one entry per (fork, class, tag)
292fjsn.fjsync = {};
293for row=1:size(forkinfo,1)
294 [f, j, r, branchheads, ~, auxmatrix, w] = forkinfo{row,:};
295 T = size(auxmatrix,2);
296 for t=1:T
297 entry = struct();
298 entry.active{1} = Event(EventType.FIRE, f, r);
299 entry.fork = f;
300 entry.join = j;
301 entry.class = r;
302 entry.tag = t;
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
307 entry.prob = 1.0;
308 fjsn.fjsync{end+1,1} = entry;
309 end
310end
311
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;
315
316fjmodel.sn = fjsn;
317
318end
Definition fjtag.m:157
Definition Station.m:245