LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sn_routing_ergodic.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_SN_SN_ROUTING_ERGODIC_H
6#define LINE_API_SN_SN_ROUTING_ERGODIC_H
7
8/**
9 * @file
10 * @ingroup api_sn
11 * Reducibility of a network's ROUTING, and the repair that makes it ergodic.
12 *
13 * Port of the MATLAB `@MNetwork` methods `isRoutingErgodic`,
14 * `getReducibilityInfo`, `getAbsorbingStations` and `makeErgodic`, and twin of
15 * the JAR `jline.lang.RoutingErgodicity` and the native Python
16 * `Network.get_reducibility_info` / `make_ergodic`.
17 *
18 * This inspects the ROUTING STRUCTURE ONLY, not the state space. Ergodic
19 * routing is NECESSARY but not sufficient for the chain to be ergodic:
20 * state-dependent routing, finite buffers and blocking can still make the CTMC
21 * reducible. The converse is the useful direction -- reducible routing is a
22 * defect the modeller can see and fix before any solver runs, and it is what
23 * otherwise surfaces as "the generator has no recurrent state" inside
24 * ctmc_solve.
25 *
26 * The adjacency is built over the NODE-level blocks `sn.P[(r,s)]`, folding
27 * every class pair into one graph: a job that leaves node i as another class
28 * has still left i. A station is ABSORBING when it has routing defined and no
29 * edge to any other node; the Sink is removed from that list, being
30 * legitimately absorbing in an open network.
31 *
32 * ARITHMETIC: comparisons against zero only, so this is exact in every
33 * instantiation and needs no transcendental gate.
34 */
35
36#include <algorithm>
37#include <cstddef>
38#include <map>
39#include <string>
40#include <utility>
41#include <vector>
42
45#include "line/util/error.h"
46#include "line/util/matrix.h"
47
48namespace line {
49namespace sn {
50
51/** The reducibility structure of a routing matrix, the MATLAB info struct. */
53 bool isRoutingErgodic = true; ///< the routing is irreducible
54 bool isReducible = false; ///< the routing is reducible
55 std::vector<std::string> absorbingStations; ///< names, the Sink excluded
56 std::vector<std::string> transientStations; ///< names in a transient component
57 std::size_t numSCCs = 1; ///< strongly connected components
58 std::vector<std::string> suggestedFixes; ///< one repair per absorbing station
59};
60
61namespace detail {
62
63/** Node-level adjacency of the routing, every class pair folded into one graph. */
64template <class T>
65Matrix<T> routing_adjacency(const qn::NetworkStruct<T>& sn) {
66 const std::size_t I = sn.nodes.size();
67 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
68 Matrix<T> adj(I, I, zero);
69 for (typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>::const_iterator it =
70 sn.P.begin();
71 it != sn.P.end(); ++it) {
72 const Matrix<T>& blk = it->second;
73 const std::size_t nr = std::min(blk.rows(), I);
74 const std::size_t nc = std::min(blk.cols(), I);
75 for (std::size_t i = 0; i < nr; ++i)
76 for (std::size_t j = 0; j < nc; ++j)
77 if (blk(i, j) > zero) adj(i, j) = one;
78 }
79 return adj;
80}
81
82/** True when node IDX has any outgoing routing probability at all. */
83template <class T>
84bool has_any_routing(const qn::NetworkStruct<T>& sn, std::size_t idx) {
85 const T zero = num_traits<T>::from_int(0);
86 for (typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>::const_iterator it =
87 sn.P.begin();
88 it != sn.P.end(); ++it) {
89 const Matrix<T>& blk = it->second;
90 if (idx >= blk.rows()) continue;
91 for (std::size_t j = 0; j < blk.cols(); ++j)
92 if (blk(idx, j) > zero) return true;
93 }
94 return false;
95}
96
97/** First Delay, else the first station that is not itself absorbing. */
98template <class T>
99std::string default_target(const qn::NetworkStruct<T>& sn,
100 const std::vector<std::string>& absorbing) {
101 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
102 if (sn.nodes[i].nodetype == lang::NodeType::Delay) return sn.nodes[i].name;
103 std::string first;
104 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
105 if (sn.nodes[i].station == 0) continue;
106 if (first.empty()) first = sn.nodes[i].name;
107 if (std::find(absorbing.begin(), absorbing.end(), sn.nodes[i].name) == absorbing.end())
108 return sn.nodes[i].name;
109 }
110 return first;
111}
112
113} // namespace detail
114
115/** Ergodicity of the routing, with the structure behind the verdict. */
116template <class T>
119 if (sn.P.empty()) return info; // no routing defined yet, nothing to refute
120
121 const Matrix<T> adj = detail::routing_adjacency(sn);
122 const mc::SccResult scc = mc::stronglyconncomp(adj);
123 info.numSCCs = scc.numSCC();
124
125 const T zero = num_traits<T>::from_int(0);
126 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
127 if (sn.nodes[i].station == 0) continue;
128 bool outgoing = false;
129 for (std::size_t j = 0; j < sn.nodes.size(); ++j)
130 if (j != i && adj(i, j) > zero) {
131 outgoing = true;
132 break;
133 }
134 if (!outgoing && detail::has_any_routing(sn, i))
135 info.absorbingStations.push_back(sn.nodes[i].name);
136 }
137 // The Sink is legitimately absorbing in an open network
138 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
139 if (sn.nodes[i].nodetype == lang::NodeType::Sink) {
140 info.absorbingStations.erase(std::remove(info.absorbingStations.begin(),
141 info.absorbingStations.end(),
142 sn.nodes[i].name),
143 info.absorbingStations.end());
144 }
145
146 std::size_t recurrentCount = 0;
147 for (std::size_t c = 1; c <= info.numSCCs; ++c) {
148 if (scc.recurrent[c - 1]) {
149 ++recurrentCount;
150 continue;
151 }
152 for (std::size_t v = 0; v < scc.scc.size(); ++v) {
153 if (scc.scc[v] != c || sn.nodes[v].station == 0) continue;
154 const std::string& nm = sn.nodes[v].name;
155 if (std::find(info.absorbingStations.begin(), info.absorbingStations.end(), nm) ==
156 info.absorbingStations.end())
157 info.transientStations.push_back(nm);
158 }
159 }
160
161 info.isReducible = !info.absorbingStations.empty() || recurrentCount > 1;
162 info.isRoutingErgodic = !info.isReducible;
163 return info;
164}
165
166/** The reducibility structure plus a suggested repair per absorbing station. */
167template <class T>
170 if (info.isRoutingErgodic) return info;
171 const std::string target = detail::default_target(sn, info.absorbingStations);
172 for (std::size_t i = 0; i < info.absorbingStations.size(); ++i) {
173 const std::string& abs = info.absorbingStations[i];
174 if (!target.empty() && target != abs)
175 info.suggestedFixes.push_back("Route jobs from " + abs + " back to " + target +
176 " (e.g., P{class}(" + abs + ", " + target + ") = 1.0)");
177 }
178 return info;
179}
180
181/** NODE indices, 1-based, of the absorbing stations. */
182template <class T>
183std::vector<std::size_t> sn_absorbing_stations(const qn::NetworkStruct<T>& sn) {
185 std::vector<std::size_t> out;
186 for (std::size_t k = 0; k < info.absorbingStations.size(); ++k)
187 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
188 if (sn.nodes[i].name == info.absorbingStations[k]) out.push_back(i + 1);
189 return out;
190}
191
192/**
193 * A routing map that makes the network ergodic, by redirecting every absorbing
194 * station to TARGETNAME (empty for the default: the first Delay, else the first
195 * non-absorbing station).
196 *
197 * Returns the modified blocks; the caller assigns them to `sn.P` and refreshes.
198 * Nothing is mutated here, which is the C++ reading of the reference's "does
199 * NOT relink": the modeller sees the repair before it is applied.
200 */
201template <class T>
202std::map<std::pair<std::size_t, std::size_t>, Matrix<T>> sn_make_ergodic(
203 const qn::NetworkStruct<T>& sn, const std::string& targetName = std::string()) {
204 std::map<std::pair<std::size_t, std::size_t>, Matrix<T>> P = sn.P;
206 if (info.isRoutingErgodic || info.absorbingStations.empty()) return P;
207
208 const std::string target =
209 targetName.empty() ? detail::default_target(sn, info.absorbingStations) : targetName;
210 if (target.empty()) throw InputError("sn_make_ergodic: no suitable target node for routing");
211 std::size_t targetIdx = sn.nodes.size();
212 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
213 if (sn.nodes[i].name == target) targetIdx = i;
214 if (targetIdx == sn.nodes.size())
215 throw InputError("sn_make_ergodic: target node \"" + target + "\" not found");
216
217 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
218 for (std::size_t k = 0; k < info.absorbingStations.size(); ++k) {
219 std::size_t absIdx = sn.nodes.size();
220 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
221 if (sn.nodes[i].name == info.absorbingStations[k]) absIdx = i;
222 if (absIdx == sn.nodes.size() || absIdx == targetIdx) continue;
223 for (typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>::iterator it =
224 P.begin();
225 it != P.end(); ++it) {
226 if (absIdx >= it->second.rows()) continue;
227 for (std::size_t j = 0; j < it->second.cols(); ++j) it->second(absIdx, j) = zero;
228 }
229 for (std::size_t r = 1; r <= sn.nclasses; ++r) {
230 typename std::map<std::pair<std::size_t, std::size_t>, Matrix<T>>::iterator it =
231 P.find(std::make_pair(r, r));
232 if (it != P.end() && absIdx < it->second.rows() && targetIdx < it->second.cols())
233 it->second(absIdx, targetIdx) = one;
234 }
235 }
236 return P;
237}
238
239} // namespace sn
240} // namespace line
241
242#endif // LINE_API_SN_SN_ROUTING_ERGODIC_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
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
std::map< std::pair< std::size_t, std::size_t >, Matrix< T > > sn_make_ergodic(const qn::NetworkStruct< T > &sn, const std::string &targetName=std::string())
A routing map that makes the network ergodic, by redirecting every absorbing station to TARGETNAME (e...
RoutingErgodicityInfo sn_reducibility_info(const qn::NetworkStruct< T > &sn)
The reducibility structure plus a suggested repair per absorbing station.
std::vector< std::size_t > sn_absorbing_stations(const qn::NetworkStruct< T > &sn)
NODE indices, 1-based, of the absorbing stations.
RoutingErgodicityInfo sn_is_routing_ergodic(const qn::NetworkStruct< T > &sn)
Ergodicity of the routing, with the structure behind the verdict.
A queueing network and its refreshed NetworkStruct.
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
std::size_t numSCC() const
std::vector< bool > recurrent
recurrent[c-1] is true when component c has no edge leaving it.
std::vector< std::size_t > scc
Component index of each state, 1-based as in MATLAB (0 is never used).
The reducibility structure of a routing matrix, the MATLAB info struct.
bool isReducible
the routing is reducible
std::vector< std::string > absorbingStations
names, the Sink excluded
std::size_t numSCCs
strongly connected components
std::vector< std::string > transientStations
names in a transient component
bool isRoutingErgodic
the routing is irreducible
std::vector< std::string > suggestedFixes
one repair per absorbing station