LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fes_build_isolated.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_BUILD_ISOLATED_H
6#define LINE_API_FES_BUILD_ISOLATED_H
7
8/**
9 * @file
10 * @ingroup api_fes
11 * Service demands and visit ratios of an isolated subnetwork, from the
12 * stochastic complement of its routing matrix.
13 *
14 * Templated port of matlab/src/api/fes/fes_build_isolated.m. Given the
15 * stochastic complement S of the routing chain restricted to a subset of
16 * stations, the per-class visit ratios are the stationary distribution of the
17 * embedded DTMC of that class,
18 *
19 * v_k P_k = v_k, sum_i v_k(i) = 1,
20 *
21 * and the demands follow as L(i,k) = v_k(i) / rate(i,k), renormalized so that
22 * the first station of the subset is visited once, which is the MVA
23 * convention the flow-equivalent-server construction expects.
24 *
25 * THE sn DEPENDENCY IS LIFTED INTO THE SIGNATURE. The MATLAB entry point takes
26 * `(sn, subsetIndices, stochCompS)` and opens with a loop that plucks five
27 * fields out of `sn` -- `nclasses`, `stationToNode`, `nodetype`, `nservers`,
28 * `rates` -- to produce the per-station server counts, the delay flags and the
29 * service rates of the subset. Nothing after that loop reads `sn`, and the
30 * routing already arrives as a plain matrix argument. The extraction is
31 * therefore not part of the algorithm: this port takes the extracted
32 * quantities directly, in the same spirit as npfqn_sqd.
33 *
34 * Note in particular that `mi` and `isDelay` are PURE PASS-THROUGH in the
35 * reference: they are assembled from `sn` and returned, and no later line
36 * reads them. They are not arguments here for that reason -- a caller that
37 * wants them holds them already. The numeric core needs only the service
38 * rates and the stochastic complement.
39 *
40 * INDEXING. stochCompS is indexed (station-1)*K + class over the SUBSET, so
41 * it is (M_sub K x M_sub K); the reference forms it that way and indexes it
42 * with the subset position i, not the original station id. Its class-k block
43 * is read at rows and columns (i-1)*K + k.
44 *
45 * ARITHMETIC. Exact at Rational. The stationary distribution is obtained from
46 * the singular system (I - P_k' + e e'/M) x = e/M, one linear solve per class,
47 * which is a finite sequence of field operations; there is no iteration and no
48 * tolerance in the solve itself. The two guards that DO carry a tolerance are
49 * reproduced from the reference and are structural rather than numerical: a
50 * row of P_k whose sum is below FineTol = 1e-8 is treated as "no routing
51 * defined" and made a self-loop, and a row whose sum differs from one by more
52 * than FineTol is renormalized. At Rational a caller supplying an exactly
53 * stochastic complement never reaches either branch.
54 *
55 * DIVERGENCE, stated because it changes which inputs take the fallback path.
56 * The reference guards the solve with `rank(A) == M_sub` and substitutes the
57 * uniform distribution when the rank is deficient. A rank test is a singular
58 * value decomposition, which is double-only in this tree and has no exact
59 * instantiation (util/eig.h documents why). The port instead attempts the
60 * solve and takes the same uniform fallback when the factorization finds an
61 * exactly zero pivot. The two agree on every non-degenerate input: A is
62 * nonsingular precisely when the class-k chain has a unique stationary
63 * distribution, which is when the solve succeeds. They can differ only for a
64 * matrix that is numerically rank deficient yet still factorizable, where the
65 * reference falls back and the port returns the (ill-conditioned) solve; that
66 * is the direction that preserves information rather than discarding it.
67 */
68
69#include <cmath>
70#include <cstddef>
71#include <vector>
72
73#include "line/num/number.h"
74#include "line/util/error.h"
75#include "line/util/linalg.h"
76#include "line/util/lu.h"
77#include "line/util/matrix.h"
78
79namespace line {
80namespace fes {
81
82/** Demands and visit ratios of the isolated subnetwork. */
83template <class T>
85 Matrix<T> L; ///< (M_sub x K) service demands
86 Matrix<T> visits; ///< (M_sub x K) visit ratios, each class summing to one
87};
88
89/**
90 * Build the isolated subnetwork's demands and visit ratios.
91 *
92 * @param rates (M_sub x K) service rates of the subset stations; an entry
93 * that is zero, negative or non-finite marks a disabled
94 * service and yields a zero demand, as in the reference
95 * @param stochCompS (M_sub K x M_sub K) stochastic complement of the routing
96 * chain over the subset, indexed (i-1)*K + k
97 */
98template <class T>
99FesIsolated<T> fes_build_isolated(const Matrix<T>& rates, const Matrix<T>& stochCompS) {
100 const std::size_t M = rates.rows();
101 const std::size_t K = rates.cols();
102 if (M == 0) throw InputError("fes_build_isolated: empty station subset");
103 if (K == 0) throw InputError("fes_build_isolated: no classes");
104 if (stochCompS.rows() < M * K || stochCompS.cols() < M * K)
105 throw InputError(
106 "fes_build_isolated: the stochastic complement is too small for the subset; it must be "
107 "at least (M_sub*K) square, indexed (station-1)*K + class over the SUBSET");
108
109 const T zero = num_traits<T>::from_int(0);
110 const T one = num_traits<T>::from_int(1);
111 const T fineTol = num_traits<T>::from_double(1e-8);
112 const T Mt = num_traits<T>::from_int(static_cast<long>(M));
113
114 FesIsolated<T> out;
115 out.L = Matrix<T>(M, K, zero);
116 out.visits = Matrix<T>(M, K, zero);
117
118 for (std::size_t k = 0; k < K; ++k) {
119 // Class-k routing block of the stochastic complement.
120 Matrix<T> P(M, M, zero);
121 for (std::size_t i = 0; i < M; ++i)
122 for (std::size_t j = 0; j < M; ++j) P(i, j) = stochCompS(i * K + k, j * K + k);
123
124 // Row repair, exactly as the reference: a row with no routing becomes
125 // a self-loop, a row that is off by more than FineTol is renormalized.
126 for (std::size_t i = 0; i < M; ++i) {
127 T rowsum = zero;
128 for (std::size_t j = 0; j < M; ++j) rowsum += P(i, j);
129 if (rowsum > fineTol && num_abs(T(rowsum - one)) > fineTol) {
130 for (std::size_t j = 0; j < M; ++j) P(i, j) /= rowsum;
131 } else if (rowsum < fineTol) {
132 for (std::size_t j = 0; j < M; ++j) P(i, j) = zero;
133 P(i, i) = one;
134 }
135 }
136
137 // Stationary distribution from (I - P' + e e'/M) x = e/M.
138 Matrix<T> A(M, M, zero);
139 for (std::size_t i = 0; i < M; ++i)
140 for (std::size_t j = 0; j < M; ++j) {
141 const T id = (i == j) ? one : zero;
142 A(i, j) = id - P(j, i) + one / Mt;
143 }
144 std::vector<T> rhs(M, T(one / Mt));
145 std::vector<T> pi;
146 bool ok = true;
147 try {
148 pi = solve(A, rhs);
149 } catch (const NumericError&) {
150 ok = false; // exactly singular: the reference's rank-deficient branch
151 }
152 if (!ok) {
153 pi.assign(M, T(one / Mt));
154 } else {
155 // Clamp and renormalize, as the reference does.
156 for (std::size_t i = 0; i < M; ++i)
157 if (pi[i] < zero) pi[i] = zero;
158 T s = zero;
159 for (std::size_t i = 0; i < M; ++i) s += pi[i];
160 if (s > zero) {
161 for (std::size_t i = 0; i < M; ++i) pi[i] /= s;
162 } else {
163 pi.assign(M, T(one / Mt));
164 }
165 }
166 for (std::size_t i = 0; i < M; ++i) out.visits(i, k) = pi[i];
167 }
168
169 // Demands L(i,k) = visits(i,k) / rate(i,k), zero where service is disabled.
170 for (std::size_t i = 0; i < M; ++i)
171 for (std::size_t k = 0; k < K; ++k) {
172 const T r = rates(i, k);
173 if (!(r > zero) || !std::isfinite(num_traits<T>::to_double(r)))
174 out.L(i, k) = zero;
175 else
176 out.L(i, k) = out.visits(i, k) / r;
177 }
178
179 // Renormalize so the first station of the subset is visited once, the MVA
180 // convention. A class that never visits station 0 is left as it is.
181 for (std::size_t k = 0; k < K; ++k) {
182 const T v0 = out.visits(0, k);
183 if (v0 > zero)
184 for (std::size_t i = 0; i < M; ++i) out.L(i, k) /= v0;
185 }
186 return out;
187}
188
189} // namespace fes
190} // namespace line
191
192#endif // LINE_API_FES_BUILD_ISOLATED_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The algorithm cannot proceed on this instance (singular matrix, ...).
Definition error.h:43
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
FesIsolated< T > fes_build_isolated(const Matrix< T > &rates, const Matrix< T > &stochCompS)
Build the isolated subnetwork's demands and visit ratios.
T num_abs(const T &v)
Definition number.h:172
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
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