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 % see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
93 if cn > Ip
94 line_error(mfilename,'Class switching between fork and join is not supported by the native CTMC/SSA fork-join implementation.');
95 end
96 for jnd=1:I
97 for s=1:K
98 if sn.rtnodes((cn-1)*K+r,(jnd-1)*K+s) > 0
99 if s ~= r
100 line_error(mfilename,'Class switching between fork and join is not supported by the native CTMC/SSA fork-join implementation.');
101 end
102 if jnd ~= j && ~ismember(jnd, visitset)
103 visitset(end+1) = jnd; %#ok<AGROW>
104 frontier(end+1) = jnd; %#ok<AGROW>
105 end
106 end
107 end
108 end
109 end
110 % trap check: every branch node must reach the join within the branch
111 canreach = j;
112 changed = true;
113 while changed
114 changed = false;
115 for cn=visitset
116 if ~ismember(cn, canreach)
117 for jnd=canreach
118 if sn.rtnodes((cn-1)*K+r,(jnd-1)*K+r) > 0
119 canreach(end+1) = cn; %#ok<AGROW>
120 changed = true;
121 break
122 end
123 end
124 end
125 end
126 end
127 if ~all(ismember(visitset, canreach))
128 line_error(mfilename,'Fork branches from which the Join is unreachable are not supported by the native CTMC/SSA fork-join implementation.');
129 end
130 branchsets{b} = visitset;
131 end
132 % tag pool size = maximum number of concurrently outstanding forked
133 % jobs of class r = population of its chain (class switching outside
134 % the fork-join section can concentrate the whole chain population
135 % in class r, e.g. a class switch on the edge into the fork)
136 c = find(sn.chains(:,r), 1);
137 T = round(sum(sn.njobs(sn.chains(c,:))));
138 if ~isfinite(T)
139 line_error(mfilename,'Chains with infinite population routed through a Fork are not supported by the native CTMC/SSA fork-join implementation.');
140 end
141 auxmatrix = zeros(B,T);
142 for t=1:T
143 for b=1:B
144 auxname = sprintf('%s_f%d_b%d_t%d', sn.classnames{r}, f, b, t);
145 auxclass = ClosedClass(fjmodel, auxname, 0, fjmodel.stations{sn.refstat(r)}, sn.classprio(r));
146 a = auxclass.index;
147 auxmatrix(b,t) = a;
148 fjclassmap(a) = r;
149 fjforkmap(a) = f;
150 fjjoinmap(a) = j;
151 fjbranchmap(a) = b;
152 fjtagmap(a) = t;
153 % sibling service on the branch copies the original class
154 for cn=branchsets{b}
155 if sn.isstation(cn) && sn.nodetype(cn) ~= NodeType.Join
156 svc = model.nodes{cn}.getService(model.classes{r});
157 fjmodel.nodes{cn}.setService(auxclass, svc.copy());
158 end
159 end
160 % register the auxiliary class at the join input section
161 fjmodel.nodes{j}.setStrategy(auxclass, JoinStrategy.STD);
162 fjmodel.nodes{j}.setRequired(auxclass, -1);
163 % sibling routing: copy the class-r branch routing; the
164 % auxiliary class terminates at the join (no outgoing row)
165 P{auxclass,auxclass} = zeros(Ip);
166 for cn=branchsets{b}
167 P{auxclass,auxclass}(cn,:) = P{r,r}(cn,:);
168 end
169 end
170 end
171 forkinfo(end+1,:) = {f, j, r, branchheads, branchsets, auxmatrix, w}; %#ok<AGROW>
172 end
173end
174
175% complete the routing cell array over all class pairs
176Kaug = length(fjmodel.classes);
177for a=1:Kaug
178 for b=1:Kaug
179 if size(P,1)<a || size(P,2)<b || isempty(P{a,b})
180 P{a,b} = zeros(Ip);
181 end
182 end
183end
184% re-initialize output dispatchers of non-station nodes over the enlarged
185% class set (resetNetwork only re-initializes station dispatchers); the
186% fork out-links are restored by relink from the unchanged P{r,r} rows
187for nd = 1:length(fjmodel.nodes)
188 if ~isa(fjmodel.nodes{nd}, 'Station') && isprop(fjmodel.nodes{nd}, 'output') ...
189 && ~isempty(fjmodel.nodes{nd}.output) && ismethod(fjmodel.nodes{nd}.output, 'initDispatcherJobClasses')
190 fjmodel.nodes{nd}.output.initDispatcherJobClasses(fjmodel.classes);
191 end
192end
193fjmodel.relink(P);
194
195% see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
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% see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
221for r=1:K
222 corig = find(sn.chains(:,r), 1);
223 cnew = find(fjsn.chains(:,r), 1);
224 for isfnew=1:fjsn.nstateful
225 ind = fjsn.statefulToNode(isfnew);
226 if sn.isstateful(ind)
227 fjsn.visits{cnew}(isfnew,r) = sn.visits{corig}(sn.nodeToStateful(ind),r);
228 else % stateful Fork: nonzero marker for capacity gating
229 fjsn.visits{cnew}(isfnew,r) = double(Vnodes(ind,r) > 0);
230 end
231 end
232 nshared = min(size(fjsn.nodevisits{cnew},1), size(sn.nodevisits{corig},1));
233 fjsn.nodevisits{cnew}(1:nshared,r) = sn.nodevisits{corig}(1:nshared,r);
234end
235
236% auxiliary-class visits: engines use them only as a zero-versus-nonzero
237% capacity gate, so set 1 on the branch support set and 0 elsewhere
238for row=1:size(forkinfo,1)
239 [f, j, r, branchheads, branchsets, auxmatrix, w] = forkinfo{row,:};
240 B = size(auxmatrix,1);
241 T = size(auxmatrix,2);
242 cnew = find(fjsn.chains(:,r), 1);
243 for b=1:B
244 support = [branchsets{b}, j];
245 for t=1:T
246 a = auxmatrix(b,t);
247 fjsn.visits{cnew}(:,a) = 0;
248 fjsn.nodevisits{cnew}(:,a) = 0;
249 for cn=support
250 fjsn.nodevisits{cnew}(cn,a) = 1;
251 if fjsn.isstateful(cn)
252 fjsn.visits{cnew}(fjsn.nodeToStateful(cn),a) = 1;
253 end
254 end
255 % capacity: each auxiliary class holds at most tasksPerLink
256 % siblings network-wide (STD join, one tag at a time)
257 fjsn.classcap(:,a) = 0;
258 for cn=support
259 if fjsn.isstation(cn)
260 fjsn.classcap(fjsn.nodeToStation(cn),a) = w;
261 end
262 end
263 end
264 end
265 % nodeparam fj blocks read by State.afterEventJoin/afterFJEvent
266 if ~isstruct(fjsn.nodeparam{f})
267 fjsn.nodeparam{f} = struct();
268 end
269 if ~isfield(fjsn.nodeparam{f},'fj')
270 fjsn.nodeparam{f}.fj = struct('classes',[],'joins',[],'auxmatrix',{cell(1,Kaug)},'branchheads',{cell(1,Kaug)});
271 end
272 fjsn.nodeparam{f}.fj.classes(end+1) = r; % original classes forked here
273 fjsn.nodeparam{f}.fj.joins(end+1) = j;
274 fjsn.nodeparam{f}.fj.auxmatrix{r} = auxmatrix;
275 fjsn.nodeparam{f}.fj.branchheads{r} = branchheads;
276 if ~isstruct(fjsn.nodeparam{j})
277 fjsn.nodeparam{j} = struct();
278 end
279 if ~isfield(fjsn.nodeparam{j},'fj')
280 fjsn.nodeparam{j}.fj = struct('fork',f,'origclasses',[],'auxmatrix',{cell(1,Kaug)},'required',{cell(1,Kaug)});
281 end
282 fjsn.nodeparam{j}.fj.origclasses(end+1) = r;
283 fjsn.nodeparam{j}.fj.auxmatrix{r} = auxmatrix;
284 fjsn.nodeparam{j}.fj.required{r} = w*ones(B,1); % STD, tasksPerLink siblings per branch; slot for PARTIAL/fanIn
285end
286
287% fork firing synchronizations: one entry per (fork, class, tag)
288fjsn.fjsync = {};
289for row=1:size(forkinfo,1)
290 [f, j, r, branchheads, ~, auxmatrix, w] = forkinfo{row,:};
291 T = size(auxmatrix,2);
292 for t=1:T
293 entry = struct();
294 entry.active{1} = Event(EventType.FIRE, f, r);
295 entry.fork = f;
296 entry.join = j;
297 entry.class = r;
298 entry.tag = t;
299 entry.branchheads = branchheads;
300 entry.auxclasses = auxmatrix(:,t)';
301 entry.auxall = auxmatrix; % B x T, for the tag-occupancy scan
302 entry.weight = w; % tasksPerLink: siblings emitted per branch
303 entry.prob = 1.0;
304 fjsn.fjsync{end+1,1} = entry;
305 end
306end
307
308% auxiliary class map, consumed by capacity gating (e.g. the SSA preamble
309% must not cap sibling multiplicities by the chain job population)
310fjsn.fjclassmap = fjclassmap;
311
312fjmodel.sn = fjsn;
313
314end
Definition fjtag.m:161