LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
spn_pf.h
Go to the documentation of this file.
1#ifndef LINE_API_SPN_SPN_PF_H
2#define LINE_API_SPN_SPN_PF_H
3
4/**
5 * @file spn_pf.h
6 * @ingroup api_spn
7 * @brief Product form of a stochastic Petri net: decide whether one exists and
8 * derive the per-level factors g_l that `mdd_rec` and `spn_metrics` take
9 * as input.
10 *
11 * THIS IS THE PART THE MDD-REC PAPER DECLARES OUT OF SCOPE (FGCS Sec. 3.2).
12 * Every other function in api/spn receives the g_l already formed; this one
13 * derives them from the net, which is what lets a solver reach them.
14 *
15 * THE THEORY, IN ONE PARAGRAPH. Write I(t), O(t) for the input and output
16 * vectors of mode t and lambda_t for its rate constant. Henderson-Taylor and
17 * Coleman-Henderson-Taylor show that a net whose firing rate has the form
18 *
19 * r_t(m) = lambda_t psi(m - I(t)) / psi(m), m >= I(t)
20 *
21 * has invariant measure pi(m) = psi(m) prod_l y_l^{m_l} whenever the positive
22 * vector y satisfies COMPLEX BALANCE: reading the distinct vectors appearing as
23 * some I(t) or O(t) as the COMPLEXES of the net, the flow into every complex
24 * equals the flow out of it,
25 *
26 * sum_{t : O(t)=v} lambda_t y^{I(t)} = ( sum_{t : I(t)=v} lambda_t ) y^v.
27 *
28 * Two choices of psi are realisable in LINE's own rate law, and they are the two
29 * tested for here:
30 *
31 * psi = 1 r_t = lambda_t, the rate of a SINGLE-SERVER mode.
32 * pi(m) = prod_l y_l^{m_l}, so g_l(k) = y_l^k.
33 * psi = prod_l 1/m_l! r_t = lambda_t prod_l m_l!/(m_l-I_l)!, MASS ACTION,
34 * reached through a marking-dependent firing rate or,
35 * for a mode drawing one token from one place, by
36 * infinite-server semantics.
37 * pi(m) = prod_l y_l^{m_l}/m_l!, so g_l(k) = y_l^k/k!.
38 *
39 * Which one holds is not guessed from the model API: the effective rate LINE
40 * would use, lambda_t min(enabling degree, servers) g(m), is EVALUATED at every
41 * reachable marking and compared against both laws. A net that matches neither
42 * under one common psi is refused by name, never approximated.
43 *
44 * SOLVING FOR y. Complex balance reads A_lambda Psi(y) = 0 with A_lambda the
45 * Laplacian of the weighted digraph on complexes and Psi(y)_v = y^v. That
46 * Laplacian is the TRANSPOSED GENERATOR of a Markov chain that hops from complex
47 * to complex at the rate of the mode joining them, so its kernel on one linkage
48 * class is that chain's stationary distribution and `ctmc_solve` returns it --
49 * strictly positive exactly when the class is strongly connected, which is weak
50 * reversibility. With that positive vector kappa in hand y follows from the
51 * LINEAR system in x = log y,
52 *
53 * (v - v0) x = log kappa_v - log kappa_v0, v, v0 in the same linkage class.
54 *
55 * Feinberg's Deficiency Zero Theorem says this system is consistent for every
56 * choice of rate constants when the net is weakly reversible and its deficiency
57 * c - l - s is zero, which is why those two numbers are reported; but
58 * consistency is CHECKED rather than assumed, so a net of positive deficiency
59 * whose particular rates still admit a complex-balanced point is accepted on the
60 * evidence.
61 *
62 * THE GAUGE, AND WHY THE MINIMUM-NORM SOLUTION IS THE CANONICAL ONE. Complex
63 * balance fixes y only up to y -> y .* exp(u) for any u orthogonal to the
64 * stoichiometric subspace S. Such a shift multiplies pi(m) by exp(u'm), which is
65 * CONSTANT on one compatibility class, so every reported measure is invariant
66 * under it -- but the normalising constant G itself is not, it scales by that
67 * constant. A gauge must therefore be FIXED, or the four codebases would report
68 * four different G on the same net. The one fixed here is x in the row space of
69 * the constraint matrix, i.e. the minimum-norm solution, reached in a form that
70 * is unique whichever least-squares primitive a codebase carries: solve
71 * (rows rows^T) w = rhs and set x = rows^T w. Any two solutions w of that system
72 * give the SAME rows^T w, so the answer does not depend on how the rank-deficient
73 * solve breaks its tie.
74 *
75 * ARITHMETIC. The logarithm and the exponential are unavoidable here -- y is the
76 * exponential of a least-squares solution -- so the whole derivation needs a
77 * transcendental field and is refused under exact arithmetic by name. What
78 * consumes the g_l afterwards, `mdd_rec` and `spn_metrics`, stays rational.
79 *
80 * References:
81 * J. L. Coleman, W. Henderson, P. G. Taylor, "Product form equilibrium
82 * distributions and a convolution algorithm for stochastic Petri nets",
83 * Performance Evaluation 26(3), 1996.
84 * M. Feinberg, "Complex balancing in general kinetic systems", Arch. Rational
85 * Mech. Anal. 49, 1972.
86 * D. F. Anderson, G. Craciun, T. G. Kurtz, "Product-form stationary
87 * distributions for deficiency zero chemical reaction networks", Bull. Math.
88 * Biol. 72, 2010.
89 *
90 * @see mdd_rec, spn_metrics, spn_mdd, spn_conv
91 */
92
93#include <algorithm>
94#include <cmath>
95#include <cstddef>
96#include <limits>
97#include <map>
98#include <string>
99#include <vector>
100
102#include "line/api/spn/spn_mdd.h"
103#include "line/util/error.h"
104#include "line/util/lstsq.h"
105#include "line/util/matrix.h"
106
107namespace line {
108namespace spn {
109
110/** Options of the product-form derivation. */
112 /** Per-place-level token bound, passed to spn_mdd; empty infers it. */
113 std::vector<double> bound;
114 /** Relative tolerance of the rate-law and complex-balance checks. */
115 double tol = 1e-9;
116 bool verbose = false;
117};
118
119/** The product form, and the certificate that it is one. */
120template <class T>
122 /** g[l][k] = g_l(k), ready for mdd_rec. */
123 std::vector<std::vector<T>> g;
124 /** Positive vector solving complex balance. */
125 std::vector<T> y;
126 /** "geometric" or "massaction", the psi that was found. */
127 std::string kind;
128 /** The distinct complexes, one row each. */
129 std::vector<std::vector<double>> complexes;
130 int deficiency = 0;
131 std::size_t linkage = 0;
132 std::size_t srank = 0;
133 bool weakly_reversible = false;
134 /** Relative complex-balance residual at y. */
135 double residual = 0.0;
137};
138
139namespace detail {
140
141/** min(enabling degree, servers): the sets of tokens firing at once. */
142template <class T>
143double spn_pf_servers(const std::vector<double>& m, const SpnMode<T>& mde, std::size_t L) {
144 double deg = std::numeric_limits<double>::infinity();
145 for (std::size_t l = 0; l < L; ++l)
146 if (mde.enab[l] > 0) deg = std::min(deg, std::floor(m[l] / mde.enab[l]));
147 if (!std::isfinite(deg)) deg = 1.0; // consumes nothing: always one set
148 return std::min(deg, mde.srv);
149}
150
151/** prod_l m_l!/(m_l - I_l)!, the ordered ways to pick the input tokens. */
152inline double spn_pf_massaction(const std::vector<double>& m, const std::vector<double>& enab,
153 std::size_t L) {
154 double r = 1.0;
155 for (std::size_t l = 0; l < L; ++l)
156 for (int j = 0; j < static_cast<int>(enab[l]); ++j) r *= (m[l] - j);
157 return r;
158}
159
160inline bool spn_pf_close(double a, double b, double tol) {
161 return std::fabs(a - b) <= tol * std::max(1.0, std::max(std::fabs(a), std::fabs(b)));
162}
163
164/** Index of one complex, appended in first-seen order so the indices agree with
165 * the MATLAB, Java and python twins. */
166inline std::size_t spn_pf_complex(const std::vector<double>& v,
167 std::vector<std::vector<double>>& clist,
168 std::map<std::string, std::size_t>& seen) {
169 std::string key;
170 for (std::size_t l = 0; l < v.size(); ++l) key += std::to_string(v[l]) + ",";
171 const std::map<std::string, std::size_t>::const_iterator it = seen.find(key);
172 if (it != seen.end()) return it->second;
173 clist.push_back(v);
174 seen[key] = clist.size() - 1;
175 return clist.size() - 1;
176}
177
178inline std::vector<bool> spn_pf_reach(std::size_t v0, const std::vector<std::size_t>& from,
179 const std::vector<std::size_t>& to, std::size_t c) {
180 std::vector<bool> seen(c, false);
181 seen[v0] = true;
182 std::vector<std::size_t> stack(1, v0);
183 while (!stack.empty()) {
184 const std::size_t u = stack.back();
185 stack.pop_back();
186 for (std::size_t e = 0; e < from.size(); ++e)
187 if (from[e] == u && !seen[to[e]]) {
188 seen[to[e]] = true;
189 stack.push_back(to[e]);
190 }
191 }
192 return seen;
193}
194
195inline std::string spn_pf_wrtext(bool wr) {
196 return wr ? "weakly reversible" : "not weakly reversible";
197}
198
199} // namespace detail
200
201/**
202 * Derive the product form of a stochastic Petri net.
203 *
204 * @param sn the network structure of a net holding Places and Transitions
205 * @param options tolerances and bounds
206 * @return the per-level factors and the certificate
207 */
208template <class T>
210 if constexpr (!num_traits<T>::has_transcendental) {
211 throw UnsupportedError(
212 "spn_pf: y is the exponential of a least-squares solution of the log-complex-balance "
213 "equations, so the derivation needs a transcendental field and is unavailable under "
214 "exact arithmetic. Supply the g_l directly to mdd_rec / spn_metrics, which stay "
215 "rational");
216 } else {
217 SpnOptions mddopt;
218 mddopt.descriptor = false;
219 mddopt.bound = options.bound;
220 SpnResult<T> spn = spn_mdd<T>(sn, mddopt);
221 const SpnInfo<T>& info = spn.info;
222
223 const std::size_t L = info.nplacelevels;
224 const std::vector<SpnMode<T>>& md = info.modes;
225 const std::size_t E = md.size();
226 if (E == 0) throw InputError("spn_pf: the net has no timed mode");
227
228 // ---- a queueing place holds an embedded server, not a token container
229 for (std::size_t pp = 0; pp < info.places.size(); ++pp) {
230 const std::size_t nd = info.places[pp];
231 const std::size_t ist = sn.nodes[nd - 1].station;
232 if (ist >= 1 && ist <= sn.stations.size() &&
233 sn.stations[ist - 1].sched != lang::SchedStrategy::INF)
234 throw UnsupportedError(
235 "spn_pf: place " + info.placenames[pp] +
236 " is a QUEUEING place: its embedded service is state that the marking does "
237 "not carry, so the net is not the token-container Petri net this product form "
238 "is written for");
239 }
240
241 // ---- the rate constants and the structural vectors
242 std::vector<double> lambda(E, 0.0);
243 std::vector<std::vector<double>> Iv(E), Ov(E);
244 for (std::size_t e = 0; e < E; ++e) {
245 lambda[e] = num_traits<T>::to_double(md[e].D1(0, 0));
246 Iv[e] = md[e].enab;
247 Ov[e] = md[e].fire;
248 bool consumes = false;
249 for (std::size_t l = 0; l < L; ++l) {
250 if (std::isfinite(md[e].inhib[l]))
251 throw UnsupportedError(
252 "spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
253 std::to_string(md[e].trans) +
254 " has an inhibitor arc. An inhibitor zeroes the firing rate on markings "
255 "that still satisfy m >= I(t), so the rate is not lambda*psi(m-I)/psi(m) "
256 "on any psi and the net has no product form of this kind");
257 if (md[e].enab[l] > 0) consumes = true;
258 }
259 if (md[e].srv != 1 && !consumes)
260 throw InputError("spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
261 std::to_string(md[e].trans) +
262 " has several servers but consumes from no place, so its "
263 "enabling degree is unbounded and its firing rate undefined");
264 if (!(lambda[e] > 0))
265 throw InputError("spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
266 std::to_string(md[e].trans) +
267 " has a non-positive firing rate");
268 }
269
270 // ---- which psi does LINE's own rate law follow on this net?
271 const std::vector<std::vector<int>> states = info.diagram.enumerate();
272 bool okgeo = true, okma = true;
273 for (std::size_t s = 0; s < states.size(); ++s) {
274 std::vector<double> m(L, 0.0);
275 for (std::size_t l = 0; l < L; ++l) m[l] = states[s][l];
276 for (std::size_t e = 0; e < E; ++e) {
277 bool enabled = true;
278 for (std::size_t l = 0; l < L && enabled; ++l)
279 if (m[l] < md[e].enab[l]) enabled = false;
280 if (!enabled) continue;
281 double actual = lambda[e] * detail::spn_pf_servers<T>(m, md[e], L);
282 if (md[e].dep) {
283 // The multiplier is written against the per-NODE, class-summed
284 // marking, which is what the CTMC hands it.
285 std::vector<T> mk(info.nnodes, num_traits<T>::from_int(0));
286 for (std::size_t pp = 0; pp < info.places.size(); ++pp)
287 mk[info.places[pp] - 1] = num_traits<T>::from_double(m[pp]);
288 actual *= num_traits<T>::to_double(md[e].dep(mk));
289 }
290 const double ma = lambda[e] * detail::spn_pf_massaction(m, md[e].enab, L);
291 okgeo = okgeo && detail::spn_pf_close(actual, lambda[e], options.tol);
292 okma = okma && detail::spn_pf_close(actual, ma, options.tol);
293 if (!okgeo && !okma)
294 throw UnsupportedError(
295 "spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
296 std::to_string(md[e].trans) + " fires at rate " + std::to_string(actual) +
297 " in a reachable marking, which is neither its rate constant "
298 "(single-server, psi = 1) nor its mass-action rate " +
299 std::to_string(ma) +
300 " (psi = prod 1/m!). LINE's rate law on this mode is "
301 "lambda*min(enabling degree, servers)*g(m), and no psi puts that in the "
302 "form lambda*psi(m-I)/psi(m)");
303 }
304 }
305 const std::string kind = okgeo ? "geometric" : "massaction";
306
307 // ---- complexes and the weighted digraph on them
308 std::vector<std::vector<double>> C;
309 std::map<std::string, std::size_t> seen;
310 std::vector<std::size_t> src(E), dst(E);
311 for (std::size_t e = 0; e < E; ++e) src[e] = detail::spn_pf_complex(Iv[e], C, seen);
312 for (std::size_t e = 0; e < E; ++e) dst[e] = detail::spn_pf_complex(Ov[e], C, seen);
313 const std::size_t c = C.size();
314
315 // ---- Laplacian: A(j,i) is the rate of the arc i -> j
316 Matrix<double> A(c, c);
317 for (std::size_t i = 0; i < c; ++i)
318 for (std::size_t j = 0; j < c; ++j) A(i, j) = 0.0;
319 for (std::size_t e = 0; e < E; ++e) {
320 if (src[e] == dst[e]) continue; // a mode that moves nothing
321 A(dst[e], src[e]) += lambda[e];
322 A(src[e], src[e]) -= lambda[e];
323 }
324
325 // ---- linkage classes, weak reversibility, deficiency
326 std::vector<int> lclass(c, -1);
327 std::size_t nlink = 0;
328 for (std::size_t v = 0; v < c; ++v) {
329 if (lclass[v] >= 0) continue;
330 std::vector<std::size_t> stack(1, v);
331 lclass[v] = static_cast<int>(nlink);
332 while (!stack.empty()) {
333 const std::size_t u = stack.back();
334 stack.pop_back();
335 for (std::size_t e = 0; e < E; ++e) {
336 std::size_t w = c;
337 if (src[e] == u) w = dst[e];
338 else if (dst[e] == u) w = src[e];
339 if (w < c && lclass[w] < 0) {
340 lclass[w] = static_cast<int>(nlink);
341 stack.push_back(w);
342 }
343 }
344 }
345 ++nlink;
346 }
347 bool wr = true;
348 for (std::size_t b = 0; b < nlink && wr; ++b) {
349 std::size_t v0 = c;
350 for (std::size_t v = 0; v < c && v0 == c; ++v)
351 if (lclass[v] == static_cast<int>(b)) v0 = v;
352 const std::vector<bool> fwd = detail::spn_pf_reach(v0, src, dst, c);
353 const std::vector<bool> bwd = detail::spn_pf_reach(v0, dst, src, c);
354 for (std::size_t v = 0; v < c; ++v)
355 if (lclass[v] == static_cast<int>(b) && (!fwd[v] || !bwd[v])) wr = false;
356 }
357 Matrix<double> netm(E, L);
358 for (std::size_t e = 0; e < E; ++e)
359 for (std::size_t l = 0; l < L; ++l) netm(e, l) = Ov[e][l] - Iv[e][l];
360 Matrix<double> netmc = netm;
361 const std::size_t srank = rref(netmc, line::detail::lstsq_tolerance(netm)).size();
362 const int deficiency = static_cast<int>(c) - static_cast<int>(nlink) -
363 static_cast<int>(srank);
364
365 // ---- kappa: the positive balance flow on each linkage class
366 std::vector<double> kappa(c, 0.0);
367 for (std::size_t b = 0; b < nlink; ++b) {
368 std::vector<std::size_t> idx;
369 for (std::size_t v = 0; v < c; ++v)
370 if (lclass[v] == static_cast<int>(b)) idx.push_back(v);
371 if (idx.size() == 1) {
372 kappa[idx[0]] = 1.0;
373 continue;
374 }
375 // The block is the transposed generator of the complex-hopping chain,
376 // so its kernel is that chain's stationary law.
377 Matrix<double> Qb(idx.size(), idx.size());
378 for (std::size_t i = 0; i < idx.size(); ++i)
379 for (std::size_t j = 0; j < idx.size(); ++j) Qb(i, j) = A(idx[j], idx[i]);
380 const std::vector<double> pb = mc::ctmc_solve<double>(Qb);
381 double mx = 0.0;
382 for (std::size_t i = 0; i < idx.size(); ++i) mx = std::max(mx, pb[i]);
383 for (std::size_t i = 0; i < idx.size(); ++i) {
384 if (!(pb[i] > 0))
385 throw UnsupportedError(
386 "spn_pf: linkage class " + std::to_string(b + 1) + " of the complex graph "
387 "carries no flow through complex " + std::to_string(idx[i] + 1) +
388 ", so the net admits no positive complex-balanced point. A weakly "
389 "reversible net has a strictly positive balance flow on every linkage "
390 "class; this one is " + detail::spn_pf_wrtext(wr));
391 kappa[idx[i]] = pb[i] / mx;
392 }
393 }
394
395 // ---- x = log y from the linear system on each linkage class
396 std::vector<std::vector<double>> rows;
397 std::vector<double> rhs;
398 for (std::size_t b = 0; b < nlink; ++b) {
399 std::size_t v0 = c;
400 for (std::size_t v = 0; v < c; ++v) {
401 if (lclass[v] != static_cast<int>(b)) continue;
402 if (v0 == c) {
403 v0 = v;
404 continue;
405 }
406 std::vector<double> row(L, 0.0);
407 for (std::size_t l = 0; l < L; ++l) row[l] = C[v][l] - C[v0][l];
408 rows.push_back(row);
409 rhs.push_back(std::log(kappa[v]) - std::log(kappa[v0]));
410 }
411 }
412 std::vector<double> x(L, 0.0);
413 if (!rows.empty()) {
414 // minimum norm through the row space; see the gauge note in the header
415 const std::size_t nr = rows.size();
416 Matrix<double> RRt(nr, nr);
417 for (std::size_t i = 0; i < nr; ++i)
418 for (std::size_t j = 0; j < nr; ++j) {
419 double s = 0.0;
420 for (std::size_t l = 0; l < L; ++l) s += rows[i][l] * rows[j][l];
421 RRt(i, j) = s;
422 }
423 const LstsqResult<double> w = lstsq<double>(RRt, rhs, line::detail::lstsq_tolerance(RRt));
424 for (std::size_t l = 0; l < L; ++l) {
425 double s = 0.0;
426 for (std::size_t i = 0; i < nr; ++i) s += rows[i][l] * w.x[i];
427 x[l] = s;
428 }
429 double res = 0.0, scale = 1.0;
430 for (std::size_t i = 0; i < nr; ++i) {
431 double s = 0.0;
432 for (std::size_t l = 0; l < L; ++l) s += rows[i][l] * x[l];
433 res = std::max(res, std::fabs(s - rhs[i]));
434 scale = std::max(scale, std::fabs(rhs[i]));
435 }
436 if (res > options.tol * scale)
437 throw UnsupportedError(
438 "spn_pf: the complex-balance equations are inconsistent (residual " +
439 std::to_string(res) + "): this net has no product form of the tested kind at "
440 "these rates. Its deficiency is " + std::to_string(deficiency) + " and it is " +
441 detail::spn_pf_wrtext(wr) + "; the Deficiency Zero Theorem guarantees a "
442 "solution only at deficiency 0 with weak reversibility");
443 }
444 std::vector<double> yd(L, 1.0);
445 for (std::size_t l = 0; l < L; ++l) yd[l] = std::exp(x[l]);
446
447 // ---- verify complex balance itself, which is what makes pi stationary
448 std::vector<double> psi(c, 1.0);
449 for (std::size_t v = 0; v < c; ++v) {
450 double p = 1.0;
451 for (std::size_t l = 0; l < L; ++l) p *= std::pow(yd[l], C[v][l]);
452 psi[v] = p;
453 }
454 double resb = 0.0, scaleb = std::numeric_limits<double>::min();
455 for (std::size_t v = 0; v < c; ++v) {
456 double s = 0.0, sa = 0.0;
457 for (std::size_t u = 0; u < c; ++u) {
458 s += A(v, u) * psi[u];
459 sa += std::fabs(A(v, u)) * psi[u];
460 }
461 resb = std::max(resb, std::fabs(s));
462 scaleb = std::max(scaleb, sa);
463 }
464 if (resb > options.tol * scaleb)
465 throw UnsupportedError(
466 "spn_pf: complex balance fails at the computed point (relative residual " +
467 std::to_string(resb / scaleb) + "), so the product form would not be stationary");
468
469 // ---- the per-level factors, tabulated over the reachable domain
470 SpnPfResult<T> out;
471 out.g.resize(L);
472 out.y.resize(L);
473 for (std::size_t l = 0; l < L; ++l) {
474 out.y[l] = num_traits<T>::from_double(yd[l]);
475 const int d = spn.mdds.domain[l];
476 out.g[l].assign(d, num_traits<T>::from_int(1));
478 for (int k = 0; k < d; ++k) {
479 if (k > 0) {
480 fact = T(fact * num_traits<T>::from_int(k));
481 pw = T(pw * out.y[l]);
482 }
483 out.g[l][k] = kind == "massaction" ? T(pw / fact) : pw;
484 }
485 }
486 out.kind = kind;
487 out.complexes = C;
488 out.deficiency = deficiency;
489 out.linkage = nlink;
490 out.srank = srank;
491 out.weakly_reversible = wr;
492 out.residual = resb / scaleb;
493 out.spn = spn;
494 return out;
495 }
496}
497
498} // namespace spn
499} // namespace line
500
501#endif // LINE_API_SPN_SPN_PF_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
std::vector< std::vector< int > > enumerate() const
All stored states as rows, in index() order.
Definition mdd.h:172
A network plus its refreshed NetworkStruct.
Steady-state distribution of a continuous-time Markov chain.
The exception types the port throws.
Least squares for a rectangular system, exact-capable.
Dense matrix and non-owning view.
std::vector< T > ctmc_solve(const Matrix< T > &Qin)
Steady-state distribution of a continuous-time Markov chain.
Definition ctmc_solve.h:122
SpnPfResult< T > spn_pf(const qn::NetworkStruct< T > &sn, const SpnPfOptions &options=SpnPfOptions())
Derive the product form of a stochastic Petri net.
Definition spn_pf.h:209
SpnResult< T > spn_mdd(const qn::NetworkStruct< T > &sn, const SpnOptions &options=SpnOptions())
Build the reachable set and Kronecker descriptor of a stochastic Petri net.
Definition spn_mdd.h:394
std::vector< std::size_t > rref(Matrix< T > &A, const T &tol)
Reduced row echelon form of A, in place, returning the pivot columns.
Definition lstsq.h:110
LstsqResult< T > lstsq(const Matrix< T > &A, const std::vector< T > &b, const T &tol)
Least-squares solution of A x = b, minimum-norm when A is rank deficient.
Definition lstsq.h:152
Decision-diagram reachable set and Kronecker rate descriptor of a stochastic Petri net,...
Outcome of lstsq: the solution and whether the system was rank deficient.
Definition lstsq.h:58
std::vector< T > x
Definition lstsq.h:59
Everything the caller needs alongside the descriptor.
Definition spn_mdd.h:108
std::size_t nplacelevels
Definition spn_mdd.h:122
std::vector< SpnMode< T > > modes
Definition spn_mdd.h:115
std::vector< std::size_t > places
1-based node indices of the places.
Definition spn_mdd.h:110
std::vector< std::string > placenames
Definition spn_mdd.h:111
mdd::MDD diagram
Definition spn_mdd.h:117
std::size_t nnodes
Node count of the model, so a firingdep argument can be rebuilt.
Definition spn_mdd.h:124
One (transition, mode) pair of the net, in level coordinates.
Definition spn_mdd.h:86
std::vector< double > enab
Enabling multiplicity per place level.
Definition spn_mdd.h:92
Options of the translation.
Definition spn_mdd.h:140
bool descriptor
Build the Kronecker rate descriptor (default true).
Definition spn_mdd.h:155
std::vector< double > bound
Per-place-level token bound; empty infers it from a place invariant.
Definition spn_mdd.h:142
Options of the product-form derivation.
Definition spn_pf.h:111
double tol
Relative tolerance of the rate-law and complex-balance checks.
Definition spn_pf.h:115
std::vector< double > bound
Per-place-level token bound, passed to spn_mdd; empty infers it.
Definition spn_pf.h:113
The product form, and the certificate that it is one.
Definition spn_pf.h:121
std::vector< std::vector< T > > g
g[l][k] = g_l(k), ready for mdd_rec.
Definition spn_pf.h:123
SpnResult< T > spn
Definition spn_pf.h:136
double residual
Relative complex-balance residual at y.
Definition spn_pf.h:135
std::size_t linkage
Definition spn_pf.h:131
std::string kind
"geometric" or "massaction", the psi that was found.
Definition spn_pf.h:127
std::size_t srank
Definition spn_pf.h:132
std::vector< std::vector< double > > complexes
The distinct complexes, one row each.
Definition spn_pf.h:129
std::vector< T > y
Positive vector solving complex balance.
Definition spn_pf.h:125
Descriptor, diagram and metadata returned together.
Definition spn_mdd.h:133