LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
aggregateFES.m
1function [fesModel, fesStation, deaggInfo] = aggregateFES(model, stationSubset, options)
2% AGGREGATEFES Replace a station subset with a Flow-Equivalent Server (FES)
3%
4% [fesModel, fesStation, deaggInfo] = AGGREGATEFES(model, stationSubset)
5% [fesModel, fesStation, deaggInfo] = AGGREGATEFES(model, stationSubset, options)
6%
7% This function replaces a subset of stations in a closed product-form
8% queueing network with a single Flow-Equivalent Server (FES). The FES has
9% Limited Joint Dependence (LJD) service rates where the rate for class-c
10% in state (n1,...,nK) equals the throughput of class-c in an isolated
11% subnetwork consisting only of the subset stations.
12%
14% model - Closed product-form Network model
15% stationSubset - Cell array of Station objects to aggregate
16% options - Optional struct with fields:
17% .solver - Solver for throughput computation ('mva' default)
18% .cutoffs - Per-class population cutoffs (default: njobs per class)
19% .verbose - Enable verbose output (default: false)
20%
21% Returns:
22% fesModel - New Network with FES replacing the subset
23% fesStation - Reference to the FES Queue station
24% deaggInfo - Struct containing:
25% .originalModel - Original model reference
26% .stationSubset - Original subset stations
27% .subsetIndices - Original station indices
28% .throughputTable - Computed throughputs for all states
29% .cutoffs - Per-class cutoffs used
30% .stochCompSubset - Stochastic complement for subset
31% .stochCompComplement - Stochastic complement for complement
32% .isolatedModel - Isolated subnetwork model
33%
34% Example:
35% model = Network('Example');
36% % ... create stations and classes ...
37% [fesModel, fesStation, info] = ModelAdapter.aggregateFES(model, {queue2, queue3});
38% solver = SolverMVA(fesModel);
39% avgTable = solver.getAvgTable();
40%
41% Copyright (c) 2012-2026, Imperial College London
42% All rights reserved.
43
44%% Input validation and defaults
45if nargin < 3
46 options = struct();
47end
48if ~isfield(options, 'solver')
49 options.solver = 'mva';
50end
51if ~isfield(options, 'cutoffs')
52 options.cutoffs = [];
53end
54if ~isfield(options, 'verbose')
55 options.verbose = false;
56end
57
58%% Get network structure
59sn = model.getStruct();
60M = sn.nstations;
61K = sn.nclasses;
62
63% Get station indices for subset
64modelNodes = model.getNodes();
65origStationIdxs = model.getStationIndexes();
66modelStations = modelNodes(origStationIdxs);
67subsetIndices = zeros(1, length(stationSubset));
68for i = 1:length(stationSubset)
69 for j = 1:M
70 if stationSubset{i} == modelStations{j}
71 subsetIndices(i) = j;
72 break;
73 end
74 end
75end
76
77% Validate inputs using sn struct and indices
78[isValid, errorMsg] = fes_validate(sn, subsetIndices);
79if ~isValid
80 line_error(mfilename, errorMsg);
81end
82
83% Get complement indices (stations not in subset)
84allIndices = 1:M;
85complementIndices = setdiff(allIndices, subsetIndices);
86
87% Set cutoffs if not provided
88if isempty(options.cutoffs)
89 % Default: use total jobs per class
90 options.cutoffs = sn.njobs';
91end
92cutoffs = options.cutoffs;
93
94if options.verbose
95 fprintf('FES Aggregation: %d subset stations, %d complement stations, %d classes\n', ...
96 length(subsetIndices), length(complementIndices), K);
97end
98
99%% Compute stochastic complement routing matrices
100% The routing matrix sn.rt is indexed by (stateful_node-1)*K + class
101% We need to partition it by stations
102
103% Build index sets for rt matrix
104subsetRtIndices = [];
105for i = subsetIndices
106 isf = sn.stationToStateful(i);
107 subsetRtIndices = [subsetRtIndices, ((isf-1)*K + 1):(isf*K)];
108end
109
110complementRtIndices = [];
111for i = complementIndices
112 isf = sn.stationToStateful(i);
113 complementRtIndices = [complementRtIndices, ((isf-1)*K + 1):(isf*K)];
114end
115
116% Compute stochastic complement for subset (routing within subset only)
117% S = P11 + P12 * inv(I - P22) * P21
118rt = sn.rt;
119stochCompSubset = dtmc_stochcomp(rt, subsetRtIndices);
120
121% Compute stochastic complement for complement
122stochCompComplement = dtmc_stochcomp(rt, complementRtIndices);
123
124if options.verbose
125 fprintf('Stochastic complements computed: subset (%dx%d), complement (%dx%d)\n', ...
126 size(stochCompSubset, 1), size(stochCompSubset, 2), ...
127 size(stochCompComplement, 1), size(stochCompComplement, 2));
128end
129
130%% Build isolated subnetwork data and compute throughputs
131[L_iso, mi_iso, visits_iso, isDelay_iso] = fes_build_isolated(sn, subsetIndices, stochCompSubset);
132
133if options.verbose
134 fprintf('Isolated subnetwork data extracted for %d stations.\n', length(subsetIndices));
135end
136
137% Compute throughputs for all population states
138scalingTable = fes_compute_throughputs(L_iso, mi_iso, isDelay_iso, cutoffs, options);
139
140% Norton composite rate: the FES service rate must be the subnetwork DEPARTURE
141% rate, not the isolated-subnet throughput XN returned above. By Kritzinger et
142% al. (1982), Eq. 4.2, tau_r(n) = XN_r(n) * sum_{j in subset} xi_jr * P_r(j->comp),
143% where xi_jr are the visit ratios embedded in the isolated demands
144% (xi_jr = L_iso(j,r)*rate(j,r)) and P_r(j->comp) is the routing probability from
145% subset station j to the complement. The escape factor is population-independent,
146% so it rescales each per-class table by a scalar. Omitting it is exact only when
147% the subset has a single exit station (escape=1); it is required for subsets with
148% multiple exit points (e.g. multi-entry subsets whose stations all exit).
149escape = zeros(1, K);
150for r = 1:K
151 for a = 1:length(subsetIndices)
152 j = subsetIndices(a);
153 Vjr = L_iso(a, r) * sn.rates(j, r); % visit ratio used in the demand
154 isf_j = sn.stationToStateful(j);
155 pexit = 0;
156 for i = complementIndices
157 isf_i = sn.stationToStateful(i);
158 pexit = pexit + rt((isf_j-1)*K + r, (isf_i-1)*K + r);
159 end
160 if isfinite(Vjr)
161 escape(r) = escape(r) + Vjr * pexit;
162 end
163 end
164 if escape(r) > GlobalConstants.FineTol
165 scalingTable{r} = scalingTable{r} * escape(r);
166 end
167end
168
169if options.verbose
170 fprintf('Throughput table computed for %d states.\n', prod(cutoffs + 1));
171 fprintf('Norton escape factors per class: %s\n', mat2str(escape, 4));
172end
173
174%% Create the FES model
175fesModel = Network(sprintf('%s_FES', model.getName()));
176
177% Copy complement stations to new model
178nodeMap = cell(sn.nnodes, 1);
179stationMap = cell(M, 1);
180
181for i = complementIndices
182 origStation = modelStations{i};
183 nodeIdx = sn.stationToNode(i);
184
185 switch class(origStation)
186 case 'Queue'
187 newStation = Queue(fesModel, origStation.name, origStation.schedStrategy);
188 if ~isinf(origStation.numberOfServers)
189 newStation.setNumberOfServers(origStation.numberOfServers);
190 end
191 if ~isempty(origStation.cap) && isfinite(origStation.cap)
192 newStation.setCapacity(origStation.cap);
193 end
194 case 'Delay'
195 newStation = Delay(fesModel, origStation.name);
196 otherwise
197 line_error(mfilename, sprintf('Unsupported station type %s.', class(origStation)));
198 end
199
200 nodeMap{nodeIdx} = newStation;
201 stationMap{i} = newStation;
202end
203
204% Create the FES station (single Queue with PS scheduling)
205fesStation = Queue(fesModel, 'FES', SchedStrategy.PS);
206fesStation.setNumberOfServers(1);
207
208% Create job classes
209newClasses = cell(1, K);
210modelClasses = model.classes;
211
212% Choose reference station (FES or first complement station)
213if ~isempty(complementIndices)
214 refStation = stationMap{complementIndices(1)};
215else
216 refStation = fesStation;
217end
218
219for k = 1:K
220 origClass = modelClasses{k};
221 population = sn.njobs(k);
222 newClass = ClosedClass(fesModel, origClass.name, population, refStation);
223 newClasses{k} = newClass;
224end
225
226% Set service distributions for complement stations
227for i = complementIndices
228 newStation = stationMap{i};
229
230 for k = 1:K
231 origPH = sn.proc{i}{k};
232
233 if isempty(origPH) || (iscell(origPH) && isempty(origPH{1})) || ...
234 (iscell(origPH) && all(isnan(origPH{1}(:))))
235 newStation.setService(newClasses{k}, Disabled.getInstance());
236 else
237 if iscell(origPH)
238 T_matrix = origPH{1}; % Sub-generator (diagonal is -rate)
239 t0_vector = origPH{2}; % Exit rate vector
240 nPhases = size(T_matrix, 1);
241
242 if nPhases == 1
243 rate = -T_matrix(1,1);
244 newStation.setService(newClasses{k}, Exp(rate));
245 else
246 alpha = ones(1, nPhases) / nPhases;
247 dist = APH(alpha, T_matrix);
248 newStation.setService(newClasses{k}, dist);
249 end
250 else
251 newStation.setService(newClasses{k}, Exp(sn.rates(i, k)));
252 end
253 end
254 end
255end
256
257% Set the class dependence on the FES from per-class throughput scaling tables
258% The scalingTable{k} contains throughputs for class k at each population state
259%
260% For multi-class FES, we use a class-dependence handle (cdscaling) which allows
261% each class to have its own state-dependent scaling factor.
262% The FES service rate for class c in state (n1,...,nK) equals throughput_c(n).
263
264% Set base service rate (will be scaled by the class dependence)
265% Use Exp(1.0) as base; the class-dependent scaling provides the actual throughput rate
266for k = 1:K
267 fesStation.setService(newClasses{k}, Exp(1.0));
268end
269
270% Handle zeros in scaling tables (replace with small positive value)
271for k = 1:K
272 scalingTable{k}(scalingTable{k} < GlobalConstants.FineTol) = GlobalConstants.FineTol;
273end
274
275if options.verbose
276 fprintf('Per-class scaling tables:\n');
277 for k = 1:K
278 fprintf(' Class %d: Max = %.6f, Min = %.6f\n', k, ...
279 max(scalingTable{k}), min(scalingTable{k}(scalingTable{k} > GlobalConstants.FineTol)));
280 end
281end
282
283% Install the FES rates as a per-class class-dependence function
284% beta_{i,r}(n) = X_r(n), i.e. Sauer's chain-dependent service rate mu_{r,i}(n)
285% (Sauer 1983, eq. (40)). scalingTable is a cell {1 x K} of linearized
286% throughput vectors; wrap it as a single handle of the per-class population
287% vector returning the length-K vector of per-class rates, with the population
288% clamped to the cutoffs the table was built on.
289fesBetaHandle = fes_beta_handle(scalingTable, cutoffs);
290% Peak per-class FES rate = normalizer for Util = T*S/peak (the FES rates are
291% Sauer's chain-dependent service rates; their lattice peak plays the role of
292% the effective server count).
293fesPeak = cd_peak_scaling(fesBetaHandle, cutoffs, numel(cutoffs));
294fesStation.setLimitedClassDependence(fesBetaHandle, fesPeak);
295
296%% Build routing matrix for FES model
297I_fes = length(fesModel.nodes);
298P = fesModel.initRoutingMatrix();
299
300% Get FES node index
301fesNodeIdx = 0;
302for n = 1:I_fes
303 if fesModel.nodes{n} == fesStation
304 fesNodeIdx = n;
305 break;
306 end
307end
308
309% Build node index mapping for complement stations
310complementNodeMap = containers.Map('KeyType', 'double', 'ValueType', 'double');
311for i = complementIndices
312 nodeIdx = sn.stationToNode(i);
313 if ~isempty(nodeMap{nodeIdx})
314 for n = 1:I_fes
315 if fesModel.nodes{n} == nodeMap{nodeIdx}
316 complementNodeMap(i) = n;
317 break;
318 end
319 end
320 end
321end
322
323% Set routing probabilities
324% Routes within complement use stochastic complement
325% Routes to/from subset go through FES
326if options.verbose
327 fprintf('Setting up routing for FES model:\n');
328 fprintf(' FES node index: %d\n', fesNodeIdx);
329 fprintf(' Complement station indices: %s\n', mat2str(complementIndices));
330 fprintf(' Subset station indices: %s\n', mat2str(subsetIndices));
331end
332
333for k = 1:K
334 P_k = zeros(I_fes, I_fes);
335
336 % Exit-distribution weights = per-class visit ratios of the subset stations,
337 % computed by fes_build_isolated from the stochastic complement (stationary
338 % vector pi*S = pi of the class-k routing among subset stations). Weighting
339 % the FES -> complement split by these ratios (Kritzinger Eq. 4.6) makes the
340 % aggregation exact for closed product-form networks with an arbitrary number
341 % of subset exit points, not just single-exit/tandem subsets.
342 nSub = length(subsetIndices);
343 visitRatios = visits_iso(:, k)'; % 1 x nSub, class-k visit ratios
344
345 % Routes within complement (DIRECT paths only, not through subset)
346 for i = complementIndices
347 if ~isKey(complementNodeMap, i)
348 continue;
349 end
350 iNode = complementNodeMap(i);
351 isf_i = sn.stationToStateful(i);
352
353 for j = complementIndices
354 if ~isKey(complementNodeMap, j)
355 continue;
356 end
357 jNode = complementNodeMap(j);
358 isf_j = sn.stationToStateful(j);
359
360 % Use ORIGINAL routing for direct complement-to-complement paths
361 rtIdx_i = (isf_i - 1) * K + k;
362 rtIdx_j = (isf_j - 1) * K + k;
363
364 if rtIdx_i <= size(rt, 1) && rtIdx_j <= size(rt, 2)
365 prob = rt(rtIdx_i, rtIdx_j);
366 if prob > GlobalConstants.FineTol
367 P_k(iNode, jNode) = prob;
368 end
369 end
370 end
371
372 % Routes from complement to subset -> FES
373 for j = subsetIndices
374 isf_j = sn.stationToStateful(j);
375 rtIdx_i = (isf_i - 1) * K + k;
376 rtIdx_j = (isf_j - 1) * K + k;
377
378 if rtIdx_i <= size(rt, 1) && rtIdx_j <= size(rt, 2)
379 prob = rt(rtIdx_i, rtIdx_j);
380 if prob > GlobalConstants.FineTol
381 P_k(iNode, fesNodeIdx) = P_k(iNode, fesNodeIdx) + prob;
382 end
383 end
384 end
385 end
386
387 % Routes from FES to complement
388 % Weight by visit ratios within subset
389 for j = complementIndices
390 if ~isKey(complementNodeMap, j)
391 continue;
392 end
393 jNode = complementNodeMap(j);
394 isf_j = sn.stationToStateful(j);
395
396 probSum = 0;
397 for idx = 1:nSub
398 i = subsetIndices(idx);
399 isf_i = sn.stationToStateful(i);
400 rtIdx_i = (isf_i - 1) * K + k;
401 rtIdx_j = (isf_j - 1) * K + k;
402
403 if rtIdx_i <= size(rt, 1) && rtIdx_j <= size(rt, 2)
404 prob = rt(rtIdx_i, rtIdx_j);
405 % Weight by visit ratio
406 probSum = probSum + visitRatios(idx) * prob;
407 end
408 end
409
410 if probSum > GlobalConstants.FineTol
411 P_k(fesNodeIdx, jNode) = probSum;
412 end
413 end
414
415 % No explicit self-loop on FES - internal routing is captured by LJD rates
416 % (the state-dependent throughput already accounts for internal circulation)
417
418 % Normalize rows
419 for n = 1:I_fes
420 rowSum = sum(P_k(n, :));
421 if rowSum > GlobalConstants.FineTol
422 P_k(n, :) = P_k(n, :) / rowSum;
423 end
424 end
425
426 P{k, k} = P_k;
427
428 if options.verbose
429 fprintf('Routing matrix for class %d:\n', k);
430 nodeNames = cell(1, I_fes);
431 for n = 1:I_fes
432 nodeNames{n} = fesModel.nodes{n}.name;
433 end
434 fprintf(' Nodes: %s\n', strjoin(nodeNames, ', '));
435 for i = 1:I_fes
436 fprintf(' %s: %s\n', nodeNames{i}, mat2str(P_k(i,:), 4));
437 end
438 end
439end
440
441% Link the model
442fesModel.link(P);
443
444%% Build deaggregation info
445deaggInfo = struct();
446deaggInfo.originalModel = model;
447deaggInfo.stationSubset = stationSubset;
448deaggInfo.subsetIndices = subsetIndices;
449deaggInfo.complementIndices = complementIndices;
450deaggInfo.throughputTable = scalingTable;
451deaggInfo.cutoffs = cutoffs;
452deaggInfo.stochCompSubset = stochCompSubset;
453deaggInfo.stochCompComplement = stochCompComplement;
454deaggInfo.isolatedDemands = L_iso;
455deaggInfo.isolatedServers = mi_iso;
456deaggInfo.isolatedVisits = visits_iso;
457deaggInfo.isolatedIsDelay = isDelay_iso;
458deaggInfo.fesNodeIdx = fesNodeIdx;
459
460if options.verbose
461 fprintf('FES model created with %d stations (1 FES + %d complement).\n', ...
462 I_fes, length(complementIndices));
463end
464
465end
Definition Station.m:245