1function [rt,rtnodes,conn,chains,rtNodesByClass,rtNodesByStation] = getRoutingMatrix(self, arvRates)
2% [
RT,RTNODES,CONNMATRIX,CHAINS,RTNODESBYCLASS,RTNODESBYSTATION] = GETROUTINGMATRIX(ARVRATES)
4% Copyright (c) 2012-2026, Imperial College London
9 idxSource = find(sn.nodetype == NodeType.Source);
10 idxSink = find(sn.nodetype == NodeType.Sink);
11 indexOpenClasses = find(isinf(sn.njobs));
12 hasOpen = ~isempty(indexOpenClasses);
14 arvRates = sn.rates(sn.nodeToStation(idxSource),:);
21 statefulNodes = find(sn.isstateful)
';
24 idxSource = self.getIndexSourceNode;
25 idxSink = self.getIndexSinkNode;
26 indexOpenClasses = self.getIndexOpenClasses;
29 conn = self.getConnectionMatrix;
31 hasOpen = hasOpenClasses(self);
33 I = self.getNumberOfNodes;
34 K = self.getNumberOfClasses;
35 NK = self.getNumberOfJobs;
38 if isempty(self.nodes{ind}.output.outputStrategy{k})
39 sn.routing(ind,k) = RoutingStrategy.DISABLED;
41 sn.routing(ind,k) = RoutingStrategy.fromText(nodes{ind}.output.outputStrategy{k}{2});
45 statefulNodes = self.getIndexStatefulNodes;
51% The first loop considers the class at which a job enters the
55 outputStrategy = node_i.output.outputStrategy;
56 switch class(node_i.output)
58 % Fork nodes send to ALL branches with probability 1.0 each
59 % (not 1.0/fanout). A Fork with arrival rate lambda sends
60 % lambda to EACH outgoing branch, not lambda/fanout.
62 outputStrategy_k = outputStrategy{k};
63 switch sn.routing(ind,k)
64 case RoutingStrategy.PROB
65 % Use output strategy destinations (includes ClassSwitch nodes)
66 if ~isempty(outputStrategy_k) && length(outputStrategy_k) >= 3 && ~isempty(outputStrategy_k{end})
67 for t=1:length(outputStrategy_k{end})
68 jnd = outputStrategy_k{end}{t}{1}.index;
69 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = 1.0;
72 % Fallback: uniform to all connected nodes
75 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = 1.0;
80 % Non-PROB routing: route to all connected nodes
83 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = 1.0;
89 isSink_i = isa(node_i,'Sink
');
91 outputStrategy_k = outputStrategy{k};
92 switch sn.routing(ind,k)
93 case RoutingStrategy.PROB
94 if isinf(NK(k)) || ~isSink_i
95 for t=1:length(outputStrategy_k{end}) % for all outgoing links
96 jnd = outputStrategy_k{end}{t}{1}.index;
97 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = outputStrategy_k{end}{t}{2};
100 case RoutingStrategy.DISABLED
103 if isa(self.classes{k},'SelfLoopingClass
')
104 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = 0;
105 elseif isa(self.classes{k},'Signal
') || isa(self.classes{k},'OpenSignal
') || isa(self.classes{k},'ClosedSignal
')
106 % Signal classes with DISABLED routing should not route
107 % (their routing is set explicitly via the P matrix)
108 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = 0;
110 % we set this to be non-zero as otherwise the
111 % classes that do not visit a classswitch are
112 % misconfigured in JMT
113 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = 1/sum(conn(ind,:));
117 case {RoutingStrategy.RAND, RoutingStrategy.RROBIN, RoutingStrategy.JSQ}
118 if isinf(NK(k)) % open class
121 rtnodes((ind-1)*K+k,(jnd-1)*K+k)=1/sum(conn(ind,:));
124 elseif ~isa(node_i,'Source
') && ~isSink_i % don't route closed
classes out of source
nodes
125 connectionsClosed = conn;
126 if connectionsClosed(ind,idxSink)
127 connectionsClosed(ind,idxSink) = 0;
130 if connectionsClosed(ind,jnd)>0
131 rtnodes((ind-1)*K+k,(jnd-1)*K+k)=1/(sum(connectionsClosed(ind,:)));
135 case RoutingStrategy.WRROBIN
136 % Weighted round-robin: compute routing probabilities from weights
137 % Weights are stored in outputStrategy_k{3} as {destination, weight} pairs
138 if ~isempty(outputStrategy_k) && length(outputStrategy_k) >= 3 && ~isempty(outputStrategy_k{3})
139 % Compute total weight (excluding sink
for closed
classes)
141 for t=1:length(outputStrategy_k{3})
142 dest = outputStrategy_k{3}{t}{1};
143 destIdx = dest.index;
144 weight = outputStrategy_k{3}{t}{2};
145 % For closed
classes, exclude sink
146 if ~isinf(NK(k)) && ~isempty(idxSink) && destIdx == idxSink
149 totalWeight = totalWeight + weight;
151 % Set routing probabilities based on weights
153 for t=1:length(outputStrategy_k{3})
154 dest = outputStrategy_k{3}{t}{1};
155 destIdx = dest.index;
156 weight = outputStrategy_k{3}{t}{2};
157 % For closed
classes, exclude sink
158 if ~isinf(NK(k)) && ~isempty(idxSink) && destIdx == idxSink
161 rtnodes((ind-1)*K+k,(destIdx-1)*K+k) = weight / totalWeight;
164 % Fallback to uniform
if no weights sum to positive
168 rtnodes((ind-1)*K+k,(jnd-1)*K+k)=1/sum(conn(ind,:));
171 elseif ~isa(node_i,
'Source') && ~isSink_i
172 connectionsClosed = conn;
173 if ~isempty(idxSink) && connectionsClosed(ind,idxSink)
174 connectionsClosed(ind,idxSink) = 0;
177 if connectionsClosed(ind,jnd)>0
178 rtnodes((ind-1)*K+k,(jnd-1)*K+k)=1/(sum(connectionsClosed(ind,:)));
184 % Fallback to uniform
if no weight data available
188 rtnodes((ind-1)*K+k,(jnd-1)*K+k)=1/sum(conn(ind,:));
191 elseif ~isa(node_i,
'Source') && ~isSink_i
192 connectionsClosed = conn;
193 if ~isempty(idxSink) && connectionsClosed(ind,idxSink)
194 connectionsClosed(ind,idxSink) = 0;
197 if connectionsClosed(ind,jnd)>0
198 rtnodes((ind-1)*K+k,(jnd-1)*K+k)=1/(sum(connectionsClosed(ind,:)));
206 rtnodes((ind-1)*K+k,(jnd-1)*K+k) = GlobalConstants.FineTol;
209 %line_error(mfilename,[outputStrategy_k{2},
' routing policy is not yet supported.']);
215% The second loop corrects the first one at
nodes that change
216% the
class of the job in the service section.
218 if isa(
nodes{ind}.server,
'StatelessClassSwitcher')
219 Pi = rtnodes(((ind-1)*K+1):ind*K,:);
220 Pcs =
nodes{ind}.server.csFun(1:K,1:K);
221 rtnodes(((ind-1)*K+1):ind*K,:) = 0;
222 for jnd=1:I % destination
223 Pij = Pi(:,((jnd-1)*K+1):jnd*K); %Pij(r,s)
224 % Find the routing probability section determined by the router section in the first loop
225 rtnodes(((ind-1)*K+1) : ((ind-1)*K+K),(jnd-1)*K+(1:K)) = Pcs.*repmat(diag(Pij)
',K,1);
227 elseif isa(nodes{ind}.server,'StatefulClassSwitcher
')
228 Pi = rtnodes(((ind-1)*K+1):ind*K,:);
231 Pcs(r,s) = nodes{ind}.server.csFun(r,s,[],[]); % get csmask
234 rtnodes(((ind-1)*K+1):ind*K,:) = 0;
235 if isa(nodes{ind}.server,'CacheClassSwitcher
')
237 if (isempty(find(r == nodes{ind}.server.hitClass)) && isempty(find(r == nodes{ind}.server.missClass)))
238 Pcs(r,:) = Pcs(r,:)/sum(Pcs(r,:));
243 if (isempty(find(r == nodes{ind}.server.hitClass)) && isempty(find(r == nodes{ind}.server.missClass)))
244 for jnd=1:I % destination
246 Pi((ind-1)*K+r,(jnd-1)*K+s) = 0;
253 if length(nodes{ind}.server.actualHitProb)>=r && length(nodes{ind}.server.hitClass)>=r
254 ph = nodes{ind}.server.actualHitProb(r);
255 pm = nodes{ind}.server.actualMissProb(r);
256 h = nodes{ind}.server.hitClass(r);
257 m = nodes{ind}.server.missClass(r);
258 rtnodes((ind-1)*K+r,(ind-1)*K+h) = ph;
259 rtnodes((ind-1)*K+r,(ind-1)*K+m) = pm;
261 if length(nodes{ind}.server.hitClass)>=r
262 h = nodes{ind}.server.hitClass(r);
263 m = nodes{ind}.server.missClass(r);
264 if h > 0 && m > 0 % Only set NaN if hitClass/missClass are defined for this class
265 rtnodes((ind-1)*K+r,(ind-1)*K+h) = NaN;
266 rtnodes((ind-1)*K+r,(ind-1)*K+m) = NaN;
271 for jnd=1:I % destination
272 Pij = Pi(1:K,((jnd-1)*K+1):jnd*K); %Pij(r,s)
274 if ~(isempty(find(r == nodes{ind}.server.hitClass)) && isempty(find(r == nodes{ind}.server.missClass)))
276 % Find the routing probability section determined by the router section in the first loop
277 %Pnodes(((i-1)*K+1):i*K,((j-1)*K+1):j*K) = Pcs*Pij;
278 rtnodes((ind-1)*K+r,(jnd-1)*K+s) = Pcs(r,s)*Pij(s,s);
287% Handle self-looping classes
289 if isa(self.classes{r},'SelfLoopingClass
')
290 sl_refstat = self.classes{r}.refstat.index;
292 for jnd=1:I % destination
294 rtnodes((ind-1)*K+r,(jnd-1)*K+s) = 0; % zero any prior setting
300 rtnodes((sl_refstat-1)*K+r,(sl_refstat-1)*K+s) = 1;
306% ignore all chains containing a Pnodes column that sums to 0,
307% since these are classes that cannot arrive to the node
308% unless this column belongs to the source
309colsToIgnore = find(sum(rtnodes,1)==0);
311 colsToIgnore = setdiff(colsToIgnore,(idxSource-1)*K+(1:K));
314% We route back from the sink to the source. Since open classes
315% have an infinite population, if there is a class switch QN
316% with the following chains
317% Source -> (A or B) -> C -> Sink
319% We can re-route class C into the source either as A or B or C.
320% We here re-route back as C and leave for the chain analyzer
321% to detect that C is in a chain with A and B and change this
324csmask = self.csMatrix;
325if isempty(csmask) % models not built with link(P)
326 % rtnodes+rtnodes' makes the matrix undirected, it helps grouping
327 % transient states with all the recurrent states their can reach
328 % into a single
"chain" of the LINE model
329 [C,WCC]=weaklyconncomp(rtnodes+rtnodes
');
330 WCC(colsToIgnore) = 0;
331 chainCandidates = cell(1,C);
333 chainCandidates{r} = find(WCC==r);
336 chains = false(length(chainCandidates),K); % columns are classes, rows are chains
338 for t=1:length(chainCandidates)
339 if length(chainCandidates{t})>1
341 chains(tmax,(mod(chainCandidates{t}-1,K)+1))=true;
344 chains(tmax+1:end,:)=[];
345else % this is faster since csmask is smaller than rtnodes
346 [C,WCC] = weaklyconncomp(csmask+csmask');
348 chainCandidates = cell(1,C);
350 chainCandidates{c} = find(WCC==c);
353 chains =
false(length(chainCandidates),K);
354 for t=1:length(chainCandidates)
355 chains(t,chainCandidates{t}) =
true;
359chains = unique(chains,
'rows');
361 chains = sortrows(chains,
'descend');
363 chains = sortrows(chains);
366% overlapping chains can occur
if a
class that enters in a Cache
367% reappars downhill in the network in rtnodes in that
case we merge
370for col=find(sum(chains,1)>1)
371 rows = find(chains(:,col));
373 chains(rows(1),:) = chains(rows(1),:) | chains(r,:);
374 splitChains(end+1)=r;
377chains(splitChains,:)=[];
379% We now obtain the routing matrix
P by ignoring the non-stateful
380%
nodes and calculating by the stochastic complement method the
381% correct transition probabilities, that includes the effects
382% of the non-stateful
nodes (e.g., ClassSwitch)
383statefulNodesClasses = [];
384for ind=statefulNodes %
#ok<FXSET>
385 statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
388%
this routes open
classes back from the sink into the source
389% it will not work with non-renewal arrivals as it choses in which open
390%
class to reroute a job with probability depending on the arrival rates
392 arvRates(isnan(arvRates)) = 0;
393 for s=indexOpenClasses
394 s_chain = find(chains(:,s));
395 others_in_chain = find(chains(s_chain,:)); %#ok<FNDSB>
396 rtnodes((idxSink-1)*K+others_in_chain,(idxSource-1)*K+others_in_chain) = repmat(arvRates(others_in_chain)/sum(arvRates(others_in_chain)),length(others_in_chain),1);
400%% Hide the
nodes that are not stateful
401rt = dtmc_stochcomp(rtnodes,statefulNodesClasses);
403% verify irreducibility
404% disabled as it casts warning in many models that are seemingly fine
406% eigen = eig(rt); % exception
if rt has NaN or Inf
408% line_warning(mfilename,
'Solutions may be invalid, the routing matrix is reducible. The path of two or more classes form independent non-interacting models.');
415%% Compute optional outputs
417 rtNodesByClass = cellzeros(K,K,I,I);
422 rtNodesByClass{s,r}(ind,jnd) = rtnodes((ind-1)*K+s,(jnd-1)*K+r);
430 rtNodesByStation = cellzeros(I,I,K,K);
431 for ind=1:I %#ok<FXSET>
435 rtNodesByStation{ind,jnd}(r,s) = rtnodes((ind-1)*K+s,(jnd-1)*K+r);