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 % BFS from (refNode, any chain class) through rtnodes to find all
134 % reachable (node, class) pairs within this chain. Used to determine
135 % which classes genuinely visit the fork (independent of DTMC solver
136 % numerics — JAR's dtmc_solve can give exact 0.0 where MATLAB gives
137 % ~1e-18 for barely-reachable states).
138 refStationIdx = sn.refstat(inchain(1));
139 refStatefulIdx = sn.stationToStateful(refStationIdx);
140 refNodeIdx = sn.statefulToNode(refStatefulIdx);
142 reachableFromRef =
false(sn.nnodes, K);
143 bfsQueue = zeros(0, 2); % [node,
class] pairs
145 reachableFromRef(refNodeIdx, ci) =
true;
146 bfsQueue(end+1,:) = [refNodeIdx, ci]; %#ok<AGROW>
148 while ~isempty(bfsQueue)
149 cn = bfsQueue(1,1); cc = bfsQueue(1,2);
151 for destNode = 1:sn.nnodes
153 srcIdx = (cn-1)*K + cc;
154 dstIdx = (destNode-1)*K + dci;
155 if ~reachableFromRef(destNode, dci) && sn.rtnodes(srcIdx, dstIdx) > 0
156 reachableFromRef(destNode, dci) =
true;
157 bfsQueue(end+1,:) = [destNode, dci]; %#ok<AGROW>
164 oclass{end+1} = OpenClass(nonfjmodel,[nonfjmodel.classes{r}.name,
'.',nonfjmodel.nodes{f}.name]); %#ok<AGROW>
165 fjclassmap(oclass{end}.index) = nonfjmodel.classes{r}.index;
166 fjforkmap(oclass{end}.index) = f;
167 s = fjclassmap(oclass{end}.index); % auxiliary
class index
168 if model.
nodes{f}.output.tasksPerLink > 1
169 line_warning(mfilename,
'There are no synchronisation delays implemented in MMT for multiple tasks per link. Results may be inaccurate.');
171 fanout(oclass{end}.index) = origfanout(f,r)*model.nodes{f}.output.tasksPerLink;
172 all_aux_class_indices(end+1) = oclass{end}.index; %#ok<AGROW>
173 disableAux = origfanout(f,r) == 0 || ~reachableFromRef(f, r);
175 source.setArrival(oclass{end},Disabled.getInstance);
177 source.setArrival(oclass{end},Exp(forkLambda(r)));
179 % Auxiliary arrivals are owned by
the forkLambda fixed point, not by
180 %
the base model. Record what a cold call sets here (note
the rate
is
181 % indexed by
the ORIGINAL
class r) so that reuse can restore it.
182 prov.auxArrival(end+1,:) = [oclass{end}.index, r, disableAux]; %#ok<AGROW>
183 prov.auxSource = source;
184 % joins are now Delays, let us set their service time
187 switch sn.nodetype(i)
189 nonfjmodel.nodes{i}.setService(oclass{end},Immediate());
190 prov.immediateSlots(end+1,:) = [i, oclass{end}.index]; %#ok<AGROW>
191 case {NodeType.Source, NodeType.Fork}
194 nonfjmodel.nodes{i}.setService(oclass{end},model.nodes{i}.getService(model.classes{r}).copy());
195 prov.serviceSrc(end+1,:) = [i, oclass{end}.index, r]; %#ok<AGROW>
204 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}} =
P{r,s};
209 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(source,:) = 0.0;
211 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(nonfjmodel.nodes{joinIdx},:) = 0.0;
214 if origfanout(f,r) > 0
215 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(source, nonfjmodel.nodes{f}) = 1.0;
217 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(nonfjmodel.nodes{joinIdx},sink) = 1.0;
221 % Confine
the auxiliary fork-branch classes to
the fork-join scope.
222 % Auxiliary tokens enter at
the Source (routed to
the Fork), follow
223 %
the original routing through
the fork-join region, and exit via
224 %
the Sink after
the Join, so a
class-aware BFS over (node,
class)
225 % pairs from
the Fork (stopping at
the Join) identifies every pair
226 % an auxiliary token can occupy. Pairs outside
this scope can never
227 % carry auxiliary tokens, yet
if their copied routing contains
228 % cycles (e.g.
the reference
class return path in lqn_bpmn) those
229 % cycles form recurrent components disconnected from
the Source in
230 %
the auxiliary open chain, which concentrate
the stationary mass
231 % and dwarf
the Source
visits that normalize
the auxiliary visit
232 % ratios used by
the fork-join
nodevisits correction: in lqn_bpmn
233 %
the reference
class switch was over-amplified to 27 instead of 1,
234 % saturating
the layer beyond solvability. This confinement must
235 % run
for every chain, including chains mixing forked classes with
236 %
class-
switch passthrough classes (origfanout=0).
237 pnnodes = size(
P{inchain(1),inchain(1)}, 1);
238 maxclass = max(inchain);
239 % Source/Sink/Fork/Join rows are mmt infrastructure: their aux
240 % routing
is set
explicitly above, so they are never cleared.
241 infra_mask =
false(1, pnnodes);
242 infra_mask(f) =
true;
243 if ~isempty(joinIdx), infra_mask(joinIdx) =
true; end
245 if isa(nonfjmodel.nodes{nd},
'Source') || isa(nonfjmodel.nodes{nd},
'Sink')
246 infra_mask(nd) = true;
249 % Class-aware BFS: find all (node, class) pairs reachable from Fork
250 visited = false(pnnodes, maxclass);
251 bfs_q = zeros(0, 2); % [node, class] pairs
252 % Seed: classes entering
the fork
253 for ri = 1:length(inchain)
255 for si = 1:length(inchain)
257 if any(
P{r,s}(f,:) > 0) && ~visited(f, r)
258 visited(f, r) =
true;
259 bfs_q(end+1,:) = [f, r]; %#ok<AGROW>
263 while ~isempty(bfs_q)
264 cn = bfs_q(1,1); cc = bfs_q(1,2);
266 for si = 1:length(inchain)
269 if P{cc,s}(cn, nd) > 0 && ~visited(nd, s)
270 visited(nd, s) =
true;
271 % Continue BFS unless
this is Join
272 if isempty(joinIdx) || nd ~= joinIdx
273 bfs_q(end+1,:) = [nd, s]; %#ok<AGROW>
279 % Clear outgoing aux routing
for every (node,
class) pair outside
280 %
the fork-join scope. Clearing must be per-(node, class) rather
281 % than per-node: a node kept in scope because one fork-branch class
282 % traverses it would otherwise retain
the routing of return-path
283 % classes too, leaving recurrent loops through in-scope
nodes
284 % intact. Only rows (outgoing routes) are cleared, not columns,
285 % because dead incoming routes are harmless and clearing columns
286 % can break
the routing matrix structure for class-switching models.
289 for ri = 1:length(inchain)
290 if ~visited(nd, inchain(ri))
291 for si = 1:length(inchain)
292 P{oclass{ri}, oclass{si}}(nd, :) = 0.0;
298 % Aux classes with no visited pair at all never enter
the fork
299 % scope (e.g.
the pre-
switch class in prefork
class-switching,
300 % which switches into
the forked
class before reaching
the fork).
301 % All their routing has just been cleared, so give them a direct
302 % source->sink route:
this keeps them well-formed (inert) open
303 % classes instead of isolated classes that degenerate
the
304 % nonfjmodel chain analysis and zero out
the whole solution.
305 for ri = 1:length(inchain)
306 if ~any(visited(:, inchain(ri)))
307 P{oclass{ri}, oclass{ri}}(source, sink) = 1.0;
312% Clear stale output strategies on non-station
nodes (e.g. fork-output Routers)
313% before relinking. resetNetwork() only re-initializes station dispatchers, so
314% Router
nodes carried over from
the original link() retain their old routing
315% (pointing at
the pre-existing class-switch
nodes). Without this, relink() would
316% append
the freshly-derived class-switch routing on top of
the stale entries,
317% producing duplicate (non-stochastic) edges at fork-output routers and inflating
318%
the visit ratio of
the join-token class. Matches
the Java mmt path, which
319% relinks over a clean node set. Source/Sink are stations and are left untouched.
320for nd = 1:length(nonfjmodel.
nodes)
321 if ~isa(nonfjmodel.
nodes{nd},
'Station') && isprop(nonfjmodel.nodes{nd},
'output') ...
322 && ~isempty(nonfjmodel.nodes{nd}.output) && ismethod(nonfjmodel.nodes{nd}.output,
'initDispatcherJobClasses')
323 nonfjmodel.nodes{nd}.output.initDispatcherJobClasses(nonfjmodel.classes);
327% Fix spurious routing
for aux classes at non-scope
nodes and CS
nodes.
328% relink() only calls setProbRouting for non-zero
P entries, so
nodes
329% where
P was zeroed retain default RAND routing that inherits physical
330% connections from original classes. CS
nodes created by link() for
331% cross-class routing also get default RAND for aux classes. Override
332% all non-PROB routing for aux classes to PROB with empty destinations.
333for nd = 1:length(nonfjmodel.
nodes)
334 os = nonfjmodel.
nodes{nd}.output.outputStrategy;
335 for ci = all_aux_class_indices
337 if isempty(os{ci}) || ~strcmp(os{ci}{2},
'Probabilities')
338 os{ci} = {nonfjmodel.classes{ci}.name,
'Probabilities', {}};
342 nonfjmodel.nodes{nd}.output.outputStrategy = os;
345 for r=1:length(nonfjmodel.nodes{f}.output.outputStrategy)
346 if strcmp(nonfjmodel.
nodes{f}.output.outputStrategy{r}{2},RoutingStrategy.RAND)
347 nonfjmodel.nodes{f}.output.outputStrategy{r}{1} = nonfjmodel.classes{r}.name;
348 nonfjmodel.nodes{f}.output.outputStrategy{r}{2} = RoutingStrategy.DISABLED;