LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
aggregateChains.m
1function [chainModel, alpha, deaggInfo] = aggregateChains(model, suffix)
2% AGGREGATECHAINS Transform a multi-class model into an equivalent chain-aggregated model
3%
4% [chainModel, alpha, deaggInfo] = AGGREGATECHAINS(model)
5% [chainModel, alpha, deaggInfo] = AGGREGATECHAINS(model, suffix)
6%
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.
11%
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
17%
18% Parameters:
19% model - Source Network model with potentially multiple classes per chain
20% suffix - Optional suffix for chain class names (default: '')
21%
22% Returns:
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
27%
28% Copyright (c) 2012-2026, Imperial College London
29% All rights reserved.
30
31if nargin < 2 || isempty(suffix)
32 suffix = '';
33end
34
35% Get network structure
36sn = model.getStruct();
37
38% Extract dimensions
39M = sn.nstations;
40K = sn.nclasses;
41C = sn.nchains;
42
43% If each class is its own chain, just return a copy
44if C == K
45 chainModel = model.copy();
46 alpha = eye(M, K);
47 deaggInfo = struct();
48 deaggInfo.alpha = alpha;
49 deaggInfo.inchain = sn.inchain;
50 deaggInfo.originalSn = sn;
51 deaggInfo.isAggregated = false;
52 return;
53end
54
55% Get aggregation parameters from existing API
56[Lchain, STchain, Vchain, alpha, Nchain, SCVchain, refstatchain] = sn_get_demands_chain(sn);
57
58% Determine which chains are open vs closed
59isOpenChain = false(1, C);
60lambdaChain = zeros(1, C);
61sourceIdx = find(sn.nodetype == NodeType.Source);
62if ~isempty(sourceIdx)
63 sourceStationIdx = sn.nodeToStation(sourceIdx);
64end
65
66for c = 1:C
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');
72 end
73end
74
75% Create the new aggregated network
76chainModel = Network(sprintf('%s_aggregated', model.getName()));
77
78% Map from original node index to new node
79nodeMap = cell(sn.nnodes, 1);
80stationMap = cell(M, 1);
81
82% First, identify which nodes to copy (skip auto-added ClassSwitch nodes)
83nodesToCopy = [];
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
88 continue;
89 end
90 nodesToCopy(end+1) = i;
91end
92
93% Create nodes in the new model
94newNodeIdx = 0;
95for idx = 1:length(nodesToCopy)
96 i = nodesToCopy(idx);
97 node = model.nodes{i};
98 newNodeIdx = newNodeIdx + 1;
99
100 switch class(node)
101 case 'Source'
102 nodeMap{i} = Source(chainModel, node.name);
103 case 'Sink'
104 nodeMap{i} = Sink(chainModel, node.name);
105 case 'Queue'
106 nodeMap{i} = Queue(chainModel, node.name, node.schedStrategy);
107 if ~isinf(node.numberOfServers)
108 nodeMap{i}.setNumberOfServers(node.numberOfServers);
109 end
110 if ~isempty(node.cap) && isfinite(node.cap)
111 nodeMap{i}.setCapacity(node.cap);
112 end
113 case 'Delay'
114 nodeMap{i} = Delay(chainModel, node.name);
115 case 'Router'
116 nodeMap{i} = Router(chainModel, node.name);
117 case 'ClassSwitch'
118 % User-defined ClassSwitch nodes should not exist in the
119 % aggregated model since class switching is eliminated
120 continue;
121 otherwise
122 line_warning(mfilename, sprintf('Node type %s not fully supported in chain aggregation.', class(node)));
123 continue;
124 end
125
126 % Store station mapping
127 if isa(nodeMap{i}, 'Station')
128 stationIdx = sn.nodeToStation(i);
129 if stationIdx > 0
130 stationMap{stationIdx} = nodeMap{i};
131 end
132 end
133end
134
135% Create chain classes
136chainClass = cell(1, C);
137for c = 1:C
138 inchain = sn.inchain{c};
139
140 % Build chain class name from original class names
141 classNames = sn.classnames(inchain);
142 if length(classNames) == 1
143 chainClassName = char(classNames{1});
144 else
145 chainClassName = sprintf('Chain%d', c);
146 end
147 if ~isempty(suffix)
148 chainClassName = [chainClassName, suffix];
149 end
150
151 if isOpenChain(c)
152 % Open chain
153 chainClass{c} = OpenClass(chainModel, chainClassName);
154 else
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));
160 end
161 chainClass{c} = ClosedClass(chainModel, chainClassName, Nchain(c), refStation);
162 end
163end
164
165% Set arrival rates for open chains
166if any(isOpenChain)
167 chainSource = chainModel.getSource();
168 for c = 1:C
169 if isOpenChain(c) && lambdaChain(c) > 0
170 chainSource.setArrival(chainClass{c}, Exp(lambdaChain(c)));
171 end
172 end
173end
174
175% Set service times at each station
176for i = 1:M
177 station = stationMap{i};
178 if isempty(station) || isa(station, 'Source') || isa(station, 'Sink')
179 continue;
180 end
181
182 for c = 1:C
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
187 end
188
189 % Choose distribution based on SCV
190 if abs(scv - 1) < GlobalConstants.FineTol
191 % Exponential (SCV = 1)
192 dist = Exp(1/STchain(i, c));
193 elseif scv < 1
194 % SCV < 1: use Erlang or deterministic
195 if scv < GlobalConstants.FineTol
196 dist = Det(STchain(i, c));
197 else
198 % Erlang: SCV = 1/k, so k = 1/SCV
199 k = round(1/scv);
200 if k < 1
201 k = 1;
202 end
203 dist = Erlang.fitMeanAndOrder(STchain(i, c), k);
204 end
205 else
206 % SCV > 1: use HyperExp or Cox2
207 dist = HyperExp.fitMeanAndSCV(STchain(i, c), scv);
208 end
209
210 station.setService(chainClass{c}, dist);
211 else
212 % No service for this chain at this station
213 station.setService(chainClass{c}, Disabled.getInstance());
214 end
215 end
216end
217
218% Build routing matrix from aggregated routing probabilities
219I_new = length(chainModel.nodes);
220P = chainModel.initRoutingMatrix();
221
222% Create mapping from original station to new model node index
223stationToNewNode = zeros(M, 1);
224for i = 1:M
225 iNode = sn.stationToNode(i);
226 if ~isempty(nodeMap{iNode})
227 % Find the index of nodeMap{iNode} in chainModel.nodes
228 for n = 1:I_new
229 if chainModel.nodes{n} == nodeMap{iNode}
230 stationToNewNode(i) = n;
231 break;
232 end
233 end
234 end
235end
236
237% For each chain, compute routing probabilities between stations
238for c = 1:C
239 inchain = sn.inchain{c};
240
241 % Build routing for this chain
242 for i = 1:M
243 iNode = sn.stationToNode(i);
244 if isempty(nodeMap{iNode}) || stationToNewNode(i) == 0
245 continue;
246 end
247
248 % Skip source/sink in routing calculations for closed chains
249 if ~isOpenChain(c) && (sn.nodetype(iNode) == NodeType.Source || sn.nodetype(iNode) == NodeType.Sink)
250 continue;
251 end
252
253 % Get stateful index for station i (sn.rt is indexed by stateful nodes)
254 isf_i = sn.stationToStateful(i);
255
256 for j = 1:M
257 jNode = sn.stationToNode(j);
258 if isempty(nodeMap{jNode}) || stationToNewNode(j) == 0
259 continue;
260 end
261
262 % Get stateful index for station j
263 isf_j = sn.stationToStateful(j);
264
265 % Compute aggregated routing probability from station i to station j
266 pij = 0;
267 for k = inchain
268 for s = inchain
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;
276 end
277 end
278 end
279 end
280
281 if pij > GlobalConstants.FineTol
282 iNodeNew = stationToNewNode(i);
283 jNodeNew = stationToNewNode(j);
284 P{c, c}(iNodeNew, jNodeNew) = pij;
285 end
286 end
287 end
288
289 % Normalize routing probabilities (should be close to 1 already if indexing is correct)
290 for iNew = 1:I_new
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);
295 end
296 P{c, c}(iNew, :) = P{c, c}(iNew, :) / rowSum;
297 end
298 end
299end
300
301% Link the model with the routing matrix
302chainModel.link(P);
303
304% Build deaggregation information
305deaggInfo = struct();
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;
321
322end
Definition mmt.m:92