1function self = link(self,
P)
4% Copyright (c) 2012-2026, Imperial College London
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.');
11if isa(
P,'RoutingMatrix')
19 self.resetNetwork; % remove artificial class switch
nodes
21K = self.getNumberOfClasses;
22I = self.getNumberOfNodes;
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.
');
29if size(P,1) == size(P,2)
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));
43 switch class(self.nodes{ind})
45 % note that since a cache needs to distinguish hits and
46 % misses, it needs to do class-switch unless the model is
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
');
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}.
75% assign routing for self-looping jobs
77 if isa(self.classes{r},'SelfLoopingClass
')
81 P{r,r}(self.classes{r}.refstat, self.classes{r}.refstat) = 1.0;
85% link virtual sinks automatically to sink
86ispool = cellisa(self.nodes,'Sink
');
88 line_error(mfilename,'The model can have at most one sink node.
');
91if sum(cellisa(self.nodes,'Source
')) > 1
92 line_error(mfilename,'The model can have at most one source node.
');
94ispool_nnz = find(ispool)';
107 P((ind-1)*K+1:ind*K,:)=0;
116 P{r,s}(ind,jnd) = Pmat((ind-1)*K+r,(jnd-1)*K+s);
128 P{r}((ind-1)*K+1:ind*K,:)=0;
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
150 if isa(self.nodes{ind},
'Cache') && isprop(self.nodes{ind},
'retrievalRoutingEntries') ...
151 && ~isempty(self.nodes{ind}.retrievalRoutingEntries)
152 rre = self.
nodes{ind}.retrievalRoutingEntries;
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);
158 P{ent(1),ent(2)}(ent(3),ent(4)) = ent(5);
163isemptyP =
false(K,K);
177csnodematrix = cell(I,I);
180 csnodematrix{ind,jnd} = zeros(K,K);
187 [If,Jf] = find(
P{r,s});
189 csnodematrix{If(k),Jf(k)}(r,s) =
P{r,s}(If(k),Jf(k));
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);
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));
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
213 S = sum(csnodematrix{ind,jnd}(r,:));
215 csnodematrix{ind,jnd}(r,:)=csnodematrix{ind,jnd}(r,:)/S;
217 csnodematrix{ind,jnd}(r,r)=1.0;
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
228nodeNames = self.getNodeNames;
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);
241 %
this is to ensure that also stateful cs like caches
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;
247 for r=find(self.nodes{ind}.server.missClass)
248 csMatrix(r,self.
nodes{ind}.server.missClass(r)) = 1.0;
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.');
254 csMatrix = csMatrix | self.nodes{ind}.server.csMatrix > 0.0;
258self.csMatrix = csMatrix~=0;
260Ip = length(self.nodes); % number of
nodes after addition of cs
nodes
265 P{r,s}((I+1):Ip,(I+1):Ip)=0;
276 P{r,r}(ind,csid(ind,jnd)) =
P{r,r}(ind,csid(ind,jnd)) +
P{r,s}(ind,jnd);
278 P{s,s}(csid(ind,jnd),jnd) = 1;
286connected = zeros(Ip);
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).
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.
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);
304 nodes{ind}.output.initDispatcherJobClasses(self.classes);
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)
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});
315 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
322 [If,Jf,S] = find(
P{r,r});
324 if connected(If(k),Jf(k)) == 0
326 connected(If(k),Jf(k)) = 1;
328 nodes{If(k)}.setProbRouting(self.classes{r},
nodes{Jf(k)}, S(k));
334% The initDispatcherJobClasses call above set sn.routing to DISABLED,
336if ~isempty(self.sn) && isfield(self.sn,
'routing')
338 if ~isa(
nodes{ind},
'Station') && ind <= size(self.sn.routing, 1)
340 if isempty(
nodes{ind}.output.outputStrategy{k})
341 self.sn.routing(ind,k) = RoutingStrategy.DISABLED;
343 self.sn.routing(ind,k) = RoutingStrategy.fromText(
nodes{ind}.output.outputStrategy{k}{2});
350% check
if the probability out of any node sums to >1.0
352isAboveOne = pSum > 1.0 + GlobalConstants.FineTol;
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));
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);
366 if isa(self.nodes{ind},
'Place')
367 self.
nodes{ind}.init;
371if isReset && ~isempty(self.sn) && isfield(self.sn,
'rates')
372 self.refreshChains; % without this exception with linkAndLog
375%% Check for reducible routing (absorbing states)
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');
388 % create java version of the network
390 jnetwork = JLINE.from_line_network(self);
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);