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%
13% Parameters:
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
140if options.verbose
141 fprintf('Throughput table computed for %d states.\n', prod(cutoffs + 1));
142end
143
144%% Create the FES model
145fesModel = Network(sprintf('%s_FES', model.getName()));
146
147% Copy complement stations to new model
148nodeMap = cell(sn.nnodes, 1);
149stationMap = cell(M, 1);
150
151for i = complementIndices
152 origStation = modelStations{i};
153 nodeIdx = sn.stationToNode(i);
154
155 switch class(origStation)
156 case 'Queue'
157 newStation = Queue(fesModel, origStation.name, origStation.schedStrategy);
158 if ~isinf(origStation.numberOfServers)
159 newStation.setNumberOfServers(origStation.numberOfServers);
160 end
161 if ~isempty(origStation.cap) && isfinite(origStation.cap)
162 newStation.setCapacity(origStation.cap);
163 end
164 case 'Delay'
165 newStation = Delay(fesModel, origStation.name);
166 otherwise
167 line_error(mfilename, sprintf('Unsupported station type %s.', class(origStation)));
168 end
169
170 nodeMap{nodeIdx} = newStation;
171 stationMap{i} = newStation;
172end
173
174% Create the FES station (single Queue with PS scheduling)
175fesStation = Queue(fesModel, 'FES', SchedStrategy.PS);
176fesStation.setNumberOfServers(1);
177
178% Create job classes
179newClasses = cell(1, K);
180modelClasses = model.classes;
181
182% Choose reference station (FES or first complement station)
183if ~isempty(complementIndices)
184 refStation = stationMap{complementIndices(1)};
185else
186 refStation = fesStation;
187end
188
189for k = 1:K
190 origClass = modelClasses{k};
191 population = sn.njobs(k);
192 newClass = ClosedClass(fesModel, origClass.name, population, refStation);
193 newClasses{k} = newClass;
194end
195
196% Set service distributions for complement stations
197for i = complementIndices
198 newStation = stationMap{i};
199
200 for k = 1:K
201 origPH = sn.proc{i}{k};
202
203 if isempty(origPH) || (iscell(origPH) && isempty(origPH{1})) || ...
204 (iscell(origPH) && all(isnan(origPH{1}(:))))
205 newStation.setService(newClasses{k}, Disabled.getInstance());
206 else
207 if iscell(origPH)
208 T_matrix = origPH{1}; % Sub-generator (diagonal is -rate)
209 t0_vector = origPH{2}; % Exit rate vector
210 nPhases = size(T_matrix, 1);
211
212 if nPhases == 1
213 rate = -T_matrix(1,1);
214 newStation.setService(newClasses{k}, Exp(rate));
215 else
216 alpha = ones(1, nPhases) / nPhases;
217 dist = APH(alpha, T_matrix);
218 newStation.setService(newClasses{k}, dist);
219 end
220 else
221 newStation.setService(newClasses{k}, Exp(sn.rates(i, k)));
222 end
223 end
224 end
225end
226
227% Set LJCD on FES with per-class throughput scaling tables
228% The scalingTable{k} contains throughputs for class k at each population state
229%
230% For multi-class FES, we use Limited Joint Class Dependence (LJCD) which allows
231% each class to have its own state-dependent scaling factor.
232% The FES service rate for class c in state (n1,...,nK) equals throughput_c(n).
233
234% Set base service rate (will be scaled by LJCD)
235% Use Exp(1.0) as base; the LJCD scaling provides the actual throughput rate
236for k = 1:K
237 fesStation.setService(newClasses{k}, Exp(1.0));
238end
239
240% Handle zeros in scaling tables (replace with small positive value)
241for k = 1:K
242 scalingTable{k}(scalingTable{k} < GlobalConstants.FineTol) = GlobalConstants.FineTol;
243end
244
245if options.verbose
246 fprintf('Per-class scaling tables:\n');
247 for k = 1:K
248 fprintf(' Class %d: Max = %.6f, Min = %.6f\n', k, ...
249 max(scalingTable{k}), min(scalingTable{k}(scalingTable{k} > GlobalConstants.FineTol)));
250 end
251end
252
253% Set per-class joint dependence (LJCD)
254% scalingTable is already a cell array {1 x K} where each cell is a linearized vector
255fesStation.setJointClassDependence(scalingTable, cutoffs);
256
257%% Build routing matrix for FES model
258I_fes = length(fesModel.nodes);
259P = fesModel.initRoutingMatrix();
260
261% Get FES node index
262fesNodeIdx = 0;
263for n = 1:I_fes
264 if fesModel.nodes{n} == fesStation
265 fesNodeIdx = n;
266 break;
267 end
268end
269
270% Build node index mapping for complement stations
271complementNodeMap = containers.Map('KeyType', 'double', 'ValueType', 'double');
272for i = complementIndices
273 nodeIdx = sn.stationToNode(i);
274 if ~isempty(nodeMap{nodeIdx})
275 for n = 1:I_fes
276 if fesModel.nodes{n} == nodeMap{nodeIdx}
277 complementNodeMap(i) = n;
278 break;
279 end
280 end
281 end
282end
283
284% Set routing probabilities
285% Routes within complement use stochastic complement
286% Routes to/from subset go through FES
287if options.verbose
288 fprintf('Setting up routing for FES model:\n');
289 fprintf(' FES node index: %d\n', fesNodeIdx);
290 fprintf(' Complement station indices: %s\n', mat2str(complementIndices));
291 fprintf(' Subset station indices: %s\n', mat2str(subsetIndices));
292end
293
294for k = 1:K
295 P_k = zeros(I_fes, I_fes);
296
297 % Compute visit ratios within subset for exit distribution
298 % For closed network, solve pi * S = pi where S is stochastic complement for subset
299 % For simplicity, assume uniform if can't compute (valid for symmetric cases)
300 nSub = length(subsetIndices);
301 visitRatios = ones(1, nSub) / nSub; % Default: uniform
302
303 % Routes within complement (DIRECT paths only, not through subset)
304 for i = complementIndices
305 if ~isKey(complementNodeMap, i)
306 continue;
307 end
308 iNode = complementNodeMap(i);
309 isf_i = sn.stationToStateful(i);
310
311 for j = complementIndices
312 if ~isKey(complementNodeMap, j)
313 continue;
314 end
315 jNode = complementNodeMap(j);
316 isf_j = sn.stationToStateful(j);
317
318 % Use ORIGINAL routing for direct complement-to-complement paths
319 rtIdx_i = (isf_i - 1) * K + k;
320 rtIdx_j = (isf_j - 1) * K + k;
321
322 if rtIdx_i <= size(rt, 1) && rtIdx_j <= size(rt, 2)
323 prob = rt(rtIdx_i, rtIdx_j);
324 if prob > GlobalConstants.FineTol
325 P_k(iNode, jNode) = prob;
326 end
327 end
328 end
329
330 % Routes from complement to subset -> FES
331 for j = subsetIndices
332 isf_j = sn.stationToStateful(j);
333 rtIdx_i = (isf_i - 1) * K + k;
334 rtIdx_j = (isf_j - 1) * K + k;
335
336 if rtIdx_i <= size(rt, 1) && rtIdx_j <= size(rt, 2)
337 prob = rt(rtIdx_i, rtIdx_j);
338 if prob > GlobalConstants.FineTol
339 P_k(iNode, fesNodeIdx) = P_k(iNode, fesNodeIdx) + prob;
340 end
341 end
342 end
343 end
344
345 % Routes from FES to complement
346 % Weight by visit ratios within subset
347 for j = complementIndices
348 if ~isKey(complementNodeMap, j)
349 continue;
350 end
351 jNode = complementNodeMap(j);
352 isf_j = sn.stationToStateful(j);
353
354 probSum = 0;
355 for idx = 1:nSub
356 i = subsetIndices(idx);
357 isf_i = sn.stationToStateful(i);
358 rtIdx_i = (isf_i - 1) * K + k;
359 rtIdx_j = (isf_j - 1) * K + k;
360
361 if rtIdx_i <= size(rt, 1) && rtIdx_j <= size(rt, 2)
362 prob = rt(rtIdx_i, rtIdx_j);
363 % Weight by visit ratio
364 probSum = probSum + visitRatios(idx) * prob;
365 end
366 end
367
368 if probSum > GlobalConstants.FineTol
369 P_k(fesNodeIdx, jNode) = probSum;
370 end
371 end
372
373 % No explicit self-loop on FES - internal routing is captured by LJD rates
374 % (the state-dependent throughput already accounts for internal circulation)
375
376 % Normalize rows
377 for n = 1:I_fes
378 rowSum = sum(P_k(n, :));
379 if rowSum > GlobalConstants.FineTol
380 P_k(n, :) = P_k(n, :) / rowSum;
381 end
382 end
383
384 P{k, k} = P_k;
385
386 if options.verbose
387 fprintf('Routing matrix for class %d:\n', k);
388 nodeNames = cell(1, I_fes);
389 for n = 1:I_fes
390 nodeNames{n} = fesModel.nodes{n}.name;
391 end
392 fprintf(' Nodes: %s\n', strjoin(nodeNames, ', '));
393 for i = 1:I_fes
394 fprintf(' %s: %s\n', nodeNames{i}, mat2str(P_k(i,:), 4));
395 end
396 end
397end
398
399% Link the model
400fesModel.link(P);
401
402%% Build deaggregation info
403deaggInfo = struct();
404deaggInfo.originalModel = model;
405deaggInfo.stationSubset = stationSubset;
406deaggInfo.subsetIndices = subsetIndices;
407deaggInfo.complementIndices = complementIndices;
408deaggInfo.throughputTable = scalingTable;
409deaggInfo.cutoffs = cutoffs;
410deaggInfo.stochCompSubset = stochCompSubset;
411deaggInfo.stochCompComplement = stochCompComplement;
412deaggInfo.isolatedDemands = L_iso;
413deaggInfo.isolatedServers = mi_iso;
414deaggInfo.isolatedVisits = visits_iso;
415deaggInfo.isolatedIsDelay = isDelay_iso;
416deaggInfo.fesNodeIdx = fesNodeIdx;
417
418if options.verbose
419 fprintf('FES model created with %d stations (1 FES + %d complement).\n', ...
420 I_fes, length(complementIndices));
421end
422
423end