LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
mmt.m
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
8%
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.
29
30%% this has been migrated to Java inside FJ.java as mmt
31
32sn = model.getStruct;
33if nargin < 2
34 forkLambda = GlobalConstants.FineTol * ones(1,sn.nclasses);
35end
36fjclassmap = [];
37fjforkmap = [];
38fanout = [];
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
42% recorded separately.
43prov = struct('serviceSrc', zeros(0,3), 'immediateSlots', zeros(0,2), ...
44 'auxArrival', zeros(0,3), 'auxSource', [], ...
45 'forkLambdaInit', forkLambda, 'baseModel', model);
46for i = 1:sn.nnodes
47 if sn.isstation(i)
48 switch sn.nodetype(i)
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.
52 otherwise
53 for r = 1:sn.nclasses
54 if ~isempty(model.nodes{i}.getService(model.classes{r}))
55 prov.serviceSrc(end+1,:) = [i, r, r]; %#ok<AGROW>
56 end
57 end
58 end
59 end
60end
61% we create an equivalent model without fj stations
62nonfjmodel = model.copy();
63nonfjmodel.allowReplace = true;
64P = nonfjmodel.getLinkedRoutingMatrix;
65nonfjmodel.resetNetwork(true);
66nonfjmodel.resetStruct();
67if isempty(P)
68 line_error(mfilename,'SolverMVA can process fork-join networks only if their routing topology has been generated using Network.link.');
69end
70Vnodes = cellsum(sn.nodevisits);
71forkedClasses = {};
72forkIndexes = find(sn.nodetype == NodeType.Fork)';
73% replaces forks and joins with routers
74fanout = [];
75for f=forkIndexes
76 for r=1:size(P,1)
77 if length(model.nodes{f}.output.outputStrategy{r})>2
78 origfanout(f,r) = length(model.nodes{f}.output.outputStrategy{r}{3});
79 for s=1:size(P,2)
80 P{r,s}(f,:) = P{r,s}(f,:) / origfanout(f,r);
81 end
82 else
83 origfanout(f,r) = 0;
84 end
85 end
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
89 % change the classes
90 %nonfjmodel.nodes{f} = ClassSwitch(nonfjmodel, nonfjmodel.nodes{f}.name, eye(sn.nclasses));
91 forkedClasses{f,1} = find(Vnodes(f,:)>0); %#ok<AGROW>
92end
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>
100 end
101end
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;
107else
108 source = Source(nonfjmodel,'Source');
109 sink = Sink(nonfjmodel,'Sink');
110end
111for r=1:size(P,1)
112 for s=1:size(P,2)
113 P{r,s}(length(nonfjmodel.nodes),length(nonfjmodel.nodes)) = 0;
114 P{s,r}(length(nonfjmodel.nodes),length(nonfjmodel.nodes)) = 0;
115 end
116end
117nonfjmodel.connections = zeros(length(nonfjmodel.nodes));
118oclass = {};
119all_aux_class_indices = []; % aux class indices for post-relink routing fix
120for f=forkIndexes
121 % find join associated to fork f
122 joinIdx = find(sn.fj(f,:));
123 if length(joinIdx)>1
124 line_error(mfilename,'SolverMVA supports at present only a single join station per fork node.');
125 end
126 % find chains associated to classes forked by f
127 forkedChains = find(sum(sn.chains(:,forkedClasses{f}),2));
128 for fc=forkedChains'
129 % create a new open class for each class in forkedChains
130 oclass = {};
131 inchain = find(sn.chains(fc,:)); inchain = inchain(:)';
132
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);
141 K = sn.nclasses;
142 reachableFromRef = false(sn.nnodes, K);
143 bfsQueue = zeros(0, 2); % [node, class] pairs
144 for ci = inchain
145 reachableFromRef(refNodeIdx, ci) = true;
146 bfsQueue(end+1,:) = [refNodeIdx, ci]; %#ok<AGROW>
147 end
148 while ~isempty(bfsQueue)
149 cn = bfsQueue(1,1); cc = bfsQueue(1,2);
150 bfsQueue(1,:) = [];
151 for destNode = 1:sn.nnodes
152 for dci = inchain
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>
158 end
159 end
160 end
161 end
162
163 for r=inchain
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.');
170 end
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);
174 if disableAux
175 source.setArrival(oclass{end},Disabled.getInstance);
176 else
177 source.setArrival(oclass{end},Exp(forkLambda(r)));
178 end
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
185 for i=1:sn.nnodes
186 if sn.isstation(i)
187 switch sn.nodetype(i)
188 case NodeType.Join
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}
192 %no-op
193 otherwise
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>
196 end
197 else
198 end
199 end
200 end
201
202 for r=inchain
203 for s=inchain
204 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}} = P{r,s};
205 end
206 end
207 for r=inchain
208 for s=inchain
209 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(source,:) = 0.0;
210 if ~isempty(joinIdx)
211 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(nonfjmodel.nodes{joinIdx},:) = 0.0;
212 end
213 end
214 if origfanout(f,r) > 0
215 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(source, nonfjmodel.nodes{f}) = 1.0;
216 if ~isempty(joinIdx)
217 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(nonfjmodel.nodes{joinIdx},sink) = 1.0;
218 end
219 end
220 end
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
244 for nd = 1:pnnodes
245 if isa(nonfjmodel.nodes{nd}, 'Source') || isa(nonfjmodel.nodes{nd}, 'Sink')
246 infra_mask(nd) = true;
247 end
248 end
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)
254 r = inchain(ri);
255 for si = 1:length(inchain)
256 s = inchain(si);
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>
260 end
261 end
262 end
263 while ~isempty(bfs_q)
264 cn = bfs_q(1,1); cc = bfs_q(1,2);
265 bfs_q(1,:) = [];
266 for si = 1:length(inchain)
267 s = inchain(si);
268 for nd = 1:pnnodes
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>
274 end
275 end
276 end
277 end
278 end
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.
287 for nd = 1:pnnodes
288 if ~infra_mask(nd)
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;
293 end
294 end
295 end
296 end
297 end
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;
308 end
309 end
310 end
311end
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);
324 end
325end
326nonfjmodel.relink(P);
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
336 if ci <= length(os)
337 if isempty(os{ci}) || ~strcmp(os{ci}{2}, 'Probabilities')
338 os{ci} = {nonfjmodel.classes{ci}.name, 'Probabilities', {}};
339 end
340 end
341 end
342 nonfjmodel.nodes{nd}.output.outputStrategy = os;
343end
344for f=forkIndexes
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;
349 end
350 end
351end
352end
Definition fjtag.m:157
Definition Station.m:245