LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
wf_auto_integration.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_AUTO_INTEGRATION_H
6#define LINE_API_WF_WF_AUTO_INTEGRATION_H
7
8/**
9 * @file
10 * @ingroup api_wf
11 * Workflow-aware solver recommendation: the port of
12 * jar/src/main/java/jline/api/wf/Wf_auto_integration.java.
13 *
14 * The routine takes the pattern analysis of `wf_analyzer.h`, turns it into a
15 * flat feature vector, and lets those features amend the recommendation the
16 * AUTO heuristic would give from the model alone. It is a HEURISTIC, not an
17 * analysis: the numbers below (0.7 seed confidence, the 5-way parallelism
18 * threshold, the 0.8 and 0.5 loop-probability bands, the 1.5 and 0.5 entropy
19 * bands, the 50-node size gate) are the reference's constants and are ported
20 * verbatim, because a caller comparing the two codebases compares the ADVICE,
21 * and advice that differs by a tuned constant is a different answer.
22 *
23 * TWO DELIBERATE DEPARTURES FROM THE JAVA, both structural rather than
24 * behavioural:
25 *
26 * 1. The feature bag is a TYPED STRUCT, not a `Map<String, Object>`. The Java
27 * reads each feature back with an `instanceof` test and substitutes 0 when
28 * the cast fails, so a mistyped or missing key degrades silently into a
29 * neutral value. Every one of those defaults is reproduced here by the
30 * struct's initializer, and the `present` flags below stand in for the
31 * Java's key-absent case -- the arms guarded by `!patterns.X.isEmpty()`
32 * never populate their keys otherwise.
33 *
34 * 2. `createOptimalSolver` returns a CHOICE, not a constructed solver. The C++
35 * solvers are free functions over a NetworkStruct rather than a runtime
36 * polymorphic family, so there is no `NetworkSolver` to hand back; the
37 * caller dispatches on the enum. Returning the choice also keeps `api/` from
38 * depending on `solvers/`, which is the tree's layering.
39 *
40 * The JAR reaches `branchNodes` by REFLECTION in generatePatternInsights, which
41 * is a workaround for its own generics and silently yields 0 on any exception;
42 * here it is a plain member read, so the insight fires when it should.
43 *
44 * ARITHMETIC: field, plus what the branch entropy needs (gated in
45 * wf_branch_detector.h).
46 */
47
48#include <algorithm>
49#include <cmath>
50#include <cstddef>
51#include <string>
52#include <vector>
53
56#include "line/num/number.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace wf {
61
62/** The solvers the reference chooses among. */
63enum class WfSolver { MVA = 0, NC, SSA, FLUID, JMT, CTMC, AUTO };
64
65/** The reference's own spelling of each choice. */
66inline std::string wf_solver_name(WfSolver s) {
67 switch (s) {
68 case WfSolver::MVA: return "MVA";
69 case WfSolver::NC: return "NC";
70 case WfSolver::SSA: return "SSA";
71 case WfSolver::FLUID: return "FLUID";
72 case WfSolver::JMT: return "JMT";
73 case WfSolver::CTMC: return "CTMC";
74 default: return "AUTO";
75 }
76}
77
78/**
79 * The flat feature vector the recommendation reads.
80 *
81 * The four `has*` flags decide which of the remaining groups carry meaning;
82 * the rest keep the Java's zero defaults so an unpopulated group is neutral.
83 */
84template <class T>
103
104/** What the recommendation returns: the choice and why. */
105template <class T>
108 double confidence = 0.0;
109 std::vector<std::string> reasoning;
111 std::vector<WfSolver> alternativeSolvers; ///< at most three, the reference's cap
112};
113
114/** The four model facts the base recommendation reads, i.e. ModelAnalyzer. */
115template <class T>
117 bool hasProductForm = false;
118 bool hasSingleChain = false;
119 bool hasMultiChain = false;
120 long totalJobs = 0;
121 double avgJobsPerChain = 0.0;
122};
123
124/**
125 * Read the base facts off a NetworkStruct.
126 *
127 * The job total SKIPS the infinite entries, which is how the Java avoids
128 * `(int) Double.POSITIVE_INFINITY` becoming MAX_VALUE and swamping the count:
129 * an open class contributes nothing to a population-based threshold.
130 */
131template <class T>
134 f.hasProductForm = sn.has_product_form();
135 f.hasSingleChain = (sn.nchains == 1);
136 f.hasMultiChain = (sn.nchains > 1);
137 const std::vector<double> N = sn.njobs();
138 for (std::size_t i = 0; i < N.size(); ++i)
139 if (!std::isinf(N[i]) && !std::isnan(N[i])) f.totalJobs += static_cast<long>(N[i]);
141 (sn.nchains > 0) ? static_cast<double>(f.totalJobs) / static_cast<double>(sn.nchains) : 0.0;
142 return f;
143}
144
145/** The AUTO heuristic before the workflow features amend it. */
146template <class T>
148 if (f.hasSingleChain) return WfSolver::NC;
149 if (f.hasMultiChain && f.hasProductForm && f.totalJobs < 10) return WfSolver::NC;
150 if (f.hasMultiChain && f.hasProductForm && f.avgJobsPerChain < 30) return WfSolver::MVA;
151 if (f.hasMultiChain && f.avgJobsPerChain > 30) return WfSolver::FLUID;
152 return WfSolver::MVA;
153}
154
155/** Flatten the analysis into the feature vector the heuristic reads. */
156template <class T>
160 f.hasSequencePatterns = !p.sequences.empty();
161 f.hasParallelPatterns = !p.parallels.empty();
162 f.hasLoopPatterns = !p.loops.empty();
163 f.hasBranchPatterns = !p.branches.empty();
164 f.numSequences = p.sequences.size();
165 f.numParallels = p.parallels.size();
166 f.numLoops = p.loops.size();
167 f.numBranches = p.branches.size();
168
169 f.originalNodeCount = a.statistics.originalComplexity.totalNodes;
170 f.originalLinkCount = a.statistics.originalComplexity.totalLinks;
171 f.optimizedNodeCount = a.statistics.optimizedComplexity.totalNodes;
172 f.optimizedLinkCount = a.statistics.optimizedComplexity.totalLinks;
173
174 if (f.hasSequencePatterns) {
175 T total = num_traits<T>::from_int(0);
176 std::size_t mx = 0;
177 for (std::size_t i = 0; i < p.sequences.size(); ++i) {
178 total += num_traits<T>::from_int(static_cast<int>(p.sequences[i].size()));
179 mx = std::max(mx, p.sequences[i].size());
180 }
181 f.avgSequenceLength = total / num_traits<T>::from_int(static_cast<int>(p.sequences.size()));
182 f.maxSequenceLength = mx;
183 }
184 if (f.hasParallelPatterns) {
185 T total = num_traits<T>::from_int(0);
186 std::size_t mx = 0;
187 for (std::size_t i = 0; i < p.parallels.size(); ++i) {
188 total += num_traits<T>::from_int(static_cast<int>(p.parallels[i].size()));
189 mx = std::max(mx, p.parallels[i].size());
190 }
191 f.avgParallelism = total / num_traits<T>::from_int(static_cast<int>(p.parallels.size()));
192 f.maxParallelism = mx;
193 }
194 if (f.hasLoopPatterns) {
195 f.avgLoopProbability = a.statistics.loopStats.avgLoopProbability;
196 f.maxLoopProbability = a.statistics.loopStats.maxLoopProbability;
197 }
198 if (f.hasBranchPatterns) {
199 f.avgBranches = a.statistics.branchStats.avgBranches;
200 f.maxBranches = a.statistics.branchStats.maxBranches;
201 T total = num_traits<T>::from_int(0);
202 for (std::size_t i = 0; i < p.branches.size(); ++i)
203 total += calculate_branch_diversity(p.branches[i]).entropy;
204 f.avgBranchEntropy = total / num_traits<T>::from_int(static_cast<int>(p.branches.size()));
205 }
206 return f;
207}
208
209namespace detail {
210
211/** Append a solver unless it is the recommendation or already listed. */
212inline void push_alternative(std::vector<WfSolver>* alt, WfSolver rec, WfSolver s) {
213 if (s == rec) return;
214 for (std::size_t i = 0; i < alt->size(); ++i)
215 if ((*alt)[i] == s) return;
216 alt->push_back(s);
217}
218
219} // namespace detail
220
221/**
222 * Amend the base recommendation with what the workflow analysis found.
223 *
224 * The reasoning strings are user-facing and reproduced verbatim, so a caller
225 * grepping them sees the same text from either codebase.
226 */
227template <class T>
229 WfSolver base, const WorkflowFeatures<T>& f, const WorkflowAnalysis<T>& a) {
231 r.workflowFeatures = f;
232 WfSolver rec = base;
233 double confidence = 0.7;
234
235 if (f.hasSequencePatterns) {
236 r.reasoning.push_back("Detected sequence patterns - suitable for analytical methods");
237 confidence += 0.1;
238 if (f.maxSequenceLength > 10) {
239 r.reasoning.push_back("Long sequences detected - consider FLUID approximation");
240 if (rec == WfSolver::MVA) r.alternativeSolvers.push_back(WfSolver::FLUID);
241 }
242 }
243 if (f.hasParallelPatterns) {
244 r.reasoning.push_back("Detected parallel patterns - fork-join structures present");
245 if (f.maxParallelism > 5) {
246 r.reasoning.push_back(
247 "High parallelism detected - exact methods may be computationally expensive");
248 if (rec == WfSolver::NC || rec == WfSolver::MVA) {
249 rec = WfSolver::SSA;
250 r.reasoning.push_back("Switching to SSA for high-parallelism workflow");
251 }
253 } else {
254 confidence += 0.05;
255 }
256 }
257 if (f.hasLoopPatterns) {
258 const double avgLoop = num_traits<T>::to_double(f.avgLoopProbability);
259 const double maxLoop = num_traits<T>::to_double(f.maxLoopProbability);
260 r.reasoning.push_back("Detected loop patterns with avg probability " +
261 std::to_string(avgLoop));
262 if (maxLoop > 0.8) {
263 r.reasoning.push_back(
264 "High loop probability detected - may cause numerical instability");
265 confidence -= 0.1;
266 if (rec == WfSolver::NC || rec == WfSolver::MVA) {
269 }
270 } else if (maxLoop > 0.5) {
271 r.reasoning.push_back("Moderate loop probability - analytical methods suitable");
272 confidence += 0.05;
273 }
274 }
275 if (f.hasBranchPatterns) {
276 const double avgEntropy = num_traits<T>::to_double(f.avgBranchEntropy);
277 r.reasoning.push_back("Detected branch patterns with avg entropy " +
278 std::to_string(avgEntropy));
279 if (avgEntropy > 1.5) {
280 r.reasoning.push_back("High branching entropy - complex decision structure");
281 if (f.maxBranches > 5) {
282 r.reasoning.push_back("Many branches detected - consider simulation methods");
285 }
286 }
287 if (avgEntropy < 0.5) {
288 r.reasoning.push_back("Low branching entropy - deterministic-like behavior");
289 confidence += 0.1;
290 }
291 }
292
294 const double reduction = static_cast<double>(f.originalNodeCount - f.optimizedNodeCount) /
295 static_cast<double>(f.originalNodeCount);
296 r.reasoning.push_back("Workflow complexity reduced by " +
297 std::to_string(static_cast<int>(reduction * 100)) +
298 "% through pattern optimization");
299 confidence += 0.1;
300 }
301 if (f.optimizedNodeCount > 50) {
302 r.reasoning.push_back("Large optimized workflow - consider approximation methods");
303 if (rec == WfSolver::NC) {
304 rec = WfSolver::MVA;
305 r.reasoning.push_back("Switching from NC to MVA for large workflow");
306 }
308 }
309 (void)a; // the reference keeps the analysis in scope without reading it here
310
311 confidence = std::min(1.0, std::max(0.1, confidence));
312
313 // The reference pads with the standard list IN ITS ORDER and then keeps the
314 // first three, so the padding order is part of the answer.
315 const WfSolver standard[6] = {WfSolver::MVA, WfSolver::NC, WfSolver::SSA,
317 for (std::size_t i = 0; i < 6; ++i) detail::push_alternative(&r.alternativeSolvers, rec, standard[i]);
318 if (r.alternativeSolvers.size() > 3) r.alternativeSolvers.resize(3);
319
320 r.recommendedSolver = rec;
321 r.confidence = confidence;
322 return r;
323}
324
325/** The entry point: analyse the workflow, then let it amend the base choice. */
326template <class T>
334
335/** The chosen solver alone, i.e. the reference's createOptimalSolver. */
336template <class T>
340
341/** The advisory text the reference's getOptimizationInsights assembles. */
343 std::vector<std::string> recommendations;
344 std::vector<std::string> patternInsights;
345 std::vector<std::string> performancePredictions;
346};
347
348/** Pattern-level advice; the strings are the reference's, verbatim. */
349template <class T>
350std::vector<std::string> generate_pattern_insights(const DetectedPatterns<T>& p) {
351 std::vector<std::string> out;
352 if (!p.sequences.empty())
353 out.push_back("Consider merging sequential services to reduce overhead");
354 if (!p.parallels.empty())
355 out.push_back("Parallel patterns can benefit from resource pooling strategies");
356 if (!p.loops.empty())
357 out.push_back("High-probability loops may benefit from caching or memoization");
358 if (!p.branches.empty()) {
359 double total = 0.0;
360 for (std::size_t i = 0; i < p.branches.size(); ++i)
361 total += static_cast<double>(p.branches[i].branchNodes.size());
362 if (total / static_cast<double>(p.branches.size()) > 3.0)
363 out.push_back("Complex branching patterns - consider load balancing strategies");
364 }
365 return out;
366}
367
368/** Solve-time advice keyed off the collapse ratio and the pattern mix. */
369template <class T>
370std::vector<std::string> generate_performance_predictions(const WorkflowAnalysis<T>& a) {
371 std::vector<std::string> out;
372 const double ratio = num_traits<T>::to_double(a.statistics.updateStats.reductionRatio);
373 if (ratio > 0.1)
374 out.push_back("Expected " + std::to_string(static_cast<int>(ratio * 100)) +
375 "% reduction in solve time");
376 if (!a.detectedPatterns.parallels.empty())
377 out.push_back("High potential for parallel execution optimization");
378 if (!a.detectedPatterns.loops.empty())
379 out.push_back("Loop patterns may affect solver convergence rates");
380 return out;
381}
382
383/** All three advisory blocks for one model. */
384template <class T>
393
394/**
395 * The reference's self-check: a usable recommendation over a valid analysis.
396 *
397 * The Java swallows every exception and returns false; here the analysis is
398 * total over a well-formed struct, so a throw is a defect and is left to
399 * propagate rather than being reported as a failed validation.
400 */
401template <class T>
407
408} // namespace wf
409} // namespace line
410
411#endif // LINE_API_WF_WF_AUTO_INTEGRATION_H
A network plus its refreshed NetworkStruct.
Dense matrix and non-owning view.
WorkflowAnalysis< T > analyze_workflow(const WorkflowRepresentation< T > &w)
Detect, collapse, and report.
WfSolver wf_base_recommendation(const WfModelFacts< T > &f)
The AUTO heuristic before the workflow features amend it.
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.
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.
WfSolver
The solvers the reference chooses among.
WfModelFacts< T > wf_model_facts(const qn::NetworkStruct< T > &sn)
Read the base facts off a NetworkStruct.
ExtendedSolverRecommendation< T > enhance_recommendation_with_workflow(WfSolver base, const WorkflowFeatures< T > &f, const WorkflowAnalysis< T > &a)
Amend the base recommendation with what the workflow analysis found.
WorkflowFeatures< T > extract_workflow_features(const WorkflowAnalysis< T > &a)
Flatten the analysis into the feature vector the heuristic reads.
WfSolver create_optimal_solver(const qn::NetworkStruct< T > &sn)
The chosen solver alone, i.e.
BranchDiversity< T > calculate_branch_diversity(const BranchPattern< T > &pattern)
Shannon entropy of the branch probabilities, the same entropy normalized by log(n),...
std::vector< std::string > generate_pattern_insights(const DetectedPatterns< T > &p)
Pattern-level advice; the strings are the reference's, verbatim.
OptimizationInsights get_optimization_insights(const qn::NetworkStruct< T > &sn)
All three advisory blocks for one model.
std::vector< std::string > generate_performance_predictions(const WorkflowAnalysis< T > &a)
Solve-time advice keyed off the collapse ratio and the pattern mix.
WorkflowRepresentation< T > wf_from_struct(const qn::NetworkStruct< T > &sn)
Build a workflow representation from a NetworkStruct.
SolverSSA SSA
Definition solver.h:176
SolverAUTO AUTO
Definition solver.h:182
SolverNC NC
Definition solver.h:174
SolverCTMC CTMC
Definition solver.h:175
SolverJMT JMT
Definition solver.h:180
SolverMVA MVA
Definition solver.h:173
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
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.
std::vector< WfSolver > alternativeSolvers
at most three, the reference's cap
The advisory text the reference's getOptimizationInsights assembles.
std::vector< std::string > recommendations
std::vector< std::string > performancePredictions
std::vector< std::string > patternInsights
The four model facts the base recommendation reads, i.e.
What analyze_workflow returns.
WorkflowStatistics< T > statistics
DetectedPatterns< T > detectedPatterns
The flat feature vector the recommendation reads.
The workflow in matrix form: the reference's WorkflowRepresentation.
Definition wf_analyzer.h:63
The workflow analyzer: detect every pattern, collapse them, report the two complexities and the recom...