1function [rt, rtfun, rtnodes, sn] = refreshRoutingMatrix(self, rates)
2% [
RT, RTFUN, CSMASK, RTNODES, SN] = REFRESHROUTINGMATRIX(RATES)
4% Copyright (c) 2012-2026, Imperial College London
10 line_error(mfilename,
'refreshRoutingMatrix cannot retrieve station rates, pass them as an input parameters.');
18stateful = find(sn.isstateful)
';
20indSource = find(sn.nodetype == NodeType.Source);
21indOpenClasses = find(sn.njobs == Inf);
23 arvRates(r) = rates(sn.nodeToStation(indSource),r);
26[rt, rtnodes, linksmat, chains] = self.getRoutingMatrix(arvRates);
32 if all(sn.routing(:,r) == -1)
33 line_error(mfilename,sprintf('Routing strategy in
class %d
is unspecified at all
nodes.
',r));
38isStateDep = any(sn.isstatedep(:,3));
40rnodefuncell = cell(M*K,M*K);
47 if sn.isstatedep(ind,3)
48 switch sn.routing(ind,r)
49 case RoutingStrategy.RROBIN
50 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_rr(ind, jnd, r, s, linksmat, state_before, state_after);
51 case RoutingStrategy.WRROBIN
52 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after);
53 case RoutingStrategy.JSQ
54 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after);
55 case RoutingStrategy.SQ
56 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_sq(ind, jnd, r, s, linksmat, state_before, state_after);
57 case RoutingStrategy.RL
58 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_rl(ind, jnd, r, s, linksmat, state_before, state_after);
60 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
63 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
71statefulNodesClasses = [];
72for ind=getIndexStatefulNodes(self)
73 statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
76% we now generate the node routing matrix for the given state and then
77% lump the states for non-stateful nodes so that run gives the routing
78% table for stateful nodes only
79statefulNodesClasses = [];
81 statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
85 rtfunraw = @(state_before, state_after) dtmc_stochcomp(cell2mat(cellfun(@(f) f(state_before, state_after), rnodefuncell,'UniformOutput
',false)), statefulNodesClasses);
87 %rtfun = memoize(rtfunraw); % memoize to reduce the number of stoch comp calls
88 %rtfun.CacheSize = 6000^2;
90 rtfun = @(state_before, state_after) dtmc_stochcomp(rtnodes, statefulNodesClasses);
93nchains = size(chains,1);
94inchain = cell(1,nchains);
96 inchain{c} = find(chains(c,:));
101%sn.rtorig = RoutingMatrix.rtnodes2rtorig(sn); %% causes issues to
102%JLINE.from_line_links in convering example_closedModel_6
108 if range(sn.refstat(inchain{c}))>0
109 line_error(mfilename,sprintf('Classes within chain %d (classes: %s) have different reference stations.
',c,mat2str(find(sn.chains(c,:)))));
114 function p = sub_rr(ind, jnd, r, s, linksmat, state_before, state_after)
115 % P = SUB_RR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
117 isf = self.sn.nodeToStateful(ind);
118 if isempty(state_before{isf})
119 p = min(linksmat(ind,jnd),1);
122 p = double(state_after{isf}(sub_routeslot(ind, r, state_after{isf}))==jnd);
129 function slot = sub_routeslot(ind, r, state_i)
130 % SLOT = SUB_ROUTESLOT(IND, R, STATE_I)
131 % Column of STATE_I holding the round-robin pointer of class R at node
134 % The per-class routing pointers are appended in class order, and only
135 % for the classes that actually route round-robin, followed by the
136 % node-level block (nvars column 2R+1) if the node has one. The pointer
137 % is therefore located by counting back from the end of the state: over
138 % the node-level block, then over the pointers of the classes after r.
140 % Reading it as state_i(end-R+r) instead, as this did, assumes that the
141 % pointers are exactly the last R columns. That silently reads the wrong
142 % column whenever the node carries a trailing block of its own (the
143 % polling controller of a POLLING station), and also whenever only some
144 % of the classes route round-robin, in which case a pointer is confused
145 % with a service phase.
146 R = self.sn.nclasses;
147 nodeblock = self.sn.nvars(ind, 2*R+1);
148 after = sum(self.sn.nvars(ind, (R+r+1):(2*R)));
149 slot = numel(state_i) - nodeblock - after;
152 function p = sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after)
153 % P = SUB_WRR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
154 % WRR slot holds a POSITION in weighted_outlinks; map it to the
155 % destination node index before comparing.
157 isf = self.sn.nodeToStateful(ind);
158 if isempty(state_before{isf})
159 p = min(linksmat(ind,jnd),1);
162 pos = state_after{isf}(sub_routeslot(ind, r, state_after{isf}));
163 np = self.sn.nodeparam{ind}{r};
164 if isfield(np,'weighted_outlinks
') && ~isempty(np.weighted_outlinks) ...
165 && pos >= 1 && pos <= length(np.weighted_outlinks)
166 dest = np.weighted_outlinks(pos);
167 elseif pos >= 1 && pos <= length(np.outlinks)
168 dest = np.outlinks(pos);
172 p = double(dest == jnd);
179 function p = sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
180 % P = SUB_JSQ(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER) %#OK<INUSD>
182 isf = self.sn.nodeToStateful(ind);
183 if isempty(state_before{isf})
184 p = min(linksmat(ind,jnd),1);
187 n = Inf*ones(1,self.sn.nnodes);
188 for knd=1:self.sn.nnodes
190 ksf = self.sn.nodeToStateful(knd);
191 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
195 p = 1 / sum(n == min(n));
206 function p = sub_rl(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
207 % P = SUB_RL(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER) %#OK<INUSD>
209 isf = self.sn.nodeToStateful(ind);
210 if isempty(state_before{isf})
211 p = min(linksmat(ind,jnd),1);
214 % ----- new added contents ----- %
215 if self.nodes{ind}.output.outputStrategy{1,r}{5}==0 % state_size=0, use tabular value fn
216 % Multi-class: each class supplies its own value function.
217 % State.toMarginal aggregates queue lengths across classes,
218 % so the per-class call works as long as outputStrategy{r}
219 % is configured per class.
220 value_function = self.nodes{ind}.output.outputStrategy{1,r}{3};
221 nodes_need_action = self.nodes{ind}.output.outputStrategy{1,r}{4};
223 if ~isempty(find(nodes_need_action==ind, 1))
224 indQueue = find(self.sn.nodetype == NodeType.Queue);
225 v = Inf*ones(1, self.sn.nnodes); % value fn
226 n = Inf*ones(1, self.sn.nnodes); % queue length
227 x = zeros(1, length(indQueue)); % current state
228 for knd_idx=1:length(indQueue)
229 knd = indQueue(knd_idx);
230 ksf = self.sn.nodeToStateful(knd); %% does the state removes the job from the departure node already?
231 x(knd_idx) = State.toMarginal(self.sn, knd, state_before{ksf});
234 for knd = 1:self.sn.nnodes
235 if linksmat(ind, knd)
237 tmp(indQueue == knd) = tmp(indQueue == knd) + 1;
238 if max(tmp) <= size(value_function, 1)
239 ttmp = num2cell(tmp);
240 v(knd) = value_function(ttmp{:});
242 ksf = self.sn.nodeToStateful(knd);
243 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
247 if min(v) < Inf && max(x+1) < size(value_function, 1) % in action space
249 p = 1 / sum(v == min(v));
253 else % not in action space, use JSQ
255 p = 1 / sum(n == min(n));
261 else % not in nodes_need_action: this node doesn't use RL results, use JSQ
262 n = Inf*ones(1,self.sn.nnodes);
263 for knd=1:self.sn.nnodes
265 ksf = self.sn.nodeToStateful(knd);
266 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
270 p = 1 / sum(n == min(n));
276 elseif self.nodes{ind}.output.outputStrategy{1,r}{5}>0 % state_size>0, use fn approx
for value fn
277 % Multi-
class:
class-specific coefficient row vector.
278 coeff = self.nodes{ind}.output.outputStrategy{1,r}{3};
279 nodes_need_action = self.nodes{ind}.output.outputStrategy{1,r}{4};
280 stateSize = self.nodes{ind}.output.outputStrategy{1,r}{5};
282 if ~isempty(find(nodes_need_action==ind, 1))
283 indQueue = find(self.sn.nodetype == NodeType.Queue);
284 v = Inf*ones(1, self.sn.nnodes); % value fn
285 n = Inf*ones(1, self.sn.nnodes); % queue length
286 x = zeros(1, length(indQueue)); % current state
287 for knd_idx=1:length(indQueue)
288 knd = indQueue(knd_idx);
289 ksf = self.sn.nodeToStateful(knd); %% does the state removes the job from the departure node already?
290 x(knd_idx) = State.toMarginal(self.sn, knd, state_before{ksf});
292 for knd = 1:self.sn.nnodes
293 if linksmat(ind, knd)
295 tmp(indQueue == knd) = tmp(indQueue == knd) + 1;
298 for i = 1:length(tmp)
299 for j = i:length(tmp)
300 tmp_vec(end+1) = tmp(i)* tmp(j);
303 v(knd) = tmp_vec * coeff.
';
305 ksf = self.sn.nodeToStateful(knd);
306 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
309 if min(v) < Inf && max(x+1) < stateSize % in action space
311 p = 1 / sum(v == min(v));
315 else % not in action space, use JSQ
317 p = 1 / sum(n == min(n));
322 else % not in nodes_need_action: this node doesn't use RL results, use JSQ
323 n = Inf*ones(1,self.sn.nnodes);
324 for knd=1:self.sn.nnodes
326 ksf = self.sn.nodeToStateful(knd);
327 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
331 p = 1 / sum(n == min(n));
337 else % no value fn, use JSQ
338 % ----- end of
new added contents ----- %
340 n = Inf*ones(1,self.sn.nnodes);
341 for knd=1:self.sn.nnodes
343 ksf = self.sn.nodeToStateful(knd);
344 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
348 p = 1 / sum(n == min(n));
359 function p = sub_sq(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
360 %
P = SUB_SQ(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
361 % SQ(d), shortest queue of d: matches LDES semantics in
362 % Solver_ssj.kt:selectSQDestination.
364 % Enumerate the ndest^d ordered tuples obtained by sampling d
365 % destinations WITH replacement,
break ties by first occurrence in the
366 % tuple, and
return the marginal probability that jnd
is chosen.
367 % Dispatcher memory
is not supported, so the closure
is a function of
368 % the queue lengths alone and carries no auxiliary state.
370 % Memoization: the marginal-probability vector p_dest(jnd) depends only
371 % on (n[1..ndest], d). Across (ind, r) we cache by the per-call
372 % queue-length signature so each unique (n, d)
is enumerated once and
373 % the per-jnd probabilities are reused.
376 sqCache = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
379 isf = self.sn.nodeToStateful(ind);
380 if isempty(state_before{isf})
381 p = min(linksmat(ind,jnd),1);
387 eligible = find(linksmat(ind,:));
388 ndest = numel(eligible);
389 if ndest == 0 || ~any(eligible == jnd)
392 np = self.sn.nodeparam{ind}{r};
393 if isfield(np,
'd') && ~isempty(np.d)
398 d = max(1, min(d, ndest));
402 ksf = self.sn.nodeToStateful(knd);
403 n(i) = State.toMarginal(self.sn, knd, state_before{ksf});
405 jnd_pos = find(eligible == jnd, 1);
407 % Cache key: (per-
class) d, ndest, n vector. Same vector reused across
408 % all jnd values
for this (ind, r) pair.
409 cacheKey = sprintf(
'd%d|nd%d|n%s', d, ndest, mat2str(n));
410 if isKey(sqCache, cacheKey)
411 pVec = sqCache(cacheKey);
413 pVec = zeros(1, ndest);
415 for ti = 0:(n_tuples-1)
419 tuple(c) = mod(rem, ndest) + 1;
420 rem = floor(rem / ndest);
424 winner_pos_in_tuple = find(sub_n == minval, 1);
425 winner = tuple(winner_pos_in_tuple);
426 pVec(winner) = pVec(winner) + 1;
428 pVec = pVec / n_tuples;
429 % Bound cache size to avoid unbounded growth in
long simulations.
430 if sqCache.Count > 50000
431 remove(sqCache, sqCache.keys);
433 sqCache(cacheKey) = pVec;
473% function [rt, rtfun, rtnodes, sn] = refreshRoutingMatrix(self, rates)
474% % [
RT, RTFUN, CSMASK, RTNODES, SN] = REFRESHROUTINGMATRIX(RATES)
476% % Copyright (c) 2012-2026, Imperial College London
477% % All rights reserved.
482% line_error(mfilename,
'refreshRoutingMatrix cannot retrieve station rates, pass them as an input parameters.');
489% arvRates = zeros(1,K);
490% stateful = find(sn.isstateful)
';
492% indSource = find(sn.nodetype == NodeType.Source);
493% indOpenClasses = find(sn.njobs == Inf);
494% for r = indOpenClasses
495% arvRates(r) = rates(sn.nodeToStation(indSource),r);
498% [rt, rtnodes, linksmat, chains] = self.getRoutingMatrix(arvRates);
502% if self.enableChecks
504% if all(sn.routing(:,r) == -1)
505% line_error(mfilename,sprintf('Routing strategy in
class %d
is unspecified at all
nodes.
',r));
510% isStateDep = any(sn.isstatedep(:,3));
512% rnodefuncell = cell(M*K,M*K);
519% if sn.isstatedep(ind,3)
520% switch sn.routing(ind,r)
521% case RoutingStrategy.RROBIN
522% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_rr(ind, jnd, r, s, linksmat, state_before, state_after);
523% case RoutingStrategy.WRROBIN
524% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after);
525% case RoutingStrategy.JSQ
526% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after);
528% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
531% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
539% statefulNodesClasses = [];
540% for ind=getIndexStatefulNodes(self)
541% statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
544% % we now generate the node routing matrix for the given state and then
545% % lump the states for non-stateful nodes so that run gives the routing
546% % table for stateful nodes only
547% statefulNodesClasses = [];
549% statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
553% rtfunraw = @(state_before, state_after) dtmc_stochcomp(cell2mat(cellfun(@(f) f(state_before, state_after), rnodefuncell,'UniformOutput
',false)), statefulNodesClasses);
555% %rtfun = memoize(rtfunraw); % memoize to reduce the number of stoch comp calls
556% %rtfun.CacheSize = 6000^2;
558% rtfun = @(state_before, state_after) dtmc_stochcomp(rtnodes, statefulNodesClasses);
561% nchains = size(chains,1);
562% inchain = cell(1,nchains);
564% inchain{c} = find(chains(c,:));
568% sn.rtnodes = rtnodes;
571% sn.nchains = nchains;
572% sn.inchain = inchain;
574% if range(sn.refstat(inchain{c}))>0
575% line_error(mfilename,sprintf('Classes within chain %d (classes: %s) have different reference stations.
',c,mat2str(find(sn.chains(c,:)))));
580% function p = sub_rr(ind, jnd, r, s, linksmat, state_before, state_after)
581% % P = SUB_RR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
584% isf = sn.nodeToStateful(ind);
585% if isempty(state_before{isf})
586% p = min(linksmat(ind,jnd),1);
589% p = double(state_after{isf}(end-R+r)==jnd);
596% function p = sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after)
597% % P = SUB_WRR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
600% isf = sn.nodeToStateful(ind);
601% if isempty(state_before{isf})
602% p = min(linksmat(ind,jnd),1);
605% p = double(state_after{isf}(end-R+r)==jnd);
612% function p = sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
613% % P = SUB_JSQ(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER) %#OK<INUSD>
615% isf = sn.nodeToStateful(ind);
616% if isempty(state_before{isf})
617% p = min(linksmat(ind,jnd),1);
620% n = Inf*ones(1,sn.nnodes);
622% if linksmat(ind,knd)
623% ksf = sn.nodeToStateful(knd);
624% n(knd) = State.toMarginal(sn, knd, state_before{ksf});
628% p = 1 / sum(n == min(n));