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.KCHOICES
56 rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_kchoices(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_kchoices(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
360 %
P = SUB_KCHOICES(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
361 % Power-of-K choices: matches LDES semantics in
362 % Solver_ssj.kt:selectKChoicesDestinationWithClass.
364 % Without memory: enumerate
the m^k ordered tuples obtained by sampling
365 % k destinations WITH replacement,
break ties by first occurrence in
366 %
the tuple, and
return the marginal probability that jnd
is chosen.
368 % With memory:
the previously selected destination
is forced as
the
369 % LAST candidate (LDES order), and
the remaining k-1 candidates are
370 % sampled iid with replacement. Memory
is read from state_before;
if
371 % it
is -1 (no prior pick) or no longer eligible,
the closure falls
372 % back to
the memory-less form.
374 % Memoization:
the marginal-probability vector p_dest(jnd) depends only
375 % on (n[1..m], k, memPos). Across (ind, r) we cache by
the per-call
376 % queue-length signature so each unique (n, k, mem)
is enumerated once
377 % and
the per-jnd probabilities are reused.
378 persistent kchoicesCache
379 if isempty(kchoicesCache)
380 kchoicesCache = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
383 isf = self.sn.nodeToStateful(ind);
384 if isempty(state_before{isf})
385 p = min(linksmat(ind,jnd),1);
391 eligible = find(linksmat(ind,:));
393 if m == 0 || ~any(eligible == jnd)
396 np = self.sn.nodeparam{ind}{r};
397 if isfield(np,
'k') && ~isempty(np.k)
402 k = max(1, min(k, m));
406 ksf = self.sn.nodeToStateful(knd);
407 n(i) = State.toMarginal(self.sn, knd, state_before{ksf});
409 jnd_pos = find(eligible == jnd, 1);
411 % Read memorized destination from
the Router
's nvars slot. The slot
412 % stores the previously chosen destination as a node index (-1 = none).
413 memEnabled = isfield(np, 'withMemory
') && np.withMemory;
416 R = self.sn.nclasses;
417 sb = state_before{isf};
419 memNode = sb(end - R + r);
423 if memEnabled && memNode > 0
424 tmpPos = find(eligible == memNode, 1);
430 % Cache key: (per-class) k, m, n vector, memPos. Same vector reused
431 % across all jnd values for this (ind, r) pair.
432 cacheKey = sprintf('k%d|m%d|mp%d|n%s
', k, m, memPos, mat2str(n));
433 if isKey(kchoicesCache, cacheKey)
434 pVec = kchoicesCache(cacheKey);
441 for ti = 0:(n_tuples-1)
444 tuple(c) = mod(rem, m) + 1;
445 rem = floor(rem / m);
449 winner_pos_in_tuple = find(sub_n == minval, 1);
450 winner = tuple(winner_pos_in_tuple);
451 pVec(winner) = pVec(winner) + 1;
453 pVec = pVec / n_tuples;
456 for ti = 0:(n_tuples-1)
460 tuple(c) = mod(rem, m) + 1;
461 rem = floor(rem / m);
465 winner_pos_in_tuple = find(sub_n == minval, 1);
466 winner = tuple(winner_pos_in_tuple);
467 pVec(winner) = pVec(winner) + 1;
469 pVec = pVec / n_tuples;
471 % Bound cache size to avoid unbounded growth in long simulations.
472 if kchoicesCache.Count > 50000
473 remove(kchoicesCache, kchoicesCache.keys);
475 kchoicesCache(cacheKey) = pVec;
515% function [rt, rtfun, rtnodes, sn] = refreshRoutingMatrix(self, rates)
516% % [RT, RTFUN, CSMASK, RTNODES, SN] = REFRESHROUTINGMATRIX(RATES)
518% % Copyright (c) 2012-2026, Imperial College London
519% % All rights reserved.
524% line_error(mfilename,'refreshRoutingMatrix cannot retrieve station rates, pass them as an input parameters.
');
531% arvRates = zeros(1,K);
532% stateful = find(sn.isstateful)';
534% indSource = find(sn.nodetype == NodeType.Source);
535% indOpenClasses = find(sn.njobs == Inf);
536%
for r = indOpenClasses
537% arvRates(r) = rates(sn.nodeToStation(indSource),r);
540% [rt, rtnodes, linksmat, chains] = self.getRoutingMatrix(arvRates);
544%
if self.enableChecks
546%
if all(sn.routing(:,r) == -1)
547% line_error(mfilename,sprintf(
'Routing strategy in class %d is unspecified at all nodes.',r));
552% isStateDep = any(sn.isstatedep(:,3));
554% rnodefuncell = cell(M*K,M*K);
561%
if sn.isstatedep(ind,3)
562%
switch sn.routing(ind,r)
563%
case RoutingStrategy.RROBIN
564% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_rr(ind, jnd, r, s, linksmat, state_before, state_after);
565%
case RoutingStrategy.WRROBIN
566% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after);
567%
case RoutingStrategy.JSQ
568% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after);
570% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
573% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
581% statefulNodesClasses = [];
582%
for ind=getIndexStatefulNodes(self)
583% statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
586% % we now generate
the node routing matrix
for the given state and then
587% % lump
the states
for non-stateful
nodes so that run gives
the routing
588% % table
for stateful
nodes only
589% statefulNodesClasses = [];
591% statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
595% rtfunraw = @(state_before, state_after) dtmc_stochcomp(cell2mat(cellfun(@(f) f(state_before, state_after), rnodefuncell,
'UniformOutput',
false)), statefulNodesClasses);
597% %rtfun = memoize(rtfunraw); % memoize to reduce
the number of stoch comp calls
598% %rtfun.CacheSize = 6000^2;
600% rtfun = @(state_before, state_after) dtmc_stochcomp(rtnodes, statefulNodesClasses);
603% nchains = size(chains,1);
604% inchain = cell(1,nchains);
606% inchain{c} = find(chains(c,:));
610% sn.rtnodes = rtnodes;
613% sn.nchains = nchains;
614% sn.inchain = inchain;
616%
if range(sn.refstat(inchain{c}))>0
617% line_error(mfilename,sprintf(
'Classes within chain %d (classes: %s) have different reference stations.',c,mat2str(find(sn.chains(c,:)))));
622% function p = sub_rr(ind, jnd, r, s, linksmat, state_before, state_after)
623% %
P = SUB_RR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
626% isf = sn.nodeToStateful(ind);
627%
if isempty(state_before{isf})
628% p = min(linksmat(ind,jnd),1);
631% p = double(state_after{isf}(end-R+r)==jnd);
638% function p = sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after)
639% %
P = SUB_WRR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
642% isf = sn.nodeToStateful(ind);
643%
if isempty(state_before{isf})
644% p = min(linksmat(ind,jnd),1);
647% p = double(state_after{isf}(end-R+r)==jnd);
654% function p = sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
655% %
P = SUB_JSQ(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER) %#OK<INUSD>
657% isf = sn.nodeToStateful(ind);
658%
if isempty(state_before{isf})
659% p = min(linksmat(ind,jnd),1);
662% n = Inf*ones(1,sn.nnodes);
664%
if linksmat(ind,knd)
665% ksf = sn.nodeToStateful(knd);
666% n(knd) = State.toMarginal(sn, knd, state_before{ksf});
670% p = 1 / sum(n == min(n));