1function [nonfjmodel, fjclassmap, fjforkmap, fanout] = 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%%
this has been migrated to Java inside FJ.java as mmt
13 forkLambda = GlobalConstants.FineTol * ones(1,sn.nclasses);
18% we create an equivalent model without fj stations
19nonfjmodel = model.copy();
20nonfjmodel.allowReplace =
true;
21P = nonfjmodel.getLinkedRoutingMatrix;
22nonfjmodel.resetNetwork(
true);
23nonfjmodel.resetStruct();
25 line_error(mfilename,
'SolverMVA can process fork-join networks only if their routing topology has been generated using Network.link.');
27Vnodes = cellsum(sn.nodevisits);
29forkIndexes = find(sn.nodetype == NodeType.Fork)
';
30% replaces forks and joins with routers
34 if length(model.nodes{f}.output.outputStrategy{r})>2
35 origfanout(f,r) = length(model.nodes{f}.output.outputStrategy{r}{3});
37 P{r,s}(f,:) = P{r,s}(f,:) / origfanout(f,r);
43 % replace Join with a Router
44 nonfjmodel.nodes{f} = Router(nonfjmodel, nonfjmodel.nodes{f}.name);
45 % replace Fork with a StatelessClassSwitcher that doesn't
47 %nonfjmodel.nodes{f} = ClassSwitch(nonfjmodel, nonfjmodel.nodes{f}.name, eye(sn.nclasses));
48 forkedClasses{f,1} = find(Vnodes(f,:)>0); %#ok<AGROW>
50for j=find(sn.nodetype == NodeType.Join)
'
51 % replace Join with an Infinite Server
52 nonfjmodel.nodes{j} = Delay(nonfjmodel, nonfjmodel.nodes{j}.name);
53 nonfjmodel.stations{model.nodes{j}.stationIndex} = nonfjmodel.nodes{j};
54 for c=1:length(nonfjmodel.classes)
55 nonfjmodel.nodes{j}.setService(nonfjmodel.classes{c},Immediate());
58nonfjmodel.stations={nonfjmodel.stations{1:model.getNumberOfStations}}'; % remove automatically added station and put it where the join was
59%
if they don
't exist already, add source and sink
60if nonfjmodel.hasOpenClasses
61 source = nonfjmodel.getSource;
62 sink = nonfjmodel.getSink;
64 source = Source(nonfjmodel,'Source
');
65 sink = Sink(nonfjmodel,'Sink
');
69 P{r,s}(length(nonfjmodel.nodes),length(nonfjmodel.nodes)) = 0;
70 P{s,r}(length(nonfjmodel.nodes),length(nonfjmodel.nodes)) = 0;
73nonfjmodel.connections = zeros(length(nonfjmodel.nodes));
75all_aux_class_indices = []; % aux class indices for post-relink routing fix
77 % find join associated to fork f
78 joinIdx = find(sn.fj(f,:));
80 line_error(mfilename,'SolverMVA supports at present only a single join station per fork node.
');
82 % find chains associated to classes forked by f
83 forkedChains = find(sum(sn.chains(:,forkedClasses{f}),2));
85 % create a
new open
class for each class in forkedChains
87 inchain = find(sn.chains(fc,:)); inchain = inchain(:)
';
89 % BFS from (refNode, any chain class) through rtnodes to find all
90 % reachable (node, class) pairs within this chain. Used to determine
91 % which classes genuinely visit the fork (independent of DTMC solver
92 % numerics — JAR's dtmc_solve can give exact 0.0 where MATLAB gives
93 % ~1e-18 for barely-reachable states).
94 refStationIdx = sn.refstat(inchain(1));
95 refStatefulIdx = sn.stationToStateful(refStationIdx);
96 refNodeIdx = sn.statefulToNode(refStatefulIdx);
98 reachableFromRef =
false(sn.nnodes, K);
99 bfsQueue = zeros(0, 2); % [node,
class] pairs
101 reachableFromRef(refNodeIdx, ci) =
true;
102 bfsQueue(end+1,:) = [refNodeIdx, ci]; %#ok<AGROW>
104 while ~isempty(bfsQueue)
105 cn = bfsQueue(1,1); cc = bfsQueue(1,2);
107 for destNode = 1:sn.nnodes
109 srcIdx = (cn-1)*K + cc;
110 dstIdx = (destNode-1)*K + dci;
111 if ~reachableFromRef(destNode, dci) && sn.rtnodes(srcIdx, dstIdx) > 0
112 reachableFromRef(destNode, dci) =
true;
113 bfsQueue(end+1,:) = [destNode, dci]; %#ok<AGROW>
120 oclass{end+1} = OpenClass(nonfjmodel,[nonfjmodel.classes{r}.name,
'.',nonfjmodel.nodes{f}.name]); %#ok<AGROW>
121 fjclassmap(oclass{end}.index) = nonfjmodel.classes{r}.index;
122 fjforkmap(oclass{end}.index) = f;
123 s = fjclassmap(oclass{end}.index); % auxiliary
class index
124 if model.
nodes{f}.output.tasksPerLink > 1
125 line_warning(mfilename,
'There are no synchronisation delays implemented in MMT for multiple tasks per link. Results may be inaccurate.');
127 fanout(oclass{end}.index) = origfanout(f,r)*model.nodes{f}.output.tasksPerLink;
128 all_aux_class_indices(end+1) = oclass{end}.index; %#ok<AGROW>
129 disableAux = origfanout(f,r) == 0 || ~reachableFromRef(f, r);
131 source.setArrival(oclass{end},Disabled.getInstance);
133 source.setArrival(oclass{end},Exp(forkLambda(r)));
135 % joins are now Delays, let us set their service time
138 switch sn.nodetype(i)
140 nonfjmodel.nodes{i}.setService(oclass{end},Immediate());
141 case {NodeType.Source, NodeType.Fork}
144 nonfjmodel.nodes{i}.setService(oclass{end},model.nodes{i}.getService(model.classes{r}).copy());
153 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}} =
P{r,s};
158 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(source,:) = 0.0;
160 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(nonfjmodel.nodes{joinIdx},:) = 0.0;
163 if origfanout(f,r) > 0
164 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(source, nonfjmodel.nodes{f}) = 1.0;
166 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(nonfjmodel.nodes{joinIdx},sink) = 1.0;
170 % Check
if all
classes in
this chain have non-zero fanout at
this fork.
171 % BFS scope clearing only applies when all
classes are actually forked;
172 % when some
classes pass through the fork via
class-switching (origfanout=0),
173 % clearing would disconnect their aux chain and
break MVA convergence.
175 for ri = 1:length(inchain)
176 if origfanout(f, inchain(ri)) == 0
182 % Determine fork-join scope via
class-aware BFS from Fork, stopping
183 % at Join. Tracks (node,
class) pairs so that shared stations (e.g.
184 % processors serving both fork-branch and return-path
classes) are
185 % correctly handled: only fork-branch class routing
is followed.
186 pnnodes = size(
P{inchain(1),inchain(1)}, 1);
187 maxclass = max(inchain);
188 fj_node_mask =
false(1, pnnodes);
189 fj_node_mask(f) =
true;
190 if ~isempty(joinIdx), fj_node_mask(joinIdx) =
true; end
191 % Source and Sink are mmt infrastructure, always in scope
193 if isa(nonfjmodel.nodes{nd},
'Source') || isa(nonfjmodel.nodes{nd},
'Sink')
194 fj_node_mask(nd) = true;
197 % Class-aware BFS: find all (node, class) pairs reachable from Fork
198 visited = false(pnnodes, maxclass);
199 bfs_q = zeros(0, 2); % [node, class] pairs
200 % Seed:
classes entering the fork
201 for ri = 1:length(inchain)
203 for si = 1:length(inchain)
205 if any(
P{r,s}(f,:) > 0) && ~visited(f, r)
206 visited(f, r) =
true;
207 bfs_q(end+1,:) = [f, r]; %#ok<AGROW>
211 while ~isempty(bfs_q)
212 cn = bfs_q(1,1); cc = bfs_q(1,2);
214 for si = 1:length(inchain)
217 if P{cc,s}(cn, nd) > 0 && ~visited(nd, s)
218 visited(nd, s) =
true;
219 fj_node_mask(nd) =
true;
220 % Continue BFS unless
this is Join
221 if isempty(joinIdx) || nd ~= joinIdx
222 bfs_q(end+1,:) = [nd, s]; %#ok<AGROW>
228 % Clear outgoing aux routing at
nodes not in fork-join scope.
229 % Only clear rows (outgoing routes), not columns (incoming),
230 % because dead incoming routes are harmless and clearing columns
231 % can
break the routing matrix structure
for class-switching models.
234 for ri = 1:length(inchain)
235 for si = 1:length(inchain)
236 P{oclass{ri}, oclass{si}}(nd, :) = 0.0;
246% relink() only calls setProbRouting for non-zero
P entries, so
nodes
247% where
P was zeroed retain default RAND routing that inherits physical
248% connections from original
classes. CS
nodes created by link() for
249% cross-class routing also get default RAND for aux
classes. Override
250% all non-PROB routing for aux
classes to PROB with empty destinations.
251for nd = 1:length(nonfjmodel.
nodes)
252 os = nonfjmodel.
nodes{nd}.output.outputStrategy;
253 for ci = all_aux_class_indices
255 if isempty(os{ci}) || ~strcmp(os{ci}{2},
'Probabilities')
256 os{ci} = {nonfjmodel.classes{ci}.name,
'Probabilities', {}};
260 nonfjmodel.nodes{nd}.output.outputStrategy = os;
263 for r=1:length(nonfjmodel.nodes{f}.output.outputStrategy)
264 if strcmp(nonfjmodel.
nodes{f}.output.outputStrategy{r}{2},RoutingStrategy.RAND)
265 nonfjmodel.nodes{f}.output.outputStrategy{r}{1} = nonfjmodel.classes{r}.name;
266 nonfjmodel.nodes{f}.output.outputStrategy{r}{2} = RoutingStrategy.DISABLED;