1%{ @file sn_refresh_visits.m
2 % @brief Solves traffic equations to compute visit ratios
4 % @author LINE Development Team
5 % Copyright (c) 2012-2026, Imperial College London
10 % @brief Solves traffic equations to compute visit ratios
13 % This function solves the traffic equations to compute the average number
14 % of
visits to
nodes and stations
for each chain in the network.
23 % <tr><th>Name<th>Description
24 % <tr><td>sn<td>Network structure
25 % <tr><td>chains<td>Chain definitions
26 % <tr><td>rt<td>Station routing matrix
27 % <tr><td>rtnodes<td>Node routing matrix
32 % <tr><th>Name<th>Description
33 % <tr><td>
visits<td>Cell array of visit ratios at stations per chain
35 % <tr><td>sn<td>Updated network structure with visit fields populated
38function [
visits, nodevisits, sn] = sn_refresh_visits(sn, chains, rt, rtnodes)
44nchains = size(chains,1);
46%% obtain chain characteristics
49 if sum(refstat(inchain{c}) == refstat(inchain{c}(1))) ~= length(inchain{c})
50 refstat(inchain{c}) = refstat(inchain{c}(1));
51 % line_error(mfilename,sprintf(
'Classes in chain %d have different reference stations. Chain %d classes: %s', c, c, int2str(inchain{c})));
56visits = cell(nchains,1); %
visits{c}(i,j)
is the number of
visits that a chain-c job pays at node i in
class j
58 cols = zeros(1,M*length(inchain{c}));
60 nIC = length(inchain{c});
62 cols(1,(ist-1)*nIC+ik) = (ist-1)*K+inchain{c}(ik);
66 Pchain = rt(cols,cols); % routing probability of the chain
68 % Handle NaN values in routing matrix (e.g., from Cache
class switching)
69 % For
visits calculation, replace NaN with equal probabilities
70 for row = 1:size(Pchain,1)
71 nan_cols = isnan(Pchain(row,:));
73 % Get the non-NaN sum for this row
74 non_nan_sum = sum(Pchain(row, ~nan_cols));
75 % Distribute remaining probability equally among NaN entries
76 remaining_prob = max(0, 1 - non_nan_sum);
77 n_nan = sum(nan_cols);
78 if n_nan > 0 && remaining_prob > 0
79 Pchain(row, nan_cols) = remaining_prob / n_nan;
81 Pchain(row, nan_cols) = 0;
86 visited = sum(Pchain,2) > 0;
88 % Normalize routing matrix for Fork-containing models
89 % Fork
nodes have row sums > 1 (sending to all branches with prob 1 each)
90 % which causes dtmc_solve_reducible to fail. Normalize to make stochastic.
91 % Record original row sums to correct visit ratios after DTMC solve.
92 row_sums = ones(size(Pchain,1), 1);
93 if any(sn.nodetype == NodeType.Fork)
94 for row = 1:size(Pchain,1)
95 rs = sum(Pchain(row,:));
97 if rs > GlobalConstants.FineTol
98 Pchain(row,:) = Pchain(row,:) / rs;
103 % Use dtmc_solve as primary, fallback to dtmc_solve_reducible for chains with transient states
104 Pchain_visited = Pchain(visited,visited);
105 alpha_visited = dtmc_solve(Pchain_visited);
106 % Fallback to dtmc_solve_reducible if dtmc_solve fails (e.g., reducible chain)
107 if all(alpha_visited == 0) || any(isnan(alpha_visited))
108 [alpha_visited, ~, ~, ~, ~] = dtmc_solve_reducible(Pchain_visited, [], struct('tol', GlobalConstants.FineTol));
110 alpha = zeros(1,M*K); alpha(visited) = alpha_visited;
111 if max(alpha)>=1-GlobalConstants.FineTol
112 %disabled because a self-looping customer
is an absorbing chain
113 %line_error(mfilename,'One chain has an absorbing state.');
116 % Apply Fork fanout correction: multiply fork-scope station
visits by fanout
117 % This corrects for the normalization of rows with sum > 1 (caused by Fork
118 % routing being collapsed into the stateful routing matrix).
119 % Block propagation through Join
nodes to prevent the correction from
120 % leaking back through cycles (which would cancel out during normalization).
121 if any(sn.nodetype == NodeType.Fork)
123 nIC = length(inchain{c});
124 % Build adjacency but block outgoing edges from Join
nodes
125 adj = Pchain > GlobalConstants.FineTol;
127 nd = sn.statefulToNode(isf);
128 if sn.nodetype(nd) == NodeType.Join
130 adj((isf-1)*nIC + k, :) = false;
134 % Compute transitive closure with join-blocked adjacency
136 for iter = 1:ceil(log2(n))
137 reachable = reachable | (reachable * reachable > 0);
140 % For each fork row, multiply only fork-scope reachable states by fanout
142 fanout = row_sums(row);
143 if fanout > 1 + GlobalConstants.FineTol
145 if reachable(row, col)
146 alpha(col) = alpha(col) * fanout;
155 for k=1:length(inchain{c})
156 visits{c}(ist,inchain{c}(k)) = alpha((ist-1)*length(inchain{c})+k);
159 normSum = sum(
visits{c}(sn.stationToStateful(refstat(inchain{c}(1))),inchain{c}));
160 if normSum > GlobalConstants.FineTol
169 nodes_cols = zeros(1,I*length(inchain{c}));
171 nIC = length(inchain{c});
173 nodes_cols(1,(ind-1)*nIC+ik) = (ind-1)*K+inchain{c}(ik);
176 nodes_Pchain = rtnodes(nodes_cols, nodes_cols); % routing probability of the chain
178 % Handle NaN values in routing matrix (e.g., from Cache
class switching)
179 % For
visits calculation, replace NaN with equal probabilities
180 for row = 1:size(nodes_Pchain,1)
181 nan_cols = isnan(nodes_Pchain(row,:));
183 % Get the non-NaN sum for this row
184 non_nan_sum = sum(nodes_Pchain(row, ~nan_cols));
185 % Distribute remaining probability equally among NaN entries
186 remaining_prob = max(0, 1 - non_nan_sum);
187 n_nan = sum(nan_cols);
188 if n_nan > 0 && remaining_prob > 0
189 nodes_Pchain(row, nan_cols) = remaining_prob / n_nan;
191 nodes_Pchain(row, nan_cols) = 0;
196 nodes_visited = sum(nodes_Pchain,2) > 0;
198 % Normalize routing matrix for Fork-containing models
199 % Record original row sums to correct visit ratios after DTMC solve.
200 nodes_row_sums = ones(size(nodes_Pchain,1), 1);
201 if any(sn.nodetype == NodeType.Fork)
202 for row = 1:size(nodes_Pchain,1)
203 rs = sum(nodes_Pchain(row,:));
204 nodes_row_sums(row) = rs;
205 if rs > GlobalConstants.FineTol
206 nodes_Pchain(row,:) = nodes_Pchain(row,:) / rs;
211 % Use dtmc_solve as primary, fallback to dtmc_solve_reducible for chains with transient states
212 nodes_Pchain_visited = nodes_Pchain(nodes_visited,nodes_visited);
213 nodes_alpha_visited = dtmc_solve(nodes_Pchain_visited);
214 % Fallback to dtmc_solve_reducible if dtmc_solve_reducible fails (e.g., reducible chain)
215 if all(nodes_alpha_visited == 0) || any(isnan(nodes_alpha_visited))
216 [nodes_alpha_visited, ~, ~, ~, ~] = dtmc_solve_reducible(nodes_Pchain_visited, [], struct('tol', GlobalConstants.FineTol));
218 nodes_alpha = zeros(1,I*K); nodes_alpha(nodes_visited) = nodes_alpha_visited;
220 % Apply Fork fanout correction for node
visits
221 % Block propagation through Join
nodes to prevent the correction from
222 % leaking back through cycles.
223 if any(sn.nodetype == NodeType.Fork)
224 n_nodes = size(nodes_Pchain, 1);
225 nIC = length(inchain{c});
226 % Build adjacency but block outgoing edges from Join
nodes
227 nodes_adj = nodes_Pchain > GlobalConstants.FineTol;
229 if sn.nodetype(nd) == NodeType.Join
231 nodes_adj((nd-1)*nIC + k, :) = false;
235 % Compute transitive closure with join-blocked adjacency
236 nodes_reachable = nodes_adj;
237 for iter = 1:ceil(log2(n_nodes))
238 nodes_reachable = nodes_reachable | (nodes_reachable * nodes_reachable > 0);
241 % For each fork row, multiply only fork-scope reachable states by fanout
243 fanout = nodes_row_sums(row);
244 if fanout > 1 + GlobalConstants.FineTol
246 if nodes_reachable(row, col)
247 nodes_alpha(col) = nodes_alpha(col) * fanout;
256 for k=1:length(inchain{c})
257 nodevisits{c}(ind,inchain{c}(k)) = nodes_alpha((ind-1)*length(inchain{c})+k);
260 nodeNormSum = sum(
nodevisits{c}(sn.statefulToNode(refstat(inchain{c}(1))),inchain{c}));
261 if nodeNormSum > GlobalConstants.FineTol