LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
wf_branch_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_BRANCH_DETECTOR_H
6#define LINE_API_WF_WF_BRANCH_DETECTOR_H
7
8/**
9 * @file
10 * @ingroup api_wf
11 * Branch (probabilistic choice) pattern detection in a workflow network.
12 *
13 * Templated port of jar/src/main/java/jline/api/wf/Wf_branch_detector.java (no
14 * MATLAB counterpart, so the JAR is the reference). A branch point is a node
15 * with more than one outgoing edge of which at least two lead to a service
16 * node; the branch is accepted when the probabilities of those service edges
17 * sum to one within 1e-2, and it is annotated with the first join node
18 * reachable from every branch alternative.
19 *
20 * The six public methods of the Java class are kept 1:1: detect_branches,
21 * validate_branch_pattern, calculate_branch_diversity, get_branch_stats,
22 * find_most_probable_branch, find_least_probable_branch.
23 *
24 * ARITHMETIC. detect_branches, validate_branch_pattern and the two extremal
25 * queries only add and compare probabilities, so they are finite field
26 * computations and instantiate at exact arithmetic; the 1e-2 slack on the
27 * branch probability sum is a structural admission threshold inherited from
28 * the reference, not a rounding allowance, so it is kept in every
29 * instantiation. calculate_branch_diversity and get_branch_stats compute the
30 * Shannon entropy of the branch probabilities and therefore need log; both
31 * carry the transcendental gate.
32 *
33 * REFERENCE DEFECT (JAR): calculateBranchDiversity divides the Gini sum by
34 * (n-1), so a one-alternative pattern yields a division by zero (NaN in Java).
35 * The port rejects n < 2 with an InputError instead of returning a NaN;
36 * detect_branches never emits such a pattern, so no detected pattern is
37 * affected.
38 */
39
40#include <algorithm>
41#include <cmath>
42#include <cstddef>
43#include <map>
44#include <set>
45#include <vector>
46
48#include "line/num/number.h"
49#include "line/util/error.h"
50#include "line/util/matrix.h"
51
52namespace line {
53namespace wf {
54
55/** Mirrors the Java BranchPattern. */
56template <class T>
58 std::vector<int> branchNodes;
59 std::vector<T> probabilities;
60 int forkNode = -1;
61 bool hasJoinNode = false; ///< the Java Integer may be null
62 int joinNode = -1;
63};
64
65/** Mirrors the Java calculateBranchDiversity map. */
66template <class T>
73
74/** Mirrors the Java getBranchStats map. */
75template <class T>
85
86/** One alternative of a branch: the node and its probability. */
87template <class T>
89 bool valid = false;
90 int node = -1;
92};
93
94namespace detail {
95
96/** Nodes reachable from startNode; the search does not continue past stopSet. */
97template <class T>
98std::set<int> wf_reachable_stop(int startNode,
99 const std::map<int, std::vector<std::pair<int, T>>>& adj,
100 const std::set<int>& stopSet) {
101 std::set<int> reachable;
102 std::set<int> visited;
103 std::vector<int> queue;
104 std::size_t head = 0;
105 queue.push_back(startNode);
106 while (head < queue.size()) {
107 const int current = queue[head++];
108 if (!visited.insert(current).second) continue;
109 typename std::map<int, std::vector<std::pair<int, T>>>::const_iterator it =
110 adj.find(current);
111 if (it == adj.end()) continue;
112 for (std::size_t k = 0; k < it->second.size(); ++k) {
113 const int nb = it->second[k].first;
114 reachable.insert(nb);
115 if (!stopSet.count(nb)) queue.push_back(nb);
116 }
117 }
118 return reachable;
119}
120
121/** log(v), ADL-visible so a non-double T can supply its own overload. */
122template <class T>
123inline T num_log(const T& v) {
124 using std::log;
125 return T(log(v));
126}
127
128} // namespace detail
129
130/**
131 * @param linkMatrix (nedges x 3) edge list
132 * @param serviceNodes ids of the service nodes
133 * @param joinNodes ids of the join nodes
134 */
135template <class T>
136std::vector<BranchPattern<T>> detect_branches(const Matrix<T>& linkMatrix,
137 const std::vector<int>& serviceNodes,
138 const std::vector<int>& joinNodes) {
139 detail::wf_check(linkMatrix);
140 const std::set<int> serviceSet(serviceNodes.begin(), serviceNodes.end());
141 const std::set<int> joinSet(joinNodes.begin(), joinNodes.end());
142 const std::map<int, std::vector<std::pair<int, T>>> adj = detail::wf_adjacency_prob(linkMatrix);
143 const T one = num_traits<T>::from_int(1);
144 const T slack = num_traits<T>::from_rational(1, 100);
145
146 std::vector<BranchPattern<T>> patterns;
147 for (typename std::map<int, std::vector<std::pair<int, T>>>::const_iterator it = adj.begin();
148 it != adj.end(); ++it) {
149 if (it->second.size() <= 1) continue;
150 std::vector<std::pair<int, T>> targets;
151 for (std::size_t k = 0; k < it->second.size(); ++k)
152 if (serviceSet.count(it->second[k].first)) targets.push_back(it->second[k]);
153 if (targets.size() < 2) continue;
154
155 T total = num_traits<T>::from_int(0);
156 for (std::size_t k = 0; k < targets.size(); ++k) total += targets[k].second;
157 if (num_abs(T(total - one)) > slack) continue;
158
160 p.forkNode = it->first;
161 for (std::size_t k = 0; k < targets.size(); ++k) {
162 p.branchNodes.push_back(targets[k].first);
163 p.probabilities.push_back(targets[k].second);
164 }
165
166 // Common join point: intersect the reachable sets of the alternatives,
167 // prefer a join node, else the smallest common successor.
168 std::set<int> common = detail::wf_reachable_stop(p.branchNodes[0], adj, joinSet);
169 for (std::size_t k = 1; k < p.branchNodes.size(); ++k) {
170 const std::set<int> r = detail::wf_reachable_stop(p.branchNodes[k], adj, joinSet);
171 std::set<int> inter;
172 std::set_intersection(common.begin(), common.end(), r.begin(), r.end(),
173 std::inserter(inter, inter.begin()));
174 common.swap(inter);
175 }
176 std::set<int> joinPoints;
177 std::set_intersection(common.begin(), common.end(), joinSet.begin(), joinSet.end(),
178 std::inserter(joinPoints, joinPoints.begin()));
179 if (!joinPoints.empty()) {
180 p.hasJoinNode = true;
181 p.joinNode = *joinPoints.begin();
182 } else if (!common.empty()) {
183 p.hasJoinNode = true;
184 p.joinNode = *common.begin();
185 }
186
187 patterns.push_back(p);
188 }
189 return patterns;
190}
191
192/** Probabilities sum to one within 1e-2 and every alternative is a fork successor. */
193template <class T>
194bool validate_branch_pattern(const BranchPattern<T>& pattern, const Matrix<T>& linkMatrix) {
195 detail::wf_check(linkMatrix);
196 const T one = num_traits<T>::from_int(1);
197 T total = num_traits<T>::from_int(0);
198 for (std::size_t k = 0; k < pattern.probabilities.size(); ++k) total += pattern.probabilities[k];
199 if (num_abs(T(total - one)) > num_traits<T>::from_rational(1, 100)) return false;
200 if (pattern.forkNode < 0) return false;
201
202 const std::map<int, std::vector<std::pair<int, T>>> adj = detail::wf_adjacency_prob(linkMatrix);
203 typename std::map<int, std::vector<std::pair<int, T>>>::const_iterator it =
204 adj.find(pattern.forkNode);
205 if (it == adj.end()) return false;
206 std::set<int> forkTargets;
207 for (std::size_t k = 0; k < it->second.size(); ++k) forkTargets.insert(it->second[k].first);
208 for (std::size_t k = 0; k < pattern.branchNodes.size(); ++k)
209 if (!forkTargets.count(pattern.branchNodes[k])) return false;
210 return true;
211}
212
213/**
214 * Shannon entropy of the branch probabilities, the same entropy normalized by
215 * log(n), the Gini coefficient of the probability vector, and the reciprocal
216 * of the largest probability.
217 */
218template <class T>
221 "calculate_branch_diversity requires transcendental arithmetic: the entropy of "
222 "the branch probabilities is a sum of p log p");
223 const std::vector<T>& probs = pattern.probabilities;
224 if (probs.size() < 2)
225 throw InputError(
226 "calculate_branch_diversity: needs at least two alternatives, the Gini coefficient "
227 "divides by (n - 1)");
228 const std::size_t n = probs.size();
229 const T zero = num_traits<T>::from_int(0);
230
232 for (std::size_t k = 0; k < n; ++k)
233 if (probs[k] > zero) d.entropy -= T(probs[k] * detail::num_log(probs[k]));
235 d.entropy / detail::num_log(num_traits<T>::from_int(static_cast<long>(n)));
236
237 std::vector<T> sorted(probs);
238 std::sort(sorted.begin(), sorted.end());
239 T sumProbs = zero;
240 for (std::size_t k = 0; k < n; ++k) sumProbs += probs[k];
241 T gini = zero;
242 for (std::size_t k = 0; k < n; ++k)
243 gini += num_traits<T>::from_int(static_cast<long>(2 * (k + 1)) -
244 static_cast<long>(n) - 1) *
245 sorted[k];
246 const T giniDen = T(num_traits<T>::from_int(static_cast<long>(n) - 1) * sumProbs);
247 gini /= giniDen;
248 d.gini = num_abs(gini);
249
250 T maxProb = zero;
251 for (std::size_t k = 0; k < n; ++k)
252 if (probs[k] > maxProb) maxProb = probs[k];
253 d.balance = num_traits<T>::from_int(1) / (maxProb == zero ? num_traits<T>::from_int(1) : maxProb);
254 return d;
255}
256
257/** Count, total, mean/max/min alternatives, and the mean entropy and balance. */
258template <class T>
259BranchStats<T> get_branch_stats(const std::vector<BranchPattern<T>>& patterns) {
261 "get_branch_stats requires transcendental arithmetic: it averages the entropy "
262 "returned by calculate_branch_diversity");
263 BranchStats<T> stats;
264 stats.numPatterns = patterns.size();
265 std::size_t total = 0;
266 for (std::size_t i = 0; i < patterns.size(); ++i) total += patterns[i].branchNodes.size();
267 stats.totalBranchNodes = total;
268 if (patterns.empty()) return stats;
269
270 stats.maxBranches = patterns[0].branchNodes.size();
271 stats.minBranches = patterns[0].branchNodes.size();
272 for (std::size_t i = 1; i < patterns.size(); ++i) {
273 const std::size_t sz = patterns[i].branchNodes.size();
274 if (sz > stats.maxBranches) stats.maxBranches = sz;
275 if (sz < stats.minBranches) stats.minBranches = sz;
276 }
277 const T np = num_traits<T>::from_int(static_cast<long>(patterns.size()));
278 stats.avgBranches = num_traits<T>::from_int(static_cast<long>(total)) / np;
279
280 T sumE = num_traits<T>::from_int(0);
281 T sumB = num_traits<T>::from_int(0);
282 for (std::size_t i = 0; i < patterns.size(); ++i) {
283 const BranchDiversity<T> d = calculate_branch_diversity(patterns[i]);
284 sumE += d.entropy;
285 sumB += d.balance;
286 }
287 stats.avgEntropy = sumE / np;
288 stats.avgBalance = sumB / np;
289 return stats;
290}
291
292/** The alternative with the largest probability. */
293template <class T>
296 if (pattern.branchNodes.empty() || pattern.probabilities.empty()) return r;
297 std::size_t best = 0;
298 for (std::size_t k = 1; k < pattern.probabilities.size(); ++k)
299 if (pattern.probabilities[k] > pattern.probabilities[best]) best = k;
300 r.valid = true;
301 r.node = pattern.branchNodes[best];
302 r.probability = pattern.probabilities[best];
303 return r;
304}
305
306/** The alternative with the smallest probability. */
307template <class T>
310 if (pattern.branchNodes.empty() || pattern.probabilities.empty()) return r;
311 std::size_t best = 0;
312 for (std::size_t k = 1; k < pattern.probabilities.size(); ++k)
313 if (pattern.probabilities[k] < pattern.probabilities[best]) best = k;
314 r.valid = true;
315 r.node = pattern.branchNodes[best];
316 r.probability = pattern.probabilities[best];
317 return r;
318}
319
320} // namespace wf
321} // namespace line
322
323#endif // LINE_API_WF_WF_BRANCH_DETECTOR_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
Dense matrix and non-owning view.
BranchAlternative< T > find_least_probable_branch(const BranchPattern< T > &pattern)
The alternative with the smallest probability.
BranchStats< T > get_branch_stats(const std::vector< BranchPattern< T > > &patterns)
Count, total, mean/max/min alternatives, and the mean entropy and balance.
BranchDiversity< T > calculate_branch_diversity(const BranchPattern< T > &pattern)
Shannon entropy of the branch probabilities, the same entropy normalized by log(n),...
std::vector< BranchPattern< T > > detect_branches(const Matrix< T > &linkMatrix, const std::vector< int > &serviceNodes, const std::vector< int > &joinNodes)
bool validate_branch_pattern(const BranchPattern< T > &pattern, const Matrix< T > &linkMatrix)
Probabilities sum to one within 1e-2 and every alternative is a fork successor.
BranchAlternative< T > find_most_probable_branch(const BranchPattern< T > &pattern)
The alternative with the largest probability.
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
One alternative of a branch: the node and its probability.
Mirrors the Java calculateBranchDiversity map.
Mirrors the Java BranchPattern.
std::vector< int > branchNodes
bool hasJoinNode
the Java Integer may be null
std::vector< T > probabilities
Mirrors the Java getBranchStats map.