1function [nonfjmodel, fjclassmap, fjforkmap, fanout, prov] = mmt(model, forkLambda)
2% build a model with fork-joins replaced by routers and delays and
3% parallelism simulated by artificial classes
4% forkLambda(s)
is the arrival rate of artificial
class s
5% s = fjclassmap(r)
for auxiliary
class r gives the index s of the original class
6% f = fjforkmap(r)
for auxiliary
class r gives the index of the associated fork node f
7% fo = fanout(r)
is the number of output jobs across all links
for the (fork f,
class s) pair modelled by auxiliary
class r
9% prov records the provenance of every service slot of nonfjmodel, so that a
10% caller holding on to
this transformation across an outer fixed point (see
11% SolverMVA.runAnalyzer, driven by SolverLN) can re-feed it from the base model
12% instead of rebuilding it -- the rebuild costs a full model.copy() per outer
13% iteration, and the transformation depends only on the fork topology, which
14% does not change. See ModelAdapter.refreshServicesFromBase. Fields:
15% prov.serviceSrc (nslots x 3) rows [nodeIdx, nonfjClassIdx, baseClassIdx]
16% -- base-derived: re-read from the base model on reuse.
17% prov.immediateSlots (nslots x 2) rows [nodeIdx, nonfjClassIdx]
18% -- owned by
this transformation (the joins it turns into
19% zero-service delays). These must be RESTORED, not merely
20% left alone: the fork loop overwrites each join with its
21% current synchronisation delay on every pass, so a reused
22% model still holds the previous outer iteration
's value.
23% prov.forkLambdaInit, prov.baseModel -- inputs needed to reproduce the
24% auxiliary-class source arrivals a cold call would set.
25% Slots in neither map are owned by the forkLambda fixed point (the auxiliary
26% arrivals) and are reset from forkLambdaInit. Reading a transformation-owned
27% slot from the base model corrupts the transform silently; keeping a stale one
28% silently warm-starts the fork loop.
30%% this has been migrated to Java inside FJ.java as mmt
34 forkLambda = GlobalConstants.FineTol * ones(1,sn.nclasses);
39% Provenance of nonfjmodel's service slots (see the header). The original
40% classes at the original stations carry over from the copy() below, so their
41% provenance
is the identity; the joins overwrite theirs with Immediate and are
43prov = struct('serviceSrc', zeros(0,3), 'immediateSlots', zeros(0,2), ...
44 'auxArrival', zeros(0,3), 'auxSource', [], ...
45 'forkLambdaInit', forkLambda, 'baseModel', model);
49 case {NodeType.Source, NodeType.Fork, NodeType.Join}
50 % Source/Fork hold no per-
class service; Join
is overwritten
51 % with Immediate below and so
is transformation-owned.
54 if ~isempty(model.nodes{i}.getService(model.classes{r}))
55 prov.serviceSrc(end+1,:) = [i, r, r]; %
#ok<AGROW>
61% we create an equivalent model without fj stations
62nonfjmodel = model.copy();
63nonfjmodel.allowReplace =
true;
64P = nonfjmodel.getLinkedRoutingMatrix;
65nonfjmodel.resetNetwork(
true);
66nonfjmodel.resetStruct();
68 line_error(mfilename,
'SolverMVA can process fork-join networks only if their routing topology has been generated using Network.link.');
70Vnodes = cellsum(sn.nodevisits);
72forkIndexes = find(sn.nodetype == NodeType.Fork)
';
73% replaces forks and joins with routers
77 if length(model.nodes{f}.output.outputStrategy{r})>2
78 origfanout(f,r) = length(model.nodes{f}.output.outputStrategy{r}{3});
80 P{r,s}(f,:) = P{r,s}(f,:) / origfanout(f,r);
86 % replace Join with a Router
87 nonfjmodel.nodes{f} = Router(nonfjmodel, nonfjmodel.nodes{f}.name);
88 % replace Fork with a StatelessClassSwitcher that doesn't
90 %nonfjmodel.nodes{f} = ClassSwitch(nonfjmodel, nonfjmodel.nodes{f}.name, eye(sn.nclasses));
91 forkedClasses{f,1} = find(Vnodes(f,:)>0); %#ok<AGROW>
93for j=find(sn.nodetype == NodeType.Join)
'
94 % replace Join with an Infinite Server
95 nonfjmodel.nodes{j} = Delay(nonfjmodel, nonfjmodel.nodes{j}.name);
96 nonfjmodel.stations{model.nodes{j}.stationIndex} = nonfjmodel.nodes{j};
97 for c=1:length(nonfjmodel.classes)
98 nonfjmodel.nodes{j}.setService(nonfjmodel.classes{c},Immediate());
99 prov.immediateSlots(end+1,:) = [j, c]; %#ok<AGROW>
102nonfjmodel.stations={nonfjmodel.stations{1:model.getNumberOfStations}}'; % remove automatically added station and put it where the join was
103%
if they don
't exist already, add source and sink
104if nonfjmodel.hasOpenClasses
105 source = nonfjmodel.getSource;
106 sink = nonfjmodel.getSink;
108 source = Source(nonfjmodel,'Source
');
109 sink = Sink(nonfjmodel,'Sink
');
113 P{r,s}(length(nonfjmodel.nodes),length(nonfjmodel.nodes)) = 0;
114 P{s,r}(length(nonfjmodel.nodes),length(nonfjmodel.nodes)) = 0;
117nonfjmodel.connections = zeros(length(nonfjmodel.nodes));
119all_aux_class_indices = []; % aux class indices for post-relink routing fix
121 % find join associated to fork f
122 joinIdx = find(sn.fj(f,:));
124 line_error(mfilename,'SolverMVA supports at present only a single join station per fork node.
');
126 % find chains associated to classes forked by f
127 forkedChains = find(sum(sn.chains(:,forkedClasses{f}),2));
129 % create a
new open
class for each class in forkedChains
131 inchain = find(sn.chains(fc,:)); inchain = inchain(:)
';
133 % see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
134 refStationIdx = sn.refstat(inchain(1));
135 refStatefulIdx = sn.stationToStateful(refStationIdx);
136 refNodeIdx = sn.statefulToNode(refStatefulIdx);
138 reachableFromRef = false(sn.nnodes, K);
139 bfsQueue = zeros(0, 2); % [node, class] pairs
141 reachableFromRef(refNodeIdx, ci) = true;
142 bfsQueue(end+1,:) = [refNodeIdx, ci]; %#ok<AGROW>
144 while ~isempty(bfsQueue)
145 cn = bfsQueue(1,1); cc = bfsQueue(1,2);
147 for destNode = 1:sn.nnodes
149 srcIdx = (cn-1)*K + cc;
150 dstIdx = (destNode-1)*K + dci;
151 if ~reachableFromRef(destNode, dci) && sn.rtnodes(srcIdx, dstIdx) > 0
152 reachableFromRef(destNode, dci) = true;
153 bfsQueue(end+1,:) = [destNode, dci]; %#ok<AGROW>
160 oclass{end+1} = OpenClass(nonfjmodel,[nonfjmodel.classes{r}.name,'.
',nonfjmodel.nodes{f}.name]); %#ok<AGROW>
161 fjclassmap(oclass{end}.index) = nonfjmodel.classes{r}.index;
162 fjforkmap(oclass{end}.index) = f;
163 s = fjclassmap(oclass{end}.index); % auxiliary class index
164 if model.nodes{f}.output.tasksPerLink > 1
165 line_warning(mfilename, 'There are no synchronisation delays implemented in MMT for multiple tasks per link. Results may be inaccurate.
');
167 fanout(oclass{end}.index) = origfanout(f,r)*model.nodes{f}.output.tasksPerLink;
168 all_aux_class_indices(end+1) = oclass{end}.index; %#ok<AGROW>
169 disableAux = origfanout(f,r) == 0 || ~reachableFromRef(f, r);
171 source.setArrival(oclass{end},Disabled.getInstance);
173 source.setArrival(oclass{end},Exp(forkLambda(r)));
175 % Auxiliary arrivals are owned by the forkLambda fixed point, not by
176 % the base model. Record what a cold call sets here (note the rate is
177 % indexed by the ORIGINAL class r) so that reuse can restore it.
178 prov.auxArrival(end+1,:) = [oclass{end}.index, r, disableAux]; %#ok<AGROW>
179 prov.auxSource = source;
180 % joins are now Delays, let us set their service time
183 switch sn.nodetype(i)
185 nonfjmodel.nodes{i}.setService(oclass{end},Immediate());
186 prov.immediateSlots(end+1,:) = [i, oclass{end}.index]; %#ok<AGROW>
187 case {NodeType.Source, NodeType.Fork}
190 nonfjmodel.nodes{i}.setService(oclass{end},model.nodes{i}.getService(model.classes{r}).copy());
191 prov.serviceSrc(end+1,:) = [i, oclass{end}.index, r]; %#ok<AGROW>
200 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}} = P{r,s};
205 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(source,:) = 0.0;
207 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(nonfjmodel.nodes{joinIdx},:) = 0.0;
210 if origfanout(f,r) > 0
211 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(source, nonfjmodel.nodes{f}) = 1.0;
213 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(nonfjmodel.nodes{joinIdx},sink) = 1.0;
217 % see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
218 pnnodes = size(P{inchain(1),inchain(1)}, 1);
219 maxclass = max(inchain);
220 % Source/Sink/Fork/Join rows are mmt infrastructure: their aux
221 % routing is set explicitly above, so they are never cleared.
222 infra_mask = false(1, pnnodes);
223 infra_mask(f) = true;
224 if ~isempty(joinIdx), infra_mask(joinIdx) = true; end
226 if isa(nonfjmodel.nodes{nd}, 'Source
') || isa(nonfjmodel.nodes{nd}, 'Sink
')
227 infra_mask(nd) = true;
230 % Class-aware BFS: find all (node, class) pairs reachable from Fork
231 visited = false(pnnodes, maxclass);
232 bfs_q = zeros(0, 2); % [node, class] pairs
233 % Seed: classes entering the fork
234 for ri = 1:length(inchain)
236 for si = 1:length(inchain)
238 if any(P{r,s}(f,:) > 0) && ~visited(f, r)
239 visited(f, r) = true;
240 bfs_q(end+1,:) = [f, r]; %#ok<AGROW>
244 while ~isempty(bfs_q)
245 cn = bfs_q(1,1); cc = bfs_q(1,2);
247 for si = 1:length(inchain)
250 if P{cc,s}(cn, nd) > 0 && ~visited(nd, s)
251 visited(nd, s) = true;
252 % Continue BFS unless this is Join
253 if isempty(joinIdx) || nd ~= joinIdx
254 bfs_q(end+1,:) = [nd, s]; %#ok<AGROW>
260 % see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
263 for ri = 1:length(inchain)
264 if ~visited(nd, inchain(ri))
265 for si = 1:length(inchain)
266 P{oclass{ri}, oclass{si}}(nd, :) = 0.0;
272 % see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
273 for ri = 1:length(inchain)
274 if ~any(visited(:, inchain(ri)))
275 P{oclass{ri}, oclass{ri}}(source, sink) = 1.0;
280% see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
281for nd = 1:length(nonfjmodel.nodes)
282 if ~isa(nonfjmodel.nodes{nd}, 'Station') && isprop(nonfjmodel.nodes{nd}, 'output
') ...
283 && ~isempty(nonfjmodel.nodes{nd}.output) && ismethod(nonfjmodel.nodes{nd}.output, 'initDispatcherJobClasses
')
284 nonfjmodel.nodes{nd}.output.initDispatcherJobClasses(nonfjmodel.classes);
288% see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
289for nd = 1:length(nonfjmodel.nodes)
290 os = nonfjmodel.nodes{nd}.output.outputStrategy;
291 for ci = all_aux_class_indices
293 if isempty(os{ci}) || ~strcmp(os{ci}{2}, 'Probabilities
')
294 os{ci} = {nonfjmodel.classes{ci}.name, 'Probabilities
', {}};
298 nonfjmodel.nodes{nd}.output.outputStrategy = os;
301 for r=1:length(nonfjmodel.nodes{f}.output.outputStrategy)
302 if strcmp(nonfjmodel.nodes{f}.output.outputStrategy{r}{2},RoutingStrategy.RAND)
303 nonfjmodel.nodes{f}.output.outputStrategy{r}{1} = nonfjmodel.classes{r}.name;
304 nonfjmodel.nodes{f}.output.outputStrategy{r}{2} = RoutingStrategy.DISABLED;