LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
workflow.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_LANG_WORKFLOW_WORKFLOW_H
6#define LINE_LANG_WORKFLOW_WORKFLOW_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * An activity workflow reduced to one phase-type law.
12 *
13 * Port of matlab/src/lang/workflow/Workflow.m and WorkflowActivity.m, and of
14 * their twins jline.lang.workflow.Workflow (JAR) and
15 * line_solver.lang.workflow.Workflow (Python). A workflow is a precedence
16 * graph over activities, each carrying a host demand; `to_ph` composes those
17 * laws into a single phase-type distribution.
18 *
19 * WHAT THIS IS NOT. `include/line/api/wf/` holds the workflow pattern
20 * DETECTORS of `jline.api.wf`, which read a link matrix and report sequences,
21 * branches, parallel blocks and loops. This header is the algebra those
22 * patterns feed: the composition that turns a precedence graph into an
23 * (alpha, T) pair. An LQN fork-join is also a different object -- its branches
24 * contend for a host, so it yields a response time under contention rather
25 * than the order statistic of independent activity times returned here.
26 *
27 * A precedence graph that is SERIES-PARALLEL is reduced exactly, by recursive
28 * composition of the series-parallel tree, which handles arbitrary nesting (a
29 * fork inside a loop, a branch that is itself a fork-join). Any other graph
30 * falls back to the block composition, which is a heuristic and is documented
31 * as one.
32 *
33 * A LOOP repeats its body a GEOMETRIC number of times of mean COUNT, the
34 * POST_LOOP semantics of an activity graph (`getStruct.m:528-567`): a count of
35 * at least one runs the body once and takes the back edge with probability
36 * 1-1/COUNT, and a fractional count runs the body at most once, with
37 * probability COUNT. The COUNT-fold convolution is a different law with the
38 * same mean; it stays available as `compose_repeat` for a caller that wants
39 * exactly that.
40 *
41 * An EXTERNAL CALL has no separate representation: an activity whose host
42 * demand is the law of the call response time composes exactly like a local
43 * computation, and an asynchronous call blocks the caller for no time and is
44 * simply left out of the workflow.
45 *
46 * A QUORUM (partial) AND-join is REFUSED by name rather than served as a full
47 * join, because the first k of n branches to finish is not their maximum.
48 */
49
50#include <algorithm>
51#include <map>
52#include <string>
53#include <vector>
54
59#include "line/util/error.h"
60#include "line/util/matrix.h"
61
62namespace line {
63namespace workflow {
64
65using lang::Distrib;
67// The precedence kinds of `ActivityPrecedenceType.m`, shared with the LQN layer
70
71/**
72 * One precedence of the activity graph.
73 *
74 * `pre_params` carries the AND-join quorum when there is one; `post_params`
75 * carries the OR-fork probabilities or the loop count.
76 */
77template <class T>
78struct Precedence {
79 std::vector<std::string> pre_acts;
80 std::vector<std::string> post_acts;
81 PrecedenceType pre_type = PrecedenceType::PRE_SEQ;
82 PrecedenceType post_type = PrecedenceType::POST_SEQ;
83 std::vector<T> pre_params;
84 std::vector<T> post_params;
85};
86
87/** A phase-type law as the composition rules pass it around. */
88template <class T>
89struct PhLaw {
90 std::vector<T> alpha;
92};
93
94/**
95 * A computational activity.
96 *
97 * Carries no call list: an external call is an activity whose host demand is
98 * the law of the call response time.
99 */
100template <class T>
102public:
103 WorkflowActivity() = default;
104 WorkflowActivity(const std::string& name, const Distrib<T>& host_demand)
105 : name_(name), host_demand_(host_demand) {}
106
107 const std::string& name() const { return name_; }
108
109 const Distrib<T>& host_demand() const { return host_demand_; }
110 void set_host_demand(const Distrib<T>& d) { host_demand_ = d; }
111
112 T host_demand_mean() const { return host_demand_.mean; }
113 T host_demand_scv() const { return host_demand_.scv; }
114
115 /**
116 * The (alpha, T) pair of this activity.
117 *
118 * A zero-time activity is one immediate phase, the representation used
119 * throughout this class; a Markovian law hands back its own pair; anything
120 * else is fitted to an acyclic phase-type on its first two moments, as the
121 * MATLAB and JAR twins do.
122 */
124 const T zero = num_traits<T>::from_int(0);
125 PhLaw<T> out;
126
127 if (host_demand_.type == ProcessType::IMMEDIATE ||
129 out.alpha.assign(1, num_traits<T>::from_int(1));
130 out.S = Matrix<T>(1, 1, zero);
132 return out;
133 }
134
135 if (host_demand_.has_map()) {
136 out.S = host_demand_.D0;
137 out.alpha = init_prob_of(host_demand_);
138 return out;
139 }
140
141 T scv = host_demand_.scv;
144 const Distrib<T> aph = lang::aph_fit_mean_scv(host_demand_.mean, scv);
145 out.S = aph.D0;
146 out.alpha = init_prob_of(aph);
147 return out;
148 }
149
150 std::size_t num_phases() const { return ph_representation().S.rows(); }
151
152private:
153 /**
154 * The initial vector of a Markovian law.
155 *
156 * `Distrib::phase_type` stores alpha in `params`, so a PH/APH/ME hands it
157 * back directly. For every other Markovian family D1 = (-D0 e) alpha, so
158 * ANY row with a positive exit rate recovers alpha -- and it cannot be row
159 * 0, since a canonical bidiagonal APH never completes from phase 1.
160 */
161 static std::vector<T> init_prob_of(const Distrib<T>& d) {
162 const std::size_t n = d.D0.rows();
163 const T zero = num_traits<T>::from_int(0);
164 if ((d.type == ProcessType::PH || d.type == ProcessType::APH ||
165 d.type == ProcessType::ME) &&
166 d.params.size() == n) {
167 return std::vector<T>(d.params.begin(), d.params.begin() + static_cast<long>(n));
168 }
169 for (std::size_t i = 0; i < n; ++i) {
170 T out = zero;
171 for (std::size_t j = 0; j < n; ++j) out += d.D1(i, j);
172 if (!(out > zero)) continue;
173 std::vector<T> alpha(n);
174 for (std::size_t j = 0; j < n; ++j) alpha[j] = T(d.D1(i, j) / out);
175 return alpha;
176 }
177 // No phase completes: start in phase 0, which is what a degenerate
178 // representation leaves as the only defensible reading
179 std::vector<T> alpha(n, zero);
180 if (n > 0) alpha[0] = num_traits<T>::from_int(1);
181 return alpha;
182 }
183
184 std::string name_;
185 Distrib<T> host_demand_;
186};
187
188/** A node of the series-parallel tree. */
189enum class SPNodeType { LEAF, SERIAL, PAR, OR, LOOP };
190
191template <class T>
192struct SPNode {
194 /** Activity index for a LEAF, npos otherwise. */
195 std::size_t act = static_cast<std::size_t>(-1);
196 std::vector<std::size_t> kids;
197 std::size_t parent = static_cast<std::size_t>(-1);
198 /** Branch probabilities for an OR node. */
199 std::vector<T> probs;
200 /** Loop count for a LOOP node. */
203 bool valid = false;
204};
205
206/**
207 * The flat series-parallel tree.
208 *
209 * `execs` carries the expected number of executions of each node per workflow
210 * execution, which is the weight by which an LQN metric reconstruction splits
211 * a layer result back over entries, activities and calls.
212 */
213template <class T>
214struct SPTree {
215 std::vector<SPNode<T>> nodes;
216 std::size_t root = static_cast<std::size_t>(-1);
217 /** Node index of each activity's leaf, npos when the activity has none. */
218 std::vector<std::size_t> leaf_of;
219 std::vector<T> execs;
220};
221
222template <class T>
223class Workflow {
224public:
225 static constexpr std::size_t npos = static_cast<std::size_t>(-1);
226
227 explicit Workflow(const std::string& name) : name_(name) {}
228
229 const std::string& name() const { return name_; }
230
231 /** Add an activity; the name must be unique. */
232 std::size_t add_activity(const std::string& name, const Distrib<T>& host_demand) {
233 if (activity_map_.find(name) != activity_map_.end())
234 throw InputError("Workflow: activity '" + name + "' is already declared");
235 activities_.push_back(WorkflowActivity<T>(name, host_demand));
236 const std::size_t idx = activities_.size() - 1;
237 activity_map_[name] = idx;
239 return idx;
240 }
241
242 /** Add an activity with an exponential host demand of the given mean. */
243 std::size_t add_activity(const std::string& name, const T& mean) {
245 }
246
247 void add_precedence(const Precedence<T>& prec) {
248 precedences_.push_back(prec);
250 }
251
252 std::size_t num_activities() const { return activities_.size(); }
253 const std::vector<WorkflowActivity<T>>& activities() const { return activities_; }
254 const std::vector<Precedence<T>>& precedences() const { return precedences_; }
255
256 std::size_t activity_index(const std::string& name) const {
257 auto it = activity_map_.find(name);
258 return it == activity_map_.end() ? npos : it->second;
259 }
260
261 WorkflowActivity<T>& activity(const std::string& name) {
262 const std::size_t i = activity_index(name);
263 if (i == npos) throw InputError("Workflow: activity '" + name + "' not found");
264 return activities_[i];
265 }
266
267 /** Activity by index, the form a series-parallel LEAF node names it in. */
268 const WorkflowActivity<T>& activity_at(std::size_t i) const {
269 if (i >= activities_.size()) throw InputError("Workflow: activity index out of range");
270 return activities_[i];
271 }
272
273 /**
274 * Validate the workflow, throwing on the first defect.
275 *
276 * A quorum AND-join is refused here rather than composed as a full join.
277 */
278 void validate() const {
279 if (activities_.empty())
280 throw InputError("Workflow must have at least one activity.");
281
282 for (const Precedence<T>& prec : precedences_) {
283 for (const std::string& nm : prec.pre_acts)
284 if (activity_index(nm) == npos)
285 throw InputError("Activity '" + nm +
286 "' referenced in precedence not found in workflow.");
287 for (const std::string& nm : prec.post_acts)
288 if (activity_index(nm) == npos)
289 throw InputError("Activity '" + nm +
290 "' referenced in precedence not found in workflow.");
291 }
292
293 for (const Precedence<T>& prec : precedences_) {
294 if (prec.post_type == PrecedenceType::POST_OR) {
295 if (prec.post_params.empty())
296 throw InputError("OR-fork must have probabilities specified.");
297 T total = num_traits<T>::from_int(0);
298 for (const T& p : prec.post_params) total += p;
299 const double gap =
300 std::abs(num_traits<T>::to_double(total) - 1.0);
301 if (gap > GlobalConstants::FineTol)
302 throw InputError("OR-fork probabilities must sum to 1.");
303 }
304 if (prec.post_type == PrecedenceType::POST_LOOP) {
305 if (prec.post_params.size() != 1)
306 throw InputError("Loop count must be a single positive number.");
307 if (!(prec.post_params[0] > num_traits<T>::from_int(0)))
308 throw InputError("Loop count must be a positive number.");
309 }
310 if (prec.pre_type == PrecedenceType::PRE_AND && !prec.pre_params.empty()) {
311 const double quorum = num_traits<T>::to_double(prec.pre_params[0]);
312 const double nb = static_cast<double>(prec.pre_acts.size());
313 if (quorum > 0.0 && quorum < nb)
314 throw UnsupportedError(
315 "AND-join with quorum " + std::to_string(static_cast<long>(quorum)) +
316 " of " + std::to_string(static_cast<long>(nb)) +
317 " is not supported by Workflow: a partial join is not the maximum of the "
318 "branches. Use a full join, or SolverLN with method='default', which "
319 "routes the join explicitly.");
320 }
321 }
322 }
323
324 /**
325 * The composed law of the workflow.
326 *
327 * The generator is acyclic unless a geometric loop closes a cycle over a
328 * multi-phase body, which `is_acyclic_generator` reports; the returned
329 * Distrib is typed APH or PH accordingly.
330 */
332 if (cached_valid_) return cached_ph_;
333 validate();
334
335 PhLaw<T> law;
336 if (!compose_series_parallel(law)) {
337 // Not series-parallel: fall back to the block composition
338 law = build_ctmc();
339 }
340
341 cached_ph_ = Distrib<T>::phase_type(law.alpha, law.S, is_acyclic_generator(law.S));
342 cached_valid_ = true;
343 return cached_ph_;
344 }
345
346 /**
347 * Recompose the law after a demand change.
348 *
349 * Only the series-parallel nodes on the path from a dirty leaf to the root
350 * recompose; nodes whose subtree is unchanged keep their cached law. The
351 * topology is not rebuilt.
352 */
354
355 /**
356 * Change the host demand of one activity.
357 *
358 * Marks only that leaf dirty, which is the entry point an iterative solver
359 * uses when it updates call-response laws at each iteration.
360 */
361 void set_activity_demand(const std::string& name, const Distrib<T>& host_demand) {
362 const std::size_t i = activity_index(name);
363 if (i == npos) throw InputError("Workflow: activity '" + name + "' not found");
364 activities_[i].set_host_demand(host_demand);
366 }
367
368 /**
369 * Change only the mean of one activity, preserving its shape.
370 *
371 * The law is scaled in time rather than refitted, so its SCV, its skewness
372 * and its order are preserved and the cached tree keeps its shape.
373 */
374 void set_activity_demand_mean(const std::string& name, const T& mean_value) {
375 if (!(mean_value > num_traits<T>::from_int(0)))
376 throw InputError("The activity mean must be a positive finite scalar.");
377 const std::size_t i = activity_index(name);
378 if (i == npos) throw InputError("Workflow: activity '" + name + "' not found");
379
380 const Distrib<T>& d = activities_[i].host_demand();
381 const T old_mean = d.mean;
382 if (d.type == ProcessType::IMMEDIATE ||
384 activities_[i].set_host_demand(Distrib<T>::exp_mean(mean_value));
386 return;
387 }
388
389 const T factor = T(old_mean / mean_value);
390 activities_[i].set_host_demand(lang::dist_scale_rate(d, factor));
391 // The SCV is invariant under a time scaling
392 rescale_activity_leaf(i, factor);
393 }
394
395 /** Discard the cached law and the decomposition. */
397 cached_valid_ = false;
398 sp_tree_.nodes.clear();
399 sp_tree_.root = npos;
400 sp_tree_.leaf_of.clear();
401 sp_tree_.execs.clear();
402 sp_built_ = false;
403 sp_failed_ = false;
404 }
405
406 /** Mark one activity law dirty, keeping every other cached block. */
407 void invalidate_activity(std::size_t act_idx) {
408 cached_valid_ = false;
409 if (!sp_built_) return;
410 if (act_idx >= sp_tree_.leaf_of.size() || sp_tree_.leaf_of[act_idx] == npos) {
411 // Activity outside the decomposition: rebuild it entirely
413 return;
414 }
415 invalidate_branch(sp_tree_, sp_tree_.leaf_of[act_idx]);
416 }
417
418 /**
419 * Time-scale a cached leaf in place: S -> S*FACTOR with alpha fixed.
420 *
421 * Ancestors still recompose, because they mix phases of several leaves,
422 * but the tree keeps its shape and no acyclic phase-type is refitted.
423 */
424 void rescale_activity_leaf(std::size_t act_idx, const T& factor) {
425 cached_valid_ = false;
426 if (!sp_built_) return;
427 if (act_idx >= sp_tree_.leaf_of.size() || sp_tree_.leaf_of[act_idx] == npos) {
429 return;
430 }
431 const std::size_t k = sp_tree_.leaf_of[act_idx];
432 invalidate_branch(sp_tree_, k);
433 SPNode<T>& node = sp_tree_.nodes[k];
434 if (node.law.S.rows() > 0) {
435 for (std::size_t i = 0; i < node.law.S.rows(); ++i)
436 for (std::size_t j = 0; j < node.law.S.cols(); ++j)
437 node.law.S(i, j) = T(node.law.S(i, j) * factor);
438 node.valid = true;
439 }
440 }
441
442 /**
443 * The cached series-parallel decomposition, or null when the precedence
444 * graph is not series-parallel.
445 */
447 if (!sp_built_ && !sp_failed_) build_sp_tree();
448 return sp_built_ ? &sp_tree_ : nullptr;
449 }
450
451 // -----------------------------------------------------------------
452 // Composition rules
453 // -----------------------------------------------------------------
454
455 /**
456 * Serial composition: the second law starts when the first absorbs.
457 *
458 * S = [S1, (-S1 e) alpha2; 0, S2]
459 *
460 * A defective alpha1 carries an atom at zero, which starts the second law
461 * immediately; this is `aph_simplify` pattern 1.
462 */
463 static PhLaw<T> compose_serial(const PhLaw<T>& a, const PhLaw<T>& b) {
464 const T zero = num_traits<T>::from_int(0);
465 const std::size_t n1 = a.S.rows(), n2 = b.S.rows();
466
467 PhLaw<T> out;
468 out.S = Matrix<T>(n1 + n2, n1 + n2, zero);
469 for (std::size_t i = 0; i < n1; ++i)
470 for (std::size_t j = 0; j < n1; ++j) out.S(i, j) = a.S(i, j);
471 for (std::size_t i = 0; i < n2; ++i)
472 for (std::size_t j = 0; j < n2; ++j) out.S(n1 + i, n1 + j) = b.S(i, j);
473
474 for (std::size_t i = 0; i < n1; ++i) {
475 T rate = zero;
476 for (std::size_t j = 0; j < n1; ++j) rate += a.S(i, j);
477 rate = T(-rate);
478 for (std::size_t j = 0; j < n2; ++j) out.S(i, n1 + j) = T(rate * b.alpha[j]);
479 }
480
481 T defect = num_traits<T>::from_int(1);
482 for (const T& v : a.alpha) defect -= v;
483 out.alpha.assign(n1 + n2, zero);
484 for (std::size_t i = 0; i < n1; ++i) out.alpha[i] = a.alpha[i];
485 for (std::size_t j = 0; j < n2; ++j) out.alpha[n1 + j] = T(defect * b.alpha[j]);
486 return out;
487 }
488
489 /**
490 * Parallel (AND-fork/join) composition: the time until BOTH complete.
491 *
492 * States (i,j) with both active carry the Kronecker sum S1 (+) S2; when one
493 * branch absorbs the chain moves into that branch's own survivor block, so
494 * the law is the maximum of the two.
495 */
496 static PhLaw<T> compose_parallel(const PhLaw<T>& a, const PhLaw<T>& b) {
497 const T zero = num_traits<T>::from_int(0);
498 const std::size_t n1 = a.S.rows(), n2 = b.S.rows();
499 const std::size_t nboth = n1 * n2;
500 const std::size_t ntot = nboth + n1 + n2;
501
502 std::vector<T> abs1(n1, zero), abs2(n2, zero);
503 for (std::size_t i = 0; i < n1; ++i) {
504 T r = zero;
505 for (std::size_t j = 0; j < n1; ++j) r += a.S(i, j);
506 abs1[i] = T(-r);
507 }
508 for (std::size_t i = 0; i < n2; ++i) {
509 T r = zero;
510 for (std::size_t j = 0; j < n2; ++j) r += b.S(i, j);
511 abs2[i] = T(-r);
512 }
513
514 PhLaw<T> out;
515 out.S = Matrix<T>(ntot, ntot, zero);
516
517 // Kronecker sum on the block where both are still running
518 for (std::size_t i = 0; i < n1; ++i)
519 for (std::size_t j = 0; j < n2; ++j) {
520 const std::size_t r = i * n2 + j;
521 for (std::size_t ii = 0; ii < n1; ++ii)
522 out.S(r, ii * n2 + j) += a.S(i, ii);
523 for (std::size_t jj = 0; jj < n2; ++jj)
524 out.S(r, i * n2 + jj) += b.S(j, jj);
525 // one branch absorbs, the other keeps running
526 out.S(r, nboth + i) += abs2[j];
527 out.S(r, nboth + n1 + j) += abs1[i];
528 }
529
530 for (std::size_t i = 0; i < n1; ++i)
531 for (std::size_t j = 0; j < n1; ++j) out.S(nboth + i, nboth + j) = a.S(i, j);
532 for (std::size_t i = 0; i < n2; ++i)
533 for (std::size_t j = 0; j < n2; ++j)
534 out.S(nboth + n1 + i, nboth + n1 + j) = b.S(i, j);
535
536 out.alpha.assign(ntot, zero);
537 for (std::size_t i = 0; i < n1; ++i)
538 for (std::size_t j = 0; j < n2; ++j)
539 out.alpha[i * n2 + j] = T(a.alpha[i] * b.alpha[j]);
540 return out;
541 }
542
543 /**
544 * Probabilistic mixture: a block-diagonal generator whose initial vector
545 * picks branch i with probability PROBS[i]. `aph_simplify` pattern 3,
546 * generalised to any number of branches.
547 */
548 static PhLaw<T> compose_mixture(const std::vector<PhLaw<T>>& laws,
549 const std::vector<T>& probs) {
550 const T zero = num_traits<T>::from_int(0);
551 std::size_t total = 0;
552 for (const PhLaw<T>& l : laws) total += l.S.rows();
553
554 PhLaw<T> out;
555 out.S = Matrix<T>(total, total, zero);
556 out.alpha.assign(total, zero);
557
558 std::size_t off = 0;
559 for (std::size_t b = 0; b < laws.size(); ++b) {
560 const std::size_t nb = laws[b].S.rows();
561 for (std::size_t i = 0; i < nb; ++i) {
562 for (std::size_t j = 0; j < nb; ++j) out.S(off + i, off + j) = laws[b].S(i, j);
563 out.alpha[off + i] = T(probs[b] * laws[b].alpha[i]);
564 }
565 off += nb;
566 }
567 return out;
568 }
569
570 /**
571 * Geometric repetition of a phase-type law, the POST_LOOP semantics.
572 *
573 * For COUNT >= 1 the body runs at least once and repeats on absorption
574 * with probability P = 1-1/COUNT, so
575 *
576 * S_out = S + P/D (-S e) alpha, alpha_out = alpha / D
577 *
578 * with D = 1 - P(1 - alpha e) the correction for an atom at zero in alpha.
579 * The ORDER is that of the body, unlike the COUNT-fold convolution, and the
580 * mean is COUNT times the body mean in both cases.
581 *
582 * For COUNT < 1 the body runs at most once, with probability COUNT; the
583 * skipped branch is an immediate phase.
584 */
585 static PhLaw<T> compose_loop_geometric(const PhLaw<T>& body, const T& count) {
586 const T zero = num_traits<T>::from_int(0);
587 const T one = num_traits<T>::from_int(1);
588 const std::size_t n = body.S.rows();
589 const double c = num_traits<T>::to_double(count);
590
591 PhLaw<T> out;
592 if (!(c > 0.0)) {
593 out.alpha.assign(1, one);
594 out.S = Matrix<T>(1, 1, zero);
596 return out; // zero-time branch
597 }
598
599 if (std::abs(c - 1.0) <= GlobalConstants::FineTol) return body;
600
601 if (c < 1.0) {
602 // Executed with probability COUNT, skipped otherwise
603 out.alpha.assign(n + 1, zero);
604 for (std::size_t i = 0; i < n; ++i) out.alpha[i] = T(count * body.alpha[i]);
605 out.alpha[n] = T(one - count);
606 out.S = Matrix<T>(n + 1, n + 1, zero);
607 for (std::size_t i = 0; i < n; ++i)
608 for (std::size_t j = 0; j < n; ++j) out.S(i, j) = body.S(i, j);
610 return out; // zero-time skip branch
611 }
612
613 const T p = T(one - one / count);
614 T defect = one;
615 for (const T& v : body.alpha) defect -= v;
616 const T denom = T(one - p * defect);
617
618 out.alpha.assign(n, zero);
619 for (std::size_t i = 0; i < n; ++i) out.alpha[i] = T(body.alpha[i] / denom);
620
621 out.S = body.S;
622 for (std::size_t i = 0; i < n; ++i) {
623 T rate = zero;
624 for (std::size_t j = 0; j < n; ++j) rate += body.S(i, j);
625 rate = T(-rate * p / denom);
626 for (std::size_t j = 0; j < n; ++j) out.S(i, j) += T(rate * body.alpha[j]);
627 }
628 return out;
629 }
630
631 /**
632 * COUNT-fold convolution of a phase-type law.
633 *
634 * The DETERMINISTIC repetition, kept for a caller that genuinely wants an
635 * exact number of executions. POST_LOOP is geometric and uses
636 * `compose_loop_geometric` instead.
637 */
638 static PhLaw<T> compose_repeat(const PhLaw<T>& body, long count) {
639 if (count <= 0) {
640 PhLaw<T> out;
641 out.alpha.assign(1, num_traits<T>::from_int(1));
642 out.S = Matrix<T>(1, 1, num_traits<T>::from_int(0));
644 return out;
645 }
646 PhLaw<T> out = body;
647 for (long i = 1; i < count; ++i) out = compose_serial(out, body);
648 return out;
649 }
650
651 /**
652 * True when the phase graph of S has no cycle.
653 *
654 * A geometric loop over a body of two or more phases closes a cycle, so the
655 * composed law is a PH and not an APH.
656 */
657 static bool is_acyclic_generator(const Matrix<T>& S) {
658 const std::size_t n = S.rows();
659 std::vector<std::vector<bool>> A(n, std::vector<bool>(n, false));
660 std::vector<std::size_t> in_deg(n, 0);
661 for (std::size_t i = 0; i < n; ++i)
662 for (std::size_t j = 0; j < n; ++j) {
663 if (i == j) continue;
664 if (std::abs(num_traits<T>::to_double(S(i, j))) > GlobalConstants::ArcTol) {
665 A[i][j] = true;
666 ++in_deg[j];
667 }
668 }
669
670 std::vector<std::size_t> queue;
671 for (std::size_t i = 0; i < n; ++i)
672 if (in_deg[i] == 0) queue.push_back(i);
673 std::size_t visited = 0, head = 0;
674 while (head < queue.size()) {
675 const std::size_t cur = queue[head++];
676 ++visited;
677 for (std::size_t j = 0; j < n; ++j) {
678 if (!A[cur][j]) continue;
679 if (--in_deg[j] == 0) queue.push_back(j);
680 }
681 }
682 return visited == n;
683 }
684
685 // -----------------------------------------------------------------
686 // Precedence factories, named as in MATLAB and the JAR
687 // -----------------------------------------------------------------
688
689 static Precedence<T> Serial(const std::string& pre, const std::string& post) {
691 p.pre_acts.push_back(pre);
692 p.post_acts.push_back(post);
693 p.pre_type = PrecedenceType::PRE_SEQ;
694 p.post_type = PrecedenceType::POST_SEQ;
695 return p;
696 }
697
698 static std::vector<Precedence<T>> SerialSequence(const std::vector<std::string>& acts) {
699 std::vector<Precedence<T>> out;
700 for (std::size_t i = 0; i + 1 < acts.size(); ++i)
701 out.push_back(Serial(acts[i], acts[i + 1]));
702 return out;
703 }
704
705 static Precedence<T> AndFork(const std::string& pre,
706 const std::vector<std::string>& posts) {
708 p.pre_acts.push_back(pre);
709 p.post_acts = posts;
710 p.pre_type = PrecedenceType::PRE_SEQ;
711 p.post_type = PrecedenceType::POST_AND;
712 return p;
713 }
714
715 /** An empty QUORUM means a full join; a partial one is refused by validate. */
716 static Precedence<T> AndJoin(const std::vector<std::string>& pres,
717 const std::string& post,
718 const std::vector<T>& quorum = std::vector<T>()) {
720 p.pre_acts = pres;
721 p.post_acts.push_back(post);
722 p.pre_type = PrecedenceType::PRE_AND;
723 p.post_type = PrecedenceType::POST_SEQ;
724 p.pre_params = quorum;
725 return p;
726 }
727
728 static Precedence<T> OrFork(const std::string& pre, const std::vector<std::string>& posts,
729 const std::vector<T>& probs) {
731 p.pre_acts.push_back(pre);
732 p.post_acts = posts;
733 p.pre_type = PrecedenceType::PRE_SEQ;
734 p.post_type = PrecedenceType::POST_OR;
735 p.post_params = probs;
736 return p;
737 }
738
739 static Precedence<T> OrJoin(const std::vector<std::string>& pres,
740 const std::string& post) {
742 p.pre_acts = pres;
743 p.post_acts.push_back(post);
744 p.pre_type = PrecedenceType::PRE_OR;
745 p.post_type = PrecedenceType::POST_SEQ;
746 return p;
747 }
748
749 /**
750 * A loop: PRE runs once, then POSTS[0..n-2] repeat a geometric number of
751 * times of mean COUNT and POSTS[n-1] continues after the loop.
752 */
753 static Precedence<T> Loop(const std::string& pre, const std::vector<std::string>& posts,
754 const T& count) {
756 p.pre_acts.push_back(pre);
757 p.post_acts = posts;
758 p.pre_type = PrecedenceType::PRE_SEQ;
759 p.post_type = PrecedenceType::POST_LOOP;
760 p.post_params.push_back(count);
761 return p;
762 }
763
764private:
765 // -----------------------------------------------------------------
766 // Series-parallel decomposition
767 // -----------------------------------------------------------------
768
769 /** How a parsed sequence terminated. */
770 enum class ParseStatus { END, STOP, JOIN, FAIL };
771
772 struct ParseState {
773 /** Index of the precedence each activity heads / is reached by. */
774 std::vector<std::size_t> out_p, in_p;
775 std::vector<bool> consumed;
776 };
777
778 bool compose_series_parallel(PhLaw<T>& out) {
779 if (!sp_built_) {
780 if (sp_failed_) return false;
781 build_sp_tree();
782 if (!sp_built_) return false;
783 }
784 out = compose_node(sp_tree_.root);
785 return true;
786 }
787
788 /**
789 * Decompose the precedence graph into a series-parallel tree.
790 *
791 * The decomposition is attempted once per topology: on failure `sp_failed_`
792 * is set and the block path takes over.
793 */
794 bool build_sp_tree() {
795 sp_tree_.nodes.clear();
796 sp_tree_.root = npos;
797 sp_tree_.leaf_of.clear();
798 sp_tree_.execs.clear();
799 sp_built_ = false;
800 sp_failed_ = true;
801
802 const std::size_t n = activities_.size();
803 if (n == 0) return false;
804
805 ParseState S;
806 S.out_p.assign(n, npos);
807 S.in_p.assign(n, npos);
808 S.consumed.assign(n, false);
809
810 // An activity may head at most one precedence and be reached by at most
811 // one precedence; otherwise the graph is not series-parallel
812 for (std::size_t p = 0; p < precedences_.size(); ++p) {
813 for (const std::string& nm : precedences_[p].pre_acts) {
814 const std::size_t i = activity_index(nm);
815 if (i == npos || S.out_p[i] != npos) return false;
816 S.out_p[i] = p;
817 }
818 for (const std::string& nm : precedences_[p].post_acts) {
819 const std::size_t j = activity_index(nm);
820 if (j == npos || S.in_p[j] != npos) return false;
821 S.in_p[j] = p;
822 }
823 }
824
825 std::vector<std::size_t> starts;
826 for (std::size_t i = 0; i < n; ++i)
827 if (S.in_p[i] == npos) starts.push_back(i);
828 if (starts.size() != 1) return false;
829
830 std::vector<std::size_t> kids;
831 std::size_t stop_at = npos;
832 const ParseStatus st = sp_parse_seq(S, starts[0], std::vector<std::size_t>(), kids, stop_at);
833 if (st != ParseStatus::END) return false;
834 for (std::size_t i = 0; i < n; ++i)
835 if (!S.consumed[i]) return false;
836
837 const std::size_t root = sp_serial_node(kids);
838 if (root == npos) return false;
839
840 sp_tree_.root = root;
841 sp_tree_.leaf_of.assign(n, npos);
842 for (std::size_t k = 0; k < sp_tree_.nodes.size(); ++k)
843 if (sp_tree_.nodes[k].type == SPNodeType::LEAF)
844 sp_tree_.leaf_of[sp_tree_.nodes[k].act] = k;
845 sp_tree_.execs = sp_execution_counts(sp_tree_);
846
847 sp_built_ = true;
848 sp_failed_ = false;
849 return true;
850 }
851
852 /** Parse a maximal sequence of blocks starting at CUR. */
853 ParseStatus sp_parse_seq(ParseState& S, std::size_t cur,
854 const std::vector<std::size_t>& stop_set,
855 std::vector<std::size_t>& kids, std::size_t& stop_at) {
856 stop_at = npos;
857 while (true) {
858 if (cur == npos) return ParseStatus::END;
859 if (std::find(stop_set.begin(), stop_set.end(), cur) != stop_set.end()) {
860 stop_at = cur;
861 return ParseStatus::STOP;
862 }
863 if (S.consumed[cur]) return ParseStatus::FAIL;
864 S.consumed[cur] = true;
865 kids.push_back(sp_add_node(SPNodeType::LEAF, cur, std::vector<std::size_t>(),
866 std::vector<T>(), num_traits<T>::from_int(0)));
867
868 const std::size_t p = S.out_p[cur];
869 if (p == npos) return ParseStatus::END;
870 const Precedence<T>& prec = precedences_[p];
871 if (prec.pre_acts.size() > 1) {
872 // CUR is the tail of a branch: the caller composes the join
873 stop_at = p;
874 return ParseStatus::JOIN;
875 }
876
877 std::vector<std::size_t> post_inds = sp_indices_of(prec.post_acts);
878 for (std::size_t i : post_inds)
879 if (i == npos) return ParseStatus::FAIL;
880
881 std::size_t knode = npos, next_act = npos;
882 switch (prec.post_type) {
883 case PrecedenceType::POST_AND:
884 if (!sp_parse_fork(S, post_inds, std::vector<T>(), stop_set, true, knode,
885 next_act))
886 return ParseStatus::FAIL;
887 kids.push_back(knode);
888 cur = next_act;
889 break;
890 case PrecedenceType::POST_OR:
891 if (prec.post_params.size() != post_inds.size()) return ParseStatus::FAIL;
892 if (!sp_parse_fork(S, post_inds, prec.post_params, stop_set, false, knode,
893 next_act))
894 return ParseStatus::FAIL;
895 kids.push_back(knode);
896 cur = next_act;
897 break;
898 case PrecedenceType::POST_LOOP:
899 if (!sp_parse_loop(S, post_inds, prec.post_params, stop_set, knode, next_act))
900 return ParseStatus::FAIL;
901 kids.push_back(knode);
902 cur = next_act;
903 break;
904 case PrecedenceType::POST_SEQ:
905 if (post_inds.size() != 1) return ParseStatus::FAIL;
906 cur = post_inds[0];
907 break;
908 default:
909 // POST_CACHE and any other pattern is not a workflow
910 // composition rule
911 return ParseStatus::FAIL;
912 }
913 }
914 }
915
916 /** Parse the branches of a fork and their join. */
917 bool sp_parse_fork(ParseState& S, const std::vector<std::size_t>& branch_heads,
918 const std::vector<T>& probs, const std::vector<std::size_t>& stop_set,
919 bool is_and, std::size_t& knode, std::size_t& next_act) {
920 const std::size_t nb = branch_heads.size();
921 std::vector<std::size_t> branch_nodes(nb, npos), bstop(nb, npos);
922 std::vector<ParseStatus> bstatus(nb, ParseStatus::FAIL);
923
924 for (std::size_t b = 0; b < nb; ++b) {
925 std::vector<std::size_t> bkids;
926 std::size_t sa = npos;
927 const ParseStatus st = sp_parse_seq(S, branch_heads[b], stop_set, bkids, sa);
928 if (st == ParseStatus::FAIL) return false;
929 const std::size_t bn = sp_serial_node(bkids);
930 if (bn == npos) return false;
931 branch_nodes[b] = bn;
932 bstatus[b] = st;
933 bstop[b] = sa;
934 }
935
936 const bool all_join =
937 std::all_of(bstatus.begin(), bstatus.end(),
938 [](ParseStatus s) { return s == ParseStatus::JOIN; });
939 const bool all_end = std::all_of(bstatus.begin(), bstatus.end(),
940 [](ParseStatus s) { return s == ParseStatus::END; });
941 const bool all_stop = std::all_of(bstatus.begin(), bstatus.end(),
942 [](ParseStatus s) { return s == ParseStatus::STOP; });
943 const bool same_stop =
944 std::all_of(bstop.begin(), bstop.end(),
945 [&bstop](std::size_t x) { return x == bstop[0]; });
946
947 if (all_join) {
948 if (!same_stop) return false;
949 const Precedence<T>& join_prec = precedences_[bstop[0]];
950 if (join_prec.pre_acts.size() != nb) return false;
951 if (is_and) {
952 if (join_prec.pre_type != PrecedenceType::PRE_AND) return false;
953 } else {
954 if (join_prec.pre_type != PrecedenceType::PRE_OR) return false;
955 }
956 const std::vector<std::size_t> post_inds = sp_indices_of(join_prec.post_acts);
957 if (post_inds.size() != 1 || post_inds[0] == npos) return false;
958 next_act = post_inds[0];
959 } else if (all_end) {
960 // Branches terminate the workflow. An AND-fork with no join still
961 // synchronises at the end of the workflow
962 next_act = npos;
963 } else if (!is_and && all_stop && same_stop) {
964 next_act = bstop[0];
965 } else {
966 return false;
967 }
968
969 knode = sp_add_node(is_and ? SPNodeType::PAR : SPNodeType::OR, npos, branch_nodes, probs,
970 num_traits<T>::from_int(0));
971 return true;
972 }
973
974 /**
975 * Parse a loop block: the last post activity continues after the loop, the
976 * others form the body.
977 */
978 bool sp_parse_loop(ParseState& S, const std::vector<std::size_t>& post_inds,
979 const std::vector<T>& counts, const std::vector<std::size_t>& stop_set,
980 std::size_t& knode, std::size_t& next_act) {
981 if (counts.size() != 1) return false;
982 const T count = counts[0];
983
984 std::vector<std::size_t> body_acts;
985 std::size_t end_act = npos;
986 if (post_inds.size() >= 2) {
987 body_acts.assign(post_inds.begin(), post_inds.end() - 1);
988 end_act = post_inds.back();
989 } else {
990 body_acts.push_back(post_inds[0]);
991 }
992
993 std::vector<std::size_t> loop_stop = stop_set;
994 loop_stop.insert(loop_stop.end(), body_acts.begin(), body_acts.end());
995 if (end_act != npos) loop_stop.push_back(end_act);
996
997 std::vector<std::size_t> body_kids;
998 std::size_t j = 0;
999 while (j < body_acts.size()) {
1000 const std::size_t a = body_acts[j];
1001 if (S.consumed[a]) {
1002 ++j;
1003 continue;
1004 }
1005 std::vector<std::size_t> this_stop;
1006 for (std::size_t x : loop_stop)
1007 if (x != a) this_stop.push_back(x);
1008
1009 std::vector<std::size_t> kk;
1010 std::size_t sa = npos;
1011 const ParseStatus st = sp_parse_seq(S, a, this_stop, kk, sa);
1012 if (st == ParseStatus::FAIL) return false;
1013 body_kids.insert(body_kids.end(), kk.begin(), kk.end());
1014
1015 if (st == ParseStatus::END) {
1016 ++j;
1017 } else if (st == ParseStatus::STOP) {
1018 const auto it = std::find(body_acts.begin(), body_acts.end(), sa);
1019 if (it != body_acts.end()) {
1020 j = static_cast<std::size_t>(it - body_acts.begin());
1021 } else if (end_act != npos && sa == end_act) {
1022 j = body_acts.size();
1023 } else {
1024 return false;
1025 }
1026 } else {
1027 // A join reached from inside the body crosses the loop
1028 // boundary, so the graph is not series-parallel
1029 return false;
1030 }
1031 }
1032
1033 const std::size_t body_node = sp_serial_node(body_kids);
1034 if (body_node == npos) return false;
1035
1036 knode = sp_add_node(SPNodeType::LOOP, npos, std::vector<std::size_t>(1, body_node),
1037 std::vector<T>(), count);
1038 next_act = end_act;
1039 return true;
1040 }
1041
1042 /** Wrap a list of nodes in a serial node; a single node is returned as is. */
1043 std::size_t sp_serial_node(const std::vector<std::size_t>& kids) {
1044 if (kids.empty()) return npos;
1045 if (kids.size() == 1) return kids[0];
1046 return sp_add_node(SPNodeType::SERIAL, npos, kids, std::vector<T>(),
1047 num_traits<T>::from_int(0));
1048 }
1049
1050 std::size_t sp_add_node(SPNodeType type, std::size_t act,
1051 const std::vector<std::size_t>& kids, const std::vector<T>& probs,
1052 const T& count) {
1053 SPNode<T> node;
1054 node.type = type;
1055 node.act = act;
1056 node.kids = kids;
1057 node.probs = probs;
1058 node.count = count;
1059 sp_tree_.nodes.push_back(node);
1060 const std::size_t k = sp_tree_.nodes.size() - 1;
1061 for (std::size_t c : kids) sp_tree_.nodes[c].parent = k;
1062 return k;
1063 }
1064
1065 std::vector<std::size_t> sp_indices_of(const std::vector<std::string>& names) const {
1066 std::vector<std::size_t> out(names.size(), npos);
1067 for (std::size_t i = 0; i < names.size(); ++i) out[i] = activity_index(names[i]);
1068 return out;
1069 }
1070
1071 /**
1072 * Compose one node. A cached node is returned untouched, so a demand change
1073 * only recomposes the path from the dirty leaf to the root.
1074 */
1075 PhLaw<T> compose_node(std::size_t k) {
1076 SPNode<T>& node = sp_tree_.nodes[k];
1077 if (node.valid) return node.law;
1078
1079 PhLaw<T> law;
1080 switch (node.type) {
1081 case SPNodeType::LEAF:
1082 law = activities_[node.act].ph_representation();
1083 break;
1084 case SPNodeType::SERIAL: {
1085 const std::vector<std::size_t> kids = node.kids;
1086 law = compose_node(kids[0]);
1087 for (std::size_t i = 1; i < kids.size(); ++i)
1088 law = compose_serial(law, compose_node(kids[i]));
1089 break;
1090 }
1091 case SPNodeType::PAR: {
1092 const std::vector<std::size_t> kids = node.kids;
1093 law = compose_node(kids[0]);
1094 for (std::size_t i = 1; i < kids.size(); ++i)
1095 law = compose_parallel(law, compose_node(kids[i]));
1096 break;
1097 }
1098 case SPNodeType::OR: {
1099 const std::vector<std::size_t> kids = node.kids;
1100 std::vector<PhLaw<T>> laws;
1101 for (std::size_t c : kids) laws.push_back(compose_node(c));
1102 law = compose_mixture(laws, sp_tree_.nodes[k].probs);
1103 break;
1104 }
1105 case SPNodeType::LOOP: {
1106 const std::size_t child = node.kids[0];
1107 law = compose_loop_geometric(compose_node(child), sp_tree_.nodes[k].count);
1108 break;
1109 }
1110 }
1111
1112 sp_tree_.nodes[k].law = law;
1113 sp_tree_.nodes[k].valid = true;
1114 return law;
1115 }
1116
1117 static void invalidate_branch(SPTree<T>& tree, std::size_t k) {
1118 while (k != npos) {
1119 tree.nodes[k].valid = false;
1120 k = tree.nodes[k].parent;
1121 }
1122 }
1123
1124 /**
1125 * Expected executions of each node per workflow run.
1126 *
1127 * Serial and parallel children inherit the count of their parent, an OR
1128 * branch is weighted by its probability, and a loop body by the loop count.
1129 */
1130 static std::vector<T> sp_execution_counts(const SPTree<T>& tree) {
1131 std::vector<T> execs(tree.nodes.size(), num_traits<T>::from_int(0));
1132 if (tree.root == npos) return execs;
1133 execs[tree.root] = num_traits<T>::from_int(1);
1134 std::vector<std::size_t> stack(1, tree.root);
1135 while (!stack.empty()) {
1136 const std::size_t k = stack.back();
1137 stack.pop_back();
1138 const SPNode<T>& node = tree.nodes[k];
1139 for (std::size_t i = 0; i < node.kids.size(); ++i) {
1140 const std::size_t c = node.kids[i];
1141 if (node.type == SPNodeType::OR) {
1142 execs[c] = T(execs[k] * node.probs[i]);
1143 } else if (node.type == SPNodeType::LOOP) {
1144 execs[c] = T(execs[k] * node.count);
1145 } else {
1146 execs[c] = execs[k];
1147 }
1148 stack.push_back(c);
1149 }
1150 }
1151 return execs;
1152 }
1153
1154 // -----------------------------------------------------------------
1155 // Block composition, the fallback for a graph that is not series-parallel
1156 // -----------------------------------------------------------------
1157
1158 struct ForkInfo {
1159 bool is_and = true;
1160 std::size_t pre_act = npos;
1161 std::vector<std::size_t> post_acts;
1162 std::vector<T> probs;
1163 };
1164
1165 struct JoinInfo {
1166 bool is_and = true;
1167 std::vector<std::size_t> pre_acts;
1168 std::size_t post_act = npos;
1169 };
1170
1171 struct LoopInfo {
1172 std::size_t pre_act = npos;
1173 std::vector<std::size_t> body_acts;
1174 std::size_t end_act = npos;
1175 T count = num_traits<T>::from_int(1);
1176 };
1177
1178 struct Structure {
1179 std::vector<std::vector<std::size_t>> adj;
1180 std::vector<std::size_t> in_deg, out_deg;
1181 std::vector<ForkInfo> forks;
1182 std::vector<JoinInfo> joins;
1183 std::vector<LoopInfo> loops;
1184 };
1185
1186 Structure analyze_structure() const {
1187 const std::size_t n = activities_.size();
1188 Structure st;
1189 st.adj.assign(n, std::vector<std::size_t>());
1190 st.in_deg.assign(n, 0);
1191 st.out_deg.assign(n, 0);
1192
1193 for (const Precedence<T>& prec : precedences_) {
1194 const std::vector<std::size_t> pre = sp_indices_of(prec.pre_acts);
1195 const std::vector<std::size_t> post = sp_indices_of(prec.post_acts);
1196 for (std::size_t i : pre)
1197 for (std::size_t j : post) {
1198 st.adj[i].push_back(j);
1199 ++st.out_deg[i];
1200 ++st.in_deg[j];
1201 }
1202
1203 if (prec.post_type == PrecedenceType::POST_AND) {
1204 ForkInfo f;
1205 f.is_and = true;
1206 f.pre_act = pre[0];
1207 f.post_acts = post;
1208 st.forks.push_back(f);
1209 } else if (prec.post_type == PrecedenceType::POST_OR) {
1210 ForkInfo f;
1211 f.is_and = false;
1212 f.pre_act = pre[0];
1213 f.post_acts = post;
1214 f.probs = prec.post_params;
1215 st.forks.push_back(f);
1216 } else if (prec.post_type == PrecedenceType::POST_LOOP) {
1217 LoopInfo l;
1218 l.pre_act = pre[0];
1219 if (post.size() > 1) {
1220 l.body_acts.assign(post.begin(), post.end() - 1);
1221 l.end_act = post.back();
1222 } else {
1223 l.body_acts = post;
1224 }
1225 l.count = prec.post_params.empty() ? num_traits<T>::from_int(1)
1226 : prec.post_params[0];
1227 st.loops.push_back(l);
1228 }
1229
1230 if (prec.pre_type == PrecedenceType::PRE_AND ||
1231 prec.pre_type == PrecedenceType::PRE_OR) {
1232 JoinInfo j;
1233 j.is_and = prec.pre_type == PrecedenceType::PRE_AND;
1234 j.pre_acts = pre;
1235 j.post_act = post[0];
1236 st.joins.push_back(j);
1237 }
1238 }
1239 return st;
1240 }
1241
1242 std::vector<std::size_t> topological_sort(
1243 const std::vector<std::vector<std::size_t>>& adj) const {
1244 const std::size_t n = activities_.size();
1245 std::vector<std::size_t> in_deg(n, 0);
1246 for (const std::vector<std::size_t>& nbrs : adj)
1247 for (std::size_t j : nbrs) ++in_deg[j];
1248
1249 std::vector<std::size_t> queue, order;
1250 for (std::size_t i = 0; i < n; ++i)
1251 if (in_deg[i] == 0) queue.push_back(i);
1252 std::size_t head = 0;
1253 while (head < queue.size()) {
1254 const std::size_t cur = queue[head++];
1255 order.push_back(cur);
1256 for (std::size_t nx : adj[cur])
1257 if (--in_deg[nx] == 0) queue.push_back(nx);
1258 }
1259 std::vector<bool> seen(n, false);
1260 for (std::size_t i : order) seen[i] = true;
1261 for (std::size_t i = 0; i < n; ++i)
1262 if (!seen[i]) order.push_back(i);
1263 return order;
1264 }
1265
1266 /**
1267 * The block composition.
1268 *
1269 * A HEURISTIC, and the reason the series-parallel reduction exists: it
1270 * folds each fork/join and loop block independently and then chains what is
1271 * left in topological order, so a block nested inside another is not
1272 * reduced exactly.
1273 */
1274 PhLaw<T> build_ctmc() {
1275 const std::size_t n = activities_.size();
1276 if (n == 1) return activities_[0].ph_representation();
1277
1278 const Structure st = analyze_structure();
1279
1280 std::vector<PhLaw<T>> block(n);
1281 std::vector<bool> absorbed(n, false);
1282 for (std::size_t i = 0; i < n; ++i) block[i] = activities_[i].ph_representation();
1283
1284 for (const LoopInfo& loop : st.loops) {
1285 PhLaw<T> body = activities_[loop.body_acts[0]].ph_representation();
1286 for (std::size_t j = 1; j < loop.body_acts.size(); ++j)
1287 body = compose_serial(body, activities_[loop.body_acts[j]].ph_representation());
1288
1289 PhLaw<T> res = compose_serial(block[loop.pre_act],
1290 compose_loop_geometric(body, loop.count));
1291 if (loop.end_act != npos) {
1292 res = compose_serial(res, activities_[loop.end_act].ph_representation());
1293 absorbed[loop.end_act] = true;
1294 }
1295 block[loop.pre_act] = res;
1296 for (std::size_t idx : loop.body_acts) absorbed[idx] = true;
1297 }
1298
1299 for (const ForkInfo& fork : st.forks) {
1300 const JoinInfo* join = find_matching_join(fork.post_acts, st.joins, fork.is_and);
1301 if (fork.is_and && join == nullptr) continue;
1302
1303 PhLaw<T> inner;
1304 if (fork.is_and) {
1305 inner = block[fork.post_acts[0]];
1306 for (std::size_t i = 1; i < fork.post_acts.size(); ++i)
1307 inner = compose_parallel(inner, block[fork.post_acts[i]]);
1308 } else {
1309 std::vector<PhLaw<T>> laws;
1310 for (std::size_t idx : fork.post_acts) laws.push_back(block[idx]);
1311 inner = compose_mixture(laws, fork.probs);
1312 }
1313
1314 PhLaw<T> res = compose_serial(block[fork.pre_act], inner);
1315 if (join != nullptr && !absorbed[join->post_act]) {
1316 res = compose_serial(res, block[join->post_act]);
1317 absorbed[join->post_act] = true;
1318 }
1319 block[fork.pre_act] = res;
1320 for (std::size_t idx : fork.post_acts) absorbed[idx] = true;
1321 }
1322
1323 const std::vector<std::size_t> order = topological_sort(st.adj);
1324 bool started = false;
1325 PhLaw<T> out;
1326 for (std::size_t idx : order) {
1327 if (absorbed[idx]) continue;
1328 if (!started) {
1329 out = block[idx];
1330 started = true;
1331 } else {
1332 out = compose_serial(out, block[idx]);
1333 }
1334 }
1335 if (!started) out = activities_[0].ph_representation();
1336 return out;
1337 }
1338
1339 static const JoinInfo* find_matching_join(const std::vector<std::size_t>& post_acts,
1340 const std::vector<JoinInfo>& joins, bool is_and) {
1341 std::vector<std::size_t> want = post_acts;
1342 std::sort(want.begin(), want.end());
1343 for (const JoinInfo& j : joins) {
1344 if (j.is_and != is_and) continue;
1345 std::vector<std::size_t> have = j.pre_acts;
1346 std::sort(have.begin(), have.end());
1347 if (have == want) return &j;
1348 }
1349 return nullptr;
1350 }
1351
1352 std::string name_;
1353 std::vector<WorkflowActivity<T>> activities_;
1354 std::map<std::string, std::size_t> activity_map_;
1355 std::vector<Precedence<T>> precedences_;
1356
1357 Distrib<T> cached_ph_;
1358 bool cached_valid_ = false;
1359 SPTree<T> sp_tree_;
1360 bool sp_built_ = false;
1361 bool sp_failed_ = false;
1362};
1363
1364} // namespace workflow
1365} // namespace line
1366
1367#endif // LINE_LANG_WORKFLOW_WORKFLOW_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
Workflow(const std::string &name)
Definition workflow.h:227
A computational activity.
Definition workflow.h:101
WorkflowActivity(const std::string &name, const Distrib< T > &host_demand)
Definition workflow.h:104
void set_host_demand(const Distrib< T > &d)
Definition workflow.h:110
const std::string & name() const
Definition workflow.h:107
std::size_t num_phases() const
Definition workflow.h:150
PhLaw< T > ph_representation() const
The (alpha, T) pair of this activity.
Definition workflow.h:123
const Distrib< T > & host_demand() const
Definition workflow.h:109
static bool is_acyclic_generator(const Matrix< T > &S)
True when the phase graph of S has no cycle.
Definition workflow.h:657
static Precedence< T > Loop(const std::string &pre, const std::vector< std::string > &posts, const T &count)
A loop: PRE runs once, then POSTS[0..n-2] repeat a geometric number of times of mean COUNT and POSTS[...
Definition workflow.h:753
static Precedence< T > AndJoin(const std::vector< std::string > &pres, const std::string &post, const std::vector< T > &quorum=std::vector< T >())
An empty QUORUM means a full join; a partial one is refused by validate.
Definition workflow.h:716
const SPTree< T > * sp_tree()
The cached series-parallel decomposition, or null when the precedence graph is not series-parallel.
Definition workflow.h:446
void invalidate_topology()
Discard the cached law and the decomposition.
Definition workflow.h:396
void set_activity_demand(const std::string &name, const Distrib< T > &host_demand)
Change the host demand of one activity.
Definition workflow.h:361
static PhLaw< T > compose_repeat(const PhLaw< T > &body, long count)
COUNT-fold convolution of a phase-type law.
Definition workflow.h:638
std::size_t add_activity(const std::string &name, const Distrib< T > &host_demand)
Add an activity; the name must be unique.
Definition workflow.h:232
static PhLaw< T > compose_loop_geometric(const PhLaw< T > &body, const T &count)
Geometric repetition of a phase-type law, the POST_LOOP semantics.
Definition workflow.h:585
void add_precedence(const Precedence< T > &prec)
Definition workflow.h:247
const std::string & name() const
Definition workflow.h:229
std::size_t add_activity(const std::string &name, const T &mean)
Add an activity with an exponential host demand of the given mean.
Definition workflow.h:243
const std::vector< WorkflowActivity< T > > & activities() const
Definition workflow.h:253
static std::vector< Precedence< T > > SerialSequence(const std::vector< std::string > &acts)
Definition workflow.h:698
static constexpr std::size_t npos
Definition workflow.h:225
void invalidate_activity(std::size_t act_idx)
Mark one activity law dirty, keeping every other cached block.
Definition workflow.h:407
static Precedence< T > AndFork(const std::string &pre, const std::vector< std::string > &posts)
Definition workflow.h:705
static Precedence< T > OrFork(const std::string &pre, const std::vector< std::string > &posts, const std::vector< T > &probs)
Definition workflow.h:728
void set_activity_demand_mean(const std::string &name, const T &mean_value)
Change only the mean of one activity, preserving its shape.
Definition workflow.h:374
Distrib< T > refresh_ph()
Recompose the law after a demand change.
Definition workflow.h:353
std::size_t num_activities() const
Definition workflow.h:252
void validate() const
Validate the workflow, throwing on the first defect.
Definition workflow.h:278
void rescale_activity_leaf(std::size_t act_idx, const T &factor)
Time-scale a cached leaf in place: S -> S*FACTOR with alpha fixed.
Definition workflow.h:424
std::size_t activity_index(const std::string &name) const
Definition workflow.h:256
static PhLaw< T > compose_parallel(const PhLaw< T > &a, const PhLaw< T > &b)
Parallel (AND-fork/join) composition: the time until BOTH complete.
Definition workflow.h:496
static PhLaw< T > compose_serial(const PhLaw< T > &a, const PhLaw< T > &b)
Serial composition: the second law starts when the first absorbs.
Definition workflow.h:463
static Precedence< T > OrJoin(const std::vector< std::string > &pres, const std::string &post)
Definition workflow.h:739
const std::vector< Precedence< T > > & precedences() const
Definition workflow.h:254
Distrib< T > to_ph()
The composed law of the workflow.
Definition workflow.h:331
Workflow(const std::string &name)
Definition workflow.h:227
static Precedence< T > Serial(const std::string &pre, const std::string &post)
Definition workflow.h:689
static PhLaw< T > compose_mixture(const std::vector< PhLaw< T > > &laws, const std::vector< T > &probs)
Probabilistic mixture: a block-diagonal generator whose initial vector picks branch i with probabilit...
Definition workflow.h:548
WorkflowActivity< T > & activity(const std::string &name)
Definition workflow.h:261
const WorkflowActivity< T > & activity_at(std::size_t i) const
Activity by index, the form a series-parallel LEAF node names it in.
Definition workflow.h:268
The moment fitters the reference distributions carry as STATIC FACTORIES: Erlang.fitMeanAndOrder,...
Rate-scaled copy of a distribution, preserving its shape.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
PrecedenceType
Activity precedence kinds, with the values of MATLAB ActivityPrecedenceType.
Definition lang_types.h:470
Distrib< T > aph_fit_mean_scv(const T &mean, const T &scv)
APH.fitMeanAndSCV(MEAN, SCV), through mam::aph_fit_mean_scv.
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
Distrib< T > dist_scale_rate(const Distrib< T > &d, const T &factor)
The law of X / factor, in the same family as d.
SPNodeType
A node of the series-parallel tree.
Definition workflow.h:189
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
std::vector< T > params
Constructor arguments, in MATLAB getParam order.
Definition lang_types.h:734
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
static Distrib phase_type(const std::vector< T > &alpha, const Matrix< T > &A, bool acyclic)
PH / APH given by (alpha, A): D0 = A and D1 = (-A e) alpha.
static Distrib exp_mean(const T &m)
Definition lang_types.h:799
static constexpr double Immediate
Rate of an Immediate distribution; its mean is 1/Immediate = 1e-8.
Definition lang_types.h:674
static constexpr double FineTol
Definition lang_types.h:668
static constexpr double ArcTol
Below this an off-diagonal entry is NO ARC of the phase / state graph.
Definition lang_types.h:672
A phase-type law as the composition rules pass it around.
Definition workflow.h:89
std::vector< T > alpha
Definition workflow.h:90
One precedence of the activity graph.
Definition workflow.h:78
std::vector< T > pre_params
Definition workflow.h:83
PrecedenceType pre_type
Definition workflow.h:81
PrecedenceType post_type
Definition workflow.h:82
std::vector< T > post_params
Definition workflow.h:84
std::vector< std::string > post_acts
Definition workflow.h:80
std::vector< std::string > pre_acts
Definition workflow.h:79
T count
Loop count for a LOOP node.
Definition workflow.h:201
std::vector< T > probs
Branch probabilities for an OR node.
Definition workflow.h:199
std::vector< std::size_t > kids
Definition workflow.h:196
std::size_t act
Activity index for a LEAF, npos otherwise.
Definition workflow.h:195
The flat series-parallel tree.
Definition workflow.h:214
std::vector< SPNode< T > > nodes
Definition workflow.h:215
std::vector< T > execs
Definition workflow.h:219
std::vector< std::size_t > leaf_of
Node index of each activity's leaf, npos when the activity has none.
Definition workflow.h:218