1function [fesModel, fesStation, deaggInfo] = aggregateFES(model, stationSubset, options)
2% AGGREGATEFES Replace a station subset with a Flow-Equivalent Server (FES)
4% [fesModel, fesStation, deaggInfo] = AGGREGATEFES(model, stationSubset)
5% [fesModel, fesStation, deaggInfo] = AGGREGATEFES(model, stationSubset, options)
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.
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)
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
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();
41% Copyright (c) 2012-2026, Imperial College London
44%% Input validation and defaults
48if ~isfield(options, 'solver')
49 options.solver = 'mva';
51if ~isfield(options, 'cutoffs')
54if ~isfield(options, 'verbose')
55 options.verbose = false;
58%% Get network structure
59sn = model.getStruct();
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)
70 if stationSubset{i} == modelStations{j}
77% Validate inputs using sn struct and indices
78[isValid, errorMsg] = fes_validate(sn, subsetIndices);
80 line_error(mfilename, errorMsg);
83% Get complement indices (stations not in subset)
85complementIndices = setdiff(allIndices, subsetIndices);
87% Set cutoffs if not provided
88if isempty(options.cutoffs)
89 % Default: use total jobs per class
90 options.cutoffs = sn.njobs';
92cutoffs = options.cutoffs;
95 fprintf('FES Aggregation: %d subset stations, %d complement stations, %d classes\n', ...
96 length(subsetIndices), length(complementIndices), K);
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
103% Build index sets for rt matrix
106 isf = sn.stationToStateful(i);
107 subsetRtIndices = [subsetRtIndices, ((isf-1)*K + 1):(isf*K)];
110complementRtIndices = [];
111for i = complementIndices
112 isf = sn.stationToStateful(i);
113 complementRtIndices = [complementRtIndices, ((isf-1)*K + 1):(isf*K)];
116% Compute stochastic complement for subset (routing within subset only)
117% S = P11 + P12 * inv(I - P22) * P21
119stochCompSubset = dtmc_stochcomp(rt, subsetRtIndices);
121% Compute stochastic complement for complement
122stochCompComplement = dtmc_stochcomp(rt, complementRtIndices);
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));
130%% Build isolated subnetwork data and compute throughputs
131[L_iso, mi_iso, visits_iso, isDelay_iso] = fes_build_isolated(sn, subsetIndices, stochCompSubset);
134 fprintf('Isolated subnetwork data extracted for %d stations.\n', length(subsetIndices));
137% Compute throughputs for all population states
138scalingTable = fes_compute_throughputs(L_iso, mi_iso, isDelay_iso, cutoffs, options);
141 fprintf('Throughput table computed for %d states.\n', prod(cutoffs + 1));
144%% Create the FES model
145fesModel = Network(sprintf('%s_FES', model.getName()));
147% Copy complement stations to new model
148nodeMap = cell(sn.nnodes, 1);
149stationMap = cell(M, 1);
151for i = complementIndices
152 origStation = modelStations{i};
153 nodeIdx = sn.stationToNode(i);
155 switch class(origStation)
157 newStation = Queue(fesModel, origStation.name, origStation.schedStrategy);
158 if ~isinf(origStation.numberOfServers)
159 newStation.setNumberOfServers(origStation.numberOfServers);
161 if ~isempty(origStation.cap) && isfinite(origStation.cap)
162 newStation.setCapacity(origStation.cap);
165 newStation = Delay(fesModel, origStation.name);
167 line_error(mfilename, sprintf('Unsupported station type %s.', class(origStation)));
170 nodeMap{nodeIdx} = newStation;
171 stationMap{i} = newStation;
174% Create the FES station (single Queue with PS scheduling)
175fesStation = Queue(fesModel, 'FES', SchedStrategy.PS);
176fesStation.setNumberOfServers(1);
179newClasses = cell(1, K);
180modelClasses = model.classes;
182% Choose reference station (FES or first complement station)
183if ~isempty(complementIndices)
184 refStation = stationMap{complementIndices(1)};
186 refStation = fesStation;
190 origClass = modelClasses{k};
191 population = sn.njobs(k);
192 newClass = ClosedClass(fesModel, origClass.name, population, refStation);
193 newClasses{k} = newClass;
196% Set service distributions for complement stations
197for i = complementIndices
198 newStation = stationMap{i};
201 origPH = sn.proc{i}{k};
203 if isempty(origPH) || (iscell(origPH) && isempty(origPH{1})) || ...
204 (iscell(origPH) && all(isnan(origPH{1}(:))))
205 newStation.setService(newClasses{k}, Disabled.getInstance());
208 T_matrix = origPH{1}; % Sub-generator (diagonal is -rate)
209 t0_vector = origPH{2}; % Exit rate vector
210 nPhases = size(T_matrix, 1);
213 rate = -T_matrix(1,1);
214 newStation.setService(newClasses{k}, Exp(rate));
216 alpha = ones(1, nPhases) / nPhases;
217 dist = APH(alpha, T_matrix);
218 newStation.setService(newClasses{k}, dist);
221 newStation.setService(newClasses{k}, Exp(sn.rates(i, k)));
227% Set LJCD on FES with per-class throughput scaling tables
228% The scalingTable{k} contains throughputs for class k at each population state
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).
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
237 fesStation.setService(newClasses{k}, Exp(1.0));
240% Handle zeros in scaling tables (replace with small positive value)
242 scalingTable{k}(scalingTable{k} < GlobalConstants.FineTol) = GlobalConstants.FineTol;
246 fprintf('Per-class scaling tables:\n');
248 fprintf(' Class %d: Max = %.6f, Min = %.6f\n', k, ...
249 max(scalingTable{k}), min(scalingTable{k}(scalingTable{k} > GlobalConstants.FineTol)));
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);
257%% Build routing matrix for FES model
258I_fes = length(fesModel.nodes);
259P = fesModel.initRoutingMatrix();
264 if fesModel.nodes{n} == fesStation
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})
276 if fesModel.nodes{n} == nodeMap{nodeIdx}
277 complementNodeMap(i) = n;
284% Set routing probabilities
285% Routes within complement use stochastic complement
286% Routes to/from subset go through FES
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));
295 P_k = zeros(I_fes, I_fes);
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
303 % Routes within complement (DIRECT paths only, not through subset)
304 for i = complementIndices
305 if ~isKey(complementNodeMap, i)
308 iNode = complementNodeMap(i);
309 isf_i = sn.stationToStateful(i);
311 for j = complementIndices
312 if ~isKey(complementNodeMap, j)
315 jNode = complementNodeMap(j);
316 isf_j = sn.stationToStateful(j);
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;
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;
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;
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;
345 % Routes from FES to complement
346 % Weight by visit ratios within subset
347 for j = complementIndices
348 if ~isKey(complementNodeMap, j)
351 jNode = complementNodeMap(j);
352 isf_j = sn.stationToStateful(j);
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;
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;
368 if probSum > GlobalConstants.FineTol
369 P_k(fesNodeIdx, jNode) = probSum;
373 % No explicit self-loop on FES - internal routing is captured by LJD rates
374 % (the state-dependent throughput already accounts for internal circulation)
378 rowSum = sum(P_k(n, :));
379 if rowSum > GlobalConstants.FineTol
380 P_k(n, :) = P_k(n, :) / rowSum;
387 fprintf('Routing matrix for class %d:\n', k);
388 nodeNames = cell(1, I_fes);
390 nodeNames{n} = fesModel.nodes{n}.name;
392 fprintf(' Nodes: %s\n', strjoin(nodeNames, ', '));
394 fprintf(' %s: %s\n', nodeNames{i}, mat2str(P_k(i,:), 4));
402%% Build deaggregation info
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;
419 fprintf('FES model created with %d stations (1 FES + %d complement).\n', ...
420 I_fes, length(complementIndices));