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