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