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 % 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);
137 K = sn.nclasses;
138 reachableFromRef = false(sn.nnodes, K);
139 bfsQueue = zeros(0, 2); % [node, class] pairs
140 for ci = inchain
141 reachableFromRef(refNodeIdx, ci) = true;
142 bfsQueue(end+1,:) = [refNodeIdx, ci]; %#ok<AGROW>
143 end
144 while ~isempty(bfsQueue)
145 cn = bfsQueue(1,1); cc = bfsQueue(1,2);
146 bfsQueue(1,:) = [];
147 for destNode = 1:sn.nnodes
148 for dci = inchain
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>
154 end
155 end
156 end
157 end
158
159 for r=inchain
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.');
166 end
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);
170 if disableAux
171 source.setArrival(oclass{end},Disabled.getInstance);
172 else
173 source.setArrival(oclass{end},Exp(forkLambda(r)));
174 end
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
181 for i=1:sn.nnodes
182 if sn.isstation(i)
183 switch sn.nodetype(i)
184 case NodeType.Join
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}
188 %no-op
189 otherwise
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>
192 end
193 else
194 end
195 end
196 end
197
198 for r=inchain
199 for s=inchain
200 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}} = P{r,s};
201 end
202 end
203 for r=inchain
204 for s=inchain
205 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(source,:) = 0.0;
206 if ~isempty(joinIdx)
207 P{oclass{find(r==inchain,1)},oclass{find(s==inchain,1)}}(nonfjmodel.nodes{joinIdx},:) = 0.0;
208 end
209 end
210 if origfanout(f,r) > 0
211 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(source, nonfjmodel.nodes{f}) = 1.0;
212 if ~isempty(joinIdx)
213 P{oclass{find(r==inchain,1)},oclass{find(r==inchain,1)}}(nonfjmodel.nodes{joinIdx},sink) = 1.0;
214 end
215 end
216 end
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
225 for nd = 1:pnnodes
226 if isa(nonfjmodel.nodes{nd}, 'Source') || isa(nonfjmodel.nodes{nd}, 'Sink')
227 infra_mask(nd) = true;
228 end
229 end
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)
235 r = inchain(ri);
236 for si = 1:length(inchain)
237 s = inchain(si);
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>
241 end
242 end
243 end
244 while ~isempty(bfs_q)
245 cn = bfs_q(1,1); cc = bfs_q(1,2);
246 bfs_q(1,:) = [];
247 for si = 1:length(inchain)
248 s = inchain(si);
249 for nd = 1:pnnodes
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>
255 end
256 end
257 end
258 end
259 end
260 % see _kb/12-interfaces-and-docs.md (@ModelAdapter: JMT export helpers) for rationale
261 for nd = 1:pnnodes
262 if ~infra_mask(nd)
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;
267 end
268 end
269 end
270 end
271 end
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;
276 end
277 end
278 end
279end
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);
285 end
286end
287nonfjmodel.relink(P);
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
292 if ci <= length(os)
293 if isempty(os{ci}) || ~strcmp(os{ci}{2}, 'Probabilities')
294 os{ci} = {nonfjmodel.classes{ci}.name, 'Probabilities', {}};
295 end
296 end
297 end
298 nonfjmodel.nodes{nd}.output.outputStrategy = os;
299end
300for f=forkIndexes
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;
305 end
306 end
307end
308end