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 <cstdio>
97#include <limits>
98#include <map>
99#include <string>
100#include <vector>
101
103#include "line/api/spn/spn_mdd.h"
104#include "line/util/error.h"
105#include "line/util/lstsq.h"
106#include "line/util/matrix.h"
107
108namespace line {
109namespace spn {
110
111/** Options of the product-form derivation. */
113 /** Per-place-level token bound, passed to spn_mdd; empty infers it. */
114 std::vector<double> bound;
115 /** Relative tolerance of the rate-law and complex-balance checks. */
116 double tol = 1e-9;
117 /** Print the certificate and y, as the reference's options.verbose does. */
118 bool verbose = false;
119};
120
121/** The product form, and the certificate that it is one. */
122template <class T>
124 /** g[l][k] = g_l(k), ready for mdd_rec. */
125 std::vector<std::vector<T>> g;
126 /** Positive vector solving complex balance. */
127 std::vector<T> y;
128 /** "geometric" or "massaction", the psi that was found. */
129 std::string kind;
130 /** The distinct complexes, one row each. */
131 std::vector<std::vector<double>> complexes;
132 int deficiency = 0;
133 std::size_t linkage = 0;
134 std::size_t srank = 0;
135 bool weakly_reversible = false;
136 /** Relative complex-balance residual at y. */
137 double residual = 0.0;
139};
140
141namespace detail {
142
143/** min(enabling degree, servers): the sets of tokens firing at once. */
144template <class T>
145double spn_pf_servers(const std::vector<double>& m, const SpnMode<T>& mde, std::size_t L) {
146 double deg = std::numeric_limits<double>::infinity();
147 for (std::size_t l = 0; l < L; ++l)
148 if (mde.enab[l] > 0) deg = std::min(deg, std::floor(m[l] / mde.enab[l]));
149 if (!std::isfinite(deg)) deg = 1.0; // consumes nothing: always one set
150 return std::min(deg, mde.srv);
151}
152
153/** prod_l m_l!/(m_l - I_l)!, the ordered ways to pick the input tokens. */
154inline double spn_pf_massaction(const std::vector<double>& m, const std::vector<double>& enab,
155 std::size_t L) {
156 double r = 1.0;
157 for (std::size_t l = 0; l < L; ++l)
158 for (int j = 0; j < static_cast<int>(enab[l]); ++j) r *= (m[l] - j);
159 return r;
160}
161
162inline bool spn_pf_close(double a, double b, double tol) {
163 return std::fabs(a - b) <= tol * std::max(1.0, std::max(std::fabs(a), std::fabs(b)));
164}
165
166/** Index of one complex, appended in first-seen order so the indices agree with
167 * the MATLAB, Java and python twins. */
168inline std::size_t spn_pf_complex(const std::vector<double>& v,
169 std::vector<std::vector<double>>& clist,
170 std::map<std::string, std::size_t>& seen) {
171 std::string key;
172 for (std::size_t l = 0; l < v.size(); ++l) key += std::to_string(v[l]) + ",";
173 const std::map<std::string, std::size_t>::const_iterator it = seen.find(key);
174 if (it != seen.end()) return it->second;
175 clist.push_back(v);
176 seen[key] = clist.size() - 1;
177 return clist.size() - 1;
178}
179
180inline std::vector<bool> spn_pf_reach(std::size_t v0, const std::vector<std::size_t>& from,
181 const std::vector<std::size_t>& to, std::size_t c) {
182 std::vector<bool> seen(c, false);
183 seen[v0] = true;
184 std::vector<std::size_t> stack(1, v0);
185 while (!stack.empty()) {
186 const std::size_t u = stack.back();
187 stack.pop_back();
188 for (std::size_t e = 0; e < from.size(); ++e)
189 if (from[e] == u && !seen[to[e]]) {
190 seen[to[e]] = true;
191 stack.push_back(to[e]);
192 }
193 }
194 return seen;
195}
196
197inline std::string spn_pf_wrtext(bool wr) {
198 return wr ? "weakly reversible" : "not weakly reversible";
199}
200
201} // namespace detail
202
203/**
204 * Derive the product form of a stochastic Petri net.
205 *
206 * @param sn the network structure of a net holding Places and Transitions
207 * @param options tolerances and bounds
208 * @return the per-level factors and the certificate
209 */
210template <class T>
212 if constexpr (!num_traits<T>::has_transcendental) {
213 throw UnsupportedError(ErrorKind{"Arithmetic"},
214 "spn_pf: y is the exponential of a least-squares solution of the log-complex-balance "
215 "equations, so the derivation needs a transcendental field and is unavailable under "
216 "exact arithmetic. Supply the g_l directly to mdd_rec / spn_metrics, which stay "
217 "rational");
218 } else {
219 SpnOptions mddopt;
220 mddopt.descriptor = false;
221 mddopt.bound = options.bound;
222 mddopt.verbose = options.verbose;
223 SpnResult<T> spn = spn_mdd<T>(sn, mddopt);
224 const SpnInfo<T>& info = spn.info;
225
226 const std::size_t L = info.nplacelevels;
227 const std::vector<SpnMode<T>>& md = info.modes;
228 const std::size_t E = md.size();
229 if (E == 0) throw InputError("spn_pf: the net has no timed mode");
230
231 // ---- a queueing place holds an embedded server, not a token container
232 for (std::size_t pp = 0; pp < info.places.size(); ++pp) {
233 const std::size_t nd = info.places[pp];
234 const std::size_t ist = sn.nodes[nd - 1].station;
235 if (ist >= 1 && ist <= sn.stations.size() &&
236 sn.stations[ist - 1].sched != lang::SchedStrategy::INF)
237 throw UnsupportedError(ErrorKind{"QueueingPlace"},
238 "spn_pf: place " + info.placenames[pp] +
239 " is a QUEUEING place: its embedded service is state that the marking does "
240 "not carry, so the net is not the token-container Petri net this product form "
241 "is written for");
242 }
243
244 // ---- the rate constants and the structural vectors
245 std::vector<double> lambda(E, 0.0);
246 std::vector<std::vector<double>> Iv(E), Ov(E);
247 for (std::size_t e = 0; e < E; ++e) {
248 lambda[e] = num_traits<T>::to_double(md[e].D1(0, 0));
249 Iv[e] = md[e].enab;
250 Ov[e] = md[e].fire;
251 bool consumes = false;
252 for (std::size_t l = 0; l < L; ++l) {
253 if (std::isfinite(md[e].inhib[l]))
254 throw UnsupportedError(ErrorKind{"Inhibiting"},
255 "spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
256 std::to_string(md[e].trans) +
257 " has an inhibitor arc. An inhibitor zeroes the firing rate on markings "
258 "that still satisfy m >= I(t), so the rate is not lambda*psi(m-I)/psi(m) "
259 "on any psi and the net has no product form of this kind");
260 if (md[e].enab[l] > 0) consumes = true;
261 }
262 if (md[e].srv != 1 && !consumes)
263 throw InputError("spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
264 std::to_string(md[e].trans) +
265 " has several servers but consumes from no place, so its "
266 "enabling degree is unbounded and its firing rate undefined");
267 if (!(lambda[e] > 0))
268 throw InputError("spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
269 std::to_string(md[e].trans) +
270 " has a non-positive firing rate");
271 }
272
273 // ---- which psi does LINE's own rate law follow on this net?
274 const std::vector<std::vector<int>> states = info.diagram.enumerate();
275 bool okgeo = true, okma = true;
276 for (std::size_t s = 0; s < states.size(); ++s) {
277 std::vector<double> m(L, 0.0);
278 for (std::size_t l = 0; l < L; ++l) m[l] = states[s][l];
279 for (std::size_t e = 0; e < E; ++e) {
280 bool enabled = true;
281 for (std::size_t l = 0; l < L && enabled; ++l)
282 if (m[l] < md[e].enab[l]) enabled = false;
283 if (!enabled) continue;
284 double actual = lambda[e] * detail::spn_pf_servers<T>(m, md[e], L);
285 if (md[e].dep) {
286 // The multiplier reads the (nnodes x nclasses) marking flattened node-major, as the
287 // CTMC (state_events.h) and the reference's i_arcmatrix hand a `firingdep` handle.
288 const std::size_t KC = info.nclasses;
289 std::vector<T> mk(info.nnodes * KC, num_traits<T>::from_int(0));
290 for (std::size_t pp = 0; pp < info.places.size(); ++pp)
291 for (std::size_t k = 0; k < KC; ++k)
292 mk[(info.places[pp] - 1) * KC + k] = num_traits<T>::from_double(m[pp * KC + k]);
293 actual *= num_traits<T>::to_double(md[e].dep(mk));
294 }
295 const double ma = lambda[e] * detail::spn_pf_massaction(m, md[e].enab, L);
296 okgeo = okgeo && detail::spn_pf_close(actual, lambda[e], options.tol);
297 okma = okma && detail::spn_pf_close(actual, ma, options.tol);
298 if (!okgeo && !okma)
299 throw UnsupportedError(ErrorKind{"MarkingDependence"},
300 "spn_pf: mode " + std::to_string(md[e].mode + 1) + " of node " +
301 std::to_string(md[e].trans) + " fires at rate " + std::to_string(actual) +
302 " in a reachable marking, which is neither its rate constant "
303 "(single-server, psi = 1) nor its mass-action rate " +
304 std::to_string(ma) +
305 " (psi = prod 1/m!). LINE's rate law on this mode is "
306 "lambda*min(enabling degree, servers)*g(m), and no psi puts that in the "
307 "form lambda*psi(m-I)/psi(m)");
308 }
309 }
310 const std::string kind = okgeo ? "geometric" : "massaction";
311
312 // ---- complexes and the weighted digraph on them
313 std::vector<std::vector<double>> C;
314 std::map<std::string, std::size_t> seen;
315 std::vector<std::size_t> src(E), dst(E);
316 for (std::size_t e = 0; e < E; ++e) src[e] = detail::spn_pf_complex(Iv[e], C, seen);
317 for (std::size_t e = 0; e < E; ++e) dst[e] = detail::spn_pf_complex(Ov[e], C, seen);
318 const std::size_t c = C.size();
319
320 // ---- Laplacian: A(j,i) is the rate of the arc i -> j
321 Matrix<double> A(c, c);
322 for (std::size_t i = 0; i < c; ++i)
323 for (std::size_t j = 0; j < c; ++j) A(i, j) = 0.0;
324 for (std::size_t e = 0; e < E; ++e) {
325 if (src[e] == dst[e]) continue; // a mode that moves nothing
326 A(dst[e], src[e]) += lambda[e];
327 A(src[e], src[e]) -= lambda[e];
328 }
329
330 // ---- linkage classes, weak reversibility, deficiency
331 std::vector<int> lclass(c, -1);
332 std::size_t nlink = 0;
333 for (std::size_t v = 0; v < c; ++v) {
334 if (lclass[v] >= 0) continue;
335 std::vector<std::size_t> stack(1, v);
336 lclass[v] = static_cast<int>(nlink);
337 while (!stack.empty()) {
338 const std::size_t u = stack.back();
339 stack.pop_back();
340 for (std::size_t e = 0; e < E; ++e) {
341 std::size_t w = c;
342 if (src[e] == u) w = dst[e];
343 else if (dst[e] == u) w = src[e];
344 if (w < c && lclass[w] < 0) {
345 lclass[w] = static_cast<int>(nlink);
346 stack.push_back(w);
347 }
348 }
349 }
350 ++nlink;
351 }
352 bool wr = true;
353 for (std::size_t b = 0; b < nlink && wr; ++b) {
354 std::size_t v0 = c;
355 for (std::size_t v = 0; v < c && v0 == c; ++v)
356 if (lclass[v] == static_cast<int>(b)) v0 = v;
357 const std::vector<bool> fwd = detail::spn_pf_reach(v0, src, dst, c);
358 const std::vector<bool> bwd = detail::spn_pf_reach(v0, dst, src, c);
359 for (std::size_t v = 0; v < c; ++v)
360 if (lclass[v] == static_cast<int>(b) && (!fwd[v] || !bwd[v])) wr = false;
361 }
362 Matrix<double> netm(E, L);
363 for (std::size_t e = 0; e < E; ++e)
364 for (std::size_t l = 0; l < L; ++l) netm(e, l) = Ov[e][l] - Iv[e][l];
365 // the reference's rank(Ov - Iv, 1e-9)
366 Matrix<double> netmc = netm;
367 const std::size_t srank = rref(netmc, 1e-9).size();
368 const int deficiency = static_cast<int>(c) - static_cast<int>(nlink) -
369 static_cast<int>(srank);
370
371 // ---- kappa: the positive balance flow on each linkage class
372 std::vector<double> kappa(c, 0.0);
373 for (std::size_t b = 0; b < nlink; ++b) {
374 std::vector<std::size_t> idx;
375 for (std::size_t v = 0; v < c; ++v)
376 if (lclass[v] == static_cast<int>(b)) idx.push_back(v);
377 if (idx.size() == 1) {
378 kappa[idx[0]] = 1.0;
379 continue;
380 }
381 // The block is the transposed generator of the complex-hopping chain,
382 // so its kernel is that chain's stationary law.
383 Matrix<double> Qb(idx.size(), idx.size());
384 for (std::size_t i = 0; i < idx.size(); ++i)
385 for (std::size_t j = 0; j < idx.size(); ++j) Qb(i, j) = A(idx[j], idx[i]);
386 const std::vector<double> pb = mc::ctmc_solve<double>(Qb);
387 double mx = 0.0;
388 for (std::size_t i = 0; i < idx.size(); ++i) mx = std::max(mx, pb[i]);
389 for (std::size_t i = 0; i < idx.size(); ++i) {
390 if (!(pb[i] > 0))
391 throw UnsupportedError(ErrorKind{"ProductForm"},
392 "spn_pf: linkage class " + std::to_string(b + 1) + " of the complex graph "
393 "carries no flow through complex " + std::to_string(idx[i] + 1) +
394 ", so the net admits no positive complex-balanced point. A weakly "
395 "reversible net has a strictly positive balance flow on every linkage "
396 "class; this one is " + detail::spn_pf_wrtext(wr));
397 kappa[idx[i]] = pb[i] / mx;
398 }
399 }
400
401 // ---- x = log y from the linear system on each linkage class
402 std::vector<std::vector<double>> rows;
403 std::vector<double> rhs;
404 for (std::size_t b = 0; b < nlink; ++b) {
405 std::size_t v0 = c;
406 for (std::size_t v = 0; v < c; ++v) {
407 if (lclass[v] != static_cast<int>(b)) continue;
408 if (v0 == c) {
409 v0 = v;
410 continue;
411 }
412 std::vector<double> row(L, 0.0);
413 for (std::size_t l = 0; l < L; ++l) row[l] = C[v][l] - C[v0][l];
414 rows.push_back(row);
415 rhs.push_back(std::log(kappa[v]) - std::log(kappa[v0]));
416 }
417 }
418 std::vector<double> x(L, 0.0);
419 if (!rows.empty()) {
420 // minimum norm through the row space; see the gauge note in the header
421 const std::size_t nr = rows.size();
422 Matrix<double> RRt(nr, nr);
423 for (std::size_t i = 0; i < nr; ++i)
424 for (std::size_t j = 0; j < nr; ++j) {
425 double s = 0.0;
426 for (std::size_t l = 0; l < L; ++l) s += rows[i][l] * rows[j][l];
427 RRt(i, j) = s;
428 }
429 const LstsqResult<double> w = lstsq<double>(RRt, rhs, line::detail::lstsq_tolerance(RRt));
430 for (std::size_t l = 0; l < L; ++l) {
431 double s = 0.0;
432 for (std::size_t i = 0; i < nr; ++i) s += rows[i][l] * w.x[i];
433 x[l] = s;
434 }
435 double res = 0.0, scale = 1.0;
436 for (std::size_t i = 0; i < nr; ++i) {
437 double s = 0.0;
438 for (std::size_t l = 0; l < L; ++l) s += rows[i][l] * x[l];
439 res = std::max(res, std::fabs(s - rhs[i]));
440 scale = std::max(scale, std::fabs(rhs[i]));
441 }
442 if (res > options.tol * scale)
443 throw UnsupportedError(ErrorKind{"ProductForm"},
444 "spn_pf: the complex-balance equations are inconsistent (residual " +
445 std::to_string(res) + "): this net has no product form of the tested kind at "
446 "these rates. Its deficiency is " + std::to_string(deficiency) + " and it is " +
447 detail::spn_pf_wrtext(wr) + "; the Deficiency Zero Theorem guarantees a "
448 "solution only at deficiency 0 with weak reversibility");
449 }
450 std::vector<double> yd(L, 1.0);
451 for (std::size_t l = 0; l < L; ++l) yd[l] = std::exp(x[l]);
452
453 // ---- verify complex balance itself, which is what makes pi stationary
454 std::vector<double> psi(c, 1.0);
455 for (std::size_t v = 0; v < c; ++v) {
456 double p = 1.0;
457 for (std::size_t l = 0; l < L; ++l) p *= std::pow(yd[l], C[v][l]);
458 psi[v] = p;
459 }
460 double resb = 0.0, scaleb = std::numeric_limits<double>::min();
461 for (std::size_t v = 0; v < c; ++v) {
462 double s = 0.0, sa = 0.0;
463 for (std::size_t u = 0; u < c; ++u) {
464 s += A(v, u) * psi[u];
465 sa += std::fabs(A(v, u)) * psi[u];
466 }
467 resb = std::max(resb, std::fabs(s));
468 scaleb = std::max(scaleb, sa);
469 }
470 if (resb > options.tol * scaleb)
471 throw UnsupportedError(ErrorKind{"ProductForm"},
472 "spn_pf: complex balance fails at the computed point (relative residual " +
473 std::to_string(resb / scaleb) + "), so the product form would not be stationary");
474
475 // ---- the per-level factors, tabulated over the reachable domain
476 SpnPfResult<T> out;
477 out.g.resize(L);
478 out.y.resize(L);
479 for (std::size_t l = 0; l < L; ++l) {
480 out.y[l] = num_traits<T>::from_double(yd[l]);
481 const int d = spn.mdds.domain[l];
482 out.g[l].assign(d, num_traits<T>::from_int(1));
484 for (int k = 0; k < d; ++k) {
485 if (k > 0) {
486 fact = T(fact * num_traits<T>::from_int(k));
487 pw = T(pw * out.y[l]);
488 }
489 out.g[l][k] = kind == "massaction" ? T(pw / fact) : pw;
490 }
491 }
492 out.kind = kind;
493 out.complexes = C;
494 out.deficiency = deficiency;
495 out.linkage = nlink;
496 out.srank = srank;
497 out.weakly_reversible = wr;
498 out.residual = resb / scaleb;
499 out.spn = spn;
500 if (options.verbose) {
501 std::printf("\nSPN product form: %s, %zu complexes, %zu linkage classes, rank %zu, "
502 "deficiency %d, %s\n",
503 kind.c_str(), c, nlink, srank, deficiency, detail::spn_pf_wrtext(wr).c_str());
504 std::string ystr = "[";
505 for (std::size_t l = 0; l < L; ++l) {
506 char buf[32];
507 std::snprintf(buf, sizeof(buf), "%s%.6g", l > 0 ? " " : "", yd[l]);
508 ystr += buf;
509 }
510 ystr += "]";
511 std::printf(" y = %s (complex-balance residual %.2e)\n", ystr.c_str(), resb / scaleb);
512 }
513 return out;
514 }
515}
516
517} // namespace spn
518} // namespace line
519
520#endif // LINE_API_SPN_SPN_PF_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
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:211
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:403
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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,...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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:112
std::size_t nplacelevels
Definition spn_mdd.h:129
std::vector< SpnMode< T > > modes
Definition spn_mdd.h:122
std::vector< std::size_t > places
1-based node indices of the places.
Definition spn_mdd.h:114
std::size_t nclasses
R, the stride of the place-major place levels (level l = pp * R + k).
Definition spn_mdd.h:118
std::vector< std::string > placenames
Definition spn_mdd.h:115
mdd::MDD diagram
Definition spn_mdd.h:124
std::size_t nnodes
Node count of the model, so a firingdep argument can be rebuilt.
Definition spn_mdd.h:131
One (transition, mode) pair of the net, in level coordinates.
Definition spn_mdd.h:90
std::vector< double > enab
Enabling multiplicity per place level.
Definition spn_mdd.h:96
Options of the translation.
Definition spn_mdd.h:147
bool descriptor
Build the Kronecker rate descriptor (default true).
Definition spn_mdd.h:164
bool verbose
Print the level and state counts, as the reference's options.verbose does.
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:149
Options of the product-form derivation.
Definition spn_pf.h:112
double tol
Relative tolerance of the rate-law and complex-balance checks.
Definition spn_pf.h:116
bool verbose
Print the certificate and y, as the reference's options.verbose does.
Definition spn_pf.h:118
std::vector< double > bound
Per-place-level token bound, passed to spn_mdd; empty infers it.
Definition spn_pf.h:114
The product form, and the certificate that it is one.
Definition spn_pf.h:123
std::vector< std::vector< T > > g
g[l][k] = g_l(k), ready for mdd_rec.
Definition spn_pf.h:125
SpnResult< T > spn
Definition spn_pf.h:138
double residual
Relative complex-balance residual at y.
Definition spn_pf.h:137
std::size_t linkage
Definition spn_pf.h:133
std::string kind
"geometric" or "massaction", the psi that was found.
Definition spn_pf.h:129
std::size_t srank
Definition spn_pf.h:134
std::vector< std::vector< double > > complexes
The distinct complexes, one row each.
Definition spn_pf.h:131
std::vector< T > y
Positive vector solving complex balance.
Definition spn_pf.h:127
Descriptor, diagram and metadata returned together.
Definition spn_mdd.h:140