LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
wf_loop_detector.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_WF_WF_LOOP_DETECTOR_H
6#define LINE_API_WF_WF_LOOP_DETECTOR_H
7
8/**
9 * @file
10 * @ingroup api_wf
11 * Loop pattern detection in a workflow network.
12 *
13 * Templated port of jar/src/main/java/jline/api/wf/Wf_loop_detector.java (no
14 * MATLAB counterpart, so the JAR is the reference). Two mechanisms:
15 *
16 * - a SIMPLE loop is a service node with an edge to a router that has an edge
17 * back to it, the two-hop rework loop that a workflow model builds for a
18 * "repeat the activity with probability p" construct;
19 * - a COMPLEX loop is a service node inside a strongly connected component of
20 * more than one node that also contains a router or a join, found by
21 * Tarjan's algorithm. Only looked for when join nodes are supplied, exactly
22 * as in the reference.
23 *
24 * The five public methods of the Java class are kept 1:1: detect_loops,
25 * get_loop_probability, validate_loop_pattern, get_expected_loop_iterations,
26 * get_loop_stats.
27 *
28 * Traversal plus, in the statistics, the geometric mean number of iterations
29 * 1/(1-p) and averages: sums, one division each. Finite field computation, so
30 * this instantiates at exact arithmetic and the expected iteration count of a
31 * rational loop probability is an exact rational. No transcendental gate.
32 *
33 * The reference returns GlobalConstants.Inf when the loop probability reaches
34 * one. An exact rational field has no infinity, so get_expected_loop_iterations
35 * returns ExpectedIterations with an explicit `infinite` flag instead of a
36 * sentinel value; get_loop_stats drops the infinite entries, which is what the
37 * reference's Double.isFinite filter does.
38 */
39
40#include <cstddef>
41#include <map>
42#include <set>
43#include <vector>
44
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/matrix.h"
49
50namespace line {
51namespace wf {
52
53/** 1/(1-p), with the p >= 1 divergence reported rather than encoded. */
54template <class T>
59
60/** Mirrors the Java getLoopStats map. */
61template <class T>
70
71namespace detail {
72
73/** service -> router -> service, the two-hop rework loop. */
74template <class T>
75bool wf_in_simple_loop(int serviceNode, const std::map<int, std::vector<std::pair<int, T>>>& adj,
76 const std::set<int>& routerSet) {
77 typename std::map<int, std::vector<std::pair<int, T>>>::const_iterator it = adj.find(serviceNode);
78 if (it == adj.end()) return false;
79 for (std::size_t a = 0; a < it->second.size(); ++a) {
80 const int router = it->second[a].first;
81 if (!routerSet.count(router)) continue;
82 typename std::map<int, std::vector<std::pair<int, T>>>::const_iterator jt = adj.find(router);
83 if (jt == adj.end()) continue;
84 for (std::size_t b = 0; b < jt->second.size(); ++b)
85 if (jt->second[b].first == serviceNode) return true;
86 }
87 return false;
88}
89
90struct TarjanState {
91 std::map<int, int> index;
92 std::map<int, int> lowlink;
93 std::set<int> onStack;
94 std::vector<int> stack;
95 std::vector<std::vector<int>> sccs;
96 int counter = 0;
97};
98
99inline void wf_strong_connect(int node, const std::map<int, std::set<int>>& graph, TarjanState& st) {
100 st.index[node] = st.counter;
101 st.lowlink[node] = st.counter;
102 st.counter++;
103 st.stack.push_back(node);
104 st.onStack.insert(node);
105
106 std::map<int, std::set<int>>::const_iterator it = graph.find(node);
107 if (it != graph.end()) {
108 for (std::set<int>::const_iterator nb = it->second.begin(); nb != it->second.end(); ++nb) {
109 if (!st.index.count(*nb)) {
110 wf_strong_connect(*nb, graph, st);
111 if (st.lowlink[*nb] < st.lowlink[node]) st.lowlink[node] = st.lowlink[*nb];
112 } else if (st.onStack.count(*nb)) {
113 if (st.index[*nb] < st.lowlink[node]) st.lowlink[node] = st.index[*nb];
114 }
115 }
116 }
117
118 if (st.lowlink[node] == st.index[node]) {
119 std::vector<int> scc;
120 int w;
121 do {
122 w = st.stack.back();
123 st.stack.pop_back();
124 st.onStack.erase(w);
125 scc.push_back(w);
126 } while (w != node);
127 st.sccs.push_back(scc);
128 }
129}
130
131} // namespace detail
132
133/**
134 * @param linkMatrix (nedges x 3) edge list
135 * @param serviceNodes ids of the service nodes
136 * @param routerNodes ids of the router nodes
137 * @param joinNodes ids of the join nodes; empty disables the SCC search,
138 * matching the two-argument Java overload
139 * @return the service nodes that sit on a loop, in detection order, distinct
140 */
141template <class T>
142std::vector<int> detect_loops(const Matrix<T>& linkMatrix, const std::vector<int>& serviceNodes,
143 const std::vector<int>& routerNodes,
144 const std::vector<int>& joinNodes = std::vector<int>()) {
145 detail::wf_check(linkMatrix);
146 const std::set<int> routerSet(routerNodes.begin(), routerNodes.end());
147 const std::map<int, std::vector<std::pair<int, T>>> adj = detail::wf_adjacency_prob(linkMatrix);
148
149 std::vector<int> loopNodes;
150 for (std::size_t i = 0; i < serviceNodes.size(); ++i)
151 if (detail::wf_in_simple_loop(serviceNodes[i], adj, routerSet))
152 loopNodes.push_back(serviceNodes[i]);
153
154 if (!joinNodes.empty()) {
155 const std::set<int> serviceSet(serviceNodes.begin(), serviceNodes.end());
156 const std::set<int> joinSet(joinNodes.begin(), joinNodes.end());
157
158 std::map<int, std::set<int>> graph;
159 for (std::size_t i = 0; i < linkMatrix.rows(); ++i)
160 graph[detail::wf_id(linkMatrix, i, 0)].insert(detail::wf_id(linkMatrix, i, 1));
161
162 detail::TarjanState st;
163 for (std::map<int, std::set<int>>::const_iterator it = graph.begin(); it != graph.end(); ++it)
164 if (!st.index.count(it->first)) detail::wf_strong_connect(it->first, graph, st);
165
166 for (std::size_t s = 0; s < st.sccs.size(); ++s) {
167 if (st.sccs[s].size() <= 1) continue;
168 std::vector<int> inScc;
169 bool hasRouterOrJoin = false;
170 for (std::size_t k = 0; k < st.sccs[s].size(); ++k) {
171 const int n = st.sccs[s][k];
172 if (serviceSet.count(n)) inScc.push_back(n);
173 if (routerSet.count(n) || joinSet.count(n)) hasRouterOrJoin = true;
174 }
175 if (!inScc.empty() && hasRouterOrJoin)
176 loopNodes.insert(loopNodes.end(), inScc.begin(), inScc.end());
177 }
178 }
179
180 std::vector<int> distinct;
181 std::set<int> seen;
182 for (std::size_t i = 0; i < loopNodes.size(); ++i)
183 if (seen.insert(loopNodes[i]).second) distinct.push_back(loopNodes[i]);
184 return distinct;
185}
186
187/**
188 * Probability on the router-to-service edge that closes the loop, 0 when the
189 * node is not on a simple loop.
190 */
191template <class T>
192T get_loop_probability(int serviceNode, const Matrix<T>& linkMatrix,
193 const std::vector<int>& routerNodes) {
194 detail::wf_check(linkMatrix);
195 const std::set<int> routerSet(routerNodes.begin(), routerNodes.end());
196 for (std::size_t i = 0; i < linkMatrix.rows(); ++i) {
197 const int start = detail::wf_id(linkMatrix, i, 0);
198 const int end = detail::wf_id(linkMatrix, i, 1);
199 if (start != serviceNode || !routerSet.count(end)) continue;
200 for (std::size_t j = 0; j < linkMatrix.rows(); ++j)
201 if (detail::wf_id(linkMatrix, j, 0) == end &&
202 detail::wf_id(linkMatrix, j, 1) == serviceNode)
203 return linkMatrix(j, 2);
204 }
205 return num_traits<T>::from_int(0);
206}
207
208/** True when the node still has the service -> router -> service structure. */
209template <class T>
210bool validate_loop_pattern(int loopNode, const Matrix<T>& linkMatrix,
211 const std::vector<int>& routerNodes) {
212 detail::wf_check(linkMatrix);
213 const std::set<int> routerSet(routerNodes.begin(), routerNodes.end());
214 return detail::wf_in_simple_loop(loopNode, detail::wf_adjacency_prob(linkMatrix), routerSet);
215}
216
217/** Mean number of visits of a geometric loop, 1/(1-p). */
218template <class T>
220 const T one = num_traits<T>::from_int(1);
222 if (loopProbability >= one) {
223 r.infinite = true;
224 return r;
225 }
226 r.value = one / (one - loopProbability);
227 return r;
228}
229
230/** Count and moments of the loop probabilities and iteration counts. */
231template <class T>
232LoopStats<T> get_loop_stats(const std::vector<int>& loopNodes, const Matrix<T>& linkMatrix,
233 const std::vector<int>& routerNodes) {
234 LoopStats<T> stats;
235 stats.numLoops = loopNodes.size();
236
237 std::vector<T> probs;
238 for (std::size_t i = 0; i < loopNodes.size(); ++i)
239 probs.push_back(get_loop_probability(loopNodes[i], linkMatrix, routerNodes));
240
241 if (!probs.empty()) {
243 T mx = probs[0];
244 T mn = probs[0];
245 for (std::size_t i = 0; i < probs.size(); ++i) {
246 sum += probs[i];
247 if (probs[i] > mx) mx = probs[i];
248 if (probs[i] < mn) mn = probs[i];
249 }
250 stats.avgLoopProbability = sum / num_traits<T>::from_int(static_cast<long>(probs.size()));
251 stats.maxLoopProbability = mx;
252 stats.minLoopProbability = mn;
253 }
254
255 std::vector<T> iters;
256 for (std::size_t i = 0; i < probs.size(); ++i) {
258 if (!e.infinite) iters.push_back(e.value);
259 }
260 if (!iters.empty()) {
262 T mx = iters[0];
263 for (std::size_t i = 0; i < iters.size(); ++i) {
264 sum += iters[i];
265 if (iters[i] > mx) mx = iters[i];
266 }
267 stats.avgExpectedIterations = sum / num_traits<T>::from_int(static_cast<long>(iters.size()));
268 stats.maxExpectedIterations = mx;
269 }
270 return stats;
271}
272
273} // namespace wf
274} // namespace line
275
276#endif // LINE_API_WF_WF_LOOP_DETECTOR_H
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< int > detect_loops(const Matrix< T > &linkMatrix, const std::vector< int > &serviceNodes, const std::vector< int > &routerNodes, const std::vector< int > &joinNodes=std::vector< int >())
ExpectedIterations< T > get_expected_loop_iterations(const T &loopProbability)
Mean number of visits of a geometric loop, 1/(1-p).
bool validate_loop_pattern(int loopNode, const Matrix< T > &linkMatrix, const std::vector< int > &routerNodes)
True when the node still has the service -> router -> service structure.
T get_loop_probability(int serviceNode, const Matrix< T > &linkMatrix, const std::vector< int > &routerNodes)
Probability on the router-to-service edge that closes the loop, 0 when the node is not on a simple lo...
LoopStats< T > get_loop_stats(const std::vector< int > &loopNodes, const Matrix< T > &linkMatrix, const std::vector< int > &routerNodes)
Count and moments of the loop probabilities and iteration counts.
Number-type abstraction for the templated API port.
1/(1-p), with the p >= 1 divergence reported rather than encoded.
Mirrors the Java getLoopStats map.