LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
workflow_net.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_WFN_WORKFLOW_NET_H
6#define LINE_API_WFN_WORKFLOW_NET_H
7
8/**
9 * @file
10 * @ingroup api_wfn
11 * A Workflow Net (WF-net) model for performance analysis.
12 *
13 * A WF-net (van der Aalst, 1997/98) is a Petri net with exactly one source
14 * place i (no incoming arcs), exactly one sink place o (no outgoing arcs), and
15 * every place and transition on a directed path from i to o. A case is one
16 * token put in i; it is complete when one token reaches o.
17 *
18 * WorkflowNet has no solver of its own. It checks that the net is a WF-net and
19 * that it is sound, and then either analyses one case exactly or compiles the
20 * net into an ordinary Network:
21 *
22 * validate() structural WF-net conditions
23 * check_soundness(k) k-soundness on the reachability graph
24 * get_cycle_time() exact phase-type law of the duration of one case
25 * without contention (any sound net, PH timing)
26 * get_cycle_time_analysis() moments, CDF, percentiles, expected firings per
27 * transition and expected token-time per place
28 * get_resource_loads(lambda) per-resource load of an open stream of cases
29 * to_workflow() equivalent Workflow, for block-structured nets
30 * to_network("forkjoin") fork-join queueing network, block-structured nets
31 * to_network("spn") stochastic Petri net with a case-counter place,
32 * any sound net
33 * get_case_metrics(solver) case throughput, WIP, cycle time and resource
34 * utilisation from a solver of a compiled network
35 *
36 * TIMING. A timed transition fires after a phase-type delay (Exp, Erlang,
37 * HyperExp, Coxian, Cox2, APH, PH) under race semantics with enabling memory: a
38 * transition disabled by a conflicting firing loses its progress. An immediate
39 * transition fires in zero time; among the enabled immediate transitions only
40 * those of the highest priority fire, chosen in proportion to their weights.
41 *
42 * SEMANTICS OF THE TWO NETWORK TARGETS WITH MANY CASES. The fork-join target
43 * keeps case identity, since LINE joins only synchronise siblings of the same
44 * job. The SPN target uses uncoloured tokens, so an AND-join may pair tokens of
45 * different cases; its case-counter place gives the mean number of cases in the
46 * system and, by Little's law, the exact mean cycle time under that semantics.
47 * Percentiles are not available from it.
48 *
49 * wfn::WorkflowNet net("SimpleWFN");
50 * net.add_place("i"); net.add_place("p"); net.add_place("o");
51 * net.add_transition("Task1", Exp(1.0));
52 * net.add_transition("Task2", Exp(2.0));
53 * net.add_arc("i", "Task1"); net.add_arc("Task1", "p");
54 * net.add_arc("p", "Task2"); net.add_arc("Task2", "o");
55 * const wfn::SoundnessReport rep = net.check_soundness();
56 * const Dist ph = net.get_cycle_time(); // hypoexponential, mean 1.5
57 *
58 * References:
59 * W.M.P. van der Aalst, "The Application of Petri Nets to Workflow
60 * Management", J. Circuits, Systems and Computers 8(1), 1998.
61 *
62 * MATLAB twin (reference): matlab/src/api/wfn/WorkflowNet.m
63 */
64
65#include <algorithm>
66#include <cmath>
67#include <cstddef>
68#include <cstdio>
69#include <deque>
70#include <limits>
71#include <map>
72#include <memory>
73#include <string>
74#include <utility>
75#include <vector>
76
82#include "line/io/pnml.h"
86#include "line/solvers/solver.h"
87#include "line/util/error.h"
88#include "line/util/expm.h"
89#include "line/util/lu.h"
90#include "line/util/matrix.h"
91
92namespace line {
93namespace wfn {
94
95namespace detail {
96
97/** `sprintf('%g', x)`. */
98inline std::string num_g(double x) {
99 char buf[64];
100 std::snprintf(buf, sizeof buf, "%g", x);
101 return buf;
102}
103
104/** `sprintf('%d', x)` for an integral double, which is how MATLAB prints a count held in a double. */
105inline std::string num_d(double x) {
106 if (std::isinf(x)) return x > 0 ? "Inf" : "-Inf";
107 char buf[64];
108 std::snprintf(buf, sizeof buf, "%.0f", x);
109 return buf;
110}
111
112/** An LU factorisation reused across right-hand sides. */
113struct Factor {
114 Matrix<double> lu;
115 std::vector<std::size_t> piv;
116 explicit Factor(const Matrix<double>& A) : lu(A) { piv = lu_factor(lu); }
117 std::vector<double> solve(std::vector<double> b) const {
118 lu_solve(lu, piv, b);
119 return b;
120 }
121};
122
123inline Matrix<double> transpose(const Matrix<double>& A) {
124 Matrix<double> B(A.cols(), A.rows(), 0.0);
125 for (std::size_t i = 0; i < A.rows(); ++i)
126 for (std::size_t j = 0; j < A.cols(); ++j) B(j, i) = A(i, j);
127 return B;
128}
129
130/** P(duration <= t) of the possibly defective PH law (alpha, Q); the defect is an atom at zero. */
131inline double ph_cdf(const std::vector<double>& alpha, const Matrix<double>& Q, double t) {
132 double mass = 0.0;
133 for (double a : alpha) mass += a;
134 if (t <= 0) return 1.0 - mass;
135 const Matrix<double> E = expm(Q, t);
136 double surv = 0.0;
137 for (std::size_t i = 0; i < alpha.size(); ++i)
138 for (std::size_t j = 0; j < alpha.size(); ++j) surv += alpha[i] * E(i, j);
139 return 1.0 - surv;
140}
141
142/** The q-quantile by bisection, bracketed by doubling from the mean MU. */
143inline double ph_quantile(const std::vector<double>& alpha, const Matrix<double>& Q, double q, double mu) {
144 if (!(q > 0 && q < 1))
145 throw InputError(ErrorKind("Argument"), "Percentile levels must lie in (0,1).");
146 if (ph_cdf(alpha, Q, 0) >= q) return 0.0;
147 double hi = mu;
148 while (ph_cdf(alpha, Q, hi) < q) hi = 2 * hi;
149 double lo = 0.0;
150 for (int it = 1; it <= 200; ++it) {
151 const double mid = (lo + hi) / 2;
152 if (ph_cdf(alpha, Q, mid) < q)
153 lo = mid;
154 else
155 hi = mid;
156 if (hi - lo <= 1e-12 * std::max(1.0, hi)) break;
157 }
158 return (lo + hi) / 2;
159}
160
161/** A process-tree node of a block-structured net, as `reduceToTree` builds it. */
162struct TreeNode {
163 enum Type { EPS, TASK, SEQ, AND, XOR, LOOP };
164 Type type = EPS;
165 std::size_t t = 0; ///< transition index of a TASK
166 std::vector<TreeNode> kids; ///< SEQ/AND/XOR children; LOOP holds {do, redo}
167 std::vector<double> probs; ///< XOR branch probabilities
168 double q = 0.0; ///< LOOP repeat probability
169};
170
171inline TreeNode tree_eps() { return TreeNode(); }
172
173inline TreeNode tree_collapse(TreeNode::Type type, std::vector<TreeNode> kids) {
174 if (kids.empty()) return tree_eps();
175 if (kids.size() == 1) return kids[0];
176 TreeNode n;
177 n.type = type;
178 n.kids = std::move(kids);
179 return n;
180}
181
182/** Flatten nested blocks of the same TYPE and drop the zero-time parts. */
183inline TreeNode tree_flat(TreeNode::Type type, const std::vector<TreeNode>& parts) {
184 std::vector<TreeNode> kids;
185 for (const TreeNode& p : parts) {
186 if (p.type == TreeNode::EPS) continue;
187 if (p.type == type)
188 kids.insert(kids.end(), p.kids.begin(), p.kids.end());
189 else
190 kids.push_back(p);
191 }
192 return tree_collapse(type, kids);
193}
194
195inline TreeNode tree_seq(const std::vector<TreeNode>& parts) { return tree_flat(TreeNode::SEQ, parts); }
196inline TreeNode tree_and(const std::vector<TreeNode>& parts) { return tree_flat(TreeNode::AND, parts); }
197
198inline TreeNode tree_xor(const std::vector<TreeNode>& parts, const std::vector<double>& probs) {
199 TreeNode n;
200 n.type = TreeNode::XOR;
201 for (std::size_t j = 0; j < parts.size(); ++j) {
202 const TreeNode& p = parts[j];
203 if (p.type == TreeNode::XOR) {
204 n.kids.insert(n.kids.end(), p.kids.begin(), p.kids.end());
205 for (double pr : p.probs) n.probs.push_back(probs[j] * pr);
206 } else {
207 n.kids.push_back(p);
208 n.probs.push_back(probs[j]);
209 }
210 }
211 bool all_eps = true;
212 for (const TreeNode& k : n.kids) all_eps = all_eps && k.type == TreeNode::EPS;
213 return all_eps ? tree_eps() : n;
214}
215
216inline TreeNode tree_loop(const TreeNode& do_part, const TreeNode& redo_part, double q) {
217 TreeNode n;
218 n.type = TreeNode::LOOP;
219 n.kids.push_back(do_part);
220 n.kids.push_back(redo_part);
221 n.q = q;
222 return n;
223}
224
225/** One row of an AvgTable; a pair the solver did not report is zero, as in MATLAB's getAvg. */
226inline double table_cell(const AvgTable& tab, const std::vector<double>& col, const std::string& station,
227 const std::string& cls) {
228 for (std::size_t i = 0; i < tab.Station.size(); ++i)
229 if (tab.Station[i] == station && tab.JobClass[i] == cls && i < col.size())
230 return std::isnan(col[i]) ? 0.0 : col[i];
231 return 0.0;
232}
233
234/** `sum(M(station, :))` over the classes. */
235inline double table_sum(const AvgTable& tab, const std::vector<double>& col, const std::string& station) {
236 double s = 0.0;
237 for (std::size_t i = 0; i < tab.Station.size(); ++i)
238 if (tab.Station[i] == station && i < col.size() && !std::isnan(col[i])) s += col[i];
239 return s;
240}
241
242} // namespace detail
243
244/** The full single-case analysis of `get_cycle_time_analysis`. */
246 /** PH law of the case duration. */
248 double mean = 0.0;
249 double variance = 0.0;
250 double scv = 0.0;
251 /** The first three raw moments. */
252 std::vector<double> moments;
253 /** Probability that a case completes in zero time. */
254 double zero_time_prob = 0.0;
255 /** The (alpha, Q) pair behind `ph`; alpha may be defective by `zero_time_prob`. */
256 std::vector<double> alpha;
258 /** Number of tangible transient states. */
259 std::size_t num_states = 0;
260
261 struct Firings {
262 std::vector<std::string> Transition;
263 std::vector<double> ExpectedFirings;
265 struct PlaceTime {
266 std::vector<std::string> Place;
267 std::vector<double> ExpectedTokenTime;
269
270 /** P(duration <= t). */
271 double cdf(double t) const { return detail::ph_cdf(alpha, Q, t); }
272 /** The q-quantile, q in (0,1). */
273 double percentile(double q) const { return detail::ph_quantile(alpha, Q, q, mean); }
274};
275
276/** The table of `get_resource_loads`. */
278 std::vector<std::string> Resource;
279 std::vector<double> Servers, DemandPerCase, Load;
280 std::vector<bool> Stable;
281 std::size_t size() const { return Resource.size(); }
282};
283
284/** Case-level metrics of `get_case_metrics`. */
286 std::string target; ///< "forkjoin" or "spn"
287 std::string mode; ///< "open" or "closed"
288 double throughput = 0.0;
289 double wip = 0.0;
290 double cycle_time = 0.0;
291 struct Resources {
292 std::vector<std::string> Resource;
293 std::vector<double> Servers, Utilization;
295 /** RespT at its station for the fork-join target, InService tokens for the SPN one. */
296 struct Tasks {
297 std::vector<std::string> Task;
298 std::vector<double> RespT, InService;
300 /** Mean tokens per place, SPN target only. */
301 struct Places {
302 std::vector<std::string> Place;
303 std::vector<double> QLen;
305};
306
307/** A place, a transition, or the name of either, where an argument may be any of them. */
309public:
310 ElementRef(const WFNPlace& p) : place_(&p) {}
311 ElementRef(const WFNTransition& t) : transition_(&t) {}
312 ElementRef(const std::string& n) : name_(n), by_name_(true) {}
313 ElementRef(const char* n) : name_(n), by_name_(true) {}
314
315private:
316 friend class WorkflowNet;
317 const WFNPlace* place_ = nullptr;
318 const WFNTransition* transition_ = nullptr;
319 std::string name_;
320 bool by_name_ = false;
321};
322
324public:
326
327 /** Names prefixed so that compiled networks never collide with user names. */
328 static constexpr const char* ReservedPrefix = "wfn_";
329
330 explicit WorkflowNet(const std::string& name) : name_(name) {}
331
334 WorkflowNet(const WorkflowNet&) = delete;
336
337 const std::string& name() const { return name_; }
338 std::size_t num_places() const { return places_.size(); }
339 std::size_t num_transitions() const { return transitions_.size(); }
340 const WFNPlace& place(std::size_t i) const { return *places_.at(i); }
341 WFNTransition& transition(std::size_t i) { return *transitions_.at(i); }
342 const WFNTransition& transition(std::size_t i) const { return *transitions_.at(i); }
343 const std::vector<WFNArc>& arcs() const { return arcs_; }
344 std::size_t num_resources() const { return resources_.size(); }
345 const WFNResource& resource(std::size_t r) const { return *resources_.at(r); }
346 /** The source place, set by `validate` when not declared; nullptr before. */
347 const WFNPlace* source_place() const { return source_ < 0 ? nullptr : places_[source_].get(); }
348 /** The sink place, set by `validate` when not declared; nullptr before. */
349 const WFNPlace* sink_place() const { return sink_ < 0 ? nullptr : places_[sink_].get(); }
350 /** The interarrival law of cases (open analysis), or nullptr. */
351 const Dist* arrival_process() const { return has_arrival_ ? &arrival_ : nullptr; }
352 long closed_cases() const { return closed_cases_; }
353 double state_cap() const { return state_cap_; }
354
355 // ------------------------------------------------------------------
356 // Construction
357 // ------------------------------------------------------------------
358
359 /**
360 * `addPlace(name, initialMarking)`. The initial marking is informational:
361 * the analyses always start from k tokens in the source place.
362 */
363 WFNPlace& add_place(const std::string& name, double initial_marking = 0.0) {
364 check_new_name(name);
365 places_.emplace_back(new WFNPlace(name, places_.size(), initial_marking));
366 place_map_[name] = places_.size() - 1;
367 return *places_.back();
368 }
369
370 /**
371 * `addTransition(name, distribution, [], servers, priority, weight)`: the
372 * timing follows the law, IMMEDIATE for an Immediate law and TIMED for any
373 * other. SERVERS is the number of concurrent firings (default Inf, i.e. no
374 * contention unless a resource is assigned); PRIORITY and WEIGHT rank and
375 * share the enabled immediate transitions.
376 */
377 WFNTransition& add_transition(const std::string& name, const Dist& distribution = Exp(1.0),
378 double servers = std::numeric_limits<double>::infinity(),
379 double priority = 1.0, double weight = 1.0) {
380 const lang::TimingStrategy timing = distribution.type == lang::ProcessType::IMMEDIATE
383 return add_transition(name, distribution, timing, servers, priority, weight);
384 }
385
386 /** `addTransition(name, distribution, timing, servers, priority, weight)`. */
387 WFNTransition& add_transition(const std::string& name, const Dist& distribution,
389 double servers = std::numeric_limits<double>::infinity(),
390 double priority = 1.0, double weight = 1.0) {
391 Dist d = distribution;
392 if (timing == lang::TimingStrategy::IMMEDIATE) {
393 d = Immediate();
394 } else if (distribution.type == lang::ProcessType::IMMEDIATE) {
395 throw InputError(ErrorKind("Argument"),
396 "Timed transition \"" + name +
397 "\" cannot have an Immediate distribution; declare it TimingStrategy.IMMEDIATE.");
398 }
399 if (!(servers >= 1))
400 throw InputError(ErrorKind("Argument"), "Number of servers must be at least 1.");
401 if (!(weight > 0) || !(priority > 0))
402 throw InputError(ErrorKind("Argument"), "Firing weight and priority must be positive.");
403 check_new_name(name);
404 transitions_.emplace_back(
405 new WFNTransition(name, transitions_.size(), d, timing, servers, priority, weight));
406 transition_map_[name] = transitions_.size() - 1;
407 return *transitions_.back();
408 }
409
410 /**
411 * `addArc(source, target, weight)`: from a place to a transition or from a
412 * transition to a place, given as elements of this net or by name. WEIGHT
413 * is a positive integer.
414 */
415 const WFNArc& add_arc(const ElementRef& source, const ElementRef& target, double weight = 1.0) {
416 const ElementRef src = resolve(source);
417 const ElementRef tgt = resolve(target);
418 check_owned(src);
419 check_owned(tgt);
420 if ((src.place_ && tgt.place_) || (src.transition_ && tgt.transition_))
421 throw InputError(ErrorKind("Model"),
422 "Arcs must connect a place to a transition or vice versa, not two places or two "
423 "transitions.");
424 if (!(weight >= 1) || weight != std::round(weight))
425 throw InputError(ErrorKind("Model"), "Arc " + element_name(src) + " -> " + element_name(tgt) +
426 " has weight " + detail::num_g(weight) +
427 "; arc weights must be positive integers.");
428 if (src.place_)
429 arcs_.push_back(WFNArc(true, src.place_, tgt.transition_, weight));
430 else
431 arcs_.push_back(WFNArc(false, tgt.place_, src.transition_, weight));
432 return arcs_.back();
433 }
434
435 /** `setSource(place)`: declare the source place (checked by validate). */
436 void set_source(const ElementRef& place) { source_ = declared_place(place, "Source"); }
437
438 /** `setSink(place)`: declare the sink place (checked by validate). */
439 void set_sink(const ElementRef& place) { sink_ = declared_place(place, "Sink"); }
440
441 /**
442 * `setResource(transition, name, servers, scheduling)`: assign a resource,
443 * shared by every task that names it and by every case. An existing NAME is
444 * reused; SERVERS and SCHEDULING, when given, update it.
445 */
446 WFNResource& set_resource(const ElementRef& transition, const std::string& name) {
447 return set_resource_impl(transition, name, nullptr, nullptr);
448 }
449 WFNResource& set_resource(const ElementRef& transition, const std::string& name, double servers) {
450 return set_resource_impl(transition, name, &servers, nullptr);
451 }
452 WFNResource& set_resource(const ElementRef& transition, const std::string& name, double servers,
453 lang::SchedStrategy scheduling) {
454 return set_resource_impl(transition, name, &servers, &scheduling);
455 }
456
457 /** `setArrivalProcess(dist)`: any interarrival distribution for open analysis. */
458 void set_arrival_process(const Dist& process) {
459 arrival_ = process;
460 has_arrival_ = true;
461 }
462 /** `setArrivalProcess('none')`, or a type that needs a parameter, which is then refused. */
463 void set_arrival_process(const std::string& type) { set_arrival_typed(type, nullptr, nullptr); }
464 /** `setArrivalProcess('poisson', rate)`. */
465 void set_arrival_process(const std::string& type, double rate) { set_arrival_typed(type, &rate, nullptr); }
466 /** `setArrivalProcess('map', MAP)`. */
467 void set_arrival_process(const std::string& type, const Dist& map) { set_arrival_typed(type, nullptr, &map); }
468
469 /** `setClosedCases(k)`: k concurrent cases (closed analysis); 0 = open. */
470 void set_closed_cases(double k) {
471 if (!(k >= 0) || k != std::round(k) || std::isinf(k))
472 throw InputError(ErrorKind("Argument"), "The number of closed cases must be a non-negative integer.");
473 closed_cases_ = static_cast<long>(k);
474 }
475
476 /** `setStateCap(cap)`: the largest state space explored by the analyses. */
477 void set_state_cap(double cap) {
478 if (!(cap >= 1)) throw InputError(ErrorKind("Argument"), "The state cap must be a positive number.");
479 state_cap_ = cap;
480 }
481
482 /** `getPlace(name)`. */
483 const WFNPlace& get_place(const std::string& name) const {
484 const auto it = place_map_.find(name);
485 if (it == place_map_.end()) throw InputError(ErrorKind("Model"), "Place \"" + name + "\" not found.");
486 return *places_[it->second];
487 }
488
489 /** `getTransition(name)`. */
490 WFNTransition& get_transition(const std::string& name) {
491 const auto it = transition_map_.find(name);
492 if (it == transition_map_.end())
493 throw InputError(ErrorKind("Model"), "Transition \"" + name + "\" not found.");
494 return *transitions_[it->second];
495 }
496
497 /** `getIncidence()`: input and output arc weights, places x transitions. */
498 std::pair<Matrix<double>, Matrix<double>> get_incidence() const {
499 const Inc inc = incidence();
500 Matrix<double> Pre(inc.nP, inc.nT, 0.0), Post(inc.nP, inc.nT, 0.0);
501 for (std::size_t p = 0; p < inc.nP; ++p)
502 for (std::size_t t = 0; t < inc.nT; ++t) {
503 Pre(p, t) = static_cast<double>(inc.pre[p][t]);
504 Post(p, t) = static_cast<double>(inc.post[p][t]);
505 }
506 return std::make_pair(Pre, Post);
507 }
508
509 // ------------------------------------------------------------------
510 // Structure and soundness
511 // ------------------------------------------------------------------
512
513 /**
514 * `validate()`: exactly one place without incoming arcs (the source i),
515 * exactly one without outgoing arcs (the sink o), and every place and
516 * transition on a directed path from i to o. A declared source or sink must
517 * be that place. On success the source and sink are set; on failure a
518 * LINE:input:Model error names the offending nodes.
519 */
520 void validate() {
521 if (places_.empty())
522 throw InputError(ErrorKind("Model"), "A workflow net must have at least one place.");
523 if (transitions_.empty())
524 throw InputError(ErrorKind("Model"), "A workflow net must have at least one transition.");
525 const Inc inc = incidence();
526 const std::size_t nP = inc.nP, nT = inc.nT;
527 for (std::size_t t = 0; t < nT; ++t) {
528 bool in = false, out = false;
529 for (std::size_t p = 0; p < nP; ++p) {
530 in = in || inc.pre[p][t] != 0;
531 out = out || inc.post[p][t] != 0;
532 }
533 if (!in)
534 throw InputError(ErrorKind("Model"), "Transition \"" + transitions_[t]->name() +
535 "\" has no input place, so it is not on a path from the "
536 "source.");
537 if (!out)
538 throw InputError(ErrorKind("Model"), "Transition \"" + transitions_[t]->name() +
539 "\" has no output place, so it is not on a path to the "
540 "sink.");
541 }
542 std::vector<std::size_t> src_cand, snk_cand;
543 for (std::size_t p = 0; p < nP; ++p) {
544 long in = 0, out = 0;
545 for (std::size_t t = 0; t < nT; ++t) {
546 in += inc.post[p][t];
547 out += inc.pre[p][t];
548 }
549 if (in == 0) src_cand.push_back(p);
550 if (out == 0) snk_cand.push_back(p);
551 }
552 if (src_cand.size() != 1)
553 throw InputError(ErrorKind("Model"),
554 "A WF-net has exactly one source place (a place without incoming arcs); found " +
555 std::to_string(src_cand.size()) + ": " + place_list(src_cand) + ".");
556 if (snk_cand.size() != 1)
557 throw InputError(ErrorKind("Model"),
558 "A WF-net has exactly one sink place (a place without outgoing arcs); found " +
559 std::to_string(snk_cand.size()) + ": " + place_list(snk_cand) + ".");
560 const std::size_t s = src_cand[0], o = snk_cand[0];
561 if (source_ >= 0 && static_cast<std::size_t>(source_) != s)
562 throw InputError(ErrorKind("Model"), "Place \"" + places_[source_]->name() +
563 "\" was declared the source, but \"" + places_[s]->name() +
564 "\" is the only place without incoming arcs.");
565 if (sink_ >= 0 && static_cast<std::size_t>(sink_) != o)
566 throw InputError(ErrorKind("Model"), "Place \"" + places_[sink_]->name() +
567 "\" was declared the sink, but \"" + places_[o]->name() +
568 "\" is the only place without outgoing arcs.");
569 // bipartite graph over places 0..nP-1 then transitions nP..nP+nT-1
570 const std::size_t n = nP + nT;
571 std::vector<std::vector<std::size_t>> fwd_adj(n), bwd_adj(n);
572 for (std::size_t p = 0; p < nP; ++p)
573 for (std::size_t t = 0; t < nT; ++t) {
574 if (inc.pre[p][t] > 0) {
575 fwd_adj[p].push_back(nP + t);
576 bwd_adj[nP + t].push_back(p);
577 }
578 if (inc.post[p][t] > 0) {
579 fwd_adj[nP + t].push_back(p);
580 bwd_adj[p].push_back(nP + t);
581 }
582 }
583 const std::vector<bool> fwd = bfs(fwd_adj, std::vector<std::size_t>(1, s));
584 const std::vector<bool> bwd = bfs(bwd_adj, std::vector<std::size_t>(1, o));
585 std::string off;
586 for (std::size_t v = 0; v < n; ++v) {
587 if (fwd[v] && bwd[v]) continue;
588 if (!off.empty()) off += ", ";
589 off += "\"" + (v < nP ? places_[v]->name() : transitions_[v - nP]->name()) + "\"";
590 }
591 if (!off.empty())
592 throw InputError(ErrorKind("Model"), "Not a WF-net: " + off + " not on a path from source \"" +
593 places_[s]->name() + "\" to sink \"" + places_[o]->name() +
594 "\".");
595 source_ = static_cast<long>(s);
596 sink_ = static_cast<long>(o);
597 }
598
599 /**
600 * `checkSoundness(k, stateCap)`: decide k-soundness on the reachability graph.
601 *
602 * From the marking with K tokens in the source the net is k-sound when (1)
603 * the final marking, K tokens in the sink, is reachable from every reachable
604 * marking (option to complete), (2) no reachable marking puts K tokens in
605 * the sink together with other tokens (proper completion) and (3) every
606 * transition fires in some reachable marking (no dead transitions). The
607 * untimed net is explored, as soundness is a property of the net, not of its
608 * timing. A marking that strictly covers one of its ancestors proves the net
609 * unbounded, hence unsound. If STATECAP markings are explored first, the
610 * verdict is "unknown", never "sound".
611 */
612 SoundnessReport check_soundness(double k = 1) { return check_soundness(k, state_cap_); }
613
615 if (!(k >= 1) || k != std::round(k) || std::isinf(k))
616 throw InputError(ErrorKind("Argument"), "k must be a positive integer.");
617 validate();
618 const Inc inc = incidence();
619 const std::size_t nP = inc.nP, nT = inc.nT;
620 const long kk = static_cast<long>(k);
621 Marking m0(nP, 0), mf(nP, 0);
622 m0[source_] = kk;
623 mf[sink_] = kk;
624
625 SoundnessReport report;
626 report.k = kk;
627 report.verdict = "unknown";
628
629 Reach rg = untimed_reachability(inc, m0, state_cap);
630 report.state_space_size = rg.M.size();
631 if (rg.status == Reach::UNBOUNDED) {
632 report.verdict = "unsound";
633 report.unbounded = true;
634 report.unbounded_witness = rg.witness_cover;
635 report.witnesses.push_back("unbounded: " + marking_string(rg.witness_cover) +
636 " strictly covers its ancestor " + marking_string(rg.witness_ancestor));
637 report.sound = false;
638 return report;
639 }
640 if (rg.status == Reach::CAP) {
641 report.verdict = "unknown";
642 report.unbounded_or_too_large = true;
643 report.witnesses.push_back("state cap of " + detail::num_d(state_cap) +
644 " markings reached before the reachability graph was complete");
645 report.sound = false;
646 return report;
647 }
648
649 const std::size_t n = rg.M.size();
650 std::vector<bool> is_final(n, false);
651 bool any_final = false;
652 for (std::size_t i = 0; i < n; ++i) {
653 is_final[i] = rg.M[i] == mf;
654 any_final = any_final || is_final[i];
655 }
656 std::vector<std::size_t> outdeg(n, 0);
657 std::vector<std::vector<std::size_t>> radj(n);
658 std::vector<bool> fired(nT, false);
659 for (const ReachEdge& e : rg.E) {
660 ++outdeg[e.from];
661 radj[e.to].push_back(e.from);
662 fired[e.t] = true;
663 }
664 for (std::size_t i = 0; i < n; ++i)
665 if (outdeg[i] == 0 && !is_final[i]) {
666 report.deadlocks.push_back(rg.M[i]);
667 report.witnesses.push_back("deadlock: " + marking_string(rg.M[i]) + " enables no transition");
668 }
669 for (std::size_t i = 0; i < n; ++i)
670 if (rg.M[i][sink_] >= kk && !is_final[i]) {
671 report.improper_completion.push_back(rg.M[i]);
672 report.witnesses.push_back("improper completion: " + marking_string(rg.M[i]));
673 }
674 std::vector<bool> coreach(n, false);
675 if (any_final) {
676 std::vector<std::size_t> finals;
677 for (std::size_t i = 0; i < n; ++i)
678 if (is_final[i]) finals.push_back(i);
679 coreach = bfs(radj, finals);
680 }
681 for (std::size_t i = 0; i < n; ++i)
682 if (!coreach[i]) {
683 report.no_option_to_complete.push_back(rg.M[i]);
684 if (outdeg[i] > 0)
685 report.witnesses.push_back("no option to complete: from " + marking_string(rg.M[i]) +
686 " the final marking is unreachable");
687 }
688 for (std::size_t t = 0; t < nT; ++t)
689 if (!fired[t]) {
690 report.dead_transitions.push_back(transitions_[t]->name());
691 report.witnesses.push_back("dead transition: \"" + transitions_[t]->name() + "\" never fires");
692 }
693 report.sound = any_final && report.deadlocks.empty() && report.improper_completion.empty() &&
694 report.no_option_to_complete.empty() && report.dead_transitions.empty();
695 report.verdict = report.sound ? "sound" : "unsound";
696 return report;
697 }
698
699 /** `formatReport(report)`: one-line summary of a soundness report. */
700 static std::string format_report(const SoundnessReport& report) {
701 if (report.verdict == "sound")
702 return std::to_string(report.k) + "-sound, " + std::to_string(report.state_space_size) +
703 " reachable markings";
704 std::string str;
705 for (std::size_t i = 0; i < report.witnesses.size(); ++i) {
706 if (i) str += "; ";
707 str += report.witnesses[i];
708 }
709 return str.empty() ? report.verdict : str;
710 }
711
712 // ------------------------------------------------------------------
713 // Exact single-case analysis
714 // ------------------------------------------------------------------
715
716 /**
717 * `getCycleTime()`: exact case cycle time of one case without contention.
718 *
719 * The reachability graph of the timed net from one token in the source
720 * becomes an absorbing CTMC whose absorbing state is the final marking;
721 * immediate transitions are folded out of it, so the case duration is
722 * exactly phase-type for any sound net with phase-type timing,
723 * series-parallel or not. Requires 1-soundness.
724 */
726
727 /** `getCycleTimeAnalysis()`: the full single-case analysis. */
729 const Chain ch = single_case_chain();
730 const std::size_t n = ch.Q.rows();
731 double mass = 0.0;
732 for (double a : ch.alpha) mass += a;
733 if (n == 0 || mass <= lang::GlobalConstants::FineTol)
734 throw UnsupportedError(ErrorKind("Structure"),
735 "Every case of \"" + name_ +
736 "\" completes in zero time, so its cycle time has no phase-type law.");
737 Matrix<double> negQ(n, n, 0.0);
738 for (std::size_t i = 0; i < n; ++i)
739 for (std::size_t j = 0; j < n; ++j) negQ(i, j) = -ch.Q(i, j);
740 // row-vector solves v / (-Q) are (-Q)' x = v'
741 const detail::Factor fT(detail::transpose(negQ));
742 const std::vector<double> Nrow = fT.solve(ch.alpha);
743 std::vector<double> m(3, 0.0), v = ch.alpha;
744 double fact = 1.0;
745 for (int j = 1; j <= 3; ++j) {
746 v = fT.solve(v);
747 fact *= j;
748 double s = 0.0;
749 for (double x : v) s += x;
750 m[j - 1] = fact * s;
751 }
753 an.ph = PH(ch.alpha, ch.Q);
754 an.mean = m[0];
755 an.moments = m;
756 an.variance = m[1] - m[0] * m[0];
757 an.scv = an.variance / (m[0] * m[0]);
758 an.zero_time_prob = 1.0 - mass;
759 an.alpha = ch.alpha;
760 an.Q = ch.Q;
761 const std::size_t nT = transitions_.size(), nP = places_.size();
762 for (std::size_t t = 0; t < nT; ++t) {
763 double f = ch.firings0[t];
764 for (std::size_t s = 0; s < n; ++s) f += Nrow[s] * (ch.Rfire(s, t) + ch.Gfire(s, t));
765 an.firings.Transition.push_back(transitions_[t]->name());
766 an.firings.ExpectedFirings.push_back(f);
767 }
768 for (std::size_t p = 0; p < nP; ++p) {
769 double x = 0.0;
770 for (std::size_t s = 0; s < n; ++s) x += Nrow[s] * static_cast<double>(ch.Mtang[s][p]);
771 an.place_time.Place.push_back(places_[p]->name());
772 an.place_time.ExpectedTokenTime.push_back(x);
773 }
774 an.num_states = n;
775 return an;
776 }
777
778 /**
779 * `getResourceLoads(lambda)`: one row per resource and per task with a
780 * finite server count, the offered load LAMBDA * sum_t E[firings of t] *
781 * E[time of t] divided by the servers, from the expected firings of one
782 * case. The open system is stable only if every load is below 1.
783 */
786 const std::vector<double>& f = an.firings.ExpectedFirings;
787 ResourceLoads loads;
788 for (std::size_t r = 0; r < resources_.size(); ++r) {
789 double d = 0.0;
790 for (std::size_t t = 0; t < transitions_.size(); ++t)
791 if (transitions_[t]->resource_ == static_cast<long>(r))
792 d += f[t] * transitions_[t]->distribution().mean;
793 loads.Resource.push_back(resources_[r]->name());
794 loads.Servers.push_back(resources_[r]->servers());
795 loads.DemandPerCase.push_back(d);
796 }
797 for (std::size_t t = 0; t < transitions_.size(); ++t) {
798 const WFNTransition& tr = *transitions_[t];
799 if (tr.is_timed() && std::isfinite(tr.servers())) {
800 loads.Resource.push_back(tr.name());
801 loads.Servers.push_back(tr.servers());
802 loads.DemandPerCase.push_back(f[t] * tr.distribution().mean);
803 }
804 }
805 for (std::size_t j = 0; j < loads.Resource.size(); ++j) {
806 loads.Load.push_back(lambda * loads.DemandPerCase[j] / loads.Servers[j]);
807 loads.Stable.push_back(loads.Load[j] < 1);
808 }
809 return loads;
810 }
811
812 /** `getResourceLoads()`: LAMBDA is the rate of the arrival process. */
814 if (!has_arrival_) throw InputError(ErrorKind("Argument"), "Give LAMBDA or set an arrival process.");
815 return get_resource_loads(1.0 / arrival_.mean);
816 }
817
818 // ------------------------------------------------------------------
819 // Block structure and compilation
820 // ------------------------------------------------------------------
821
822 /**
823 * `isBlockStructured()`: true if the net reduces to a process tree. Five
824 * rules apply until nothing does: fold a single-input single-output
825 * transition between two places into a place, fold a single-input
826 * single-output place between two transitions into a transition, merge
827 * parallel places (AND block), merge parallel transitions (XOR block,
828 * choices resolved by immediate transitions of equal priority, in
829 * proportion to their weights) and absorb a self-loop transition (geometric
830 * loop). It is block-structured if it reduces to one place.
831 */
833 validate();
834 detail::TreeNode tree;
835 std::string reason;
836 return reduce_to_tree(tree, reason);
837 }
838
839 /**
840 * `toWorkflow()`: the equivalent Workflow of a block-structured net. Each
841 * timed transition is an activity with its law and each block the Workflow
842 * precedence of the same meaning. Immediate transitions disappear; where a
843 * fork or join needs an anchor the net does not provide, an Immediate
844 * activity wfn_aux_<n> is added. Workflow::Loop repeats one body a
845 * geometric number of times >= 1, so a loop with work on its return path is
846 * refused. Workflow models an Immediate activity at rate
847 * GlobalConstants::Immediate, so its to_ph() agrees with get_cycle_time()
848 * up to that rate, not to machine precision.
849 */
851 require_sound(1);
852 detail::TreeNode tree;
853 std::string reason;
854 if (!reduce_to_tree(tree, reason))
855 throw UnsupportedError(ErrorKind("Structure"),
856 "toWorkflow requires a block-structured WF-net: " + reason);
857 WfState st{workflow::Workflow<double>(name_ + "_workflow"), 0};
858 std::string h, tl;
859 wf_branch(tree, st, h, tl);
860 return st.wf;
861 }
862
863 /**
864 * `toNetwork(target, 'checkSoundness', tf)`: compile the net to a Network.
865 *
866 * "forkjoin": block-structured nets. AND-split -> Fork, AND-join -> Join,
867 * XOR and loops -> probabilistic routing, a task -> Delay (no contention), a
868 * Queue of its own (finite servers) or the Queue of its resource. Tasks
869 * sharing a resource are told apart by class. Solvers: MVA (fork-join
870 * approximations), JMT, LDES.
871 *
872 * "spn": any sound net, as a stochastic Petri net with a case-counter place.
873 * A task on a resource becomes an immediate acquire followed by the timed
874 * task, which releases the resource; waiting tasks take a free server in
875 * random order, in proportion to their weights, whatever the resource
876 * scheduling. Its transitions create and destroy tokens, which SolverSSA and
877 * SolverLDES simulate.
878 *
879 * Open analysis (set_arrival_process) gives a Source and a Sink; closed
880 * analysis (set_closed_cases(k)) recirculates k cases through a zero-time
881 * restart. The soundness gate (k-soundness, k the number of closed cases or
882 * 1) is on by default; pass false to skip it on nets too large to explore.
883 */
884 std::shared_ptr<Network> to_network(const std::string& target, bool check_soundness = true) {
885 if (target.empty())
886 throw InputError(ErrorKind("Argument"), "Target must be specified as 'forkjoin' or 'spn'.");
887 if (closed_cases_ == 0 && !has_arrival_)
888 throw InputError(ErrorKind("Model"), "Set an arrival process (open analysis) or setClosedCases(k) "
889 "(closed analysis) before compiling to a Network.");
890 if (closed_cases_ > 0 && has_arrival_)
891 throw InputError(ErrorKind("Model"),
892 "Both an arrival process and closed cases are set; clear one of them "
893 "(setArrivalProcess('none') or setClosedCases(0)).");
894 validate();
895 if (check_soundness) require_sound(std::max(1L, closed_cases_));
896 std::string tg = target;
897 std::transform(tg.begin(), tg.end(), tg.begin(), [](unsigned char c) { return std::tolower(c); });
898 if (tg == "forkjoin") return compile_to_fork_join();
899 if (tg == "spn") return compile_to_spn();
900 throw InputError(ErrorKind("Argument"), "Unknown compilation target: " + target);
901 }
902
903 /**
904 * `getCaseMetrics(solver)`: case-level metrics from a solver built on the
905 * network returned by the last to_network call. The fork-join open cycle
906 * time follows the process tree; the closed one, and the SPN one, are
907 * Little's law on the case population.
908 */
910 if (!compiled_ || &solver.model() != compiled_->network.get())
911 throw InputError(ErrorKind("Argument"), "The solver is not built on the network returned by the last "
912 "toNetwork call of this WorkflowNet.");
913 const Compiled& c = *compiled_;
914 const AvgTable& tab = solver.avg_table();
915 CaseMetrics metrics;
916 metrics.target = c.target;
917 metrics.mode = c.mode;
918 if (c.target == "forkjoin") {
919 double X;
920 if (c.mode == "open") {
921 X = c.lambda;
922 std::size_t jidx = 0;
923 metrics.cycle_time = fj_tree_time(c.tree, c, tab, jidx);
924 metrics.wip = X * metrics.cycle_time;
925 } else {
926 X = detail::table_cell(tab, tab.Tput, c.restart, "Case");
927 metrics.wip = static_cast<double>(c.k);
928 metrics.cycle_time = static_cast<double>(c.k) / X;
929 }
930 metrics.throughput = X;
931 for (const Compiled::Task& task : c.tasks) {
932 metrics.tasks.Task.push_back(task.name);
933 metrics.tasks.RespT.push_back(detail::table_cell(tab, tab.RespT, task.station, task.cls));
934 }
935 for (const Compiled::Res& res : c.resources) {
936 metrics.resources.Resource.push_back(res.name);
937 metrics.resources.Servers.push_back(res.servers);
938 metrics.resources.Utilization.push_back(detail::table_sum(tab, tab.Util, res.station));
939 }
940 } else {
941 metrics.wip = detail::table_sum(tab, tab.QLen, c.counter);
942 const double X = c.mode == "open" ? c.lambda : detail::table_sum(tab, tab.Tput, c.counter);
943 metrics.throughput = X;
944 metrics.cycle_time = metrics.wip / X;
945 for (const std::string& p : c.places) {
946 metrics.places.Place.push_back(p);
947 metrics.places.QLen.push_back(detail::table_sum(tab, tab.QLen, p));
948 }
949 for (const Compiled::Busy& b : c.busy) {
950 metrics.tasks.Task.push_back(b.name);
951 metrics.tasks.InService.push_back(detail::table_sum(tab, tab.QLen, b.place));
952 }
953 for (const Compiled::Res& res : c.resources) {
954 double busy = 0.0;
955 for (std::size_t b : res.busy) busy += detail::table_sum(tab, tab.QLen, c.busy[b].place);
956 metrics.resources.Resource.push_back(res.name);
957 metrics.resources.Servers.push_back(res.servers);
958 metrics.resources.Utilization.push_back(busy / res.servers);
959 }
960 }
961 return metrics;
962 }
963
964 // ------------------------------------------------------------------
965 // PNML and conversion
966 // ------------------------------------------------------------------
967
968 /**
969 * `savePNML(filename)`: write the plain Petri net (no case counter, no
970 * resources) with one token in the source, so that from_pnml reads it back
971 * with its timing.
972 */
973 void save_pnml(const std::string& filename) {
974 validate();
975 std::shared_ptr<Network> model = build_plain_network(1);
976 io::pnml_save(model->get_struct(), filename);
977 }
978
979 /**
980 * `WorkflowNet.fromPNML(filename, netName)`: read a net with pnml_load and
981 * convert it with from_network; the source and sink are found from the
982 * structure. A file from another tool is untimed and reads with every
983 * transition exponential at rate 1.
984 */
985 static WorkflowNet from_pnml(const std::string& filename, const std::string& net_name = std::string()) {
986 Network model = io::pnml_load<double>(filename, net_name);
987 return from_network(model);
988 }
989
990 /**
991 * `WorkflowNet.fromNetwork(model)`: copy the places, transitions and arcs of
992 * a single-class Network of Place and Transition nodes. A transition must
993 * have exactly one mode and no inhibitor arcs or firing-rate dependence,
994 * since a WF-net transition has one firing rule. The result is validated.
995 */
997 const qn::NetworkStruct<double>& sn = model.get_struct();
998 if (sn.classes.size() != 1)
999 throw UnsupportedError(ErrorKind("MultiClass"),
1000 "A WF-net has uncoloured tokens; the Network must have exactly one class.");
1001 WorkflowNet net(sn.name + "_wfn");
1002 const std::size_t nn = sn.nodes.size();
1003 for (std::size_t i = 0; i < nn; ++i) {
1004 const lang::NodeType nt = sn.nodes[i].nodetype;
1005 if (nt == lang::NodeType::Place) {
1006 double st = 0.0;
1007 const auto it = sn.initmarking.find(i + 1);
1008 if (it != sn.initmarking.end())
1009 for (double x : it->second) st += x;
1010 net.add_place(sn.nodes[i].name, st);
1011 } else if (nt != lang::NodeType::Transition) {
1012 throw UnsupportedError(ErrorKind("Node"), "Node \"" + sn.nodes[i].name + "\" is a " +
1014 "; a WF-net holds only places and transitions.");
1015 }
1016 }
1017 for (std::size_t i = 0; i < nn; ++i) {
1018 if (sn.nodes[i].nodetype != lang::NodeType::Transition) continue;
1019 const std::string& tname = sn.nodes[i].name;
1020 const auto it = sn.transparam.find(i + 1);
1021 const std::size_t nmodes = it == sn.transparam.end() ? 0 : it->second.nmodes;
1022 if (nmodes != 1)
1023 throw UnsupportedError(ErrorKind("Transition"), "Transition \"" + tname + "\" has " +
1024 std::to_string(nmodes) +
1025 " modes; a WF-net transition has exactly one "
1026 "firing rule.");
1027 const qn::TransitionParam<double>& tp = it->second;
1028 const Matrix<double>& Ein = tp.enabling[0];
1029 const Matrix<double>& Eout = tp.firing[0];
1030 const Matrix<double> Inh = tp.inhibiting.empty() ? Matrix<double>() : tp.inhibiting[0];
1031 for (std::size_t r = 0; r < Inh.rows(); ++r)
1032 for (std::size_t c = 0; c < Inh.cols(); ++c)
1033 if (std::isfinite(Inh(r, c)) && Inh(r, c) > 0)
1034 throw UnsupportedError(ErrorKind("Transition"), "Transition \"" + tname +
1035 "\" has inhibitor arcs, which WF-nets do "
1036 "not have.");
1037 if (!tp.firingdep.empty() && tp.firingdep[0])
1038 throw UnsupportedError(ErrorKind("Transition"),
1039 "Transition \"" + tname +
1040 "\" has a marking-dependent firing rate, which WF-net analysis does not "
1041 "support.");
1042 const lang::TimingStrategy timing = tp.timing[0];
1043 const Dist dist = timing == lang::TimingStrategy::IMMEDIATE ? Dist(Immediate()) : tp.firingproc[0];
1044 net.add_transition(tname, dist, timing, tp.nmodeservers[0], tp.firingprio[0], tp.fireweight[0]);
1045 for (std::size_t j = 0; j < Ein.rows() && j < nn; ++j)
1046 if (Ein(j, 0) > 0) net.add_arc(sn.nodes[j].name, tname, Ein(j, 0));
1047 for (std::size_t j = 0; j < Eout.rows() && j < nn; ++j) {
1048 if (!(Eout(j, 0) > 0)) continue;
1049 if (sn.nodes[j].nodetype != lang::NodeType::Place)
1050 throw UnsupportedError(ErrorKind("Node"), "Transition \"" + tname + "\" fires into \"" +
1051 sn.nodes[j].name + "\", which is not a place.");
1052 net.add_arc(tname, sn.nodes[j].name, Eout(j, 0));
1053 }
1054 }
1055 net.validate();
1056 return net;
1057 }
1058
1059private:
1060 typedef std::vector<long> Marking;
1061
1062 struct Inc {
1063 std::size_t nP = 0, nT = 0;
1064 std::vector<std::vector<long>> pre, post; ///< [place][transition]
1065 };
1066
1067 struct ReachEdge {
1068 std::size_t from, to, t;
1069 };
1070
1071 struct Reach {
1072 enum Status { COMPLETE, CAP, UNBOUNDED };
1073 std::vector<Marking> M;
1074 std::vector<ReachEdge> E;
1075 Status status = COMPLETE;
1076 Marking witness_cover, witness_ancestor;
1077 };
1078
1079 /** The single-case chain of `singleCaseChain`, zero-time states folded out. */
1080 struct Chain {
1081 std::vector<double> alpha;
1082 Matrix<double> Q;
1083 Matrix<double> Rfire, Gfire; ///< tangible x transitions
1084 std::vector<Marking> Mtang; ///< marking of each tangible state
1085 std::vector<double> firings0; ///< immediate firings before the first tangible state
1086 };
1087
1088 struct WfState {
1089 workflow::Workflow<double> wf;
1090 std::size_t naux;
1091 };
1092
1093 /** The element map of the last network built by `to_network`. */
1094 struct Compiled {
1095 std::shared_ptr<Network> network;
1096 std::string target, mode;
1097 long k = 0;
1098 double lambda = std::numeric_limits<double>::quiet_NaN();
1099 std::string restart; ///< closed fork-join: the restart Delay
1100 struct Task {
1101 std::string name, station, cls;
1102 };
1103 std::vector<Task> tasks;
1104 detail::TreeNode tree;
1105 std::vector<std::string> joins; ///< Join names, in the preorder fjEmit created them
1106 std::vector<std::string> task_station; ///< per transition, empty when it has none
1107 std::vector<std::string> task_class;
1108 struct Res {
1109 std::string name;
1110 double servers;
1111 std::string station;
1112 std::vector<std::size_t> busy;
1113 };
1114 std::vector<Res> resources;
1115 std::string counter; ///< SPN: the wfn_cases place
1116 std::vector<std::string> places;
1117 struct Busy {
1118 std::string name, place;
1119 };
1120 std::vector<Busy> busy;
1121 };
1122
1123 std::string name_;
1124 std::vector<std::unique_ptr<WFNPlace>> places_;
1125 std::vector<std::unique_ptr<WFNTransition>> transitions_;
1126 std::vector<WFNArc> arcs_;
1127 std::vector<std::unique_ptr<WFNResource>> resources_;
1128 std::map<std::string, std::size_t> place_map_, transition_map_;
1129 long source_ = -1, sink_ = -1;
1130 Dist arrival_;
1131 bool has_arrival_ = false;
1132 long closed_cases_ = 0;
1133 double state_cap_ = 1e5;
1134 std::shared_ptr<Compiled> compiled_;
1135
1136 // ------------------------------------------------------------------
1137 // Helpers of the public API
1138 // ------------------------------------------------------------------
1139
1140 void check_new_name(const std::string& name) const {
1141 if (place_map_.count(name) || transition_map_.count(name))
1142 throw InputError(ErrorKind("Model"), "A place or transition named \"" + name +
1143 "\" already exists in workflow net \"" + name_ + "\".");
1144 const std::string pre = ReservedPrefix;
1145 if (name.compare(0, pre.size(), pre) == 0)
1146 throw InputError(ErrorKind("Model"), "Names starting with \"" + pre +
1147 "\" are reserved for compiled networks: \"" + name + "\".");
1148 }
1149
1150 /** A name resolved to the place or transition it denotes. */
1151 ElementRef resolve(const ElementRef& ref) const {
1152 if (!ref.by_name_) return ref;
1153 const auto ip = place_map_.find(ref.name_);
1154 if (ip != place_map_.end()) return ElementRef(*places_[ip->second]);
1155 const auto it = transition_map_.find(ref.name_);
1156 if (it != transition_map_.end()) return ElementRef(*transitions_[it->second]);
1157 throw InputError(ErrorKind("Model"), "No place or transition named \"" + ref.name_ + "\" found.");
1158 }
1159
1160 void check_owned(const ElementRef& ref) const {
1161 bool ok = false;
1162 if (ref.place_)
1163 ok = ref.place_->index() < places_.size() && places_[ref.place_->index()].get() == ref.place_;
1164 else if (ref.transition_)
1165 ok = ref.transition_->index() < transitions_.size() &&
1166 transitions_[ref.transition_->index()].get() == ref.transition_;
1167 if (!ok)
1168 throw InputError(ErrorKind("Model"),
1169 "Element is not a place or transition of workflow net \"" + name_ + "\".");
1170 }
1171
1172 static std::string element_name(const ElementRef& ref) {
1173 return ref.place_ ? ref.place_->name() : ref.transition_->name();
1174 }
1175
1176 long declared_place(const ElementRef& ref, const char* what) const {
1177 ElementRef r = ref;
1178 if (ref.by_name_) r = ElementRef(get_place(ref.name_));
1179 if (!r.place_) throw InputError(ErrorKind("Model"), std::string(what) + " must be a WFNPlace object.");
1180 check_owned(r);
1181 return static_cast<long>(r.place_->index());
1182 }
1183
1184 WFNResource& set_resource_impl(const ElementRef& transition, const std::string& name, const double* servers,
1185 const lang::SchedStrategy* scheduling) {
1186 ElementRef r = transition;
1187 if (transition.by_name_) {
1188 const auto it = transition_map_.find(transition.name_);
1189 if (it == transition_map_.end())
1190 throw InputError(ErrorKind("Model"), "Transition \"" + transition.name_ + "\" not found.");
1191 r = ElementRef(*transitions_[it->second]);
1192 }
1193 if (!r.transition_) throw InputError(ErrorKind("Argument"), "First argument must be a transition.");
1194 check_owned(r);
1195 WFNTransition& tr = *transitions_[r.transition_->index()];
1196 if (tr.is_immediate())
1197 throw InputError(ErrorKind("Argument"), "Immediate transition \"" + tr.name() +
1198 "\" takes no time and cannot hold a resource.");
1199 if (std::isfinite(tr.servers()))
1200 throw InputError(ErrorKind("Argument"),
1201 "Transition \"" + tr.name() + "\" already limits its own concurrency to " +
1202 detail::num_d(tr.servers()) +
1203 " servers; a task is constrained either by its own servers or by a shared "
1204 "resource, not both.");
1205 long idx = -1;
1206 for (std::size_t i = 0; i < resources_.size(); ++i)
1207 if (resources_[i]->name() == name) {
1208 idx = static_cast<long>(i);
1209 break;
1210 }
1211 if (idx < 0) {
1212 resources_.emplace_back(new WFNResource(name, servers ? *servers : 1.0,
1213 scheduling ? *scheduling : lang::SchedStrategy::FCFS));
1214 idx = static_cast<long>(resources_.size()) - 1;
1215 } else {
1216 if (servers) resources_[idx]->set_servers(*servers);
1217 if (scheduling) resources_[idx]->set_scheduling(*scheduling);
1218 }
1219 tr.resource_ = idx;
1220 return *resources_[idx];
1221 }
1222
1223 void set_arrival_typed(const std::string& type, const double* rate, const Dist* map) {
1224 std::string tp = type;
1225 std::transform(tp.begin(), tp.end(), tp.begin(), [](unsigned char c) { return std::tolower(c); });
1226 if (tp == "none") {
1227 has_arrival_ = false;
1228 arrival_ = Dist();
1229 } else if (tp == "poisson") {
1230 if (!rate || !(*rate > 0))
1231 throw InputError(ErrorKind("Argument"), "A Poisson stream needs a positive rate.");
1232 arrival_ = Exp(*rate);
1233 has_arrival_ = true;
1234 } else if (tp == "map") {
1235 const bool modulated =
1236 map && (map->type == lang::ProcessType::MAP || map->type == lang::ProcessType::MMPP2 ||
1237 map->type == lang::ProcessType::MMAP || map->type == lang::ProcessType::BMAP);
1238 if (!modulated) throw InputError(ErrorKind("Argument"), "A MAP stream needs a MAP object.");
1239 arrival_ = *map;
1240 has_arrival_ = true;
1241 } else {
1242 throw InputError(ErrorKind("Argument"), "Unknown arrival process type: " + type);
1243 }
1244 }
1245
1246 std::string place_list(const std::vector<std::size_t>& idx) const {
1247 std::string s;
1248 for (std::size_t j = 0; j < idx.size(); ++j) s += (j ? ", \"" : "\"") + places_[idx[j]]->name() + "\"";
1249 return s.empty() ? "none" : s;
1250 }
1251
1252 std::string transition_list(const std::vector<std::size_t>& idx) const {
1253 std::string s;
1254 for (std::size_t j = 0; j < idx.size(); ++j)
1255 s += (j ? ", \"" : "\"") + transitions_[idx[j]]->name() + "\"";
1256 return s.empty() ? "none" : s;
1257 }
1258
1259 std::string marking_string(const Marking& m) const {
1260 std::string s = "[";
1261 bool first = true;
1262 for (std::size_t p = 0; p < m.size(); ++p) {
1263 if (m[p] == 0) continue;
1264 if (!first) s += ", ";
1265 s += places_[p]->name() + ":" + std::to_string(m[p]);
1266 first = false;
1267 }
1268 return s + "]";
1269 }
1270
1271 Inc incidence() const {
1272 Inc inc;
1273 inc.nP = places_.size();
1274 inc.nT = transitions_.size();
1275 inc.pre.assign(inc.nP, std::vector<long>(inc.nT, 0));
1276 inc.post.assign(inc.nP, std::vector<long>(inc.nT, 0));
1277 for (const WFNArc& a : arcs_) {
1278 const long w = static_cast<long>(std::llround(a.weight()));
1279 if (a.from_place())
1280 inc.pre[a.place().index()][a.transition().index()] += w;
1281 else
1282 inc.post[a.place().index()][a.transition().index()] += w;
1283 }
1284 return inc;
1285 }
1286
1287 static std::vector<bool> bfs(const std::vector<std::vector<std::size_t>>& adj,
1288 const std::vector<std::size_t>& starts) {
1289 std::vector<bool> visited(adj.size(), false);
1290 std::deque<std::size_t> queue;
1291 for (std::size_t s : starts) {
1292 visited[s] = true;
1293 queue.push_back(s);
1294 }
1295 while (!queue.empty()) {
1296 const std::size_t cur = queue.front();
1297 queue.pop_front();
1298 for (std::size_t nxt : adj[cur])
1299 if (!visited[nxt]) {
1300 visited[nxt] = true;
1301 queue.push_back(nxt);
1302 }
1303 }
1304 return visited;
1305 }
1306
1307 static std::vector<double> enabling_degree(const Inc& inc, const Marking& m) {
1308 std::vector<double> deg(inc.nT, std::numeric_limits<double>::infinity());
1309 for (std::size_t t = 0; t < inc.nT; ++t) {
1310 for (std::size_t p = 0; p < inc.nP; ++p)
1311 if (inc.pre[p][t] > 0)
1312 deg[t] = std::min(deg[t], std::floor(static_cast<double>(m[p]) / inc.pre[p][t]));
1313 }
1314 return deg;
1315 }
1316
1317 /**
1318 * Breadth-first reachability graph of the P/T net. A new marking that
1319 * strictly covers a marking on its parent chain proves unboundedness; the
1320 * state cap stops the search with status CAP.
1321 */
1322 static Reach untimed_reachability(const Inc& inc, const Marking& m0, double cap) {
1323 Reach rg;
1324 std::vector<std::size_t> parent;
1325 std::map<Marking, std::size_t> keys;
1326 rg.M.push_back(m0);
1327 parent.push_back(npos_parent());
1328 keys[m0] = 0;
1329 for (std::size_t head = 0; head < rg.M.size(); ++head) {
1330 const Marking m = rg.M[head];
1331 for (std::size_t t = 0; t < inc.nT; ++t) {
1332 bool en = true;
1333 for (std::size_t p = 0; p < inc.nP && en; ++p) en = m[p] >= inc.pre[p][t];
1334 if (!en) continue;
1335 Marking m2(inc.nP);
1336 for (std::size_t p = 0; p < inc.nP; ++p) m2[p] = m[p] + inc.post[p][t] - inc.pre[p][t];
1337 std::size_t to;
1338 const auto it = keys.find(m2);
1339 if (it != keys.end()) {
1340 to = it->second;
1341 } else {
1342 for (std::size_t a = head; a != npos_parent(); a = parent[a]) {
1343 bool ge = true, gt = false;
1344 for (std::size_t p = 0; p < inc.nP; ++p) {
1345 ge = ge && m2[p] >= rg.M[a][p];
1346 gt = gt || m2[p] > rg.M[a][p];
1347 }
1348 if (ge && gt) {
1349 rg.status = Reach::UNBOUNDED;
1350 rg.witness_cover = m2;
1351 rg.witness_ancestor = rg.M[a];
1352 return rg;
1353 }
1354 }
1355 if (static_cast<double>(rg.M.size()) >= cap) {
1356 rg.status = Reach::CAP;
1357 return rg;
1358 }
1359 rg.M.push_back(m2);
1360 parent.push_back(head);
1361 to = rg.M.size() - 1;
1362 keys[m2] = to;
1363 }
1364 rg.E.push_back(ReachEdge{head, to, t});
1365 }
1366 }
1367 return rg;
1368 }
1369
1370 static std::size_t npos_parent() { return static_cast<std::size_t>(-1); }
1371
1372 void require_sound(long k) {
1373 const SoundnessReport report = check_soundness(static_cast<double>(k));
1374 if (report.verdict == "unknown")
1375 throw NumericError(ErrorKind("StateSpace"), "Soundness of \"" + name_ + "\" (k=" + std::to_string(k) +
1376 ") is undecided: " + format_report(report) +
1377 ". Raise stateCap, or skip the gate where the method "
1378 "allows it.");
1379 if (!report.sound)
1380 throw UnsupportedError(ErrorKind("Structure"), "The WF-net \"" + name_ + "\" is not " +
1381 std::to_string(k) +
1382 "-sound, so the case cycle time is undefined: " +
1383 format_report(report));
1384 }
1385
1386 /** `phRepresentation`: the (alpha, S) pair of a phase-type firing law. */
1387 static void ph_representation(const Dist& d, const std::string& name, std::vector<double>& alpha,
1388 Matrix<double>& S) {
1389 const lang::ProcessType ty = d.type;
1390 const bool ph_family = ty == lang::ProcessType::EXP || ty == lang::ProcessType::ERLANG ||
1394 if (!ph_family || d.disabled || !d.has_map())
1395 throw UnsupportedError(ErrorKind("Distribution"),
1396 "Transition \"" + name + "\" has a " + lang::process_to_text(ty) +
1397 " firing law; the exact single-case analysis needs a phase-type law (Exp, "
1398 "Erlang, HyperExp, Coxian, Cox2, APH, PH). Compile with toNetwork('spn') and "
1399 "use a simulator (SolverSSA, SolverLDES) for other laws.");
1400 const std::size_t n = d.D0.rows();
1401 S = d.D0;
1402 alpha.assign(n, 0.0);
1403 if ((ty == lang::ProcessType::PH || ty == lang::ProcessType::APH) && d.params.size() == n) {
1404 alpha.assign(d.params.begin(), d.params.end());
1405 } else {
1406 // D1 = (-D0 e) alpha for a renewal law, so any row with a positive exit recovers alpha
1407 for (std::size_t i = 0; i < n; ++i) {
1408 double out = 0.0;
1409 for (std::size_t j = 0; j < n; ++j) out += d.D1(i, j);
1410 if (!(out > 0)) continue;
1411 for (std::size_t j = 0; j < n; ++j) alpha[j] = d.D1(i, j) / out;
1412 break;
1413 }
1414 }
1415 double sum = 0.0;
1416 bool negative = false;
1417 for (double a : alpha) {
1418 sum += a;
1419 negative = negative || a < -lang::GlobalConstants::FineTol;
1420 }
1421 if (std::abs(sum - 1) > lang::GlobalConstants::CoarseTol || negative)
1422 throw UnsupportedError(ErrorKind("Distribution"), "The firing law of transition \"" + name +
1423 "\" has a defective or negative initial vector.");
1424 }
1425
1426 /** `afterFiring`: marking and phases after T fires, under enabling memory. */
1427 static std::vector<long> after_firing(const Inc& inc, const std::vector<long>& x, std::size_t t,
1428 const std::vector<std::size_t>& ord) {
1429 const std::size_t nP = inc.nP, nT = inc.nT;
1430 Marking m2(nP);
1431 for (std::size_t p = 0; p < nP; ++p) m2[p] = x[p] + inc.post[p][t] - inc.pre[p][t];
1432 const std::vector<double> deg = enabling_degree(inc, m2);
1433 std::vector<long> y(nP + nT);
1434 for (std::size_t p = 0; p < nP; ++p) y[p] = m2[p];
1435 for (std::size_t u = 0; u < nT; ++u) {
1436 long ph = x[nP + u];
1437 if (deg[u] == 0 || u == t || ord[u] <= 1) ph = 0;
1438 y[nP + u] = ph;
1439 }
1440 return y;
1441 }
1442
1443 /**
1444 * `singleCaseChain`: the absorbing CTMC of one case. A state is the marking
1445 * plus the phase of every enabled phase-type transition with more than one
1446 * phase. Zero-time states (an immediate transition is enabled, or a newly
1447 * enabled transition still has to draw its initial phase) are eliminated by
1448 * solving for their absorption probabilities into tangible states, and the
1449 * expected immediate firings are carried along.
1450 */
1451 Chain single_case_chain() {
1452 require_sound(1);
1453 const Inc inc = incidence();
1454 const std::size_t nP = inc.nP, nT = inc.nT;
1455 std::vector<bool> is_imm(nT, false);
1456 std::vector<double> prio(nT), wgt(nT), srv(nT);
1457 std::vector<std::vector<double>> pha(nT);
1458 std::vector<Matrix<double>> phS(nT);
1459 std::vector<std::size_t> ord(nT, 0);
1460 for (std::size_t t = 0; t < nT; ++t) {
1461 const WFNTransition& tr = *transitions_[t];
1462 is_imm[t] = tr.is_immediate();
1463 prio[t] = tr.priority();
1464 wgt[t] = tr.weight();
1465 srv[t] = tr.servers();
1466 if (!is_imm[t]) {
1467 ph_representation(tr.distribution(), tr.name(), pha[t], phS[t]);
1468 ord[t] = pha[t].size();
1469 }
1470 }
1471 Marking mf(nP, 0);
1472 mf[sink_] = 1;
1473 std::vector<long> x0(nP + nT, 0);
1474 x0[source_] = 1;
1475
1476 struct Edge {
1477 std::size_t from, to;
1478 double val;
1479 long lab; ///< firing transition, -1 for a phase move
1480 };
1481 std::map<std::vector<long>, std::size_t> keys;
1482 std::vector<std::vector<long>> S;
1483 std::vector<int> kind; // 1 = zero-time, 2 = tangible, 3 = absorbing
1484 std::vector<Edge> edges;
1485 S.push_back(x0);
1486 keys[x0] = 0;
1487 for (std::size_t head = 0; head < S.size(); ++head) {
1488 const std::vector<long> x = S[head];
1489 const Marking m(x.begin(), x.begin() + static_cast<long>(nP));
1490 const std::vector<double> deg = enabling_degree(inc, m);
1491 std::vector<std::vector<long>> succ;
1492 std::vector<double> val;
1493 std::vector<long> lab;
1494 bool imm_enabled = false;
1495 for (std::size_t t = 0; t < nT; ++t) imm_enabled = imm_enabled || (is_imm[t] && deg[t] > 0);
1496 int k;
1497 if (m == mf) {
1498 k = 3;
1499 } else if (imm_enabled) {
1500 k = 1;
1501 double pmax = -std::numeric_limits<double>::infinity();
1502 for (std::size_t t = 0; t < nT; ++t)
1503 if (is_imm[t] && deg[t] > 0) pmax = std::max(pmax, prio[t]);
1504 std::vector<std::size_t> en;
1505 double wsum = 0.0;
1506 for (std::size_t t = 0; t < nT; ++t)
1507 if (is_imm[t] && deg[t] > 0 && prio[t] == pmax) {
1508 en.push_back(t);
1509 wsum += wgt[t];
1510 }
1511 for (std::size_t t : en) {
1512 succ.push_back(after_firing(inc, x, t, ord));
1513 val.push_back(wgt[t] / wsum);
1514 lab.push_back(static_cast<long>(t));
1515 }
1516 } else {
1517 std::vector<double> act(nT);
1518 for (std::size_t t = 0; t < nT; ++t) act[t] = std::min(deg[t], srv[t]);
1519 for (std::size_t t = 0; t < nT; ++t)
1520 if (!is_imm[t] && ord[t] > 1 && act[t] > 1)
1521 throw UnsupportedError(
1522 ErrorKind("Distribution"),
1523 "Transition \"" + transitions_[t]->name() + "\" is enabled " + detail::num_d(act[t]) +
1524 " times at once with a " + std::to_string(ord[t]) +
1525 "-phase firing law; the exact single-case analysis keeps one phase per transition, "
1526 "so it supports concurrent firings only for exponential laws.");
1527 std::size_t need = nT;
1528 for (std::size_t t = 0; t < nT && need == nT; ++t)
1529 if (!is_imm[t] && ord[t] > 1 && act[t] > 0 && x[nP + t] == 0) need = t;
1530 if (need < nT) {
1531 k = 1;
1532 for (std::size_t j = 0; j < pha[need].size(); ++j)
1533 if (pha[need][j] > 0) {
1534 std::vector<long> y = x;
1535 y[nP + need] = static_cast<long>(j + 1);
1536 succ.push_back(y);
1537 val.push_back(pha[need][j]);
1538 lab.push_back(-1);
1539 }
1540 } else {
1541 k = 2;
1542 bool any = false;
1543 for (std::size_t t = 0; t < nT; ++t) {
1544 if (is_imm[t] || !(act[t] > 0)) continue;
1545 any = true;
1546 if (ord[t] == 1) {
1547 succ.push_back(after_firing(inc, x, t, ord));
1548 val.push_back(-phS[t](0, 0) * act[t]);
1549 lab.push_back(static_cast<long>(t));
1550 } else {
1551 const std::size_t j = static_cast<std::size_t>(x[nP + t] - 1);
1552 for (std::size_t j2 = 0; j2 < ord[t]; ++j2)
1553 if (j2 != j && phS[t](j, j2) > 0) {
1554 std::vector<long> y = x;
1555 y[nP + t] = static_cast<long>(j2 + 1);
1556 succ.push_back(y);
1557 val.push_back(phS[t](j, j2));
1558 lab.push_back(-1);
1559 }
1560 double exit_rate = 0.0;
1561 for (std::size_t j2 = 0; j2 < ord[t]; ++j2) exit_rate -= phS[t](j, j2);
1562 if (exit_rate > lang::GlobalConstants::FineTol) {
1563 succ.push_back(after_firing(inc, x, t, ord));
1564 val.push_back(exit_rate);
1565 lab.push_back(static_cast<long>(t));
1566 }
1567 }
1568 }
1569 if (!any)
1570 throw InternalError(ErrorKind("Invariant"), "Sound net \"" + name_ +
1571 "\" reached a dead marking " +
1572 marking_string(m) + " in the timed chain.");
1573 }
1574 }
1575 kind.push_back(k);
1576 for (std::size_t e = 0; e < succ.size(); ++e) {
1577 std::size_t to;
1578 const auto it = keys.find(succ[e]);
1579 if (it != keys.end()) {
1580 to = it->second;
1581 } else {
1582 to = S.size();
1583 if (static_cast<double>(to + 1) > state_cap_)
1584 throw NumericError(ErrorKind("StateSpace"), "The single-case chain of \"" + name_ +
1585 "\" exceeds the state cap of " +
1586 detail::num_d(state_cap_) + " states.");
1587 S.push_back(succ[e]);
1588 keys[succ[e]] = to;
1589 }
1590 edges.push_back(Edge{head, to, val[e], lab[e]});
1591 }
1592 }
1593
1594 const std::size_t n = S.size();
1595 std::vector<std::size_t> pos(n, 0);
1596 std::size_t nZ = 0, nTT = 0;
1597 for (std::size_t i = 0; i < n; ++i) {
1598 if (kind[i] == 1) pos[i] = nZ++;
1599 else if (kind[i] == 2) pos[i] = nTT++;
1600 }
1601 // zero-time part: absorption probabilities into tangible states, and the firings on the way
1602 Matrix<double> IZ(nZ, nZ, 0.0), PZT(nZ, nTT, 0.0), CZ(nZ, nT, 0.0);
1603 for (std::size_t z = 0; z < nZ; ++z) IZ(z, z) = 1.0;
1604 for (const Edge& e : edges) {
1605 if (kind[e.from] != 1) continue;
1606 const std::size_t z = pos[e.from];
1607 if (kind[e.to] == 1) IZ(z, pos[e.to]) -= e.val;
1608 else if (kind[e.to] == 2) PZT(z, pos[e.to]) += e.val;
1609 if (e.lab >= 0) CZ(z, static_cast<std::size_t>(e.lab)) += e.val;
1610 }
1611 Matrix<double> BT(nZ, nTT, 0.0), FZ(nZ, nT, 0.0);
1612 if (nZ > 0) {
1613 const detail::Factor f(IZ);
1614 for (std::size_t c = 0; c < nTT; ++c) {
1615 std::vector<double> b(nZ);
1616 for (std::size_t z = 0; z < nZ; ++z) b[z] = PZT(z, c);
1617 b = f.solve(b);
1618 for (std::size_t z = 0; z < nZ; ++z) BT(z, c) = b[z];
1619 }
1620 for (std::size_t c = 0; c < nT; ++c) {
1621 std::vector<double> b(nZ);
1622 for (std::size_t z = 0; z < nZ; ++z) b[z] = CZ(z, c);
1623 b = f.solve(b);
1624 for (std::size_t z = 0; z < nZ; ++z) FZ(z, c) = b[z];
1625 }
1626 }
1627 // tangible part: rates, with each zero-time successor replaced by where it lands
1628 Chain ch;
1629 ch.Q = Matrix<double>(nTT, nTT, 0.0);
1630 ch.Rfire = Matrix<double>(nTT, nT, 0.0);
1631 ch.Gfire = Matrix<double>(nTT, nT, 0.0);
1632 for (const Edge& e : edges) {
1633 if (kind[e.from] != 2) continue;
1634 const std::size_t s = pos[e.from];
1635 const double r = e.val;
1636 ch.Q(s, s) -= r;
1637 if (e.lab >= 0) ch.Rfire(s, static_cast<std::size_t>(e.lab)) += r;
1638 if (kind[e.to] == 2) {
1639 ch.Q(s, pos[e.to]) += r;
1640 } else if (kind[e.to] == 1) {
1641 const std::size_t z = pos[e.to];
1642 for (std::size_t j = 0; j < nTT; ++j) ch.Q(s, j) += r * BT(z, j);
1643 for (std::size_t j = 0; j < nT; ++j) ch.Gfire(s, j) += r * FZ(z, j);
1644 }
1645 }
1646 for (std::size_t i = 0; i < n; ++i)
1647 if (kind[i] == 2) ch.Mtang.push_back(Marking(S[i].begin(), S[i].begin() + static_cast<long>(nP)));
1648 ch.firings0.assign(nT, 0.0);
1649 if (kind[0] == 2) {
1650 ch.alpha.assign(nTT, 0.0);
1651 ch.alpha[pos[0]] = 1.0;
1652 } else if (kind[0] == 1) {
1653 ch.alpha.assign(nTT, 0.0);
1654 for (std::size_t j = 0; j < nTT; ++j) ch.alpha[j] = BT(pos[0], j);
1655 for (std::size_t j = 0; j < nT; ++j) ch.firings0[j] = FZ(pos[0], j);
1656 }
1657 return ch;
1658 }
1659
1660 /** `choiceResolvable`: a choice the block structure can express. */
1661 bool choice_resolvable(const std::vector<std::size_t>& comp, const std::vector<bool>& head_imm,
1662 const std::vector<double>& head_prio, const std::string& place_name,
1663 std::string& why) const {
1664 bool ok = true;
1665 for (std::size_t c : comp) ok = ok && head_imm[c] && head_prio[c] == head_prio[comp[0]];
1666 why.clear();
1667 if (!ok)
1668 why = "the choice at place \"" + place_name +
1669 "\" is not resolved by immediate transitions of equal priority, so it is a race or a priority "
1670 "choice that the block structure cannot express";
1671 return ok;
1672 }
1673
1674 /**
1675 * `reduceToTree`: reduce a block-structured WF-net to a process tree. A
1676 * LOOP node carries {do, redo} and the repeat probability q: the case runs
1677 * do, then with probability q runs redo and starts over. Immediate
1678 * transitions become EPS.
1679 */
1680 bool reduce_to_tree(detail::TreeNode& tree, std::string& reason) const {
1681 using detail::TreeNode;
1682 const Inc inc = incidence();
1683 reason.clear();
1684 const std::size_t nP = inc.nP, nT = inc.nT;
1685 for (std::size_t p = 0; p < nP; ++p)
1686 for (std::size_t t = 0; t < nT; ++t)
1687 if (inc.pre[p][t] > 1 || inc.post[p][t] > 1) {
1688 reason = "an arc has weight greater than one";
1689 return false;
1690 }
1691 std::vector<std::vector<bool>> pre(nP, std::vector<bool>(nT)), post(nP, std::vector<bool>(nT));
1692 for (std::size_t p = 0; p < nP; ++p)
1693 for (std::size_t t = 0; t < nT; ++t) {
1694 pre[p][t] = inc.pre[p][t] > 0;
1695 post[p][t] = inc.post[p][t] > 0;
1696 }
1697 std::vector<bool> alive_p(nP, true), alive_t(nT, true);
1698 const std::size_t src = static_cast<std::size_t>(source_);
1699 std::size_t snk = static_cast<std::size_t>(sink_);
1700 std::vector<TreeNode> ptree(nP, detail::tree_eps()), ttree(nT);
1701 std::vector<bool> head_imm(nT, false);
1702 std::vector<double> head_w(nT), head_prio(nT);
1703 for (std::size_t t = 0; t < nT; ++t) {
1704 const WFNTransition& tr = *transitions_[t];
1705 if (tr.is_immediate()) {
1706 ttree[t] = detail::tree_eps();
1707 head_imm[t] = true;
1708 } else {
1709 ttree[t].type = TreeNode::TASK;
1710 ttree[t].t = t;
1711 }
1712 head_w[t] = tr.weight();
1713 head_prio[t] = tr.priority();
1714 }
1715 auto col = [&](const std::vector<std::vector<bool>>& A, std::size_t t) {
1716 std::vector<std::size_t> out;
1717 for (std::size_t p = 0; p < nP; ++p)
1718 if (A[p][t]) out.push_back(p);
1719 return out;
1720 };
1721 auto row = [&](const std::vector<std::vector<bool>>& A, std::size_t p) {
1722 std::vector<std::size_t> out;
1723 for (std::size_t t = 0; t < nT; ++t)
1724 if (A[p][t]) out.push_back(t);
1725 return out;
1726 };
1727 auto alive = [](const std::vector<bool>& a) {
1728 std::vector<std::size_t> out;
1729 for (std::size_t i = 0; i < a.size(); ++i)
1730 if (a[i]) out.push_back(i);
1731 return out;
1732 };
1733 auto one = [](std::size_t x) { return std::vector<std::size_t>(1, x); };
1734 bool changed = true;
1735 while (changed) {
1736 changed = false;
1737 // R1: fold transition t between p1 and p2 into place p1
1738 for (std::size_t t : alive(alive_t)) {
1739 const std::vector<std::size_t> ins = col(pre, t), outs = col(post, t);
1740 if (ins.size() == 1 && outs.size() == 1 && ins[0] != outs[0]) {
1741 const std::size_t p1 = ins[0], p2 = outs[0];
1742 if (row(pre, p1) == one(t) && row(post, p2) == one(t)) {
1743 ptree[p1] = detail::tree_seq({ptree[p1], ttree[t], ptree[p2]});
1744 pre[p1] = pre[p2];
1745 pre[p2].assign(nT, false);
1746 post[p2].assign(nT, false);
1747 for (std::size_t p = 0; p < nP; ++p) pre[p][t] = post[p][t] = false;
1748 alive_t[t] = false;
1749 alive_p[p2] = false;
1750 if (p2 == snk) snk = p1;
1751 changed = true;
1752 }
1753 }
1754 }
1755 // R2: fold place p between t1 and t2 into transition t1
1756 for (std::size_t p : alive(alive_p)) {
1757 if (p == src || p == snk) continue;
1758 const std::vector<std::size_t> prod = row(post, p), cons = row(pre, p);
1759 if (prod.size() == 1 && cons.size() == 1 && prod[0] != cons[0]) {
1760 const std::size_t t1 = prod[0], t2 = cons[0];
1761 if (col(post, t1) == one(p) && col(pre, t2) == one(p)) {
1762 ttree[t1] = detail::tree_seq({ttree[t1], ptree[p], ttree[t2]});
1763 for (std::size_t q = 0; q < nP; ++q) {
1764 post[q][t1] = post[q][t2];
1765 pre[q][t2] = post[q][t2] = false;
1766 }
1767 pre[p].assign(nT, false);
1768 post[p].assign(nT, false);
1769 alive_t[t2] = false;
1770 alive_p[p] = false;
1771 changed = true;
1772 }
1773 }
1774 }
1775 // R3: AND block, parallel places with the same single producer and consumer
1776 const std::vector<std::size_t> ap = alive(alive_p);
1777 for (std::size_t a : ap) {
1778 if (!alive_p[a] || a == src || a == snk) continue;
1779 const std::vector<std::size_t> pa = row(post, a), ca = row(pre, a);
1780 if (pa.size() != 1 || ca.size() != 1) continue;
1781 for (std::size_t b : ap) {
1782 if (b <= a || !alive_p[b] || b == src || b == snk) continue;
1783 if (row(post, b) == pa && row(pre, b) == ca) {
1784 ptree[a] = detail::tree_and({ptree[a], ptree[b]});
1785 pre[b].assign(nT, false);
1786 post[b].assign(nT, false);
1787 alive_p[b] = false;
1788 changed = true;
1789 }
1790 }
1791 }
1792 // R4: XOR block, parallel transitions with the same single input and output
1793 const std::vector<std::size_t> at = alive(alive_t);
1794 for (std::size_t a : at) {
1795 if (!alive_t[a]) continue;
1796 const std::vector<std::size_t> ia = col(pre, a), oa = col(post, a);
1797 if (ia.size() != 1 || oa.size() != 1) continue;
1798 for (std::size_t b : at) {
1799 if (b <= a || !alive_t[b]) continue;
1800 if (col(pre, b) == ia && col(post, b) == oa) {
1801 const std::vector<std::size_t> comp = row(pre, ia[0]);
1802 std::string why;
1803 if (!choice_resolvable(comp, head_imm, head_prio, places_[ia[0]]->name(), why)) {
1804 reason = why;
1805 continue;
1806 }
1807 const double wa = head_w[a], wb = head_w[b];
1808 ttree[a] = detail::tree_xor({ttree[a], ttree[b]}, {wa / (wa + wb), wb / (wa + wb)});
1809 head_w[a] = wa + wb;
1810 for (std::size_t p = 0; p < nP; ++p) pre[p][b] = post[p][b] = false;
1811 alive_t[b] = false;
1812 changed = true;
1813 }
1814 }
1815 }
1816 // R5: loop, a transition whose only input and only output is p
1817 for (std::size_t t : alive(alive_t)) {
1818 const std::vector<std::size_t> ins = col(pre, t), outs = col(post, t);
1819 if (ins.size() == 1 && ins == outs) {
1820 const std::size_t p = ins[0];
1821 const std::vector<std::size_t> comp = row(pre, p);
1822 if (comp.size() < 2) continue;
1823 std::string why;
1824 if (!choice_resolvable(comp, head_imm, head_prio, places_[p]->name(), why)) {
1825 reason = why;
1826 continue;
1827 }
1828 double wsum = 0.0;
1829 for (std::size_t c : comp) wsum += head_w[c];
1830 ptree[p] = detail::tree_loop(ptree[p], ttree[t], head_w[t] / wsum);
1831 for (std::size_t q = 0; q < nP; ++q) pre[q][t] = post[q][t] = false;
1832 alive_t[t] = false;
1833 changed = true;
1834 }
1835 }
1836 }
1837 const std::vector<std::size_t> left_t = alive(alive_t), left_p = alive(alive_p);
1838 const bool ok = left_t.empty() && left_p.size() == 1;
1839 if (ok) {
1840 tree = ptree[left_p[0]];
1841 } else if (reason.empty()) {
1842 reason = "the reduction to series, AND, XOR and loop blocks stops with transitions " +
1843 transition_list(left_t) + " left, so the net is not block-structured";
1844 }
1845 return ok;
1846 }
1847
1848 // ------------------------------------------------------------------
1849 // to_workflow
1850 // ------------------------------------------------------------------
1851
1852 std::string wf_task(std::size_t t, WfState& st) const {
1853 const WFNTransition& tr = *transitions_[t];
1854 if (st.wf.activity_index(tr.name()) == workflow::Workflow<double>::npos)
1855 st.wf.add_activity(tr.name(), tr.distribution());
1856 return tr.name();
1857 }
1858
1859 static std::string wf_aux(WfState& st) {
1860 ++st.naux;
1861 const std::string name = std::string(ReservedPrefix) + "aux_" + std::to_string(st.naux);
1862 st.wf.add_activity(name, Immediate());
1863 return name;
1864 }
1865
1866 /** `wfBranch`: emit NODE as a self-contained fragment, returning its head and tail activities. */
1867 void wf_branch(const detail::TreeNode& node, WfState& st, std::string& h, std::string& tl) const {
1868 using detail::TreeNode;
1869 switch (node.type) {
1870 case TreeNode::TASK:
1871 h = wf_task(node.t, st);
1872 tl = h;
1873 break;
1874 case TreeNode::SEQ: {
1875 std::vector<TreeNode> kids = node.kids;
1876 if (kids.front().type == TreeNode::TASK) {
1877 h = wf_task(kids.front().t, st);
1878 kids.erase(kids.begin());
1879 } else {
1880 h = wf_aux(st);
1881 }
1882 if (!kids.empty() && kids.back().type == TreeNode::TASK) {
1883 tl = wf_task(kids.back().t, st);
1884 kids.pop_back();
1885 } else if (kids.empty()) {
1886 tl = h;
1887 } else {
1888 tl = wf_aux(st);
1889 }
1890 if (h != tl) wf_emit(detail::tree_seq(kids), h, tl, st);
1891 break;
1892 }
1893 case TreeNode::EPS:
1894 h = wf_aux(st);
1895 tl = h;
1896 break;
1897 default:
1898 h = wf_aux(st);
1899 tl = wf_aux(st);
1900 wf_emit(node, h, tl, st);
1901 }
1902 }
1903
1904 /** `wfEmit`: emit the precedences leading from activity PRE through NODE to activity POST. */
1905 void wf_emit(const detail::TreeNode& node, const std::string& pre, const std::string& post,
1906 WfState& st) const {
1907 using detail::TreeNode;
1908 typedef workflow::Workflow<double> Wf;
1909 switch (node.type) {
1910 case TreeNode::EPS:
1911 st.wf.add_precedence(Wf::Serial(pre, post));
1912 break;
1913 case TreeNode::TASK: {
1914 const std::string a = wf_task(node.t, st);
1915 st.wf.add_precedence(Wf::Serial(pre, a));
1916 st.wf.add_precedence(Wf::Serial(a, post));
1917 break;
1918 }
1919 case TreeNode::SEQ: {
1920 const std::vector<TreeNode>& kids = node.kids;
1921 std::string cur = pre;
1922 std::size_t i = 0;
1923 while (i < kids.size()) {
1924 const TreeNode& k = kids[i];
1925 if (k.type == TreeNode::TASK) {
1926 const std::string a = wf_task(k.t, st);
1927 st.wf.add_precedence(Wf::Serial(cur, a));
1928 cur = a;
1929 ++i;
1930 } else {
1931 std::string nxt;
1932 std::size_t step;
1933 if (i + 1 < kids.size() && kids[i + 1].type == TreeNode::TASK) {
1934 nxt = wf_task(kids[i + 1].t, st);
1935 step = 2;
1936 } else if (i + 1 == kids.size()) {
1937 nxt = post;
1938 step = 1;
1939 } else {
1940 nxt = wf_aux(st);
1941 step = 1;
1942 }
1943 wf_emit(k, cur, nxt, st);
1944 cur = nxt;
1945 i += step;
1946 }
1947 }
1948 if (cur != post) st.wf.add_precedence(Wf::Serial(cur, post));
1949 break;
1950 }
1951 case TreeNode::AND:
1952 case TreeNode::XOR: {
1953 std::vector<std::string> heads(node.kids.size()), tails(node.kids.size());
1954 for (std::size_t j = 0; j < node.kids.size(); ++j) wf_branch(node.kids[j], st, heads[j], tails[j]);
1955 if (node.type == TreeNode::AND) {
1956 st.wf.add_precedence(Wf::AndFork(pre, heads));
1957 st.wf.add_precedence(Wf::AndJoin(tails, post));
1958 } else {
1959 st.wf.add_precedence(Wf::OrFork(pre, heads, node.probs));
1960 st.wf.add_precedence(Wf::OrJoin(tails, post));
1961 }
1962 break;
1963 }
1964 case TreeNode::LOOP: {
1965 if (node.kids[1].type != TreeNode::EPS)
1966 throw UnsupportedError(ErrorKind("Structure"),
1967 "toWorkflow cannot express a loop with work on its return path: "
1968 "Workflow.Loop repeats a single body a geometric number of times. "
1969 "getCycleTime and toNetwork handle this net.");
1970 std::string h, tl;
1971 wf_branch(node.kids[0], st, h, tl);
1972 st.wf.add_precedence(Wf::Loop(pre, {h, post}, 1.0 / (1.0 - node.q)));
1973 break;
1974 }
1975 }
1976 }
1977
1978 // ------------------------------------------------------------------
1979 // to_network("forkjoin")
1980 // ------------------------------------------------------------------
1981
1982 /** A vertex of the flow graph fjEmit builds; VIRT vertices are eliminated by fj_resolve. */
1983 struct FjVertex {
1984 std::string kind; ///< endpoint, sink, virt, station, fork, join, router
1985 std::size_t node = 0; ///< 1-based network node, 0 for a virtual vertex
1986 std::size_t task = 0; ///< 1-based transition whose class the vertex carries, 0 = case class
1987 };
1988 struct FjEdge {
1989 std::size_t u, v;
1990 double p;
1991 };
1992 struct FjGraph {
1993 std::vector<FjVertex> g;
1994 std::vector<FjEdge> E;
1995 std::size_t nfork = 0;
1996 std::vector<std::size_t> task_vertex; ///< per transition, npos when it has none
1997 std::vector<std::size_t> res_node; ///< per resource, 0 until created
1998 std::vector<std::size_t> task_node; ///< per transition with its own Queue, else 0
1999 std::vector<std::string> joins;
2000 std::map<std::size_t, std::string> names; ///< station node -> its name, for the AvgTable lookups
2001 };
2002
2003 static std::size_t fj_vertex(FjGraph& fj, const std::string& kind, std::size_t node, std::size_t task) {
2004 FjVertex v;
2005 v.kind = kind;
2006 v.node = node;
2007 v.task = task;
2008 fj.g.push_back(v);
2009 return fj.g.size() - 1;
2010 }
2011
2012 /** `fjEmit`: add the flow graph of NODE between the virtual vertices VIN and VOUT. */
2013 void fj_emit(const detail::TreeNode& node, std::size_t vin, std::size_t vout, FjGraph& fj, Network& net) const {
2014 using detail::TreeNode;
2015 switch (node.type) {
2016 case TreeNode::EPS:
2017 fj.E.push_back(FjEdge{vin, vout, 1.0});
2018 break;
2019 case TreeNode::TASK: {
2020 const std::size_t t = node.t;
2021 const WFNTransition& tr = *transitions_[t];
2022 std::size_t nd;
2023 if (tr.resource_ >= 0) {
2024 const std::size_t r = static_cast<std::size_t>(tr.resource_);
2025 if (fj.res_node[r] == 0) {
2026 const WFNResource& res = *resources_[r];
2027 fj.res_node[r] = net.add_queue(res.name(), res.scheduling());
2028 net.set_number_of_servers(fj.res_node[r], res.servers());
2029 fj.names[fj.res_node[r]] = res.name();
2030 }
2031 nd = fj.res_node[r];
2032 } else if (std::isfinite(tr.servers())) {
2033 nd = net.add_queue(tr.name(), lang::SchedStrategy::FCFS);
2034 net.set_number_of_servers(nd, tr.servers());
2035 fj.task_node[t] = nd;
2036 fj.names[nd] = tr.name();
2037 } else {
2038 nd = net.add_delay(tr.name());
2039 fj.names[nd] = tr.name();
2040 }
2041 const std::size_t v = fj_vertex(fj, "station", nd, t + 1);
2042 fj.task_vertex[t] = v;
2043 fj.E.push_back(FjEdge{vin, v, 1.0});
2044 fj.E.push_back(FjEdge{v, vout, 1.0});
2045 break;
2046 }
2047 case TreeNode::SEQ: {
2048 std::size_t cur = vin;
2049 for (std::size_t j = 0; j < node.kids.size(); ++j) {
2050 const std::size_t nxt = j + 1 == node.kids.size() ? vout : fj_vertex(fj, "virt", 0, 0);
2051 fj_emit(node.kids[j], cur, nxt, fj, net);
2052 cur = nxt;
2053 }
2054 break;
2055 }
2056 case TreeNode::XOR:
2057 for (std::size_t j = 0; j < node.kids.size(); ++j) {
2058 const std::size_t b = fj_vertex(fj, "virt", 0, 0);
2059 fj.E.push_back(FjEdge{vin, b, node.probs[j]});
2060 fj_emit(node.kids[j], b, vout, fj, net);
2061 }
2062 break;
2063 case TreeNode::LOOP: {
2064 const std::size_t v1 = fj_vertex(fj, "virt", 0, 0);
2065 const std::size_t v2 = fj_vertex(fj, "virt", 0, 0);
2066 const std::size_t v3 = fj_vertex(fj, "virt", 0, 0);
2067 fj.E.push_back(FjEdge{vin, v1, 1.0});
2068 fj_emit(node.kids[0], v1, v2, fj, net);
2069 fj.E.push_back(FjEdge{v2, v3, node.q});
2070 fj_emit(node.kids[1], v3, v1, fj, net);
2071 fj.E.push_back(FjEdge{v2, vout, 1.0 - node.q});
2072 break;
2073 }
2074 case TreeNode::AND: {
2075 ++fj.nfork;
2076 const std::string pre = ReservedPrefix;
2077 const std::size_t fk = net.add_fork(pre + "fork" + std::to_string(fj.nfork));
2078 const std::string jname = pre + "join" + std::to_string(fj.nfork);
2079 const std::size_t jn = net.add_join(jname, fk);
2080 fj.joins.push_back(jname);
2081 const std::size_t vf = fj_vertex(fj, "fork", fk, 0);
2082 const std::size_t vj = fj_vertex(fj, "join", jn, 0);
2083 fj.E.push_back(FjEdge{vin, vf, 1.0});
2084 for (std::size_t j = 0; j < node.kids.size(); ++j) {
2085 const std::size_t b = fj_vertex(fj, "virt", 0, 0);
2086 const std::size_t bo = fj_vertex(fj, "virt", 0, 0);
2087 fj.E.push_back(FjEdge{vf, b, 1.0});
2088 fj_emit(node.kids[j], b, bo, fj, net);
2089 fj.E.push_back(FjEdge{bo, vj, 1.0});
2090 }
2091 fj.E.push_back(FjEdge{vj, vout, 1.0});
2092 break;
2093 }
2094 }
2095 }
2096
2097 /**
2098 * `fjResolve`: eliminate the virtual vertices. A fork branch that is not
2099 * deterministic is routed through a Router, since a Fork sends one job down
2100 * every outgoing link.
2101 */
2102 static std::vector<FjEdge> fj_resolve(FjGraph& fj, Network& net) {
2103 const std::size_t nv = fj.g.size();
2104 std::vector<bool> is_virt(nv);
2105 std::vector<std::size_t> iv, ip, vpos(nv, 0), ppos(nv, 0);
2106 for (std::size_t v = 0; v < nv; ++v) {
2107 is_virt[v] = fj.g[v].kind == "virt";
2108 if (is_virt[v]) {
2109 vpos[v] = iv.size();
2110 iv.push_back(v);
2111 } else {
2112 ppos[v] = ip.size();
2113 ip.push_back(v);
2114 }
2115 }
2116 const std::size_t ni = iv.size(), np = ip.size();
2117 Matrix<double> Iv(ni, ni, 0.0), Pvp(ni, np, 0.0);
2118 for (std::size_t i = 0; i < ni; ++i) Iv(i, i) = 1.0;
2119 for (const FjEdge& e : fj.E) {
2120 if (!is_virt[e.u]) continue;
2121 if (is_virt[e.v])
2122 Iv(vpos[e.u], vpos[e.v]) -= e.p;
2123 else
2124 Pvp(vpos[e.u], ppos[e.v]) += e.p;
2125 }
2126 const std::string loop_msg =
2127 "The net has a loop of zero-time choices that repeats with probability one, so a case never leaves it.";
2128 Matrix<double> Res(ni, np, 0.0);
2129 if (ni > 0) {
2130 Matrix<double> inv(ni, ni, 0.0);
2131 try {
2132 const detail::Factor f(Iv);
2133 for (std::size_t c = 0; c < ni; ++c) {
2134 std::vector<double> b(ni, 0.0);
2135 b[c] = 1.0;
2136 b = f.solve(b);
2137 for (std::size_t r = 0; r < ni; ++r) inv(r, c) = b[r];
2138 }
2139 } catch (const NumericError&) {
2140 throw UnsupportedError(ErrorKind("Structure"), loop_msg);
2141 }
2142 // condest(Iv) as the exact 1-norm condition number
2143 double n1 = 0.0, n1i = 0.0;
2144 for (std::size_t c = 0; c < ni; ++c) {
2145 double s = 0.0, si = 0.0;
2146 for (std::size_t r = 0; r < ni; ++r) {
2147 s += std::abs(Iv(r, c));
2148 si += std::abs(inv(r, c));
2149 }
2150 n1 = std::max(n1, s);
2151 n1i = std::max(n1i, si);
2152 }
2153 if (!(n1 * n1i <= 1.0 / lang::GlobalConstants::FineTol))
2154 throw UnsupportedError(ErrorKind("Structure"), loop_msg);
2155 for (std::size_t r = 0; r < ni; ++r)
2156 for (std::size_t c = 0; c < np; ++c) {
2157 double s = 0.0;
2158 for (std::size_t k = 0; k < ni; ++k) s += inv(r, k) * Pvp(k, c);
2159 Res(r, c) = s;
2160 }
2161 }
2162 const double tol = lang::GlobalConstants::FineTol;
2163 std::vector<FjEdge> dist;
2164 std::size_t nrouter = 0;
2165 for (std::size_t u : ip) {
2166 if (fj.g[u].kind == "sink") continue;
2167 std::vector<double> d;
2168 if (fj.g[u].kind == "fork") {
2169 for (std::size_t ei = 0; ei < fj.E.size(); ++ei) {
2170 const FjEdge e = fj.E[ei];
2171 if (e.u != u) continue;
2172 d.assign(np, 0.0);
2173 if (is_virt[e.v])
2174 for (std::size_t j = 0; j < np; ++j) d[j] = Res(vpos[e.v], j);
2175 else
2176 d[ppos[e.v]] = 1.0;
2177 std::vector<std::size_t> tgt;
2178 for (std::size_t j = 0; j < np; ++j)
2179 if (d[j] > tol) tgt.push_back(j);
2180 if (tgt.size() == 1 && std::abs(d[tgt[0]] - 1) < tol) {
2181 dist.push_back(FjEdge{u, ip[tgt[0]], 1.0});
2182 } else {
2183 ++nrouter;
2184 const std::size_t rt =
2185 net.add_router(std::string(ReservedPrefix) + "router" + std::to_string(nrouter));
2186 const std::size_t r = fj_vertex(fj, "router", rt, 0);
2187 dist.push_back(FjEdge{u, r, 1.0});
2188 for (std::size_t j : tgt) dist.push_back(FjEdge{r, ip[j], d[j]});
2189 }
2190 }
2191 } else {
2192 d.assign(np, 0.0);
2193 for (const FjEdge& e : fj.E) {
2194 if (e.u != u) continue;
2195 if (is_virt[e.v])
2196 for (std::size_t j = 0; j < np; ++j) d[j] += e.p * Res(vpos[e.v], j);
2197 else
2198 d[ppos[e.v]] += e.p;
2199 }
2200 for (std::size_t j = 0; j < np; ++j)
2201 if (d[j] > tol) dist.push_back(FjEdge{u, ip[j], d[j]});
2202 }
2203 }
2204 return dist;
2205 }
2206
2207 /** `compileToForkJoin`: the fork-join queueing network of a block-structured net. */
2208 std::shared_ptr<Network> compile_to_fork_join() {
2209 detail::TreeNode tree;
2210 std::string reason;
2211 if (!reduce_to_tree(tree, reason))
2212 throw UnsupportedError(ErrorKind("Structure"), "toNetwork('forkjoin') requires a block-structured "
2213 "WF-net: " + reason + ". Use toNetwork('spn').");
2214 const bool closed = closed_cases_ > 0;
2215 const std::string pre = ReservedPrefix;
2216 std::shared_ptr<Network> network = std::make_shared<Network>(name_ + "_forkjoin");
2217 Network& net = *network;
2218 const std::size_t nT = transitions_.size(), nR = resources_.size();
2219 // tasks on a resource shared with another task need their own class
2220 std::vector<std::size_t> use_count(nR, 0);
2221 for (std::size_t t = 0; t < nT; ++t)
2222 if (transitions_[t]->resource_ >= 0) ++use_count[static_cast<std::size_t>(transitions_[t]->resource_)];
2223 FjGraph fj;
2224 fj.task_vertex.assign(nT, static_cast<std::size_t>(-1));
2225 fj.res_node.assign(nR, 0);
2226 fj.task_node.assign(nT, 0);
2227 if (closed) {
2228 fj_vertex(fj, "endpoint", net.add_delay(pre + "restart"), 0);
2229 } else {
2230 fj_vertex(fj, "endpoint", net.add_source(pre + "source"), 0);
2231 fj_vertex(fj, "sink", net.add_sink(pre + "sink"), 0);
2232 }
2233 const std::size_t vin = fj_vertex(fj, "virt", 0, 0);
2234 const std::size_t vout = fj_vertex(fj, "virt", 0, 0);
2235 fj.E.push_back(FjEdge{0, vin, 1.0});
2236 fj.E.push_back(FjEdge{vout, closed ? 0u : 1u, 1.0});
2237 fj_emit(tree, vin, vout, fj, net);
2238 const std::vector<FjEdge> dist = fj_resolve(fj, net);
2239 // classes
2240 const std::size_t endpoint = fj.g[0].node;
2241 const std::size_t case_class = closed
2242 ? net.add_closed_class("Case", static_cast<double>(closed_cases_), endpoint, 0)
2243 : net.add_open_class("Case", 0);
2244 std::vector<std::size_t> cls_of(nT, case_class);
2245 std::vector<std::string> cls_name(nT, "Case");
2246 for (std::size_t t = 0; t < nT; ++t) {
2247 const long r = transitions_[t]->resource_;
2248 if (r >= 0 && use_count[static_cast<std::size_t>(r)] > 1) {
2249 cls_name[t] = "Case_" + transitions_[t]->name();
2250 cls_of[t] = closed ? net.add_closed_class(cls_name[t], 0.0, endpoint, 0)
2251 : net.add_open_class(cls_name[t], 0);
2252 }
2253 }
2254 std::vector<std::size_t> vcls(fj.g.size(), case_class);
2255 for (std::size_t v = 0; v < fj.g.size(); ++v)
2256 if (fj.g[v].task > 0) vcls[v] = cls_of[fj.g[v].task - 1];
2257 // service
2258 if (closed)
2259 net.set_service(endpoint, case_class, Immediate());
2260 else
2261 net.set_arrival(endpoint, case_class, arrival_);
2262 std::shared_ptr<Compiled> c = std::make_shared<Compiled>();
2263 c->task_station.assign(nT, std::string());
2264 for (std::size_t t = 0; t < nT; ++t) {
2265 if (fj.task_vertex[t] == static_cast<std::size_t>(-1)) continue;
2266 const WFNTransition& tr = *transitions_[t];
2267 const std::size_t nd = fj.g[fj.task_vertex[t]].node;
2268 net.set_service(nd, cls_of[t], tr.distribution());
2269 const std::string station = fj.names.at(nd);
2270 c->tasks.push_back(Compiled::Task{tr.name(), station, cls_name[t]});
2271 c->task_station[t] = station;
2272 }
2273 // routing
2275 for (const FjEdge& e : dist) {
2276 const std::size_t r = vcls[e.u];
2277 const std::size_t s = fj.g[e.v].kind == "sink" ? r : vcls[e.v];
2278 const std::size_t nu = fj.g[e.u].node, nv = fj.g[e.v].node;
2279 P.set(r, s, nu, nv, P.get(r, s, nu, nv) + e.p);
2280 }
2281 net.link(P);
2282 for (std::size_t r = 0; r < nR; ++r)
2283 if (fj.res_node[r] != 0)
2284 c->resources.push_back(Compiled::Res{resources_[r]->name(), resources_[r]->servers(),
2285 fj.names.at(fj.res_node[r]), {}});
2286 for (std::size_t t = 0; t < nT; ++t)
2287 if (fj.task_node[t] != 0)
2288 c->resources.push_back(Compiled::Res{transitions_[t]->name(), transitions_[t]->servers(),
2289 fj.names.at(fj.task_node[t]), {}});
2290 c->network = network;
2291 c->target = "forkjoin";
2292 c->k = closed_cases_;
2293 if (closed) {
2294 c->mode = "closed";
2295 c->restart = pre + "restart";
2296 } else {
2297 c->mode = "open";
2298 c->lambda = 1.0 / arrival_.mean;
2299 }
2300 c->tree = tree;
2301 c->joins = fj.joins;
2302 c->task_class = cls_name;
2303 compiled_ = c;
2304 return network;
2305 }
2306
2307 /**
2308 * `fjTreeTime`: mean case time through NODE from the per-visit response
2309 * times of a fork-join network. An AND block takes the mean of its branch
2310 * times plus the response time of its join: a join reports the
2311 * synchronisation wait averaged over the sibling tasks, one per branch, and
2312 * every sibling leaves the block when the last one arrives. Joins are
2313 * visited in the preorder fjEmit created them in.
2314 */
2315 static double fj_tree_time(const detail::TreeNode& node, const Compiled& c, const AvgTable& tab,
2316 std::size_t& jidx) {
2317 using detail::TreeNode;
2318 switch (node.type) {
2319 case TreeNode::EPS:
2320 return 0.0;
2321 case TreeNode::TASK:
2322 return detail::table_cell(tab, tab.RespT, c.task_station[node.t], c.task_class[node.t]);
2323 case TreeNode::SEQ: {
2324 double x = 0.0;
2325 for (const TreeNode& k : node.kids) x += fj_tree_time(k, c, tab, jidx);
2326 return x;
2327 }
2328 case TreeNode::XOR: {
2329 double x = 0.0;
2330 for (std::size_t j = 0; j < node.kids.size(); ++j)
2331 x += node.probs[j] * fj_tree_time(node.kids[j], c, tab, jidx);
2332 return x;
2333 }
2334 case TreeNode::LOOP: {
2335 const double d = fj_tree_time(node.kids[0], c, tab, jidx);
2336 const double r = fj_tree_time(node.kids[1], c, tab, jidx);
2337 return (d + node.q * r) / (1.0 - node.q);
2338 }
2339 case TreeNode::AND: {
2340 const std::string& jn = c.joins.at(jidx++);
2341 double y = 0.0;
2342 for (const TreeNode& k : node.kids) y += fj_tree_time(k, c, tab, jidx);
2343 // fjEmit routes every task into a join in the case class
2344 return y / static_cast<double>(node.kids.size()) + detail::table_cell(tab, tab.RespT, jn, "Case");
2345 }
2346 }
2347 return 0.0;
2348 }
2349
2350 // ------------------------------------------------------------------
2351 // to_network("spn") and the plain net
2352 // ------------------------------------------------------------------
2353
2354 /** `setTransitionTiming`: one mode of a LINE Transition from a WF-net transition. */
2355 static void set_transition_timing(Network& net, std::size_t node, std::size_t mode, const WFNTransition& w,
2356 double servers) {
2357 if (w.is_immediate()) {
2358 net.set_mode_timing(node, mode, lang::TimingStrategy::IMMEDIATE);
2359 net.set_firing_weight(node, mode, w.weight());
2360 net.set_firing_priority(node, mode, w.priority());
2361 } else {
2362 net.set_mode_timing(node, mode, lang::TimingStrategy::TIMED);
2363 net.set_mode_distribution(node, mode, w.distribution());
2364 net.set_mode_servers(node, mode, servers);
2365 net.set_firing_priority(node, mode, w.priority());
2366 }
2367 }
2368
2369 /**
2370 * `compileToSPN`: a stochastic Petri net with a case-counter place. Arrivals
2371 * (open) or the k recirculating cases (closed) wait in wfn_arrivals; the
2372 * immediate wfn_start moves one into the source and adds a token to
2373 * wfn_cases, and the immediate wfn_end takes the token in the sink together
2374 * with one of wfn_cases. The mean of wfn_cases is therefore the mean number
2375 * of cases in the system.
2376 */
2377 std::shared_ptr<Network> compile_to_spn() {
2378 const bool closed = closed_cases_ > 0;
2379 const Inc inc = incidence();
2380 const std::size_t nP = inc.nP, nT = inc.nT, nR = resources_.size();
2381 for (std::size_t t = 0; t < nT; ++t) {
2382 const WFNTransition& tr = *transitions_[t];
2383 if (tr.resource_ < 0) continue;
2384 for (std::size_t p = 0; p < nP; ++p) {
2385 if (inc.pre[p][t] == 0) continue;
2386 std::vector<std::size_t> rivals;
2387 for (std::size_t u = 0; u < nT; ++u)
2388 if (u != t && inc.pre[p][u] > 0) rivals.push_back(u);
2389 if (!rivals.empty())
2390 throw UnsupportedError(ErrorKind("Structure"),
2391 "Task \"" + tr.name() + "\" holds resource \"" +
2392 resources_[static_cast<std::size_t>(tr.resource_)]->name() +
2393 "\" and competes for place \"" + places_[p]->name() + "\" with " +
2394 transition_list(rivals) +
2395 "; in the SPN the resource is acquired by an immediate transition, "
2396 "which would always win that race.");
2397 }
2398 }
2399 const std::string pre = ReservedPrefix;
2400 std::shared_ptr<Network> network = std::make_shared<Network>(name_ + "_spn");
2401 Network& net = *network;
2402 std::size_t source = 0, sink = 0;
2403 if (!closed) {
2404 source = net.add_source(pre + "source");
2405 sink = net.add_sink(pre + "sink");
2406 }
2407 std::vector<std::size_t> pl(nP);
2408 for (std::size_t p = 0; p < nP; ++p) pl[p] = net.add_place(places_[p]->name());
2409 const std::size_t p_arr = net.add_place(pre + "arrivals");
2410 const std::size_t p_cnt = net.add_place(pre + "cases");
2411 std::vector<std::size_t> p_res(nR);
2412 for (std::size_t r = 0; r < nR; ++r) p_res[r] = net.add_place(pre + "res_" + resources_[r]->name());
2413 std::vector<std::size_t> tr_(nT), acq(nT, 0), p_busy(nT, 0);
2414 for (std::size_t t = 0; t < nT; ++t) {
2415 tr_[t] = net.add_transition(transitions_[t]->name());
2416 if (transitions_[t]->resource_ >= 0) {
2417 acq[t] = net.add_transition(pre + "acq_" + transitions_[t]->name());
2418 p_busy[t] = net.add_place(pre + "busy_" + transitions_[t]->name());
2419 }
2420 }
2421 const std::size_t t_start = net.add_transition(pre + "start");
2422 const std::size_t t_end = net.add_transition(pre + "end");
2423 std::size_t jc;
2424 if (closed) {
2425 // resource tokens are tokens of the same uncoloured class, so they count in the population
2426 double n_res = 0.0;
2427 for (std::size_t r = 0; r < nR; ++r) n_res += resources_[r]->servers();
2428 jc = net.add_closed_class("Case", static_cast<double>(closed_cases_) + n_res, p_arr, 0);
2429 } else {
2430 jc = net.add_open_class("Case", 0);
2431 net.set_arrival(source, jc, arrival_);
2432 }
2433 RoutingMatrix R;
2434 auto arc = [&R](std::size_t a, std::size_t b) { R.set(1, 1, a, b, 1.0); };
2435 // start and end
2436 std::size_t m = net.add_mode(t_start, "start");
2437 net.set_mode_timing(t_start, m, lang::TimingStrategy::IMMEDIATE);
2438 net.set_enabling_conditions(t_start, m, jc, p_arr, 1);
2439 net.set_firing_outcome(t_start, m, jc, pl[source_], 1);
2440 net.set_firing_outcome(t_start, m, jc, p_cnt, 1);
2441 arc(p_arr, t_start);
2442 arc(t_start, pl[source_]);
2443 arc(t_start, p_cnt);
2444 m = net.add_mode(t_end, "end");
2445 net.set_mode_timing(t_end, m, lang::TimingStrategy::IMMEDIATE);
2446 net.set_enabling_conditions(t_end, m, jc, pl[sink_], 1);
2447 net.set_enabling_conditions(t_end, m, jc, p_cnt, 1);
2448 arc(pl[sink_], t_end);
2449 arc(p_cnt, t_end);
2450 if (closed) {
2451 net.set_firing_outcome(t_end, m, jc, p_arr, 1);
2452 arc(t_end, p_arr);
2453 } else {
2454 net.set_firing_outcome(t_end, m, jc, sink, 1);
2455 arc(t_end, sink);
2456 arc(source, p_arr);
2457 }
2458 std::shared_ptr<Compiled> c = std::make_shared<Compiled>();
2459 for (std::size_t r = 0; r < nR; ++r)
2460 c->resources.push_back(Compiled::Res{resources_[r]->name(), resources_[r]->servers(), std::string(), {}});
2461 for (std::size_t t = 0; t < nT; ++t) {
2462 const WFNTransition& w = *transitions_[t];
2463 if (w.resource_ < 0) {
2464 m = net.add_mode(tr_[t], w.name());
2465 set_transition_timing(net, tr_[t], m, w, w.servers());
2466 for (std::size_t p = 0; p < nP; ++p)
2467 if (inc.pre[p][t] > 0) {
2468 net.set_enabling_conditions(tr_[t], m, jc, pl[p], static_cast<double>(inc.pre[p][t]));
2469 arc(pl[p], tr_[t]);
2470 }
2471 for (std::size_t p = 0; p < nP; ++p)
2472 if (inc.post[p][t] > 0) {
2473 net.set_firing_outcome(tr_[t], m, jc, pl[p], static_cast<double>(inc.post[p][t]));
2474 arc(tr_[t], pl[p]);
2475 }
2476 } else {
2477 const std::size_t r = static_cast<std::size_t>(w.resource_);
2478 const std::size_t ma = net.add_mode(acq[t], "acq_" + w.name());
2479 net.set_mode_timing(acq[t], ma, lang::TimingStrategy::IMMEDIATE);
2480 net.set_firing_weight(acq[t], ma, w.weight());
2481 net.set_firing_priority(acq[t], ma, w.priority());
2482 for (std::size_t p = 0; p < nP; ++p)
2483 if (inc.pre[p][t] > 0) {
2484 net.set_enabling_conditions(acq[t], ma, jc, pl[p], static_cast<double>(inc.pre[p][t]));
2485 arc(pl[p], acq[t]);
2486 }
2487 net.set_enabling_conditions(acq[t], ma, jc, p_res[r], 1);
2488 arc(p_res[r], acq[t]);
2489 net.set_firing_outcome(acq[t], ma, jc, p_busy[t], 1);
2490 arc(acq[t], p_busy[t]);
2491 m = net.add_mode(tr_[t], w.name());
2492 set_transition_timing(net, tr_[t], m, w, std::numeric_limits<double>::infinity());
2493 net.set_enabling_conditions(tr_[t], m, jc, p_busy[t], 1);
2494 arc(p_busy[t], tr_[t]);
2495 for (std::size_t p = 0; p < nP; ++p)
2496 if (inc.post[p][t] > 0) {
2497 net.set_firing_outcome(tr_[t], m, jc, pl[p], static_cast<double>(inc.post[p][t]));
2498 arc(tr_[t], pl[p]);
2499 }
2500 net.set_firing_outcome(tr_[t], m, jc, p_res[r], 1);
2501 arc(tr_[t], p_res[r]);
2502 c->busy.push_back(Compiled::Busy{w.name(), pre + "busy_" + w.name()});
2503 c->resources[r].busy.push_back(c->busy.size() - 1);
2504 }
2505 }
2506 net.link(R);
2507 for (std::size_t p = 0; p < nP; ++p) net.set_initial_marking(pl[p], {0.0});
2508 net.set_initial_marking(p_cnt, {0.0});
2509 for (std::size_t t = 0; t < nT; ++t)
2510 if (p_busy[t] != 0) net.set_initial_marking(p_busy[t], {0.0});
2511 for (std::size_t r = 0; r < nR; ++r) net.set_initial_marking(p_res[r], {resources_[r]->servers()});
2512 net.set_initial_marking(p_arr, {closed ? static_cast<double>(closed_cases_) : 0.0});
2513 c->network = network;
2514 c->target = "spn";
2515 c->k = closed_cases_;
2516 if (closed) {
2517 c->mode = "closed";
2518 } else {
2519 c->mode = "open";
2520 c->lambda = 1.0 / arrival_.mean;
2521 }
2522 c->counter = pre + "cases";
2523 for (std::size_t p = 0; p < nP; ++p) c->places.push_back(places_[p]->name());
2524 std::vector<Compiled::Res> kept;
2525 for (const Compiled::Res& res : c->resources)
2526 if (res.servers > 0 && !res.busy.empty()) kept.push_back(res);
2527 c->resources = kept;
2528 compiled_ = c;
2529 return network;
2530 }
2531
2532 /** `buildPlainNetwork(k)`: the net itself as a closed LINE Petri net, k tokens in the source. */
2533 std::shared_ptr<Network> build_plain_network(long k) const {
2534 const Inc inc = incidence();
2535 const std::size_t nP = inc.nP, nT = inc.nT;
2536 std::shared_ptr<Network> model = std::make_shared<Network>(name_);
2537 Network& net = *model;
2538 std::vector<std::size_t> pl(nP), tr(nT);
2539 for (std::size_t p = 0; p < nP; ++p) pl[p] = net.add_place(places_[p]->name());
2540 for (std::size_t t = 0; t < nT; ++t) tr[t] = net.add_transition(transitions_[t]->name());
2541 const std::size_t jc = net.add_closed_class("Case", static_cast<double>(k), pl[source_], 0);
2542 RoutingMatrix R;
2543 for (std::size_t t = 0; t < nT; ++t) {
2544 const WFNTransition& w = *transitions_[t];
2545 const std::size_t m = net.add_mode(tr[t], w.name());
2546 set_transition_timing(net, tr[t], m, w, w.servers());
2547 for (std::size_t p = 0; p < nP; ++p)
2548 if (inc.pre[p][t] > 0) {
2549 net.set_enabling_conditions(tr[t], m, jc, pl[p], static_cast<double>(inc.pre[p][t]));
2550 R.set(1, 1, pl[p], tr[t], 1.0);
2551 }
2552 for (std::size_t p = 0; p < nP; ++p)
2553 if (inc.post[p][t] > 0) {
2554 net.set_firing_outcome(tr[t], m, jc, pl[p], static_cast<double>(inc.post[p][t]));
2555 R.set(1, 1, tr[t], pl[p], 1.0);
2556 }
2557 }
2558 net.link(R);
2559 for (std::size_t p = 0; p < nP; ++p)
2560 net.set_initial_marking(pl[p], {static_cast<double>(static_cast<long>(p) == source_ ? k : 0)});
2561 return model;
2562 }
2563};
2564
2565} // namespace wfn
2566} // namespace line
2567
2568#endif // LINE_API_WFN_WORKFLOW_NET_H
InputError(const std::string &what)
Definition error.h:105
InternalError(const std::string &what)
Definition error.h:133
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The shared surface of every solver, Python's NetworkSolver.
Definition solver.h:123
const AvgTable & avg_table()
getAvgTable(): the average table, solving on first demand.
Network & model() const
The model this solver was built on.
Definition solver.h:132
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
A place, a transition, or the name of either, where an argument may be any of them.
ElementRef(const WFNPlace &p)
friend class WorkflowNet
ElementRef(const std::string &n)
ElementRef(const WFNTransition &t)
ElementRef(const char *n)
WFNResource(const std::string &name, double servers=1.0, lang::SchedStrategy scheduling=lang::SchedStrategy::FCFS)
std::shared_ptr< Network > to_network(const std::string &target, bool check_soundness=true)
toNetwork(target, 'checkSoundness', tf): compile the net to a Network.
workflow::Workflow< double > to_workflow()
toWorkflow(): the equivalent Workflow of a block-structured net.
WFNResource & set_resource(const ElementRef &transition, const std::string &name, double servers)
void set_arrival_process(const std::string &type, const Dist &map)
setArrivalProcess('map', MAP).
WorkflowNet(const std::string &name)
WorkflowNet(const WorkflowNet &)=delete
std::pair< Matrix< double >, Matrix< double > > get_incidence() const
getIncidence(): input and output arc weights, places x transitions.
const Dist * arrival_process() const
The interarrival law of cases (open analysis), or nullptr.
void set_sink(const ElementRef &place)
setSink(place): declare the sink place (checked by validate).
static WorkflowNet from_network(Network &model)
WorkflowNet.fromNetwork(model): copy the places, transitions and arcs of a single-class Network of Pl...
static std::string format_report(const SoundnessReport &report)
formatReport(report): one-line summary of a soundness report.
WFNPlace & add_place(const std::string &name, double initial_marking=0.0)
addPlace(name, initialMarking).
const WFNPlace & place(std::size_t i) const
const WFNPlace & get_place(const std::string &name) const
getPlace(name).
WorkflowNet & operator=(WorkflowNet &&)=default
bool is_block_structured()
isBlockStructured(): true if the net reduces to a process tree.
std::size_t num_transitions() const
const WFNPlace * sink_place() const
The sink place, set by validate when not declared; nullptr before.
WFNTransition & add_transition(const std::string &name, const Dist &distribution, lang::TimingStrategy timing, double servers=std::numeric_limits< double >::infinity(), double priority=1.0, double weight=1.0)
addTransition(name, distribution, timing, servers, priority, weight).
const std::vector< WFNArc > & arcs() const
void set_arrival_process(const std::string &type)
setArrivalProcess('none'), or a type that needs a parameter, which is then refused.
SoundnessReport check_soundness(double k, double state_cap)
void set_arrival_process(const std::string &type, double rate)
setArrivalProcess('poisson', rate).
WorkflowNet & operator=(const WorkflowNet &)=delete
ResourceLoads get_resource_loads(double lambda)
getResourceLoads(lambda): one row per resource and per task with a finite server count,...
void set_source(const ElementRef &place)
setSource(place): declare the source place (checked by validate).
WFNResource & set_resource(const ElementRef &transition, const std::string &name, double servers, lang::SchedStrategy scheduling)
SoundnessReport check_soundness(double k=1)
checkSoundness(k, stateCap): decide k-soundness on the reachability graph.
long closed_cases() const
void set_state_cap(double cap)
setStateCap(cap): the largest state space explored by the analyses.
WFNResource & set_resource(const ElementRef &transition, const std::string &name)
setResource(transition, name, servers, scheduling): assign a resource, shared by every task that name...
const WFNTransition & transition(std::size_t i) const
void save_pnml(const std::string &filename)
savePNML(filename): write the plain Petri net (no case counter, no resources) with one token in the s...
double state_cap() const
std::size_t num_places() const
lang::Distrib< double > Dist
const std::string & name() const
void set_arrival_process(const Dist &process)
setArrivalProcess(dist): any interarrival distribution for open analysis.
CycleTimeAnalysis get_cycle_time_analysis()
getCycleTimeAnalysis(): the full single-case analysis.
const WFNResource & resource(std::size_t r) const
void validate()
validate(): exactly one place without incoming arcs (the source i), exactly one without outgoing arcs...
ResourceLoads get_resource_loads()
getResourceLoads(): LAMBDA is the rate of the arrival process.
const WFNPlace * source_place() const
The source place, set by validate when not declared; nullptr before.
static WorkflowNet from_pnml(const std::string &filename, const std::string &net_name=std::string())
WorkflowNet.fromPNML(filename, netName): read a net with pnml_load and convert it with from_network; ...
CaseMetrics get_case_metrics(NetworkSolver &solver)
getCaseMetrics(solver): case-level metrics from a solver built on the network returned by the last to...
void set_closed_cases(double k)
setClosedCases(k): k concurrent cases (closed analysis); 0 = open.
std::size_t num_resources() const
Dist get_cycle_time()
getCycleTime(): exact case cycle time of one case without contention.
WFNTransition & get_transition(const std::string &name)
getTransition(name).
WFNTransition & add_transition(const std::string &name, const Dist &distribution=Exp(1.0), double servers=std::numeric_limits< double >::infinity(), double priority=1.0, double weight=1.0)
addTransition(name, distribution, [], servers, priority, weight): the timing follows the law,...
WorkflowNet(WorkflowNet &&)=default
WFNTransition & transition(std::size_t i)
static constexpr const char * ReservedPrefix
Names prefixed so that compiled networks never collide with user names.
const WFNArc & add_arc(const ElementRef &source, const ElementRef &target, double weight=1.0)
addArc(source, target, weight): from a place to a transition or from a transition to a place,...
static constexpr std::size_t npos
Definition workflow.h:239
The distributions a user names, spelled as their Python twins.
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
void pnml_save(const qn::NetworkStruct< T > &sn, const std::string &path)
Write the Petri net of a refreshed NetworkStruct to a PNML place/transition file.
Definition pnml.h:273
qn::Network< T > pnml_load(const std::string &path, const std::string &net_id=std::string())
Read one net of a PNML place/transition document into a Network.
Definition pnml.h:492
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
TimingStrategy
SPN transition timing, with the values of MATLAB TimingStrategy.
Definition lang_types.h:368
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:369
const char * node_type_to_text(NodeType t)
Name of a node kind, for diagnostics.
Definition lang_types.h:348
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
std::vector< const Element * > kids(const Element *el)
Model children of el: results and output requests are skipped.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
qn::RoutingMatrix< double > RoutingMatrix
model.init_routing_matrix()'s type, Python's RoutingMatrix.
Definition solver.h:59
qn::Network< double > Network
Network as a user names it: the double model, Python's Network.
Definition solver.h:57
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
PNML (ISO/IEC 15909-2) place/transition nets, read and written.
The solver API a user writes, spelled as its Python twin.
The report of WorkflowNet::check_soundness.
getAvgTable, one row per (station, class) that carries a metric.
Definition avg_table.h:38
std::vector< double > Tput
Definition avg_table.h:40
std::vector< double > RespT
Definition avg_table.h:40
std::vector< double > Util
Definition avg_table.h:40
std::vector< double > QLen
Definition avg_table.h:40
ErrorKind(std::string n)
Definition error.h:39
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double CoarseTol
Definition lang_types.h:766
The parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
std::vector< double > firingprio
firing priority per mode
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< lang::Distrib< T > > firingproc
firing distribution per mode
std::vector< double > nmodeservers
servers per mode, may be infinite
std::vector< T > fireweight
weight among simultaneously enabled modes
std::vector< Matrix< T > > firing
firing[m](p,r): class-r tokens mode m moves to/from place p when it fires.
std::vector< Matrix< T > > enabling
enabling[m](p,r): class-r tokens of place p (0-based node) mode m needs.
std::vector< std::function< T(const std::vector< T > &)> > firingdep
Marking-dependent firing-rate multiplier g_m(marking); an empty entry is the unit multiplier.
std::vector< Matrix< T > > inhibiting
inhibiting[m](p,r): class-r tokens of p that BLOCK mode m (Inf = never).
Mean tokens per place, SPN target only.
std::vector< double > QLen
std::vector< std::string > Place
std::vector< std::string > Resource
std::vector< double > Servers
std::vector< double > Utilization
RespT at its station for the fork-join target, InService tokens for the SPN one.
std::vector< std::string > Task
std::vector< double > RespT
std::vector< double > InService
Case-level metrics of get_case_metrics.
std::string target
"forkjoin" or "spn"
struct line::wfn::CaseMetrics::Places places
struct line::wfn::CaseMetrics::Tasks tasks
struct line::wfn::CaseMetrics::Resources resources
std::string mode
"open" or "closed"
std::vector< std::string > Transition
std::vector< double > ExpectedFirings
std::vector< std::string > Place
The full single-case analysis of get_cycle_time_analysis.
std::vector< double > moments
The first three raw moments.
lang::Distrib< double > ph
PH law of the case duration.
std::size_t num_states
Number of tangible transient states.
std::vector< double > alpha
The (alpha, Q) pair behind ph; alpha may be defective by zero_time_prob.
struct line::wfn::CycleTimeAnalysis::PlaceTime place_time
double zero_time_prob
Probability that a case completes in zero time.
struct line::wfn::CycleTimeAnalysis::Firings firings
double cdf(double t) const
P(duration <= t).
double percentile(double q) const
The q-quantile, q in (0,1).
The table of get_resource_loads.
std::vector< double > DemandPerCase
std::vector< bool > Stable
std::vector< double > Servers
std::vector< double > Load
std::vector< std::string > Resource
std::size_t size() const
std::vector< Marking > improper_completion
std::vector< Marking > deadlocks
std::vector< Marking > no_option_to_complete
std::vector< std::string > dead_transitions
bool sound
The first output of MATLAB's checkSoundness: true only when the verdict is "sound".
std::vector< std::string > witnesses
One readable line per defect.
std::string verdict
"sound", "unsound" or "unknown" (state cap reached first).
std::vector< std::size_t > busy
An arc of a WorkflowNet, from a place to a transition or from a transition to a place.
A place of a WorkflowNet.
A resource shared by the tasks of a WorkflowNet that name it, and by every case.
A transition (task) of a WorkflowNet.
An activity workflow reduced to one phase-type law.