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 R = self.sn.nclasses;
118 isf = self.sn.nodeToStateful(ind);
119 if isempty(state_before{isf})
120 p = min(linksmat(ind,jnd),1);
123 p = double(state_after{isf}(end-R+r)==jnd);
130 function p = sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after)
131 % P = SUB_WRR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
132 % WRR slot holds a POSITION in weighted_outlinks; map it to the
133 % destination node index before comparing.
135 R = self.sn.nclasses;
136 isf = self.sn.nodeToStateful(ind);
137 if isempty(state_before{isf})
138 p = min(linksmat(ind,jnd),1);
141 pos = state_after{isf}(end-R+r);
142 np = self.sn.nodeparam{ind}{r};
143 if isfield(np,'weighted_outlinks
') && ~isempty(np.weighted_outlinks) ...
144 && pos >= 1 && pos <= length(np.weighted_outlinks)
145 dest = np.weighted_outlinks(pos);
146 elseif pos >= 1 && pos <= length(np.outlinks)
147 dest = np.outlinks(pos);
151 p = double(dest == jnd);
158 function p = sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
159 % P = SUB_JSQ(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER) %#OK<INUSD>
161 isf = self.sn.nodeToStateful(ind);
162 if isempty(state_before{isf})
163 p = min(linksmat(ind,jnd),1);
166 n = Inf*ones(1,self.sn.nnodes);
167 for knd=1:self.sn.nnodes
169 ksf = self.sn.nodeToStateful(knd);
170 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
174 p = 1 / sum(n == min(n));
185 function p = sub_rl(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
186 % P = SUB_RL(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER) %#OK<INUSD>
188 isf = self.sn.nodeToStateful(ind);
189 if isempty(state_before{isf})
190 p = min(linksmat(ind,jnd),1);
193 % ----- new added contents ----- %
194 if self.nodes{ind}.output.outputStrategy{1,r}{5}==0 % state_size=0, use tabular value fn
195 % Multi-class: each class supplies its own value function.
196 % State.toMarginal aggregates queue lengths across classes,
197 % so the per-class call works as long as outputStrategy{r}
198 % is configured per class.
199 value_function = self.nodes{ind}.output.outputStrategy{1,r}{3};
200 nodes_need_action = self.nodes{ind}.output.outputStrategy{1,r}{4};
202 if ~isempty(find(nodes_need_action==ind, 1))
203 indQueue = find(self.sn.nodetype == NodeType.Queue);
204 v = Inf*ones(1, self.sn.nnodes); % value fn
205 n = Inf*ones(1, self.sn.nnodes); % queue length
206 x = zeros(1, length(indQueue)); % current state
207 for knd_idx=1:length(indQueue)
208 knd = indQueue(knd_idx);
209 ksf = self.sn.nodeToStateful(knd); %% does the state removes the job from the departure node already?
210 x(knd_idx) = State.toMarginal(self.sn, knd, state_before{ksf});
213 for knd = 1:self.sn.nnodes
214 if linksmat(ind, knd)
216 tmp(indQueue == knd) = tmp(indQueue == knd) + 1;
217 if max(tmp) <= size(value_function, 1)
218 ttmp = num2cell(tmp);
219 v(knd) = value_function(ttmp{:});
221 ksf = self.sn.nodeToStateful(knd);
222 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
226 if min(v) < Inf && max(x+1) < size(value_function, 1) % in action space
228 p = 1 / sum(v == min(v));
232 else % not in action space, use JSQ
234 p = 1 / sum(n == min(n));
240 else % not in nodes_need_action: this node doesn't use RL results, use JSQ
241 n = Inf*ones(1,self.sn.nnodes);
242 for knd=1:self.sn.nnodes
244 ksf = self.sn.nodeToStateful(knd);
245 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
249 p = 1 / sum(n == min(n));
255 elseif self.nodes{ind}.output.outputStrategy{1,r}{5}>0 % state_size>0, use fn approx
for value fn
256 % Multi-
class:
class-specific coefficient row vector.
257 coeff = self.nodes{ind}.output.outputStrategy{1,r}{3};
258 nodes_need_action = self.nodes{ind}.output.outputStrategy{1,r}{4};
259 stateSize = self.nodes{ind}.output.outputStrategy{1,r}{5};
261 if ~isempty(find(nodes_need_action==ind, 1))
262 indQueue = find(self.sn.nodetype == NodeType.Queue);
263 v = Inf*ones(1, self.sn.nnodes); % value fn
264 n = Inf*ones(1, self.sn.nnodes); % queue length
265 x = zeros(1, length(indQueue)); % current state
266 for knd_idx=1:length(indQueue)
267 knd = indQueue(knd_idx);
268 ksf = self.sn.nodeToStateful(knd); %% does the state removes the job from the departure node already?
269 x(knd_idx) = State.toMarginal(self.sn, knd, state_before{ksf});
271 for knd = 1:self.sn.nnodes
272 if linksmat(ind, knd)
274 tmp(indQueue == knd) = tmp(indQueue == knd) + 1;
277 for i = 1:length(tmp)
278 for j = i:length(tmp)
279 tmp_vec(end+1) = tmp(i)* tmp(j);
282 v(knd) = tmp_vec * coeff.
';
284 ksf = self.sn.nodeToStateful(knd);
285 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
288 if min(v) < Inf && max(x+1) < stateSize % in action space
290 p = 1 / sum(v == min(v));
294 else % not in action space, use JSQ
296 p = 1 / sum(n == min(n));
301 else % not in nodes_need_action: this node doesn't use RL results, use JSQ
302 n = Inf*ones(1,self.sn.nnodes);
303 for knd=1:self.sn.nnodes
305 ksf = self.sn.nodeToStateful(knd);
306 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
310 p = 1 / sum(n == min(n));
316 else % no value fn, use JSQ
317 % ----- end of
new added contents ----- %
319 n = Inf*ones(1,self.sn.nnodes);
320 for knd=1:self.sn.nnodes
322 ksf = self.sn.nodeToStateful(knd);
323 n(knd) = State.toMarginal(self.sn, knd, state_before{ksf});
327 p = 1 / sum(n == min(n));
338 function p = sub_kchoices(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
339 %
P = SUB_KCHOICES(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
340 % Power-of-K choices: matches LDES semantics in
341 % Solver_ssj.kt:selectKChoicesDestinationWithClass.
343 % Without memory: enumerate the m^k ordered tuples obtained by sampling
344 % k destinations WITH replacement,
break ties by first occurrence in
345 % the tuple, and
return the marginal probability that jnd
is chosen.
347 % With memory: the previously selected destination
is forced as the
348 % LAST candidate (LDES order), and the remaining k-1 candidates are
349 % sampled iid with replacement. Memory
is read from state_before;
if
350 % it
is -1 (no prior pick) or no longer eligible, the closure falls
351 % back to the memory-less form.
353 % Memoization: the marginal-probability vector p_dest(jnd) depends only
354 % on (n[1..m], k, memPos). Across (ind, r) we cache by the per-call
355 % queue-length signature so each unique (n, k, mem)
is enumerated once
356 % and the per-jnd probabilities are reused.
357 persistent kchoicesCache
358 if isempty(kchoicesCache)
359 kchoicesCache = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
362 isf = self.sn.nodeToStateful(ind);
363 if isempty(state_before{isf})
364 p = min(linksmat(ind,jnd),1);
370 eligible = find(linksmat(ind,:));
372 if m == 0 || ~any(eligible == jnd)
375 np = self.sn.nodeparam{ind}{r};
376 if isfield(np,
'k') && ~isempty(np.k)
381 k = max(1, min(k, m));
385 ksf = self.sn.nodeToStateful(knd);
386 n(i) = State.toMarginal(self.sn, knd, state_before{ksf});
388 jnd_pos = find(eligible == jnd, 1);
390 % Read memorized destination from the Router
's nvars slot. The slot
391 % stores the previously chosen destination as a node index (-1 = none).
392 memEnabled = isfield(np, 'withMemory
') && np.withMemory;
395 R = self.sn.nclasses;
396 sb = state_before{isf};
398 memNode = sb(end - R + r);
402 if memEnabled && memNode > 0
403 tmpPos = find(eligible == memNode, 1);
409 % Cache key: (per-class) k, m, n vector, memPos. Same vector reused
410 % across all jnd values for this (ind, r) pair.
411 cacheKey = sprintf('k%d|m%d|mp%d|n%s
', k, m, memPos, mat2str(n));
412 if isKey(kchoicesCache, cacheKey)
413 pVec = kchoicesCache(cacheKey);
420 for ti = 0:(n_tuples-1)
423 tuple(c) = mod(rem, m) + 1;
424 rem = floor(rem / m);
428 winner_pos_in_tuple = find(sub_n == minval, 1);
429 winner = tuple(winner_pos_in_tuple);
430 pVec(winner) = pVec(winner) + 1;
432 pVec = pVec / n_tuples;
435 for ti = 0:(n_tuples-1)
439 tuple(c) = mod(rem, m) + 1;
440 rem = floor(rem / m);
444 winner_pos_in_tuple = find(sub_n == minval, 1);
445 winner = tuple(winner_pos_in_tuple);
446 pVec(winner) = pVec(winner) + 1;
448 pVec = pVec / n_tuples;
450 % Bound cache size to avoid unbounded growth in long simulations.
451 if kchoicesCache.Count > 50000
452 remove(kchoicesCache, kchoicesCache.keys);
454 kchoicesCache(cacheKey) = pVec;
494% function [rt, rtfun, rtnodes, sn] = refreshRoutingMatrix(self, rates)
495% % [RT, RTFUN, CSMASK, RTNODES, SN] = REFRESHROUTINGMATRIX(RATES)
497% % Copyright (c) 2012-2026, Imperial College London
498% % All rights reserved.
503% line_error(mfilename,'refreshRoutingMatrix cannot retrieve station rates, pass them as an input parameters.
');
510% arvRates = zeros(1,K);
511% stateful = find(sn.isstateful)';
513% indSource = find(sn.nodetype == NodeType.Source);
514% indOpenClasses = find(sn.njobs == Inf);
515%
for r = indOpenClasses
516% arvRates(r) = rates(sn.nodeToStation(indSource),r);
519% [rt, rtnodes, linksmat, chains] = self.getRoutingMatrix(arvRates);
523%
if self.enableChecks
525%
if all(sn.routing(:,r) == -1)
526% line_error(mfilename,sprintf(
'Routing strategy in class %d is unspecified at all nodes.',r));
531% isStateDep = any(sn.isstatedep(:,3));
533% rnodefuncell = cell(M*K,M*K);
540%
if sn.isstatedep(ind,3)
541%
switch sn.routing(ind,r)
542%
case RoutingStrategy.RROBIN
543% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_rr(ind, jnd, r, s, linksmat, state_before, state_after);
544%
case RoutingStrategy.WRROBIN
545% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after);
546%
case RoutingStrategy.JSQ
547% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(state_before, state_after) sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after);
549% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
552% rnodefuncell{(ind-1)*K+r, (jnd-1)*K+s} = @(~,~) rtnodes((ind-1)*K+r, (jnd-1)*K+s);
560% statefulNodesClasses = [];
561%
for ind=getIndexStatefulNodes(self)
562% statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
565% % we now generate the node routing matrix
for the given state and then
566% % lump the states
for non-stateful
nodes so that run gives the routing
567% % table
for stateful
nodes only
568% statefulNodesClasses = [];
570% statefulNodesClasses(end+1:end+K)= ((ind-1)*K+1):(ind*K);
574% rtfunraw = @(state_before, state_after) dtmc_stochcomp(cell2mat(cellfun(@(f) f(state_before, state_after), rnodefuncell,
'UniformOutput',
false)), statefulNodesClasses);
576% %rtfun = memoize(rtfunraw); % memoize to reduce the number of stoch comp calls
577% %rtfun.CacheSize = 6000^2;
579% rtfun = @(state_before, state_after) dtmc_stochcomp(rtnodes, statefulNodesClasses);
582% nchains = size(chains,1);
583% inchain = cell(1,nchains);
585% inchain{c} = find(chains(c,:));
589% sn.rtnodes = rtnodes;
592% sn.nchains = nchains;
593% sn.inchain = inchain;
595%
if range(sn.refstat(inchain{c}))>0
596% line_error(mfilename,sprintf(
'Classes within chain %d (classes: %s) have different reference stations.',c,mat2str(find(sn.chains(c,:)))));
601% function p = sub_rr(ind, jnd, r, s, linksmat, state_before, state_after)
602% %
P = SUB_RR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
605% isf = sn.nodeToStateful(ind);
606%
if isempty(state_before{isf})
607% p = min(linksmat(ind,jnd),1);
610% p = double(state_after{isf}(end-R+r)==jnd);
617% function p = sub_wrr(ind, jnd, r, s, linksmat, state_before, state_after)
618% %
P = SUB_WRR(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER)
621% isf = sn.nodeToStateful(ind);
622%
if isempty(state_before{isf})
623% p = min(linksmat(ind,jnd),1);
626% p = double(state_after{isf}(end-R+r)==jnd);
633% function p = sub_jsq(ind, jnd, r, s, linksmat, state_before, state_after) %#ok<INUSD>
634% %
P = SUB_JSQ(IND, JND, R, S, LINKSMAT, STATE_BEFORE, STATE_AFTER) %#OK<INUSD>
636% isf = sn.nodeToStateful(ind);
637%
if isempty(state_before{isf})
638% p = min(linksmat(ind,jnd),1);
641% n = Inf*ones(1,sn.nnodes);
643%
if linksmat(ind,knd)
644% ksf = sn.nodeToStateful(knd);
645% n(knd) = State.toMarginal(sn, knd, state_before{ksf});
649% p = 1 / sum(n == min(n));