LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
link.m
1function self = link(self, P)
2% SELF = LINK(P)
3
4% Copyright (c) 2012-2026, Imperial College London
5% All rights reserved.
6
7if ~isempty(self.connections)
8 line_error(mfilename,'The Network.link method cannot be used after calling the addLink() method. Use Node.setProbRouting instead to configure routing probabilities.');
9end
10
11if isa(P,'RoutingMatrix')
12 P = P.getCell();
13end
14sanitize(self);
15
16isReset = false;
17if ~isempty(self.sn)
18 isReset = true;
19 self.resetNetwork; % remove artificial class switch nodes
20end
21K = self.getNumberOfClasses;
22I = self.getNumberOfNodes;
23
24if ~iscell(P) && K>1
25 line_error(mfilename,'Multiclass model: the linked routing matrix P must be a cell array, e.g., P = model.initRoutingMatrix; P{1} = Pclass1; P{2} = Pclass2.');
26end
27
28isLinearP = true;
29if size(P,1) == size(P,2)
30 for s=2:K
31 for r=1:K
32 if nnz(P{r,s})>0
33 isLinearP = false;
34 break;
35 end
36 end
37 end
38 % in this case it is possible that P is linear but just because the
39 % routing is state-dependent and therefore some zero entries are
40 % actually unspecified
41 %cacheNodes = find(cellfun(@(c) isa(c,'Cache'), self.getStatefulNodes));
42 for ind=1:I
43 switch class(self.nodes{ind})
44 case 'Cache'
45 % note that since a cache needs to distinguish hits and
46 % misses, it needs to do class-switch unless the model is
47 % degenerate
48 isLinearP = false;
49 if self.nodes{ind}.server.hitClass == self.nodes{ind}.server.missClass
50 line_warning(mfilename,'Ambiguous use of hitClass and missClass at cache, it is recommended to use different classes.\n');
51 end
52 end
53 end
54end
55
56% This block is to make sure that P = model.initRoutingMatrix; P{2} writes
57% into P{2,2} rather than being interpreted as P{2,1}.
58if isLinearP
59 Ptmp = P;
60 P = cell(K,K);
61 for r=1:K
62 if iscell(Ptmp)
63 P{r,r} = Ptmp{r};
64 else
65 P{r,r} = Ptmp;
66 end
67 for s=1:K
68 if s~=r
69 P{r,s} = 0*Ptmp{r};
70 end
71 end
72 end
73end
74
75% assign routing for self-looping jobs
76for r=1:K
77 if isa(self.classes{r},'SelfLoopingClass')
78 for s=1:K
79 P{r,s} = 0 * P{r,s};
80 end
81 P{r,r}(self.classes{r}.refstat, self.classes{r}.refstat) = 1.0;
82 end
83end
84
85% link virtual sinks automatically to sink
86ispool = cellisa(self.nodes,'Sink');
87if sum(ispool) > 1
88 line_error(mfilename,'The model can have at most one sink node.');
89end
90
91if sum(cellisa(self.nodes,'Source')) > 1
92 line_error(mfilename,'The model can have at most one source node.');
93end
94ispool_nnz = find(ispool)';
95
96
97if ~iscell(P)
98 if K>1
99 newP = cell(1,K);
100 for r=1:K
101 newP{r} = P;
102 end
103 P = newP;
104 else %R==1
105 % single class
106 for ind=ispool_nnz
107 P((ind-1)*K+1:ind*K,:)=0;
108 end
109 Pmat = P;
110 P = cell(K,K);
111 for r=1:K
112 for s=1:K
113 P{r,s} = zeros(I);
114 for ind=1:I
115 for jnd=1:I
116 P{r,s}(ind,jnd) = Pmat((ind-1)*K+r,(jnd-1)*K+s);
117 end
118 end
119 end
120 end
121 end
122end
123
124if numel(P) == K
125 % 1 matrix per class
126 for r=1:K
127 for ind=ispool_nnz
128 P{r}((ind-1)*K+1:ind*K,:)=0;
129 end
130 end
131 Pmat = P;
132 P = cell(K,K);
133 for r=1:K
134 P{r,r} = Pmat{r};
135 for s=setdiff(1:K,r)
136 P{r,s} = zeros(I);
137 end
138 end
139end
140
141% Inject deferred retrieval-system routing entries registered by
142% Cache.setRetrievalSystem. The auto-generated retrieval-pending / -complete
143% classes are not part of the user-supplied P, so their routing edges
144% (cache->queue, queue->queue, queue->cache with the pending->complete class
145% switch) are recorded on the Cache node and merged into P here, before the
146% routing matrix is processed. link() then auto-creates the artificial
147% class-switch node for the pending->complete edge like any other P-encoded
148% class switch.
149for ind=1:I
150 if isa(self.nodes{ind}, 'Cache') && isprop(self.nodes{ind}, 'retrievalRoutingEntries') ...
151 && ~isempty(self.nodes{ind}.retrievalRoutingEntries)
152 rre = self.nodes{ind}.retrievalRoutingEntries;
153 for e=1:numel(rre)
154 ent = rre{e}; % [fromCls, toCls, srcNode, dstNode, prob]
155 if isempty(P{ent(1),ent(2)})
156 P{ent(1),ent(2)} = zeros(I);
157 end
158 P{ent(1),ent(2)}(ent(3),ent(4)) = ent(5);
159 end
160 end
161end
162
163isemptyP = false(K,K);
164for r=1:K
165 for s=1:K
166 if isempty(P{r,s})
167 isemptyP(r,s)= true;
168 P{r,s} = zeros(I);
169 else
170 for ind=ispool_nnz
171 P{r,s}(ind,:)=0;
172 end
173 end
174 end
175end
176
177csnodematrix = cell(I,I);
178for ind=1:I
179 for jnd=1:I
180 csnodematrix{ind,jnd} = zeros(K,K);
181 end
182end
183
184for r=1:K
185 for s=1:K
186 if ~isemptyP(r,s)
187 [If,Jf] = find(P{r,s});
188 for k=1:size(If,1)
189 csnodematrix{If(k),Jf(k)}(r,s) = P{r,s}(If(k),Jf(k));
190 end
191 end
192 end
193end
194
195
196% for r=1:R
197% Psum=cellsum({P{r,:}})*ones(M,1);
198% if min(Psum)<1-GlobalConstants.CoarseTol
199% line_error(mfilename,'Invalid routing probabilities (Node %d departures, switching from class %d).',minpos(Psum),r);
200% end
201% if max(Psum)>1+GlobalConstants.CoarseTol
202% line_error(mfilename,sprintf('Invalid routing probabilities (Node %d departures, switching from class %d).',maxpos(Psum),r));
203% end
204% end
205
206self.sn.rtorig = P;
207
208% As we will now create a CS for each link i->j,
209% we now condition on the job going from node i to j
210for ind=1:I
211 for jnd=1:I
212 for r=1:K
213 S = sum(csnodematrix{ind,jnd}(r,:));
214 if S>0
215 csnodematrix{ind,jnd}(r,:)=csnodematrix{ind,jnd}(r,:)/S;
216 else
217 csnodematrix{ind,jnd}(r,r)=1.0;
218 end
219 end
220 end
221end
222
223csid = zeros(I);
224% eye(K) is because any job that travels to an autoAdded class
225% switch stays in the same class prior to reaching the ClassSwitch
226% and anyway the diagonal of csMatrix is irrelevant for the chains
227csMatrix = eye(K);
228nodeNames = self.getNodeNames;
229for ind=1:I
230 for jnd=1:I
231 csMatrix = csMatrix + csnodematrix{ind,jnd};
232 if ~isdiag(csnodematrix{ind,jnd})
233 self.nodes{end+1} = ClassSwitch(self, sprintf('CS_%s_to_%s',nodeNames{ind},nodeNames{jnd}),csnodematrix{ind,jnd});
234 self.nodes{end}.autoAdded = true;
235 csid(ind,jnd) = length(self.nodes);
236 end
237 end
238end
239
240for ind=1:I
241 % this is to ensure that also stateful cs like caches
242 % are accounted
243 if isa(self.nodes{ind},'Cache')
244 for r=find(self.nodes{ind}.server.hitClass)
245 csMatrix(r,self.nodes{ind}.server.hitClass(r)) = 1.0;
246 end
247 for r=find(self.nodes{ind}.server.missClass)
248 csMatrix(r,self.nodes{ind}.server.missClass(r)) = 1.0;
249 end
250 elseif isa(self.nodes{ind},'ClassSwitch')
251 if isempty(self.nodes{ind}.server.csMatrix )
252 line_error(mfilename,'Uninitialized ClassSwitch node, use the setClassSwitchingMatrix method.');
253 end
254 csMatrix = csMatrix | self.nodes{ind}.server.csMatrix > 0.0;
255 end
256end
257
258self.csMatrix = csMatrix~=0;
259
260Ip = length(self.nodes); % number of nodes after addition of cs nodes
261
262% resize matrices
263for r=1:K
264 for s=1:K
265 P{r,s}((I+1):Ip,(I+1):Ip)=0;
266 end
267end
268
269for ind=1:I
270 for jnd=1:I
271 if csid(ind,jnd)>0
272 % re-route
273 for r=1:K
274 for s=1:K
275 if P{r,s}(ind,jnd)>0
276 P{r,r}(ind,csid(ind,jnd)) = P{r,r}(ind,csid(ind,jnd)) + P{r,s}(ind,jnd);
277 P{r,s}(ind,jnd) = 0;
278 P{s,s}(csid(ind,jnd),jnd) = 1;
279 end
280 end
281 end
282 end
283 end
284end
285
286connected = zeros(Ip);
287nodes = self.nodes;
288% Clear non-station nodes' outputStrategy before setting new routing.
289% This prevents accumulation from previous link() calls,
290% since setProbRouting appends entries and resetNetwork only
291% clears station nodes (e.g., Queue, Delay), not non-station
292% stateful nodes (e.g., Router).
293%
294% Use the "PreservingRouted" variant when available so that classes already
295% configured upstream (e.g. by Cache.setRetrievalSystem on its auto-generated
296% ClassSwitch and Queues for retrieval-pending classes that don't appear in
297% P) survive the clearing step. Falls back to the legacy clearing for
298% Dispatcher subclasses (Forker/Firing/Linkage) that don't define the variant.
299for ind=1:Ip
300 if ~isa(nodes{ind}, 'Station') && ismethod(nodes{ind}.output, 'initDispatcherJobClasses')
301 if ismethod(nodes{ind}.output, 'initDispatcherJobClassesPreservingRouted')
302 nodes{ind}.output.initDispatcherJobClassesPreservingRouted(self.classes);
303 else
304 nodes{ind}.output.initDispatcherJobClasses(self.classes);
305 end
306 % Sync sn.routing with the post-clear outputStrategy so getRoutingMatrix
307 % won't try PROB routing for classes whose entry was cleared, while
308 % preserved entries continue to advertise their routing strategy.
309 if ~isempty(self.sn) && isfield(self.sn, 'routing') && ind <= size(self.sn.routing, 1)
310 for k=1:K
311 if k <= numel(nodes{ind}.output.outputStrategy) && ~isempty(nodes{ind}.output.outputStrategy{k})
312 entry = nodes{ind}.output.outputStrategy{k};
313 self.sn.routing(ind,k) = RoutingStrategy.fromText(entry{2});
314 else
315 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
316 end
317 end
318 end
319 end
320end
321for r=1:K
322 [If,Jf,S] = find(P{r,r});
323 for k=1:length(If)
324 if connected(If(k),Jf(k)) == 0
325 self.addLink(nodes{If(k)}, nodes{Jf(k)});
326 connected(If(k),Jf(k)) = 1;
327 end
328 nodes{If(k)}.setProbRouting(self.classes{r}, nodes{Jf(k)}, S(k));
329 end
330end
331self.nodes = nodes;
332
333% Refresh sn.routing from outputStrategy for non-station nodes.
334% The initDispatcherJobClasses call above set sn.routing to DISABLED,
335% but setProbRouting has since repopulated outputStrategy for routed classes.
336if ~isempty(self.sn) && isfield(self.sn, 'routing')
337 for ind=1:Ip
338 if ~isa(nodes{ind}, 'Station') && ind <= size(self.sn.routing, 1)
339 for k=1:K
340 if isempty(nodes{ind}.output.outputStrategy{k})
341 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
342 else
343 self.sn.routing(ind,k) = RoutingStrategy.fromText(nodes{ind}.output.outputStrategy{k}{2});
344 end
345 end
346 end
347 end
348end
349
350% check if the probability out of any node sums to >1.0
351pSum = cellsum(P);
352isAboveOne = pSum > 1.0 + GlobalConstants.FineTol;
353if any(isAboveOne)
354 for ind=find(isAboveOne)
355 if SchedStrategy.toId(self.nodes{ind}.schedStrategy) ~= SchedStrategy.FORK
356 line_error(mfilename,sprintf('The total routing probability for jobs leaving node %s in class %s is greater than 1.0.',self.nodes{ind}.name,self.classes{r}.name));
357 end
358 % elseif pSum < 1.0 - GlobalConstants.FineTol % we cannot check this case as class r may not reach station i, in which case its outgoing routing prob is zero
359 % if self.nodes{i}.schedStrategy ~= SchedStrategy.EXT % if not a sink
360 % line_error(mfilename,'The total routing probability for jobs leaving node %s in class %s is less than 1.0.',self.nodes{i}.name,self.classes{r}.name);
361 % end
362 end
363end
364
365for ind=1:I
366 if isa(self.nodes{ind},'Place')
367 self.nodes{ind}.init;
368 end
369end
370
371if isReset && ~isempty(self.sn) && isfield(self.sn,'rates')
372 self.refreshChains; % without this exception with linkAndLog
373end
374
375%% Check for reducible routing (absorbing states)
376if self.enableChecks
377 % Pass routing matrix directly to avoid getStruct() call during link()
378 [isErg, ergInfo] = self.isRoutingErgodic(self.sn.rtorig);
379 if ~isErg && ~isempty(ergInfo.absorbingStations)
380 % Build warning message
381 absNames = strjoin(ergInfo.absorbingStations, ', ');
382 line_warning(mfilename, 'Reducible network topology detected, results may be unreliable.\n');
383 end
384end
385
386%% DEBUGGING
387if false
388 % create java version of the network
389 if isempty(self.obj)
390 jnetwork = JLINE.from_line_network(self);
391 else
392 jnetwork = self.obj;
393 end
394 % compare sn data structures
395 jsn = JLINE.from_jline_struct(jnetwork);
396 fprintf(1,'* Comparison with Java NetworkStruct: ');
397 bool = testJavaStruct(self.getName(),self.getStruct(),jsn);
398end
399
400end
Definition mmt.m:124