1function [chainModel, alpha, deaggInfo] = aggregateChains(model, suffix)
2% AGGREGATECHAINS Transform a multi-
class model into an equivalent chain-aggregated model
4% [chainModel, alpha, deaggInfo] = AGGREGATECHAINS(model)
5% [chainModel, alpha, deaggInfo] = AGGREGATECHAINS(model, suffix)
7% This function transforms a queueing network model with multiple
classes
8% into a stochastically equivalent model where each chain becomes a single
9%
class. Classes belonging to the same chain (i.e.,
classes that can
switch
10% into each other) are merged into one aggregate
class.
12% The aggregated model preserves:
13% - Total chain population (closed chains)
14% - Total arrival rate (open chains)
15% - Service demands at each station
16% - Routing structure at the chain level
19% model - Source Network model with potentially multiple
classes per chain
20% suffix - Optional suffix
for chain
class names (default: '')
23% chainModel - New Network model with one class per chain
24% alpha - Aggregation factors matrix (M x K) from original model
25% deaggInfo - Struct containing all information needed to deaggregate
26% chain-level results back to class-level results
28% Copyright (c) 2012-2026, Imperial College London
31if nargin < 2 || isempty(suffix)
35% Get network structure
36sn = model.getStruct();
43% If each class is its own chain, just return a copy
45 chainModel = model.copy();
48 deaggInfo.alpha = alpha;
49 deaggInfo.inchain = sn.inchain;
50 deaggInfo.originalSn = sn;
51 deaggInfo.isAggregated = false;
55% Get aggregation parameters from existing API
56[Lchain, STchain, Vchain, alpha, Nchain, SCVchain, refstatchain] = sn_get_demands_chain(sn);
58% Determine which chains are open vs closed
59isOpenChain = false(1, C);
60lambdaChain = zeros(1, C);
61sourceIdx = find(sn.nodetype == NodeType.Source);
63 sourceStationIdx = sn.nodeToStation(sourceIdx);
67 inchain = sn.inchain{c};
68 isOpenChain(c) = any(isinf(sn.njobs(inchain)));
69 if isOpenChain(c) && ~isempty(sourceIdx)
70 % Sum arrival rates for all classes in this chain
71 lambdaChain(c) = sum(sn.rates(sourceStationIdx, inchain), 'omitnan');
75% Create the new aggregated network
76chainModel = Network(sprintf('%s_aggregated', model.getName()));
78% Map from original node index to new node
79nodeMap = cell(sn.nnodes, 1);
80stationMap = cell(M, 1);
82% First, identify which nodes to copy (skip auto-added ClassSwitch nodes)
84for i = 1:length(model.nodes)
85 node = model.nodes{i};
86 % Skip auto-added class switch nodes
87 if isa(node, 'ClassSwitch') && node.autoAdded
90 nodesToCopy(end+1) = i;
93% Create nodes in the new model
95for idx = 1:length(nodesToCopy)
97 node = model.nodes{i};
98 newNodeIdx = newNodeIdx + 1;
102 nodeMap{i} = Source(chainModel, node.name);
104 nodeMap{i} = Sink(chainModel, node.name);
106 nodeMap{i} = Queue(chainModel, node.name, node.schedStrategy);
107 if ~isinf(node.numberOfServers)
108 nodeMap{i}.setNumberOfServers(node.numberOfServers);
110 if ~isempty(node.cap) && isfinite(node.cap)
111 nodeMap{i}.setCapacity(node.cap);
114 nodeMap{i} = Delay(chainModel, node.name);
116 nodeMap{i} = Router(chainModel, node.name);
118 % User-defined ClassSwitch nodes should not exist in the
119 % aggregated model since class switching is eliminated
122 line_warning(mfilename, sprintf('Node type %s not fully supported in chain aggregation.', class(node)));
126 % Store station mapping
127 if isa(nodeMap{i}, 'Station')
128 stationIdx = sn.nodeToStation(i);
130 stationMap{stationIdx} = nodeMap{i};
136chainClass = cell(1, C);
138 inchain = sn.inchain{c};
140 % Build chain
class name from original class names
141 classNames = sn.classnames(inchain);
142 if length(classNames) == 1
143 chainClassName = char(classNames{1});
145 chainClassName = sprintf(
'Chain%d', c);
148 chainClassName = [chainClassName, suffix];
153 chainClass{c} = OpenClass(chainModel, chainClassName);
155 % Closed chain - find reference station
156 refStationIdx = refstatchain(c);
157 refStation = stationMap{refStationIdx};
158 if isempty(refStation)
159 line_error(mfilename, sprintf(
'Reference station %d for chain %d not found in aggregated model.', refStationIdx, c));
161 chainClass{c} = ClosedClass(chainModel, chainClassName, Nchain(c), refStation);
165% Set arrival rates
for open chains
167 chainSource = chainModel.getSource();
169 if isOpenChain(c) && lambdaChain(c) > 0
170 chainSource.setArrival(chainClass{c}, Exp(lambdaChain(c)));
175% Set service times at each station
177 station = stationMap{i};
178 if isempty(station) || isa(station,
'Source') || isa(station,
'Sink')
183 if STchain(i, c) > 0 && isfinite(STchain(i, c))
184 scv = SCVchain(i, c);
185 if isnan(scv) || scv <= 0
186 scv = 1; % Default to exponential
189 % Choose distribution based on SCV
190 if abs(scv - 1) < GlobalConstants.FineTol
191 % Exponential (SCV = 1)
192 dist = Exp(1/STchain(i, c));
194 % SCV < 1: use Erlang or deterministic
195 if scv < GlobalConstants.FineTol
196 dist = Det(STchain(i, c));
198 % Erlang: SCV = 1/k, so k = 1/SCV
203 dist = Erlang.fitMeanAndOrder(STchain(i, c), k);
206 % SCV > 1: use HyperExp or Cox2
207 dist = HyperExp.fitMeanAndSCV(STchain(i, c), scv);
210 station.setService(chainClass{c}, dist);
212 % No service
for this chain at
this station
213 station.setService(chainClass{c}, Disabled.getInstance());
218% Build routing matrix from aggregated routing probabilities
219I_new = length(chainModel.nodes);
220P = chainModel.initRoutingMatrix();
222% Create mapping from original station to
new model node index
223stationToNewNode = zeros(M, 1);
225 iNode = sn.stationToNode(i);
226 if ~isempty(nodeMap{iNode})
227 % Find the index of nodeMap{iNode} in chainModel.nodes
229 if chainModel.nodes{n} == nodeMap{iNode}
230 stationToNewNode(i) = n;
237% For each chain, compute routing probabilities between stations
239 inchain = sn.inchain{c};
241 % Build routing
for this chain
243 iNode = sn.stationToNode(i);
244 if isempty(nodeMap{iNode}) || stationToNewNode(i) == 0
248 % Skip source/sink in routing calculations
for closed chains
249 if ~isOpenChain(c) && (sn.nodetype(iNode) == NodeType.Source || sn.nodetype(iNode) == NodeType.Sink)
253 % Get stateful index
for station i (sn.rt
is indexed by stateful
nodes)
254 isf_i = sn.stationToStateful(i);
257 jNode = sn.stationToNode(j);
258 if isempty(nodeMap{jNode}) || stationToNewNode(j) == 0
262 % Get stateful index
for station j
263 isf_j = sn.stationToStateful(j);
265 % Compute aggregated routing probability from station i to station j
269 % Use stateful indices to access sn.rt
270 fromIdx = (isf_i-1)*K + k;
271 toIdx = (isf_j-1)*K + s;
272 if fromIdx <= size(sn.rt, 1) && toIdx <= size(sn.rt, 2)
273 p_ks = sn.rt(fromIdx, toIdx);
274 if alpha(i, k) > 0 && p_ks > 0
275 pij = pij + alpha(i, k) * p_ks;
281 if pij > GlobalConstants.FineTol
282 iNodeNew = stationToNewNode(i);
283 jNodeNew = stationToNewNode(j);
284 P{c, c}(iNodeNew, jNodeNew) = pij;
289 % Normalize routing probabilities (should be close to 1 already
if indexing
is correct)
291 rowSum = sum(
P{c, c}(iNew, :));
292 if rowSum > GlobalConstants.FineTol
293 if abs(rowSum - 1) > 0.01
294 line_warning(mfilename,
'Large normalization correction at node %d for chain %d: rowSum=%.4f', iNew, c, rowSum);
296 P{c, c}(iNew, :) =
P{c, c}(iNew, :) / rowSum;
301% Link the model with the routing matrix
304% Build deaggregation information
306deaggInfo.alpha = alpha;
307deaggInfo.Vchain = Vchain;
308deaggInfo.STchain = STchain;
309deaggInfo.Lchain = Lchain;
310deaggInfo.SCVchain = SCVchain;
311deaggInfo.Nchain = Nchain;
312deaggInfo.lambdaChain = lambdaChain;
313deaggInfo.isOpenChain = isOpenChain;
314deaggInfo.inchain = sn.inchain;
315deaggInfo.refstat = sn.refstat;
316deaggInfo.refstatchain = refstatchain;
317deaggInfo.originalSn = sn;
318deaggInfo.isAggregated =
true;
319deaggInfo.nclasses = K;
320deaggInfo.nchains = C;