LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
fjFixedPoint.m
1function out = fjFixedPoint(self, options, solveFcn)
2% OUT = FJFIXEDPOINT(OPTIONS, SOLVEFCN)
3%
4% Solver-agnostic driver of the fork-join fixed point. The transformation
5% (ModelAdapter.mmt, or ModelAdapter.ht under options.config.fork_join) turns
6% the model into a plain network in which every fork is a router, every join a
7% zero-service delay, and the parallelism is carried by auxiliary open classes
8% of arrival rate (fanout-1)*forkLambda. Each pass solves that network,
9% recomputes the synchronisation delays from the resulting metrics and updates
10% forkLambda; the loop ends when the queue lengths stop moving.
11%
12% SOLVEFCN(sn, options) is the inner solve. It must return a struct with the
13% fields QN, UN, RN, TN, CN, XN, lG, runtime, lastiter, method and
14% actualmethod, i.e. the contract of @SolverMVA/mvaDispatch.m. Nothing in the
15% loop reads a solver internal, so any NetworkSolver whose analyzer honours
16% that contract can be driven here.
17%
18% On a model without forks the loop runs SOLVEFCN exactly once and returns its
19% result unchanged.
20%
21% OUT carries QN, UN, RN, TN, CN, XN, lG, runtime, iter, method and
22% actualmethod.
23%
24% Copyright (c) 2012-2026, Imperial College London
25% All rights reserved.
26
27iter = 0;
28 sn = getStruct(self); % doesn't need initial state
29 forkLoop = true;
30 forkIter = 0;
31 % create artificial classes arrival rates; warm-start from the retained
32 % iterate. see _kb/05-solvers-overview.md for rationale
33 forkLambda = GlobalConstants.FineTol * ones(1, 2*sn.nclasses*sum(sn.nodetype==NodeType.Fork));
34 if options.config.fj_warmstart && ~isempty(self.fjForkLambda) ...
35 && isequal(size(self.fjForkLambda), size(forkLambda))
36 forkLambda = self.fjForkLambda;
37 end
38 QN = GlobalConstants.Immediate * ones(1, sn.nclasses);
39 QN_1 = 0*QN;
40 UN = 0*QN;
41while (forkLoop && forkIter < options.iter_max)
42 if self.model.hasFork
43 forkIter = forkIter + 1;
44 line_debug(options, 'Fork-join iteration %d', forkIter);
45 if forkIter == 1
46 switch options.config.fork_join
47 case {'heidelberger-trivedi', 'ht'}
48 [nonfjmodel, fjclassmap, fjforkmap, fj_auxiliary_delays] = ModelAdapter.ht(self.model);
49 line_debug(options, 'Fork-join method: heidelberger-trivedi');
50 case {'mmt', 'default', 'fjt'}
51 % Reuse the MMT transformation across outer iterations
52 % while structVersion is unchanged; re-fed from the base
53 % model first. see _kb/05-solvers-overview.md for rationale
54 cacheUsable = ~isempty(self.mmtCache) && ...
55 self.mmtCache.structVersion == self.model.structVersion && ...
56 ModelAdapter.refreshServicesFromBase(self.mmtCache.nonfjmodel, self.mmtCache.prov);
57 if cacheUsable
58 nonfjmodel = self.mmtCache.nonfjmodel;
59 fjclassmap = self.mmtCache.fjclassmap;
60 fjforkmap = self.mmtCache.fjforkmap;
61 fanout = self.mmtCache.fanout;
62 outer_forks = self.mmtCache.outer_forks;
63 parent_forks = self.mmtCache.parent_forks;
64 nonfjmodel.refreshRates();
65 line_debug(options, 'Fork-join method: mmt (cached transformation)');
66 else
67 [nonfjmodel, fjclassmap, fjforkmap, fanout, prov] = ModelAdapter.mmt(self.model, forkLambda);
68 line_debug(options, 'Fork-join method: mmt');
69 [outer_forks, parent_forks] = ModelAdapter.sortForks(sn, fjforkmap, fjclassmap, nonfjmodel);
70 self.mmtCache = struct('nonfjmodel', nonfjmodel, 'prov', prov, ...
71 'fjclassmap', fjclassmap, 'fjforkmap', fjforkmap, 'fanout', fanout, ...
72 'outer_forks', outer_forks, 'parent_forks', parent_forks, ...
73 'structVersion', self.model.structVersion);
74 end
75 end
76 elseif ~strcmp(options.config.fork_join, 'heidelberger-trivedi') & ~strcmp(options.config.fork_join, 'ht')
77 %line_printf('Fork-join iteration %d\n',forkIter);
78 nonfjSource = nonfjmodel.getSource;
79 for r=1:length(fjclassmap) % r is the auxiliary class
80 s = fjclassmap(r);
81 if s>0
82 if fanout(r)>0
83 if ~nonfjSource.arrivalProcess{r}.isDisabled
84 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
85 end
86 end
87 end
88 end
89 nonfjmodel.refreshRates();
90 end
91 sn = nonfjmodel.getStruct(false); % this ensures that we solve nonfjmodel instead of the original model
92 line_debug(options, 'Fork-join iter %d: rebuilt nonfjmodel struct (nstations=%d, nclasses=%d)', forkIter, sn.nstations, sn.nclasses);
93 % Mixed absolute/relative convergence test on the MMT iterate,
94 % using the SOLVER's iter_tol (not CoarseTol). see
95 % _kb/05-solvers-overview.md for rationale
96 qn_converged = abs(QN_1 - QN) <= GlobalConstants.Zero + options.iter_tol*abs(QN);
97 if isequal(size(QN_1), size(QN)) && all(qn_converged(:)) && (forkIter > 2)
98 line_debug(options, 'Fork-join iter %d: converged (mixed abs/rel test)', forkIter);
99 forkLoop = false;
100 else
101 if self.model.hasOpenClasses
102 sourceIndex = self.model.getSource.index;
103 UNnosource = UN; UNnosource(sourceIndex,:) = 0;
104 if any(find(sum(UNnosource(:,isinf(sn.njobs(1:size(QN,2)))),2)>0.99 * sn.nservers))
105 line_warning(mfilename,'The model may be unstable: the utilization of station %i for open classes exceeds 99 percent.\n',maxpos(sum(UNnosource,2)));
106 end
107 end
108 QN_1 = QN;
109 end
110 else
111 forkLoop = false;
112 end
113 fjres = solveFcn(sn, options);
114 QN = fjres.QN; UN = fjres.UN; RN = fjres.RN; TN = fjres.TN;
115 CN = fjres.CN; XN = fjres.XN; lG = fjres.lG;
116 runtime = fjres.runtime; lastiter = fjres.lastiter;
117 method = fjres.method; actualmethod = fjres.actualmethod;
118 if self.model.hasFork
119 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
120 nonfjstruct = sn;
121 sn = self.getStruct;
122 % Pre-compute linked routing matrix once (loop-invariant)
123 switch options.config.fork_join
124 case {'mmt', 'default', 'fjt'}
125 Pcs = cell2mat(nonfjmodel.getLinkedRoutingMatrix);
126 end
127 for f=find(sn.nodetype == NodeType.Fork)'
128 switch options.config.fork_join
129 case {'mmt', 'default', 'fjt'}
130 TNfork = zeros(1,sn.nclasses);
131 for c=1:sn.nchains
132 inchain = find(sn.chains(c,:));
133 for r=inchain(:)'
134 TNfork(r) = (sn.nodevisits{c}(parent_forks(f),r) / sum(sn.visits{c}(sn.stationToStateful(sn.refstat(r)),inchain))) * sum(TN(sn.refstat(r),inchain));
135 end
136 end
137 % find join associated to fork f
138 joinIdx = find(sn.fj(f,:));
139 forkauxclasses = find(fjforkmap==f);
140 for s=forkauxclasses(:)'
141 r = fjclassmap(s); % original class associated to auxiliary class s
142 if isempty(joinIdx)
143 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
144 else
145 joinStat = sn.nodeToStation(joinIdx);
146 TN(sn.nodeToStation(joinIdx),r) = TN(sn.nodeToStation(joinIdx),r) + sum(TN(sn.nodeToStation(joinIdx), find(fjclassmap == r))) - TN(sn.nodeToStation(joinIdx), s);
147 forkLambda(s) = mean([forkLambda(s); TN(sn.nodeToStation(joinIdx),r)],1);
148 end
149 if isempty(joinIdx) || ~outer_forks(f, r)
150 % No join nodes for this fork, no synchronisation delay
151 continue;
152 end
153 % Find the parallel paths coming out of the fork
154 ri = ModelAdapter.findPathsCS(sn, Pcs, f, joinIdx, r, [r,s], QN, TN, 0, fjclassmap, fjforkmap, nonfjmodel);
155 if isempty(ri)
156 % No routing from fork for this class - set sync delay to 0 (Immediate)
157 % Matches JAR behavior where empty Matrix gives syncDelay = 0
158 syncDelay = 0;
159 else
160 lambdai = 1./ri;
161 d0 = 0;
162 parallel_branches = length(ri);
163 for pow=0:(parallel_branches - 1)
164 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
165 d0 = d0 + (-1)^pow * current_sum;
166 end
167 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
168 end
169 % Set the synchronisation delays
170 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
171 if outer_forks(f, r)
172 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
173 end
174 end
175 case {'heidelberger-trivedi', 'ht'}
176 joinIdx = find(sn.fj(f,:));
177 for c=1:sn.nchains
178 inchain = find(sn.chains(c,:));
179 for r=inchain(:)'
180 if sn.nodevisits{c}(f,r) == 0
181 continue;
182 end
183 % Obtain the response times on the parallel branches
184 ri = RN(:, find(fjclassmap == r));
185 ri(isnan(ri) | isinf(ri)) = 0;
186 ri = sum(ri, 1, "omitnan") - RN(nonfjstruct.nodeToStation(fj_auxiliary_delays{joinIdx}), find(fjclassmap == r)) - RN(nonfjstruct.nodeToStation(joinIdx), find(fjclassmap == r));
187 lambdai = 1./ri;
188 d0 = 0;
189 parallel_branches = length(self.model.nodes{f}.output.outputStrategy{r}{3});
190 for pow=0:(parallel_branches - 1)
191 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
192 d0 = d0 + (-1)^pow * current_sum;
193 end
194 di = d0*sn.nodeparam{f}.fanOut - ri;
195 r0 = sum(RN(:, inchain), 2);
196 r0(isnan(r0) | isinf(r0)) = 0;
197 r0 = sum(r0, 1, "omitnan") - RN(nonfjstruct.nodeToStation(joinIdx), r);
198 % Update the delays at the join node and at the auxiliary delay
199 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(d0*sn.nodeparam{f}.fanOut));
200 idx = 1;
201 for s=find(fjclassmap == r)
202 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
203 idx = idx + 1;
204 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
205 end
206
207 end
208 end
209 end
210 end
211 % Batch refreshRates after all sync delay updates (moved out of inner loops for performance)
212 switch options.config.fork_join
213 case {'mmt', 'default', 'fjt'}
214 nonfjmodel.refreshRates();
215 end
216 switch options.config.fork_join
217 case {'heidelberger-trivedi', 'ht'}
218 nonfjmodel.refreshStruct();
219 % Delete the queue lengths, response times, throughputs and utilizations of the original classes at the join nodes
220 QN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
221 RN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
222 % Save the throughputs of the original classes at the join node
223 TN_orig = TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap));
224 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
225 UN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
226
227 % Remove the times at the auxiliary delay
228 QN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
229 UN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
230 RN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
231 TN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
232 % merge back artificial classes into their original classes
233 for r=1:length(fjclassmap)
234 s = fjclassmap(r);
235 if s>0
236 QN(:,s) = QN(:,s) + QN(:,r);
237 UN(:,s) = UN(:,s) + UN(:,r);
238 % Add all throughputs of the auxiliary classes to facilitate the computation of the response times
239 TN(:,s) = TN(:,s) + TN(:,r);
240 RN(:,s) = QN(:,s) ./ TN(:,s);
241 end
242 end
243 % Re-set the throughputs for the original classes
244 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = TN_orig;
245 case {'mmt', 'default', 'fjt'}
246 TN_orig = TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap));
247 % merge back artificial classes into their original classes
248 for r=1:length(fjclassmap)
249 s = fjclassmap(r);
250 if s>0
251 QN(:,s) = QN(:,s) + QN(:,r);
252 UN(:,s) = UN(:,s) + UN(:,r);
253 TN(:,s) = TN(:,s) + TN(:,r);
254 %RN(:,s) = RN(:,s) + RN(:,r);
255 % for i=find(snorig.nodetype == NodeType.Delay | snorig.nodetype == NodeType.Queue)'
256 % TN(snorig.nodeToStation(i),s) = TN(snorig.nodeToStation(i),s) + TN(snorig.nodeToStation(i),r);
257 % end
258 RN(:,s) = QN(:,s) ./ TN(:,s);
259 %CN(:,s) = CN(:,s) + CN(:,r);
260 %XN(:,s) = XN(:,s) + XN(:,r);
261 end
262 end
263 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
264 end
265 % Drop the auxiliary-class columns. CN is left untouched when not
266 % class-indexed. see _kb/05-solvers-overview.md for rationale
267 auxmask = fjclassmap>0;
268 QN = dropAuxCols(QN, auxmask);
269 UN = dropAuxCols(UN, auxmask);
270 RN = dropAuxCols(RN, auxmask);
271 TN = dropAuxCols(TN, auxmask);
272 CN = dropAuxCols(CN, auxmask);
273 XN = dropAuxCols(XN, auxmask);
274 end
275 iter = iter + lastiter;
276 % Cap accumulated iterations for stability (Python parity)
277 if iter > 10000
278 iter = 10000;
279 break; % Exit forkLoop early
280 end
281 end
282 % see _kb/05-solvers-overview.md for rationale (non-convergence now warns)
283 if self.model.hasFork
284 if forkLoop && forkIter >= options.iter_max
285 line_warning(mfilename,'The fork-join (%s) fixed point did not converge in options.iter_max=%d iterations; returning the interim solution.\n', options.config.fork_join, options.iter_max);
286 end
287 % Retain the MMT iterate so that a subsequent runAnalyzer call on
288 % this solver (an outer LN iteration) resumes the fixed point here.
289 self.fjForkLambda = forkLambda;
290 end
291
292out = struct('QN', QN, 'UN', UN, 'RN', RN, 'TN', TN, 'CN', CN, 'XN', XN, ...
293 'lG', lG, 'runtime', runtime, 'iter', iter, 'method', method, ...
294 'actualmethod', actualmethod);
295end
296
297function A = dropAuxCols(A, auxmask)
298% A = DROPAUXCOLS(A, AUXMASK)
299% Remove the auxiliary-class columns from a class-indexed metric. Arrays that
300% are not indexed by the transformed model's classes (e.g. a per-chain system
301% response time) are returned unchanged.
302if ~isempty(A) && size(A,2) == numel(auxmask)
303 A(:,auxmask) = [];
304end
305end
Definition fjtag.m:161