LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
wf_sequence_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_SEQUENCE_DETECTOR_H
6#define LINE_API_WF_WF_SEQUENCE_DETECTOR_H
7
8/**
9 * @file
10 * @ingroup api_wf
11 * Sequence pattern detection in a workflow network.
12 *
13 * Templated port of jar/src/main/java/jline/api/wf/Wf_sequence_detector.java
14 * (no MATLAB counterpart exists, so the JAR is the reference). A sequence is a
15 * maximal chain of service nodes connected service-to-service; the chain is
16 * grown from an unused edge in both directions until no edge extends it, and
17 * the number of chains looked for is half the number of service nodes that
18 * appear exactly once among the service-to-service edges, i.e. half the number
19 * of chain endpoints.
20 *
21 * detect_sequences, validate_sequence and get_sequence_stats are the three
22 * public methods of the Java class, kept 1:1.
23 *
24 * REFERENCE DEFECT (JAR), NOT reproduced: validateSequence builds a
25 * HashSet<Pair<Integer,Integer>> of the edges and asks whether each consecutive
26 * pair of the chain is in it, but jline.util.Pair implements neither equals nor
27 * hashCode, so contains() falls back to reference identity and NEVER matches.
28 * The Java method therefore returns false for every sequence of length >= 2,
29 * including the chains its own detectSequences just produced (verified against
30 * common/jline.jar: validateSequence([2,3,4]) on 1->2->3->4->9 returns false).
31 * Fixing Pair is outside this port, so validate_sequence here implements the
32 * intended semantics - std::pair has value equality - and returns true for a
33 * genuinely connected chain. This is the one place where the port deliberately
34 * disagrees with the reference.
35 *
36 * Everything here is graph traversal and counting; the only arithmetic is the
37 * mean chain length, one division. Finite field computation, instantiates at
38 * exact arithmetic, no transcendental gate: for T = Rational the mean length
39 * is an exact rational and the structural counts are exact integers.
40 */
41
42#include <algorithm>
43#include <cstddef>
44#include <map>
45#include <set>
46#include <vector>
47
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace wf {
55
56/** Mirrors the Java getSequenceStats map. */
57template <class T>
59 std::size_t numSequences = 0;
60 std::size_t totalNodes = 0;
62 std::size_t maxLength = 0;
63 std::size_t minLength = 0;
64};
65
66namespace detail {
67
68/**
69 * Grow one chain out of the first remaining edge and delete the edges it used,
70 * as Java's buildSequenceChain does (including the fact that the search for an
71 * extension starts at index 1, edge 0 being the seed).
72 */
73inline std::vector<int> wf_build_sequence_chain(std::vector<std::pair<int, int>>& connections) {
74 std::vector<int> sequence;
75 if (connections.empty()) return sequence;
76
77 std::vector<std::size_t> used;
78 int first = connections[0].first;
79 int last = connections[0].second;
80 sequence.push_back(first);
81 sequence.push_back(last);
82 used.push_back(0);
83
84 bool foundExtension = true;
85 while (foundExtension) {
86 foundExtension = false;
87 const std::size_t currentSize = sequence.size();
88 for (std::size_t i = 1; i < connections.size(); ++i) {
89 if (std::find(used.begin(), used.end(), i) != used.end()) continue;
90 const int start = connections[i].first;
91 const int end = connections[i].second;
92 if (start == last) {
93 last = end;
94 sequence.push_back(end);
95 used.push_back(i);
96 foundExtension = true;
97 } else if (end == first) {
98 first = start;
99 sequence.insert(sequence.begin(), start);
100 used.push_back(i);
101 foundExtension = true;
102 }
103 }
104 foundExtension = foundExtension && sequence.size() > currentSize;
105 }
106
107 std::sort(used.begin(), used.end(), std::greater<std::size_t>());
108 for (std::size_t idx : used) connections.erase(connections.begin() + static_cast<long>(idx));
109 return sequence;
110}
111
112} // namespace detail
113
114/**
115 * @param linkMatrix (nedges x 3) edge list
116 * @param serviceNodes ids of the service nodes
117 * @return the detected chains, each as an ordered list of node ids
118 */
119template <class T>
120std::vector<std::vector<int>> detect_sequences(const Matrix<T>& linkMatrix,
121 const std::vector<int>& serviceNodes) {
122 detail::wf_check(linkMatrix);
123 std::vector<std::vector<int>> chains;
124 const std::set<int> serviceSet(serviceNodes.begin(), serviceNodes.end());
125
126 std::vector<std::pair<int, int>> connections;
127 for (std::size_t i = 0; i < linkMatrix.rows(); ++i) {
128 const int s = detail::wf_id(linkMatrix, i, 0);
129 const int e = detail::wf_id(linkMatrix, i, 1);
130 if (serviceSet.count(s) && serviceSet.count(e))
131 connections.push_back(std::make_pair(s, e));
132 }
133 if (connections.empty()) return chains;
134
135 std::map<int, std::size_t> counts;
136 for (std::size_t i = 0; i < connections.size(); ++i) {
137 counts[connections[i].first] += 1;
138 counts[connections[i].second] += 1;
139 }
140 std::size_t countOnce = 0;
141 for (std::map<int, std::size_t>::const_iterator it = counts.begin(); it != counts.end(); ++it)
142 if (it->second == 1) ++countOnce;
143 const std::size_t numSequences = countOnce / 2;
144
145 for (std::size_t seq = 0; seq < numSequences; ++seq) {
146 if (connections.empty()) break;
147 std::vector<int> chain = detail::wf_build_sequence_chain(connections);
148 if (!chain.empty()) chains.push_back(chain);
149 }
150 return chains;
151}
152
153/** Every consecutive pair of the chain must be an edge of the workflow. */
154template <class T>
155bool validate_sequence(const std::vector<int>& sequence, const Matrix<T>& linkMatrix) {
156 detail::wf_check(linkMatrix);
157 if (sequence.size() < 2) return false;
158 std::set<std::pair<int, int>> edges;
159 for (std::size_t i = 0; i < linkMatrix.rows(); ++i)
160 edges.insert(std::make_pair(detail::wf_id(linkMatrix, i, 0),
161 detail::wf_id(linkMatrix, i, 1)));
162 for (std::size_t i = 0; i + 1 < sequence.size(); ++i)
163 if (!edges.count(std::make_pair(sequence[i], sequence[i + 1]))) return false;
164 return true;
165}
166
167/** Count, total, mean, maximum and minimum chain length. */
168template <class T>
169SequenceStats<T> get_sequence_stats(const std::vector<std::vector<int>>& sequences) {
170 SequenceStats<T> stats;
171 stats.numSequences = sequences.size();
172 std::size_t total = 0;
173 for (std::size_t i = 0; i < sequences.size(); ++i) total += sequences[i].size();
174 stats.totalNodes = total;
175 if (sequences.empty()) return stats;
176
177 stats.avgLength = num_traits<T>::from_int(static_cast<long>(total)) /
178 num_traits<T>::from_int(static_cast<long>(sequences.size()));
179 stats.maxLength = sequences[0].size();
180 stats.minLength = sequences[0].size();
181 for (std::size_t i = 1; i < sequences.size(); ++i) {
182 if (sequences[i].size() > stats.maxLength) stats.maxLength = sequences[i].size();
183 if (sequences[i].size() < stats.minLength) stats.minLength = sequences[i].size();
184 }
185 return stats;
186}
187
188} // namespace wf
189} // namespace line
190
191#endif // LINE_API_WF_WF_SEQUENCE_DETECTOR_H
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
bool validate_sequence(const std::vector< int > &sequence, const Matrix< T > &linkMatrix)
Every consecutive pair of the chain must be an edge of the workflow.
std::vector< std::vector< int > > detect_sequences(const Matrix< T > &linkMatrix, const std::vector< int > &serviceNodes)
SequenceStats< T > get_sequence_stats(const std::vector< std::vector< int > > &sequences)
Count, total, mean, maximum and minimum chain length.
Number-type abstraction for the templated API port.
Mirrors the Java getSequenceStats map.