LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fes_aggregate.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_FES_FES_AGGREGATE_H
6#define LINE_API_FES_FES_AGGREGATE_H
7
8/**
9 * @file
10 * @ingroup api_fes
11 * Flow-equivalent-server aggregation: replace a station subset by one station.
12 *
13 * Templated port of `matlab/src/io/@ModelAdapter/aggregateFES.m` (the JAR twin is
14 * the deprecated `jline.api.fes.FESAggregator`, reached through
15 * `ModelAdapter.aggregateFES`; MATLAB is the reference here).
16 *
17 * This is Chandy-Herzog-Woo's Norton theorem in its state-dependent form. The
18 * subset is short-circuited and solved in isolation at every population on the
19 * lattice; the resulting per-class throughputs X_r(n) become the service rates
20 * of a single limited-class-dependent station, and the complement is rewired to
21 * route through it. For a product-form closed network the replacement is EXACT
22 * at every population, which is what makes the transform worth doing rather
23 * than an approximation to be tuned.
24 *
25 * THREE THINGS THE ARITHMETIC DEPENDS ON, each easy to get wrong:
26 *
27 * 1. The routing seen by the subset is the STOCHASTIC COMPLEMENT of the full
28 * chain on the subset's rows, not the raw submatrix. A job that leaves the
29 * subset and comes back through the complement must re-enter with the right
30 * probability, and `dtmc_stochcomp` is what folds those excursions back in.
31 *
32 * 2. The isolated throughput is a rate INSIDE the subnetwork, whose visit
33 * ratios are normalized to the subset. Turning it into a rate the outside
34 * sees needs the per-class ESCAPE factor -- the visit-weighted probability
35 * of leaving the subset per subset visit. Without it the FES completes jobs
36 * at the subnetwork's internal circulation rate, which is too fast by
37 * exactly the number of internal hops per escape.
38 *
39 * 3. The class dependence carries `beta_r(n) = X_r(n) |n| / n_r`, not X_r(n).
40 * The |n|/n_r cancels the processor-sharing split the convolution applies
41 * (Sauer 1983, eq. 40), so the aggregate really completes class r at X_r(n).
42 * `fes_beta_handle` owns that factor; this file must not apply it twice.
43 *
44 * ONE DELIBERATE DEPARTURE FROM MATLAB, and it is a correctness fix rather than
45 * a convention: the reference rebuilds each complement station's service law
46 * from `sn.proc{i}{k}` as `APH(ones(1,n)/n, T)` whenever the process has more
47 * than one phase, i.e. it DISCARDS the true initial phase vector and substitutes
48 * a uniform one. That silently changes the distribution of every non-exponential
49 * complement station (an Erlang(k) becomes a mixture starting in a random
50 * phase, with a different mean and a much larger SCV). The port copies the
51 * station's own `Distrib` verbatim instead, so a complement station keeps
52 * exactly the law it had. On exponential service -- the case the reference's own
53 * tests exercise -- the two agree, since a one-phase process is rebuilt as
54 * Exp(rate) either way.
55 *
56 * ARITHMETIC: transcendental, inherited from the convolution behind
57 * fes_compute_throughputs.
58 */
59
60#include <algorithm>
61#include <cmath>
62#include <cstddef>
63#include <string>
64#include <vector>
65
74#include "line/num/number.h"
75#include "line/util/error.h"
76#include "line/util/matrix.h"
77
78namespace line {
79namespace fes {
80
81using lang::CdScaling;
83
84/** `options` of the reference; the solver field is implied by the convolution. */
85struct FesOptions {
86 std::vector<int> cutoffs; ///< per-class population bound; empty means sn.njobs
87 bool verbose = false;
88};
89
90/** Everything needed to map an FES result back onto the original model. */
91template <class T>
93 std::vector<std::size_t> subsetIndices; ///< 1-based, as given
94 std::vector<std::size_t> complementIndices; ///< 1-based
95 std::vector<std::vector<T>> throughputTable; ///< per class, linearized on the lattice
96 std::vector<int> cutoffs;
98 Matrix<T> isolatedDemands; ///< (M_sub x K)
99 Matrix<T> isolatedVisits; ///< (M_sub x K)
100 std::vector<int> isolatedServers;
101 std::vector<bool> isolatedIsDelay;
102 std::vector<T> escape; ///< per-class Norton escape factor
103 std::size_t fesNode = 0; ///< 1-based node index of the FES in the new model
104};
105
106/** What fes_aggregate returns. */
107template <class T>
109 explicit FesAggregateResult(const qn::Network<T>& m) : model(m) {}
111 std::size_t fesNode = 0; ///< 1-based node index of the FES station
113};
114
115/**
116 * @brief Flow-equivalent-server aggregation: replace a station subset by one
117 * station.
118 *
119 * @param sn the original closed product-form network
120 * @param subsetIndices 1-BASED station indices to aggregate, the reference's base
121 * @param options cutoffs and verbosity
122 */
123template <class T>
125 const std::vector<std::size_t>& subsetIndices,
126 const FesOptions& options = FesOptions()) {
127 const T zero = num_traits<T>::from_int(0);
128 const T one = num_traits<T>::from_int(1);
129 const double fineTol = lang::GlobalConstants::FineTol;
130 const std::size_t M = sn.nstations, K = sn.nclasses;
131
132 const FesValidateResult ok = fes_validate(sn, subsetIndices);
133 if (!ok.isValid) throw InputError("aggregateFES: " + ok.errorMsg);
134
135 std::vector<std::size_t> complementIndices;
136 for (std::size_t i = 1; i <= M; ++i) {
137 bool inSubset = false;
138 for (std::size_t a = 0; a < subsetIndices.size(); ++a)
139 if (subsetIndices[a] == i) inSubset = true;
140 if (!inSubset) complementIndices.push_back(i);
141 }
142
143 const std::vector<double> N = sn.njobs();
144 std::vector<int> cutoffs = options.cutoffs;
145 if (cutoffs.empty())
146 for (std::size_t r = 0; r < K; ++r) cutoffs.push_back(static_cast<int>(N[r]));
147 if (cutoffs.size() != K)
148 throw InputError("aggregateFES: cutoffs must carry one entry per class");
149
150 // ---- the two stochastic complements ---------------------------------
151 // `rt` is indexed (stateful-1)*K + class, so a station contributes the K
152 // consecutive rows of its own stateful index.
153 std::vector<std::size_t> subsetRt, complementRt;
154 for (std::size_t a = 0; a < subsetIndices.size(); ++a) {
155 const std::size_t isf = sn.stateful_of_station(subsetIndices[a]);
156 for (std::size_t r = 0; r < K; ++r) subsetRt.push_back((isf - 1) * K + r);
157 }
158 for (std::size_t a = 0; a < complementIndices.size(); ++a) {
159 const std::size_t isf = sn.stateful_of_station(complementIndices[a]);
160 for (std::size_t r = 0; r < K; ++r) complementRt.push_back((isf - 1) * K + r);
161 }
162 const Matrix<T> stochCompSubset = mc::dtmc_stochcomp(sn.rt, subsetRt);
163 const Matrix<T> stochCompComplement = mc::dtmc_stochcomp(sn.rt, complementRt);
164
165 // ---- the isolated subnetwork ----------------------------------------
166 const std::size_t nSub = subsetIndices.size();
167 Matrix<T> subRates(nSub, K, zero);
168 std::vector<int> mi(nSub, 1);
169 std::vector<bool> isDelay(nSub, false);
170 for (std::size_t a = 0; a < nSub; ++a) {
171 const qn::Station<T>& st = sn.stations[subsetIndices[a] - 1];
172 for (std::size_t r = 0; r < K; ++r) subRates(a, r) = sn.rates(subsetIndices[a] - 1, r);
173 isDelay[a] = (st.nodetype == qn::NodeType::Delay || st.sched == SchedStrategy::INF);
174 mi[a] = std::isinf(st.nservers) ? 1 : static_cast<int>(st.nservers);
175 }
176 const FesIsolated<T> iso = fes_build_isolated(subRates, stochCompSubset);
177 std::vector<std::vector<T>> scalingTable =
178 fes_compute_throughputs(iso.L, mi, isDelay, cutoffs);
179
180 // ---- the Norton escape factor ---------------------------------------
181 // Per subset visit, the visit-weighted probability of leaving the subset.
182 // The isolated throughput counts every internal hop, so without this the
183 // FES would complete jobs at the internal circulation rate.
184 std::vector<T> escape(K, zero);
185 for (std::size_t r = 0; r < K; ++r) {
186 for (std::size_t a = 0; a < nSub; ++a) {
187 const std::size_t j = subsetIndices[a];
188 const T Vjr = T(iso.L(a, r) * sn.rates(j - 1, r));
189 const std::size_t isf_j = sn.stateful_of_station(j);
190 T pexit = zero;
191 for (std::size_t b = 0; b < complementIndices.size(); ++b) {
192 const std::size_t isf_i = sn.stateful_of_station(complementIndices[b]);
193 pexit += sn.rt((isf_j - 1) * K + r, (isf_i - 1) * K + r);
194 }
195 if (std::isfinite(num_traits<T>::to_double(Vjr))) escape[r] += T(Vjr * pexit);
196 }
197 if (num_traits<T>::to_double(escape[r]) > fineTol)
198 for (std::size_t idx = 0; idx < scalingTable[r].size(); ++idx)
199 scalingTable[r][idx] *= escape[r];
200 }
201
202 // ---- the FES model ---------------------------------------------------
203 qn::Network<T> fesModel(sn.name.empty() ? std::string("FES") : sn.name + "_FES");
204
205 // The complement stations, in their original order, then the FES.
206 std::vector<std::size_t> complementNode(M + 1, 0); // 1-based station -> new node
207 for (std::size_t b = 0; b < complementIndices.size(); ++b) {
208 const std::size_t i = complementIndices[b];
209 const qn::Station<T>& st = sn.stations[i - 1];
210 std::size_t nd = 0;
211 if (st.nodetype == qn::NodeType::Delay) {
212 nd = fesModel.add_delay(st.name);
213 } else if (st.nodetype == qn::NodeType::Queue) {
214 nd = fesModel.add_queue(st.name, st.sched);
215 if (!std::isinf(st.nservers)) fesModel.set_number_of_servers(nd, st.nservers);
216 if (st.cap > 0.0 && std::isfinite(st.cap))
217 fesModel.set_capacity(nd, static_cast<int>(st.cap));
218 } else {
219 throw InputError("aggregateFES: unsupported station type in the complement");
220 }
221 complementNode[i] = nd;
222 }
223 const std::size_t fesNode = fesModel.add_queue("FES", SchedStrategy::PS);
224 fesModel.set_number_of_servers(fesNode, 1.0);
225
226 // The reference anchors the classes at the first complement station, and
227 // at the FES only when the complement is empty (which fes_validate rules
228 // out, since the subset must be proper).
229 const std::size_t refNode =
230 complementIndices.empty() ? fesNode : complementNode[complementIndices[0]];
231 std::vector<std::size_t> newClass(K, 0);
232 for (std::size_t r = 0; r < K; ++r)
233 newClass[r] = fesModel.add_closed_class(sn.classes[r].name, N[r], refNode);
234
235 // The complement keeps its own service laws, verbatim; see the header note
236 // on why the reference's APH rebuild is not reproduced.
237 for (std::size_t b = 0; b < complementIndices.size(); ++b) {
238 const std::size_t i = complementIndices[b];
239 for (std::size_t r = 0; r < K; ++r)
240 fesModel.set_service(complementNode[i], newClass[r], sn.service[i - 1][r]);
241 }
242
243 // The FES serves at rate one and is scaled entirely by the class dependence.
244 for (std::size_t r = 0; r < K; ++r)
245 fesModel.set_service(fesNode, newClass[r], lang::Distrib<T>::exp_rate(one));
246
247 // A zero entry would stall the recurrence, so the reference floors the
248 // table rather than letting the handle return zero.
249 const T floorVal = num_traits<T>::from_double(fineTol);
250 for (std::size_t r = 0; r < K; ++r)
251 for (std::size_t idx = 0; idx < scalingTable[r].size(); ++idx)
252 if (num_traits<T>::to_double(scalingTable[r][idx]) < fineTol)
253 scalingTable[r][idx] = floorVal;
254
255 const FesBetaFun<T> beta = fes_beta_handle(scalingTable, cutoffs);
256 const T peak = pfqn::cd_peak_scaling<T>(beta, cutoffs);
257 // `sn.cdscaling` is evaluated on a population vector in the WORKING
258 // arithmetic while the FES table is indexed by integer counts, so the
259 // handle is wrapped rather than re-tabulated. The rounding is the
260 // reference's own -- fes_beta_handle rounds before linearizing.
261 const CdScaling<T> cd = [beta](const std::vector<T>& n) {
262 std::vector<int> ni(n.size(), 0);
263 for (std::size_t r = 0; r < n.size(); ++r)
264 ni[r] = static_cast<int>(std::lround(num_traits<T>::to_double(n[r])));
265 return beta(ni);
266 };
267 fesModel.set_class_dependence(fesNode, cd, std::vector<T>(1, peak));
268
269 // ---- the routing -----------------------------------------------------
271 for (std::size_t r = 0; r < K; ++r) {
272 const std::size_t I = complementIndices.size() + 1; // the complement plus the FES
273 std::vector<std::vector<T>> Pk(I + 1, std::vector<T>(I + 1, zero));
274
275 for (std::size_t b = 0; b < complementIndices.size(); ++b) {
276 const std::size_t i = complementIndices[b];
277 const std::size_t iNode = complementNode[i];
278 const std::size_t isf_i = sn.stateful_of_station(i);
279
280 // Complement to complement: the ORIGINAL probability, since those
281 // paths do not pass through the aggregated subset.
282 for (std::size_t c = 0; c < complementIndices.size(); ++c) {
283 const std::size_t j = complementIndices[c];
284 const std::size_t isf_j = sn.stateful_of_station(j);
285 const T p = sn.rt((isf_i - 1) * K + r, (isf_j - 1) * K + r);
286 if (num_traits<T>::to_double(p) > fineTol) Pk[iNode][complementNode[j]] = p;
287 }
288 // Complement to the subset: every such path now ends at the FES.
289 for (std::size_t a = 0; a < nSub; ++a) {
290 const std::size_t isf_j = sn.stateful_of_station(subsetIndices[a]);
291 const T p = sn.rt((isf_i - 1) * K + r, (isf_j - 1) * K + r);
292 if (num_traits<T>::to_double(p) > fineTol) Pk[iNode][fesNode] += p;
293 }
294 }
295
296 // The FES leaves for the complement with the VISIT-WEIGHTED exit
297 // probability of the subset: which subset station a job departs from is
298 // no longer represented, so its visit ratio is what stands in.
299 for (std::size_t c = 0; c < complementIndices.size(); ++c) {
300 const std::size_t j = complementIndices[c];
301 const std::size_t isf_j = sn.stateful_of_station(j);
302 T probSum = zero;
303 for (std::size_t a = 0; a < nSub; ++a) {
304 const std::size_t isf_i = sn.stateful_of_station(subsetIndices[a]);
305 probSum += T(iso.visits(a, r) * sn.rt((isf_i - 1) * K + r, (isf_j - 1) * K + r));
306 }
307 if (num_traits<T>::to_double(probSum) > fineTol)
308 Pk[fesNode][complementNode[j]] = probSum;
309 }
310
311 // No FES self-loop: the internal circulation is already inside the
312 // state-dependent rate, and adding it would count those hops twice.
313 for (std::size_t nd = 1; nd <= I; ++nd) {
314 T rowSum = zero;
315 for (std::size_t md = 1; md <= I; ++md) rowSum += Pk[nd][md];
316 if (num_traits<T>::to_double(rowSum) > fineTol)
317 for (std::size_t md = 1; md <= I; ++md) Pk[nd][md] /= rowSum;
318 }
319 for (std::size_t nd = 1; nd <= I; ++nd)
320 for (std::size_t md = 1; md <= I; ++md)
321 if (num_traits<T>::to_double(Pk[nd][md]) != 0.0)
322 P.set(newClass[r], newClass[r], nd, md, Pk[nd][md]);
323 }
324 fesModel.link(P);
325
326 FesAggregateResult<T> out(fesModel);
327 out.fesNode = fesNode;
328 out.deagg.subsetIndices = subsetIndices;
329 out.deagg.complementIndices = complementIndices;
330 out.deagg.throughputTable = scalingTable;
331 out.deagg.cutoffs = cutoffs;
332 out.deagg.stochCompSubset = stochCompSubset;
333 out.deagg.stochCompComplement = stochCompComplement;
334 out.deagg.isolatedDemands = iso.L;
335 out.deagg.isolatedVisits = iso.visits;
336 out.deagg.isolatedServers = mi;
337 out.deagg.isolatedIsDelay = isDelay;
338 out.deagg.escape = escape;
339 out.deagg.fesNode = fesNode;
340 return out;
341}
342
343} // namespace fes
344} // namespace line
345
346#endif // LINE_API_FES_FES_AGGREGATE_H
Peak of a class-dependence handle over the reachable population lattice.
InputError(const std::string &what)
Definition error.h:39
A network plus its refreshed NetworkStruct.
A queueing network under construction.
std::size_t add_delay(const std::string &nm)
An infinite-server station (a Delay, MATLAB's Delay / DelayStation).
void set_number_of_servers(std::size_t node, double n)
queue.setNumberOfServers(n).
std::size_t add_queue(const std::string &nm, SchedStrategy sched=SchedStrategy::FCFS)
A queueing station.
std::size_t add_closed_class(const std::string &nm, double njobs, std::size_t refstat_node, int prio=0)
A closed class of the given population, referencing a station node.
void set_class_dependence(std::size_t node, const CdScaling< T > &fun, const std::vector< T > &peak=std::vector< T >())
station.setClassDependence(beta, peakRatePerClass).
void set_capacity(std::size_t node, double k)
station.setCapacity(k), the K of Kendall's notation.
void link(const RoutingMatrix< T > &Pm)
model.link(P): install the routing.
void set_service(std::size_t node, std::size_t cls, const Distrib< T > &d)
station.setService(class, dist).
The routing matrix a model script fills in, MATLAB's P cell array.
void set(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
The exception types the port throws.
Wraps a flow-equivalent-server throughput table as a per-class class-dependence function beta_{i,...
Service demands and visit ratios of an isolated subnetwork, from the stochastic complement of its rou...
Per-class throughput table of an isolated subnetwork, tabulated over the population lattice,...
Input validation for Flow-Equivalent Server (FES) aggregation.
Dense matrix and non-owning view.
FesBetaFun< T > fes_beta_handle(const std::vector< std::vector< T > > &scalingTable, const std::vector< int > &cutoffs)
Wraps a flow-equivalent-server throughput table as a per-class class-dependence function beta_{i,...
std::vector< std::vector< T > > fes_compute_throughputs(const Matrix< T > &L, const std::vector< int > &mi, const std::vector< bool > &isDelay, const std::vector< int > &cutoffs)
Per-class throughput table of an isolated subnetwork, tabulated over the population lattice,...
FesIsolated< T > fes_build_isolated(const Matrix< T > &rates, const Matrix< T > &stochCompS)
Build the isolated subnetwork's demands and visit ratios.
std::function< std::vector< T >(const std::vector< int > &)> FesBetaFun
Class-dependence handle: population vector -> per-class beta.
FesValidateResult fes_validate(const qn::NetworkStruct< T > &sn, const std::vector< std::size_t > &subsetIndices)
Input validation for Flow-Equivalent Server (FES) aggregation.
FesAggregateResult< T > fes_aggregate(const qn::NetworkStruct< T > &sn, const std::vector< std::size_t > &subsetIndices, const FesOptions &options=FesOptions())
Flow-equivalent-server aggregation: replace a station subset by one station.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A class-dependent scaling map, sn.cdscaling.
Definition lang_types.h:639
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
T cd_peak_scaling(const std::function< std::vector< T >(const std::vector< int > &)> &beta, const std::vector< int > &NK)
Peak of a class-dependence handle over the reachable population lattice.
The Network constructor API: Queue, Delay, Source, Sink, Router, ClassSwitch, Cache,...
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
What fes_aggregate returns.
std::size_t fesNode
1-based node index of the FES station
FesAggregateResult(const qn::Network< T > &m)
Everything needed to map an FES result back onto the original model.
Matrix< T > isolatedDemands
(M_sub x K)
std::vector< std::size_t > subsetIndices
1-based, as given
std::vector< std::vector< T > > throughputTable
per class, linearized on the lattice
std::vector< std::size_t > complementIndices
1-based
Matrix< T > isolatedVisits
(M_sub x K)
Matrix< T > stochCompComplement
std::vector< bool > isolatedIsDelay
std::vector< int > isolatedServers
std::size_t fesNode
1-based node index of the FES in the new model
std::vector< T > escape
per-class Norton escape factor
std::vector< int > cutoffs
Demands and visit ratios of the isolated subnetwork.
Matrix< T > visits
(M_sub x K) visit ratios, each class summing to one
Matrix< T > L
(M_sub x K) service demands
options of the reference; the solver field is implied by the convolution.
std::vector< int > cutoffs
per-class population bound; empty means sn.njobs
The reference's (isValid, errorMsg) pair.
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
static constexpr double FineTol
Definition lang_types.h:668
One station of the network.
double cap
Station capacity in Kendall's K, as setCapacity sets it.
SchedStrategy sched
double nservers
may be infinite (a Delay, or an inf-scheduled task)