LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
workflow_manager.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_WORKFLOW_MANAGER_H
6#define LINE_API_WF_WORKFLOW_MANAGER_H
7
8/**
9 * @file
10 * @ingroup api_wf
11 * The workflow facade: the port of
12 * jar/src/main/java/jline/api/wf/WorkflowManager.java.
13 *
14 * It composes `wf_analyzer.h` and `wf_auto_integration.h` into the things a
15 * user asks for -- one analysis object, a complexity report, efficiency
16 * metrics, a benchmark table and three export formats. There is no new
17 * queueing content here; what must be faithful is the arithmetic of the scores
18 * and the exact bytes of the exports, since both are what a caller compares
19 * across codebases.
20 *
21 * THREE DEPARTURES FROM THE JAVA, each named:
22 *
23 * 1. `factorial` is computed in DOUBLE, not int. The Java's `int factorial(int)`
24 * silently overflows at n = 13 and goes NEGATIVE at n = 17, so a workflow
25 * with a wide fork would report a negative parallel complexity and a
26 * complexity score that DROPS as the model grows. That is a defect, not a
27 * convention, so it is not reproduced; the port saturates at infinity in
28 * double instead, which keeps the score monotone. Below 13 branches the two
29 * agree exactly.
30 *
31 * 2. `benchmarkSolvers` takes a CALLER-SUPPLIED runner. The C++ solvers are
32 * free functions over a NetworkStruct rather than a runtime polymorphic
33 * family, and `api/` does not depend on `solvers/` in this tree. Everything
34 * the Java method actually does -- time the call, record success, aggregate
35 * the queue lengths, turn a thrown exception into a failed row -- is here;
36 * only the construction of the solver is injected.
37 *
38 * 3. `validateWorkflow` reads the node count and the routing off the
39 * NetworkStruct rather than the Network object, which is where the port
40 * keeps that information.
41 *
42 * The Java's `calculateLoopEfficiency` ignores its own `linkMatrix` argument
43 * and returns the constant 0.5, which its comment admits ("Simplified"). It is
44 * ported as written, because changing it would change every metric a caller
45 * has recorded; the constant is documented at the function instead.
46 *
47 * ARITHMETIC: field, plus what the branch entropy needs.
48 */
49
50#include <algorithm>
51#include <chrono>
52#include <cstddef>
53#include <functional>
54#include <exception>
55#include <sstream>
56#include <string>
57#include <utility>
58#include <vector>
59
63#include "line/num/number.h"
64#include "line/util/error.h"
65#include "line/util/matrix.h"
66
67namespace line {
68namespace wf {
69
70/** The four efficiency scores plus the two headline numbers. */
72 double complexityReduction = 0.0;
73 double solverConfidence = 0.0;
74 double sequenceEfficiency = 1.0;
75 double parallelEfficiency = 1.0;
76 double loopEfficiency = 1.0;
77 double branchEfficiency = 1.0;
78};
79
80/** Everything analyze_workflow_full returns, i.e. WorkflowAnalysisResult. */
81template <class T>
88
89/** Per-pattern-family complexity, one entry of the report's patternComplexity. */
91 std::size_t count = 0;
92 std::size_t totalNodes = 0; ///< totalBranches for the branch family
93 double complexity = 0.0;
94};
95
96/** The complexity report, one band per pattern family plus the overall score. */
97template <class T>
105
106/** One row of the benchmark table. */
108 bool success = false;
109 double solveTimeMs = 0.0;
110 bool hasResults = false;
111 double totalQueueLength = 0.0;
112 double avgQueueLength = 0.0;
113 std::string error;
114};
115
116/** What validate_workflow reports. */
118 bool isValid = false;
119 std::vector<std::string> issues;
120 std::size_t nodeCount = 0;
121 bool hasRouting = false;
122};
123
124/** The export formats the facade offers. */
125enum class WfExportFormat { Json = 0, Csv, Summary };
126
127namespace detail {
128
129/**
130 * n! in double.
131 *
132 * The Java computes this in int and overflows at 13, going negative at 17; see
133 * the header note. Double saturates to infinity instead, so the score stays
134 * monotone in the fork width.
135 */
136inline double wf_factorial(std::size_t n) {
137 double f = 1.0;
138 for (std::size_t k = 2; k <= n; ++k) f *= static_cast<double>(k);
139 return f;
140}
141
142} // namespace detail
143
144/** avg sequence length capped at 5, the reference's saturation point. */
145inline double calculate_sequence_efficiency(const std::vector<std::vector<int>>& sequences) {
146 if (sequences.empty()) return 1.0;
147 std::size_t total = 0;
148 for (std::size_t i = 0; i < sequences.size(); ++i) total += sequences[i].size();
149 const double avg = static_cast<double>(total) / static_cast<double>(sequences.size());
150 return std::min(1.0, avg / 5.0);
151}
152
153/** Efficiency falls linearly past two-way parallelism, floored at 0.1. */
154inline double calculate_parallel_efficiency(const std::vector<std::vector<int>>& parallels) {
155 if (parallels.empty()) return 1.0;
156 std::size_t total = 0;
157 for (std::size_t i = 0; i < parallels.size(); ++i) total += parallels[i].size();
158 const double avg = static_cast<double>(total) / static_cast<double>(parallels.size());
159 return std::max(0.1, 1.0 - (avg - 2.0) / 10.0);
160}
161
162/**
163 * The reference's placeholder: any loop at all costs half the efficiency.
164 *
165 * It assumes a loop probability of 0.5 and never reads the link matrix, which
166 * its own comment calls "Simplified". Ported as written -- a caller comparing
167 * the two codebases compares this number.
168 */
169inline double calculate_loop_efficiency(const std::vector<int>& loops) {
170 if (loops.empty()) return 1.0;
171 return 1.0 - 0.5;
172}
173
174/** The mean NORMALIZED entropy over the branches, i.e. how evenly they split. */
175template <class T>
176double calculate_branch_efficiency(const std::vector<BranchPattern<T>>& branches) {
177 if (branches.empty()) return 1.0;
178 double sum = 0.0;
179 for (std::size_t i = 0; i < branches.size(); ++i)
180 sum += num_traits<T>::to_double(calculate_branch_diversity(branches[i]).normalizedEntropy);
181 return sum / static_cast<double>(branches.size());
182}
183
184/** The weighted size-plus-pattern score behind the complexity level. */
185template <class T>
186double calculate_complexity_score(std::size_t nodes, std::size_t links,
187 const DetectedPatterns<T>& p) {
188 double score = static_cast<double>(nodes) + static_cast<double>(links) * 0.5;
189 for (std::size_t i = 0; i < p.sequences.size(); ++i) {
190 const double n = static_cast<double>(p.sequences[i].size());
191 score += n * n * 0.1;
192 }
193 for (std::size_t i = 0; i < p.parallels.size(); ++i)
194 score += detail::wf_factorial(p.parallels[i].size()) * 0.2;
195 score += static_cast<double>(p.loops.size()) * 10.0;
196 for (std::size_t i = 0; i < p.branches.size(); ++i)
197 score += static_cast<double>(p.branches[i].branchNodes.size()) * 2.0;
198 return score;
199}
200
201/** The six metrics of the analysis result. */
202template <class T>
214
215/** The facade's headline call: analysis, recommendation, insights, metrics. */
216template <class T>
226
227/** The patterns alone. */
228template <class T>
232
233/** The recommendation strings alone. */
234template <class T>
238
239/** The complexity report, with the reference's four bands on the score. */
240template <class T>
245 r.originalMetrics = a.statistics.originalComplexity;
246 r.optimizedMetrics = a.statistics.optimizedComplexity;
247
248 r.sequences.count = p.sequences.size();
249 for (std::size_t i = 0; i < p.sequences.size(); ++i) {
250 const double n = static_cast<double>(p.sequences[i].size());
251 r.sequences.totalNodes += p.sequences[i].size();
252 r.sequences.complexity += n * n;
253 }
254 r.parallels.count = p.parallels.size();
255 for (std::size_t i = 0; i < p.parallels.size(); ++i) {
256 r.parallels.totalNodes += p.parallels[i].size();
257 r.parallels.complexity += detail::wf_factorial(p.parallels[i].size());
258 }
259 r.loops.count = p.loops.size();
260 r.loops.complexity = static_cast<double>(p.loops.size()) * 10.0;
261 r.branches.count = p.branches.size();
262 for (std::size_t i = 0; i < p.branches.size(); ++i) {
263 r.branches.totalNodes += p.branches[i].branchNodes.size();
264 r.branches.complexity += static_cast<double>(p.branches[i].branchNodes.size());
265 }
266
268 a.statistics.originalComplexity.totalNodes, a.statistics.originalComplexity.totalLinks, p);
269 if (r.overallComplexityScore < 10.0) r.complexityLevel = "LOW";
270 else if (r.overallComplexityScore < 50.0) r.complexityLevel = "MEDIUM";
271 else if (r.overallComplexityScore < 100.0) r.complexityLevel = "HIGH";
272 else r.complexityLevel = "VERY_HIGH";
273 return r;
274}
275
276/**
277 * Time each solver and aggregate its queue lengths.
278 *
279 * @param solvers the solvers to try, in order
280 * @param runner runs one solver and returns its QN; it may throw, and a throw
281 * becomes a failed row exactly as the Java's catch does
282 */
283template <class T>
284std::vector<std::pair<WfSolver, BenchmarkRow>> benchmark_solvers(
285 const std::vector<WfSolver>& solvers,
286 const std::function<Matrix<T>(WfSolver)>& runner) {
287 std::vector<std::pair<WfSolver, BenchmarkRow>> out;
288 for (std::size_t i = 0; i < solvers.size(); ++i) {
289 BenchmarkRow row;
290 const std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now();
291 try {
292 const Matrix<T> QN = runner(solvers[i]);
293 const std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
294 row.success = true;
295 row.solveTimeMs =
296 std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(t1 - t0)
297 .count();
298 const std::size_t n = QN.rows() * QN.cols();
299 row.hasResults = (n > 0);
300 double sum = 0.0;
301 for (std::size_t rr = 0; rr < QN.rows(); ++rr)
302 for (std::size_t cc = 0; cc < QN.cols(); ++cc)
303 sum += num_traits<T>::to_double(QN(rr, cc));
304 row.totalQueueLength = sum;
305 row.avgQueueLength = (n > 0) ? sum / static_cast<double>(n) : 0.0;
306 } catch (const std::exception& e) {
307 row.success = false;
308 const char* what = e.what();
309 row.error = (what != nullptr && what[0] != '\0') ? what : "Unknown error";
310 }
311 out.push_back(std::make_pair(solvers[i], row));
312 }
313 return out;
314}
315
316/** The reference's default benchmark set. */
317inline std::vector<WfSolver> default_benchmark_solvers() {
318 std::vector<WfSolver> v;
319 v.push_back(WfSolver::MVA);
320 v.push_back(WfSolver::NC);
321 v.push_back(WfSolver::SSA);
322 v.push_back(WfSolver::FLUID);
323 return v;
324}
325
326/** The model is well formed and both analyses validate. */
327template <class T>
330 v.nodeCount = sn.nodes.size();
331 v.hasRouting = (sn.rtnodes.rows() > 0 && sn.rtnodes.cols() > 0);
332 if (sn.nodes.empty()) v.issues.push_back("Network has no nodes");
334 v.issues.push_back("Workflow analysis validation failed");
336 v.issues.push_back("Workflow-enhanced solver selection validation failed");
337 v.isValid = v.issues.empty();
338 return v;
339}
340
341namespace detail {
342
343/** The Java's `String.format("%.2f", x)`. */
344inline std::string fixed2(double x) {
345 std::ostringstream s;
346 s.setf(std::ios::fixed);
347 s.precision(2);
348 s << x;
349 return s.str();
350}
351
352/** The Java's default double rendering, which drops a trailing `.0` never. */
353inline std::string plain(double x) {
354 std::ostringstream s;
355 s << x;
356 return s.str();
357}
358
359} // namespace detail
360
361/**
362 * Render the analysis.
363 *
364 * The three layouts are byte-for-byte the reference's, including the header
365 * rule, the blank lines and the one-based numbering of the reasoning list: a
366 * consumer that parses this text is parsing a contract.
367 */
368template <class T>
371 const DetectedPatterns<T>& p = a.patternAnalysis.detectedPatterns;
372 const std::string rec = wf_solver_name(a.solverRecommendation.recommendedSolver);
373 std::ostringstream o;
374 if (format == WfExportFormat::Json) {
375 o << "{\n";
376 o << " \"solver_recommendation\": \"" << rec << "\",\n";
377 o << " \"confidence\": " << detail::plain(a.solverRecommendation.confidence) << ",\n";
378 o << " \"patterns\": {\n";
379 o << " \"sequences\": " << p.sequences.size() << ",\n";
380 o << " \"parallels\": " << p.parallels.size() << ",\n";
381 o << " \"loops\": " << p.loops.size() << ",\n";
382 o << " \"branches\": " << p.branches.size() << "\n";
383 o << " }\n";
384 o << "}";
385 } else if (format == WfExportFormat::Csv) {
386 o << "Metric,Value\n";
387 o << "Recommended Solver," << rec << "\n";
388 o << "Confidence," << detail::plain(a.solverRecommendation.confidence) << "\n";
389 o << "Sequences," << p.sequences.size() << "\n";
390 o << "Parallels," << p.parallels.size() << "\n";
391 o << "Loops," << p.loops.size() << "\n";
392 o << "Branches," << p.branches.size() << "\n";
393 } else {
394 o << "=== Workflow Analysis Summary ===\n\n";
395 o << "Recommended Solver: " << rec << "\n";
396 o << "Confidence: " << detail::fixed2(a.solverRecommendation.confidence) << "\n\n";
397 o << "Detected Patterns:\n";
398 o << "- Sequences: " << p.sequences.size() << "\n";
399 o << "- Parallels: " << p.parallels.size() << "\n";
400 o << "- Loops: " << p.loops.size() << "\n";
401 o << "- Branches: " << p.branches.size() << "\n\n";
402 o << "Reasoning:\n";
403 for (std::size_t i = 0; i < a.solverRecommendation.reasoning.size(); ++i)
404 o << (i + 1) << ". " << a.solverRecommendation.reasoning[i] << "\n";
405 }
406 return o.str();
407}
408
409/** The reference's one-call summary. */
410template <class T>
414
415/** The chosen solver without the rest of the report. */
416template <class T>
420
421} // namespace wf
422} // namespace line
423
424#endif // LINE_API_WF_WORKFLOW_MANAGER_H
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
WorkflowAnalysis< T > analyze_workflow(const WorkflowRepresentation< T > &w)
Detect, collapse, and report.
ExtendedSolverRecommendation< T > recommend_solver_with_workflow_analysis(const qn::NetworkStruct< T > &sn)
The entry point: analyse the workflow, then let it amend the base choice.
bool validate_workflow_enhancement(const qn::NetworkStruct< T > &sn)
The reference's self-check: a usable recommendation over a valid analysis.
WorkflowAnalysisResult< T > analyze_workflow_full(const qn::NetworkStruct< T > &sn)
The facade's headline call: analysis, recommendation, insights, metrics.
std::string wf_solver_name(WfSolver s)
The reference's own spelling of each choice.
bool validate_analysis(const WorkflowAnalysis< T > &a)
The collapsed workflow is consistent and every detected pattern validates.
std::vector< std::string > get_optimization_recommendations(const WorkflowAnalysis< T > &a)
The reference's recommendation strings, in its order.
double calculate_complexity_score(std::size_t nodes, std::size_t links, const DetectedPatterns< T > &p)
The weighted size-plus-pattern score behind the complexity level.
WfSolver
The solvers the reference chooses among.
double calculate_sequence_efficiency(const std::vector< std::vector< int > > &sequences)
avg sequence length capped at 5, the reference's saturation point.
std::string quick_analysis(const qn::NetworkStruct< T > &sn)
The reference's one-call summary.
WfExportFormat
The export formats the facade offers.
double calculate_parallel_efficiency(const std::vector< std::vector< int > > &parallels)
Efficiency falls linearly past two-way parallelism, floored at 0.1.
std::vector< std::pair< WfSolver, BenchmarkRow > > benchmark_solvers(const std::vector< WfSolver > &solvers, const std::function< Matrix< T >(WfSolver)> &runner)
Time each solver and aggregate its queue lengths.
WfSolver create_optimal_solver(const qn::NetworkStruct< T > &sn)
The chosen solver alone, i.e.
std::vector< WfSolver > default_benchmark_solvers()
The reference's default benchmark set.
DetectedPatterns< T > get_pattern_analysis(const qn::NetworkStruct< T > &sn)
The patterns alone.
ComplexityReport< T > generate_complexity_report(const qn::NetworkStruct< T > &sn)
The complexity report, with the reference's four bands on the score.
BranchDiversity< T > calculate_branch_diversity(const BranchPattern< T > &pattern)
Shannon entropy of the branch probabilities, the same entropy normalized by log(n),...
WorkflowValidation validate_workflow(const qn::NetworkStruct< T > &sn)
The model is well formed and both analyses validate.
OptimizationInsights get_optimization_insights(const qn::NetworkStruct< T > &sn)
All three advisory blocks for one model.
double calculate_branch_efficiency(const std::vector< BranchPattern< T > > &branches)
The mean NORMALIZED entropy over the branches, i.e.
std::string export_analysis(const WorkflowAnalysisResult< T > &a, WfExportFormat format=WfExportFormat::Summary)
Render the analysis.
double calculate_loop_efficiency(const std::vector< int > &loops)
The reference's placeholder: any loop at all costs half the efficiency.
WfSolver get_optimal_solver(const qn::NetworkStruct< T > &sn)
The chosen solver without the rest of the report.
WorkflowRepresentation< T > wf_from_struct(const qn::NetworkStruct< T > &sn)
Build a workflow representation from a NetworkStruct.
std::vector< std::string > get_workflow_recommendations(const qn::NetworkStruct< T > &sn)
The recommendation strings alone.
WorkflowPerformanceMetrics calculate_performance_metrics(const WorkflowAnalysis< T > &a, const ExtendedSolverRecommendation< T > &r)
The six metrics of the analysis result.
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
One row of the benchmark table.
Mirrors the Java BranchPattern.
The complexity report, one band per pattern family plus the overall score.
std::string complexityLevel
LOW, MEDIUM, HIGH or VERY_HIGH.
PatternComplexityEntry branches
PatternComplexityEntry parallels
PatternComplexityEntry sequences
PatternComplexityEntry loops
WorkflowComplexity< T > originalMetrics
WorkflowComplexity< T > optimizedMetrics
Everything the four detectors found.
Definition wf_analyzer.h:71
std::vector< int > loops
Definition wf_analyzer.h:74
std::vector< std::vector< int > > sequences
Definition wf_analyzer.h:72
std::vector< BranchPattern< T > > branches
Definition wf_analyzer.h:75
std::vector< std::vector< int > > parallels
Definition wf_analyzer.h:73
What the recommendation returns: the choice and why.
The advisory text the reference's getOptimizationInsights assembles.
Per-pattern-family complexity, one entry of the report's patternComplexity.
std::size_t totalNodes
totalBranches for the branch family
Everything analyze_workflow_full returns, i.e.
OptimizationInsights optimizationInsights
WorkflowPerformanceMetrics performanceMetrics
ExtendedSolverRecommendation< T > solverRecommendation
WorkflowAnalysis< T > patternAnalysis
What analyze_workflow returns.
WorkflowStatistics< T > statistics
DetectedPatterns< T > detectedPatterns
The reference's complexity map, for either the original or the collapsed graph.
Definition wf_analyzer.h:80
The four efficiency scores plus the two headline numbers.
What validate_workflow reports.
std::vector< std::string > issues
The workflow analyzer: detect every pattern, collapse them, report the two complexities and the recom...
Workflow-aware solver recommendation: the port of jar/src/main/java/jline/api/wf/Wf_auto_integration....