LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
spn_mdd.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_SPN_SPN_MDD_H
6#define LINE_API_SPN_SPN_MDD_H
7
8/**
9 * @file
10 * @ingroup api_spn
11 * Decision-diagram reachable set and Kronecker rate descriptor of a stochastic
12 * Petri net, so that `mdd::mdd_mcd` can analyse it.
13 *
14 * Port of matlab/src/api/spn/spn_mdd.m, jline.api.spn.Spn_mdd and
15 * python/line_solver/api/spn/mdd.py.
16 *
17 * Levels are of two kinds. PLACE LEVELS hold a token count, one per Place.
18 * PHASE LEVELS hold the phase the running server of a multi-phase mode
19 * occupies, one per such mode.
20 *
21 * ONE LEVEL PER (PLACE, CLASS), place-major, as in the reference: level
22 * l = pp * R + k holds the class-k tokens of place pp, and the arcs are read per
23 * class off `TransitionParam::enabling / inhibiting / firing`. A single-class
24 * net has one level per place and agrees level for level with the other
25 * codebases; a multiclass net gets its P*R levels here too.
26 *
27 * The rate structure factorises exactly under single-server firing semantics: a
28 * mode fires at a constant rate whenever every input level holds its enabling
29 * multiplicity and no inhibitor level has reached its threshold, so
30 * W_l^e[i, i + fire(l) - enab(l)] = 1 for enab(l) <= i < inhib(l) at every place
31 * level. A phase-type mode contributes two event families on its phase level,
32 * the internal phase changes D0 (marking unchanged) and the firings D1 (marking
33 * moved), each gated by the same per-level enabling indicators. Both are
34 * products of per-level terms, which is what Eq. 1 of the paper requires.
35 *
36 * PHASE-TYPE FIRING AND THE MEMORY POLICY. LINE discards a running server's
37 * phase when its mode becomes disabled, i.e. preemptive repeat. Resetting a
38 * mode's phase is then triggered by a JOINT condition on the place levels, which
39 * is not a product of per-level terms and has no Kronecker form. What this
40 * descriptor encodes is preemptive resume: a disabled mode's phase freezes and
41 * continues when the mode is re-enabled. The two policies coincide exactly when
42 * a mode is never disabled while running, so reachability records, for free,
43 * whether any phase-type mode was ever found disabled. phmemory "exact" (the
44 * default) errors when one was; "resume" proceeds deliberately with the resume
45 * semantics.
46 *
47 * OTHER RESTRICTIONS, each an error and never a silent approximation: no
48 * immediate transitions (they make vanishing states, which must be eliminated
49 * before a Kronecker rate descriptor exists) and no marking-dependent firing
50 * rates. A multi-server mode is accepted only when its enabling touches ONE
51 * level, because the enabling degree min_l floor(m(l)/enab(l)) is otherwise not
52 * a product of per-level terms.
53 *
54 * The initial marking is read, in the reference's order, from the declared
55 * per-place state (`NetworkStruct::initmarking`, `Place.setState`) when any
56 * place declares a token, else from the reference station of each closed class;
57 * `SpnOptions::init` overrides both.
58 *
59 * The entry law `pie` of a phase-type mode is derived from its (D0, D1) pair
60 * (`mdd_entry_law`), which is what the reference's `firingpie` holds for every
61 * law refreshPetriNetNodes builds; this struct carries no separate firingpie to
62 * override it with.
63 */
64
65#include <algorithm>
66#include <cmath>
67#include <cstddef>
68#include <cstdio>
69#include <limits>
70#include <map>
71#include <set>
72#include <string>
73#include <vector>
74
75#include "line/api/mdd/mdd.h"
81#include "line/num/number.h"
82#include "line/util/error.h"
83#include "line/util/matrix.h"
84
85namespace line {
86namespace spn {
87
88/** One (transition, mode) pair of the net, in level coordinates. */
89template <class T>
90struct SpnMode {
91 /** 1-based node index of the transition. */
92 std::size_t trans = 0;
93 /** Mode index within the transition, 0-based. */
94 std::size_t mode = 0;
95 /** Enabling multiplicity per place level. */
96 std::vector<double> enab;
97 /** Inhibition threshold per place level; infinite when absent. */
98 std::vector<double> inhib;
99 /** Firing outcome per place level. */
100 std::vector<double> fire;
103 std::vector<T> pie;
104 std::size_t nph = 1;
105 double srv = 1.0;
106 /** Marking-dependent firing-rate multiplier; empty for the unit one. */
107 std::function<T(const std::vector<T>&)> dep;
108};
109
110/** Everything the caller needs alongside the descriptor. */
111template <class T>
112struct SpnInfo {
113 /** 1-based node indices of the places. */
114 std::vector<std::size_t> places;
115 std::vector<std::string> placenames;
116 std::vector<std::string> classnames;
117 /** R, the stride of the place-major place levels (level l = pp * R + k). */
118 std::size_t nclasses = 1;
119 /** 1 for a place level, 2 for a phase level. */
120 std::vector<int> levelkind;
121 std::vector<std::string> levelname;
122 std::vector<SpnMode<T>> modes;
123 std::vector<int> init;
125 /** 1-based phase level of each mode, 0 when the mode has one phase. */
126 std::vector<std::size_t> phaseof;
127 /** Whether each mode was ever found disabled in a reachable marking. */
128 std::vector<bool> ever_disabled;
129 std::size_t nplacelevels = 0;
130 /** Node count of the model, so a firingdep argument can be rebuilt. */
131 std::size_t nnodes = 0;
132 /** Whether the Kronecker descriptor was built. */
133 bool descriptor = true;
134
135 SpnInfo() : diagram(std::vector<int>(1, 1)) {}
136};
137
138/** Descriptor, diagram and metadata returned together. */
139template <class T>
145
146/** Options of the translation. */
148 /** Per-place-level token bound; empty infers it from a place invariant. */
149 std::vector<double> bound;
150 /** Initial marking per place level (P * R, place-major); empty reads the model. */
151 std::vector<double> init;
152 /** "exact" (default) or "resume". */
153 std::string phmemory = "exact";
154 /** Print the level and state counts, as the reference's options.verbose does. */
155 bool verbose = false;
156 /**
157 * Build the Kronecker rate descriptor (default true). Pass false for the
158 * MDD-rec route, which reads only the reachable set: the restrictions that
159 * exist purely because a Kronecker form must factorise per level
160 * (marking-dependent firing rates, multi-server modes drawing from several
161 * places) are then lifted, in exchange for the firing times having to be
162 * exponential.
163 */
164 bool descriptor = true;
165};
166
167namespace detail {
168
169/**
170 * Local matrix of one mode at place level l.
171 *
172 * Move the count by net, but only from local states that satisfy this level's
173 * enabling and inhibition. apply_degree scales each row by the enabling degree
174 * min(floor(m/enab), srv), i.e. the number of concurrently firing servers; it is
175 * carried by the single enabling level of a multi-server mode. scale carries the
176 * scalar firing rate on whichever level the caller chose to put it.
177 */
178template <class T>
179mdd::MddLocalMatrix<T> spn_placemat(std::size_t l, const SpnMode<T>& mde, double net, int d,
180 bool apply_degree, const T& scale) {
181 typename mdd::MddLocalMatrix<T>::Builder bld(static_cast<std::size_t>(d));
182 for (int i = 0; i < d; ++i) {
183 if (!(static_cast<double>(i) >= mde.enab[l] && static_cast<double>(i) < mde.inhib[l]))
184 continue;
185 const int j = i + static_cast<int>(net);
186 if (j < 0 || j > d - 1) continue;
187 double val = 1.0;
188 if (apply_degree && mde.enab[l] > 0) {
189 const double deg = std::floor(static_cast<double>(i) / mde.enab[l]);
190 val = deg < mde.srv ? deg : mde.srv;
191 }
192 bld.add(static_cast<std::size_t>(i), static_cast<std::size_t>(j),
193 T(scale * num_traits<T>::from_double(val)));
194 }
195 return bld.build();
196}
197
198/** gcd of the integral entries of both vectors, 0 when any is not integral. */
199inline long spn_gcd_vec(const std::vector<double>& a, const std::vector<double>& b) {
200 long g = 0;
201 const std::vector<double>* all[2] = {&a, &b};
202 for (int i = 0; i < 2; ++i)
203 for (std::size_t k = 0; k < all[i]->size(); ++k) {
204 const double x = (*all[i])[k];
205 if (std::fabs(x - std::rint(x)) > 1e-9) return 0;
206 long y = std::labs(static_cast<long>(std::rint(x)));
207 while (y != 0) {
208 const long t = g % y;
209 g = y;
210 y = t;
211 }
212 }
213 return g;
214}
215
216/**
217 * Drop every row whose support strictly contains another's, which is what leaves
218 * the minimal supports and stops the pair expansion from blowing up.
219 */
220inline void spn_minimal_support(std::vector<std::vector<double>>& M,
221 std::vector<std::vector<double>>& B, std::size_t L) {
222 const std::size_t n = B.size();
223 std::vector<bool> drop(n, false);
224 for (std::size_t i = 0; i < n; ++i) {
225 if (drop[i]) continue;
226 for (std::size_t j = 0; j < n; ++j) {
227 if (i == j || drop[j]) continue;
228 bool contained = true, strict = false;
229 for (std::size_t l = 0; l < L && contained; ++l) {
230 const bool si = std::fabs(B[i][l]) > 1e-12;
231 const bool sj = std::fabs(B[j][l]) > 1e-12;
232 if (sj && !si) contained = false;
233 if (si && !sj) strict = true;
234 }
235 if (contained && strict) {
236 drop[i] = true;
237 break;
238 }
239 }
240 }
241 std::vector<std::vector<double>> Mk, Bk;
242 for (std::size_t i = 0; i < n; ++i)
243 if (!drop[i]) {
244 Mk.push_back(M[i]);
245 Bk.push_back(B[i]);
246 }
247 M.swap(Mk);
248 B.swap(Bk);
249}
250
251/**
252 * A strictly positive place invariant w, i.e. w >= 0 with netm w = 0.
253 *
254 * Scanning a null-space BASIS for a positive vector is not enough, and the
255 * fork-join net is the counterexample: its two minimal-support invariants are
256 * (1,1,0,1) and (1,0,1,1), neither positive, while their sum (2,1,1,2) is. Which
257 * basis a codebase's null space routine returns then decides whether the net is
258 * accepted, which is how MATLAB and the JAR came to disagree on it. So the
259 * non-negative generators are computed directly, by Farkas' algorithm on
260 * [netm' | I] -- the same construction `spn_sinvariants` uses on the incidence
261 * matrix -- and summed.
262 */
263inline bool spn_place_invariant(const std::vector<std::vector<double>>& netm, std::size_t L,
264 std::vector<double>& w) {
265 const std::size_t E = netm.size();
266 bool conservative = true;
267 for (std::size_t e = 0; e < E; ++e) {
268 double s = 0;
269 for (std::size_t l = 0; l < L; ++l) s += netm[e][l];
270 if (std::fabs(s) > 1e-12) {
271 conservative = false;
272 break;
273 }
274 }
275 if (conservative) {
276 w.assign(L, 1.0);
277 return true;
278 }
279 if (E == 0) return false;
280
281 std::vector<std::vector<double>> M(L, std::vector<double>(E, 0.0)); // level l's column
282 std::vector<std::vector<double>> B(L, std::vector<double>(L, 0.0)); // its combination
283 for (std::size_t l = 0; l < L; ++l) {
284 for (std::size_t e = 0; e < E; ++e) M[l][e] = netm[e][l];
285 B[l][l] = 1.0;
286 }
287
288 for (std::size_t e = 0; e < E; ++e) {
289 std::vector<std::vector<double>> Mn, Bn;
290 for (std::size_t i = 0; i < M.size(); ++i)
291 if (std::fabs(M[i][e]) < 1e-12) {
292 Mn.push_back(M[i]);
293 Bn.push_back(B[i]);
294 }
295 for (std::size_t a = 0; a < M.size(); ++a) {
296 if (!(M[a][e] > 1e-12)) continue;
297 for (std::size_t b = 0; b < M.size(); ++b) {
298 if (!(M[b][e] < -1e-12)) continue;
299 const double ca = -M[b][e], cb = M[a][e];
300 std::vector<double> c(E, 0.0), d(L, 0.0);
301 for (std::size_t k = 0; k < E; ++k) c[k] = ca * M[a][k] + cb * M[b][k];
302 for (std::size_t k = 0; k < L; ++k) d[k] = ca * B[a][k] + cb * B[b][k];
303 const long g = spn_gcd_vec(c, d);
304 if (g > 0) {
305 for (std::size_t k = 0; k < E; ++k) c[k] /= static_cast<double>(g);
306 for (std::size_t k = 0; k < L; ++k) d[k] /= static_cast<double>(g);
307 }
308 Mn.push_back(c);
309 Bn.push_back(d);
310 }
311 }
312 spn_minimal_support(Mn, Bn, L);
313 M.swap(Mn);
314 B.swap(Bn);
315 }
316
317 if (B.empty()) return false;
318 std::vector<double> s(L, 0.0);
319 for (std::size_t i = 0; i < B.size(); ++i)
320 for (std::size_t l = 0; l < L; ++l) s[l] += B[i][l];
321 double mn = std::numeric_limits<double>::max();
322 for (std::size_t l = 0; l < L; ++l) {
323 if (!(s[l] > 1e-9)) return false;
324 mn = std::min(mn, s[l]);
325 }
326 w.assign(L, 0.0);
327 for (std::size_t l = 0; l < L; ++l) w[l] = s[l] / mn;
328 return true;
329}
330
331/** Successors of s, recording which modes were found disabled here. */
332template <class T>
333void spn_successors(const std::vector<int>& s, const std::vector<SpnMode<T>>& md,
334 const std::vector<std::vector<double>>& netm,
335 const std::vector<std::size_t>& phaseof, const std::vector<int>& domain,
336 std::size_t L, std::vector<std::vector<int>>& out,
337 std::vector<bool>& ever_disabled) {
338 const T zero = num_traits<T>::from_int(0);
339 for (std::size_t e = 0; e < md.size(); ++e) {
340 const SpnMode<T>& mde = md[e];
341 bool enabled = true;
342 for (std::size_t l = 0; l < L; ++l)
343 if (static_cast<double>(s[l]) < mde.enab[l] ||
344 static_cast<double>(s[l]) >= mde.inhib[l]) {
345 enabled = false;
346 break;
347 }
348 if (!enabled) {
349 ever_disabled[e] = true;
350 continue;
351 }
352 if (phaseof[e] == 0) {
353 std::vector<int> t = s;
354 bool ok = true;
355 for (std::size_t l = 0; l < L; ++l) {
356 const int nv = s[l] + static_cast<int>(netm[e][l]);
357 if (nv < 0 || nv > domain[l] - 1) {
358 ok = false;
359 break;
360 }
361 t[l] = nv;
362 }
363 if (ok) out.push_back(t);
364 } else {
365 const std::size_t q = phaseof[e] - 1;
366 const int ph = s[q];
367 for (std::size_t j = 0; j < mde.nph; ++j)
368 if (static_cast<int>(j) != ph && mde.D0(ph, j) != zero) {
369 std::vector<int> t = s;
370 t[q] = static_cast<int>(j);
371 out.push_back(t);
372 }
373 std::vector<int> moved = s;
374 bool ok = true;
375 for (std::size_t l = 0; l < L; ++l) {
376 const int nv = s[l] + static_cast<int>(netm[e][l]);
377 if (nv < 0 || nv > domain[l] - 1) {
378 ok = false;
379 break;
380 }
381 moved[l] = nv;
382 }
383 if (ok)
384 for (std::size_t j = 0; j < mde.nph; ++j)
385 if (mde.D1(ph, j) != zero) {
386 std::vector<int> t = moved;
387 t[q] = static_cast<int>(j);
388 out.push_back(t);
389 }
390 }
391 }
392}
393
394} // namespace detail
395
396/**
397 * Build the reachable set and Kronecker descriptor of a stochastic Petri net.
398 *
399 * @param sn a NetworkStruct holding Places and Transitions
400 * @param options translation options
401 */
402template <class T>
404 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
405 const double inf = std::numeric_limits<double>::infinity();
406
407 std::vector<std::size_t> places, transitions;
408 for (std::size_t i = 1; i <= sn.nodes.size(); ++i) {
409 if (sn.nodes[i - 1].nodetype == lang::NodeType::Place) places.push_back(i);
410 else if (sn.nodes[i - 1].nodetype == lang::NodeType::Transition) transitions.push_back(i);
411 }
412 if (places.empty() || transitions.empty())
413 throw InputError("spn_mdd: the model holds no Place or no Transition node");
414 const std::size_t P = places.size();
415 const std::size_t R = sn.nclasses;
416 const std::size_t L = P * R; // place levels, place-major: l = pp * R + k
417
418 // ---- collect the (transition, mode) pairs
419 std::vector<SpnMode<T>> md;
420 for (std::size_t t = 0; t < transitions.size(); ++t) {
421 const std::size_t ind = transitions[t];
422 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
423 sn.transparam.find(ind);
424 if (it == sn.transparam.end()) continue;
425 const qn::TransitionParam<T>& tp = it->second;
426 for (std::size_t m = 0; m < tp.nmodes; ++m) {
427 if (m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE)
428 throw UnsupportedError(ErrorKind{"Immediate"}, "spn_mdd: mode " + std::to_string(m + 1) + " of node " +
429 std::to_string(ind) +
430 " is IMMEDIATE; vanishing states must be eliminated before "
431 "the net has a Kronecker rate descriptor");
432 if (m < tp.firingdep.size() && tp.firingdep[m] && options.descriptor)
433 throw UnsupportedError(ErrorKind{"MarkingDependence"}, "spn_mdd: mode " + std::to_string(m + 1) + " of node " +
434 std::to_string(ind) +
435 " has a marking-dependent firing rate; g(marking) is not a "
436 "product of per-level terms");
437 if (m >= tp.firingproc.size() || tp.firingproc[m].disabled)
438 throw InputError("spn_mdd: mode " + std::to_string(m + 1) + " of node " +
439 std::to_string(ind) + " has no firing process");
440 SpnMode<T> e;
441 e.trans = ind;
442 e.mode = m;
443 e.enab.assign(L, 0.0);
444 e.inhib.assign(L, inf);
445 e.fire.assign(L, 0.0);
446 // the reference's i_arcvec: the per-class arc at (place, class), 0
447 // (max'ed) for enabling and firing, Inf for an absent inhibitor
448 const Matrix<T>* en = m < tp.enabling.size() ? &tp.enabling[m] : nullptr;
449 const Matrix<T>* ih = m < tp.inhibiting.size() ? &tp.inhibiting[m] : nullptr;
450 const Matrix<T>* fi = m < tp.firing.size() ? &tp.firing[m] : nullptr;
451 for (std::size_t pp = 0; pp < P; ++pp) {
452 const std::size_t q = places[pp] - 1; // arcs are indexed by node
453 for (std::size_t k = 0; k < R; ++k) {
454 const std::size_t l = pp * R + k;
455 if (en && q < en->rows() && k < en->cols())
456 e.enab[l] = std::max(0.0, num_traits<T>::to_double((*en)(q, k)));
457 if (ih && q < ih->rows() && k < ih->cols())
458 e.inhib[l] = num_traits<T>::to_double((*ih)(q, k));
459 if (fi && q < fi->rows() && k < fi->cols())
460 e.fire[l] = std::max(0.0, num_traits<T>::to_double((*fi)(q, k)));
461 }
462 }
463 const mam::Map<T> proc = lang::dist_to_map(tp.firingproc[m]);
464 e.D0 = proc.D0;
465 e.D1 = proc.D1;
466 e.nph = proc.order();
467 e.pie = mdd::mdd_entry_law(std::vector<T>(), e.D1, e.nph, m, "spn_mdd");
468 e.srv = m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0;
469 if (m < tp.firingdep.size()) e.dep = tp.firingdep[m];
470 if (!options.descriptor && e.nph > 1)
471 throw UnsupportedError(ErrorKind{"NonExponential"}, "spn_mdd: mode " + std::to_string(m + 1) + " of node " +
472 std::to_string(ind) +
473 " has a phase-type firing time; the reachable-set-only "
474 "mode carries no phase level, and a product-form marking "
475 "process must be memoryless in the marking alone");
476 md.push_back(e);
477 }
478 }
479 const std::size_t E = md.size();
480 for (std::size_t e = 0; options.descriptor && e < E; ++e) {
481 std::size_t nz = 0;
482 for (std::size_t l = 0; l < L; ++l)
483 if (md[e].enab[l] != 0) ++nz;
484 if (md[e].srv != 1 && nz > 1)
485 throw UnsupportedError(ErrorKind{"MultiServer"}, "spn_mdd: mode " + std::to_string(md[e].mode + 1) +
486 " of node " + std::to_string(md[e].trans) + " has " +
487 std::to_string(md[e].srv) + " servers and draws from " +
488 std::to_string(nz) +
489 " levels; the enabling degree min_l floor(m(l)/enab(l)) is then "
490 "not a product of per-level terms and admits no Kronecker form");
491 if (md[e].srv != 1 && nz == 0)
492 throw InputError("spn_mdd: mode " + std::to_string(md[e].mode + 1) + " of node " +
493 std::to_string(md[e].trans) + " has " + std::to_string(md[e].srv) +
494 " servers but consumes from no place, so its enabling degree is "
495 "unbounded and its firing rate undefined");
496 }
497
498 // ---- phase levels for the multi-phase modes
499 std::vector<std::size_t> phaseof(E, 0); // 0 = none; else 1-based level
500 std::size_t Q = 0;
501 for (std::size_t e = 0; options.descriptor && e < E; ++e)
502 if (md[e].nph > 1) {
503 ++Q;
504 phaseof[e] = L + Q;
505 }
506 const std::size_t K = L + Q;
507
508 std::vector<std::vector<double>> netm(E, std::vector<double>(L, 0.0));
509 for (std::size_t e = 0; e < E; ++e)
510 for (std::size_t l = 0; l < L; ++l) netm[e][l] = md[e].fire[l] - md[e].enab[l];
511
512 // ---- initial marking and per-level bounds
513 std::vector<double> init0(L, 0.0);
514 if (!options.init.empty()) {
515 if (options.init.size() != L)
516 throw InputError("spn_mdd: options.init must hold one token count per place level");
517 init0 = options.init;
518 } else {
519 // the reference's i_initmarking: the declared per-place state first
520 // (node.getState()), the reference-station default when none is set or
521 // every declared count is zero
522 bool anyset = false;
523 for (std::size_t pp = 0; pp < P; ++pp) {
524 const typename std::map<std::size_t, std::vector<T>>::const_iterator it =
525 sn.initmarking.find(places[pp]);
526 if (it == sn.initmarking.end()) continue;
527 anyset = true;
528 for (std::size_t k = 0; k < R && k < it->second.size(); ++k)
529 init0[pp * R + k] = num_traits<T>::to_double(it->second[k]);
530 }
531 bool allzero = true;
532 for (std::size_t l = 0; l < L && allzero; ++l) allzero = init0[l] == 0.0;
533 if (!anyset || allzero) {
534 std::fill(init0.begin(), init0.end(), 0.0);
535 for (std::size_t k = 0; k < R; ++k) {
536 const double njobs = sn.classes[k].population;
537 if (!std::isfinite(njobs))
538 throw UnsupportedError(ErrorKind{"OpenClass"}, "spn_mdd: class " + std::to_string(k + 1) +
539 " is open; an unbounded token population has no "
540 "finite place level");
541 const std::size_t ref_node = sn.station_to_node[sn.classes[k].refstat - 1];
542 for (std::size_t pp = 0; pp < P; ++pp)
543 if (places[pp] == ref_node) init0[pp * R + k] += njobs;
544 }
545 }
546 }
547 std::vector<double> winv;
548 const bool has_inv = detail::spn_place_invariant(netm, L, winv);
549 double vinv = 0;
550 if (has_inv)
551 for (std::size_t l = 0; l < L; ++l) vinv += winv[l] * init0[l];
552
553 std::vector<double> bound(L, 0.0);
554 if (!options.bound.empty()) {
555 for (std::size_t l = 0; l < L; ++l)
556 bound[l] = options.bound.size() == 1 ? options.bound[0] : options.bound[l];
557 } else if (has_inv) {
558 for (std::size_t l = 0; l < L; ++l)
559 bound[l] = winv[l] > 0 ? std::floor(vinv / winv[l]) : vinv;
560 } else {
561 throw UnsupportedError(ErrorKind{"Unbounded"}, "spn_mdd: the net has no place invariant with positive weights, so the "
562 "marking is not bounded a priori; pass options.bound");
563 }
564
565 std::vector<int> domain(K, 1);
566 for (std::size_t l = 0; l < L; ++l) domain[l] = static_cast<int>(bound[l]) + 1;
567 for (std::size_t e = 0; e < E; ++e)
568 if (phaseof[e] > 0) domain[phaseof[e] - 1] = static_cast<int>(md[e].nph);
569
570 std::vector<int> init(K, 0);
571 for (std::size_t l = 0; l < L; ++l) init[l] = static_cast<int>(init0[l]);
572 for (std::size_t e = 0; e < E; ++e)
573 if (phaseof[e] > 0)
574 for (std::size_t a = 0; a < md[e].nph; ++a)
575 if (md[e].pie[a] > zero) {
576 init[phaseof[e] - 1] = static_cast<int>(a);
577 break;
578 }
579
580 // ---- reachable set; the closure records which modes were ever disabled, so
581 // the phase-memory question is answered without a second pass over |S|
582 std::vector<bool> ever_disabled(E, false);
583 mdd::MDD diagram(domain);
584 diagram.insert(init);
585 std::vector<std::vector<int>> frontier;
586 frontier.push_back(init);
587 std::size_t head = 0;
588 while (head < frontier.size()) {
589 const std::vector<int> s = frontier[head];
590 ++head;
591 std::vector<std::vector<int>> succ;
592 detail::spn_successors(s, md, netm, phaseof, domain, L, succ, ever_disabled);
593 for (std::size_t r = 0; r < succ.size(); ++r)
594 if (!diagram.member(succ[r])) {
595 diagram.insert(succ[r]);
596 frontier.push_back(succ[r]);
597 }
598 if (head > 1024 && 2 * head > frontier.size()) {
599 frontier.erase(frontier.begin(), frontier.begin() + static_cast<long>(head));
600 head = 0;
601 }
602 }
603 diagram.compact();
604
605 for (std::size_t e = 0; e < E; ++e)
606 if (phaseof[e] > 0 && options.descriptor && ever_disabled[e] &&
607 options.phmemory != "resume" &&
608 options.phmemory != "RESUME")
609 throw UnsupportedError(ErrorKind{"NonExponential"},
610 "spn_mdd: mode " + std::to_string(md[e].mode + 1) + " of node " +
611 std::to_string(md[e].trans) +
612 " has a phase-type firing time AND is disabled in some reachable marking. LINE "
613 "discards the phase on disabling (preemptive repeat) but that reset is a joint "
614 "condition on the place levels and has no Kronecker form, so this descriptor would "
615 "encode preemptive resume instead and disagree with SolverCTMC. Pass "
616 "phmemory=\"resume\" to accept the resume semantics.");
617
618 // ---- Kronecker event matrices
620 for (std::size_t e = 0; options.descriptor && e < E; ++e) {
621 const SpnMode<T>& mde = md[e];
622 std::set<std::size_t> gate, touched;
623 for (std::size_t l = 0; l < L; ++l)
624 if (mde.enab[l] > 0 || std::isfinite(mde.inhib[l])) {
625 gate.insert(l);
626 touched.insert(l);
627 }
628 for (std::size_t l = 0; l < L; ++l)
629 if (netm[e][l] != 0) touched.insert(l);
630 const std::vector<std::size_t> touched_sorted(touched.begin(), touched.end());
631 long degl = -1;
632 if (mde.srv != 1) {
633 for (std::size_t l = 0; l < L; ++l)
634 if (mde.enab[l] > 0) {
635 degl = static_cast<long>(l); // level carrying the degree
636 break;
637 }
638 }
639 if (phaseof[e] == 0) {
640 if (touched_sorted.empty()) continue;
641 mdd::MddEvent<T> event;
642 event.a = mde.trans;
643 event.b = mde.mode;
644 for (std::size_t t = 0; t < touched_sorted.size(); ++t) {
645 const std::size_t l = touched_sorted[t];
646 event.lev.push_back(l);
647 event.W.push_back(detail::spn_placemat(l, mde, netm[e][l], domain[l],
648 static_cast<long>(l) == degl, one));
649 }
650 // the scalar firing rate rides on the first touched level
651 const std::size_t l0 = touched_sorted[0];
652 event.W[0] = detail::spn_placemat(l0, mde, netm[e][l0], domain[l0],
653 static_cast<long>(l0) == degl, mde.D1(0, 0));
654 desc.events.push_back(event);
655 } else {
656 const std::size_t q = phaseof[e] - 1;
657 // (1) internal phase changes: marking unchanged, gated by enabling
658 std::size_t nnz_off = 0;
659 for (std::size_t a = 0; a < mde.nph; ++a)
660 for (std::size_t b = 0; b < mde.nph; ++b)
661 if (a != b && mde.D0(a, b) != zero) ++nnz_off;
662 if (nnz_off > 0) {
663 mdd::MddEvent<T> event;
664 event.a = mde.trans;
665 event.b = mde.mode;
666 for (std::set<std::size_t>::const_iterator g = gate.begin(); g != gate.end();
667 ++g) {
668 event.lev.push_back(*g);
669 event.W.push_back(
670 detail::spn_placemat(*g, mde, 0.0, domain[*g], false, one));
671 }
672 typename mdd::MddLocalMatrix<T>::Builder bld(mde.nph);
673 for (std::size_t a = 0; a < mde.nph; ++a)
674 for (std::size_t b = 0; b < mde.nph; ++b)
675 if (a != b) bld.add(a, b, mde.D0(a, b));
676 event.lev.push_back(q);
677 event.W.push_back(bld.build());
678 desc.events.push_back(event);
679 }
680 // (2) firings: marking moved, phase redrawn through D1
681 mdd::MddEvent<T> event;
682 event.a = mde.trans;
683 event.b = mde.mode;
684 for (std::size_t t = 0; t < touched_sorted.size(); ++t) {
685 const std::size_t l = touched_sorted[t];
686 event.lev.push_back(l);
687 event.W.push_back(detail::spn_placemat(l, mde, netm[e][l], domain[l],
688 static_cast<long>(l) == degl, one));
689 }
690 typename mdd::MddLocalMatrix<T>::Builder bld(mde.nph);
691 for (std::size_t a = 0; a < mde.nph; ++a)
692 for (std::size_t b = 0; b < mde.nph; ++b) bld.add(a, b, mde.D1(a, b));
693 event.lev.push_back(q);
694 event.W.push_back(bld.build());
695 desc.events.push_back(event);
696 }
697 }
698
699 desc.K = K;
700 desc.N = 0; // the invariant below replaces it
701 desc.domain = domain;
702 if (has_inv) {
703 desc.invariant_weights.assign(K, 0.0);
704 for (std::size_t l = 0; l < L; ++l) desc.invariant_weights[l] = winv[l];
705 desc.invariant_value = vinv;
706 }
707
708 // ---- descriptive information
709 SpnInfo<T> info;
710 info.places = places;
711 for (std::size_t pp = 0; pp < P; ++pp) info.placenames.push_back(sn.nodes[places[pp] - 1].name);
712 for (std::size_t k = 0; k < R; ++k) info.classnames.push_back(sn.classes[k].name);
713 info.nclasses = R;
714 info.levelkind.assign(K, 2);
715 for (std::size_t l = 0; l < L; ++l) info.levelkind[l] = 1;
716 info.levelname.assign(K, std::string());
717 for (std::size_t pp = 0; pp < P; ++pp)
718 for (std::size_t k = 0; k < R; ++k)
719 info.levelname[pp * R + k] = info.placenames[pp] + "." + info.classnames[k];
720 for (std::size_t e = 0; e < E; ++e)
721 if (phaseof[e] > 0)
722 info.levelname[phaseof[e] - 1] = "phase(" + sn.nodes[md[e].trans - 1].name + ".m" +
723 std::to_string(md[e].mode + 1) + ")";
724 info.modes = md;
725 info.init = init;
726 info.diagram = diagram;
727 info.phaseof = phaseof;
728 info.ever_disabled = ever_disabled;
729 info.nplacelevels = L;
730 info.nnodes = sn.nodes.size();
731 info.descriptor = options.descriptor;
732
733 if (options.verbose) {
734 std::printf("\nSPN -> MDD: %zu places x %zu classes = %zu place levels, %zu phase levels\n",
735 P, R, L, Q);
736 std::string bstr = "[";
737 for (std::size_t l = 0; l < L; ++l)
738 bstr += (l > 0 ? " " : "") + std::to_string(static_cast<long long>(bound[l]));
739 bstr += "]";
740 std::printf(" modes = %zu, |S| = %lld, bounds = %s\n", E, diagram.cardinality(),
741 bstr.c_str());
742 }
743
744 SpnResult<T> res;
745 res.mdds = diagram.to_struct();
746 res.desc = desc;
747 res.info = info;
748 return res;
749}
750
751} // namespace spn
752} // namespace line
753
754#endif // LINE_API_SPN_SPN_MDD_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
The diagram: insert / member / index / enumerate / cardinality.
Definition mdd.h:97
long long cardinality() const
|S|, the number of stored states.
Definition mdd.h:145
MddStruct to_struct() const
Export the diagram as plain arrays for downstream algorithms.
Definition mdd.h:181
void insert(const std::vector< int > &state)
Add a K-tuple of 0-based local values to the set.
Definition mdd.h:128
void compact()
Reclaim dead nodes left by the append-only build.
Definition mdd.h:202
bool member(const std::vector< int > &state) const
True iff state is in the set; O(K).
Definition mdd.h:135
Incremental triplet builder; duplicate entries are accumulated.
Definition mdd_types.h:75
Builder & add(std::size_t i, std::size_t j, const T &value)
Accumulate value into entry (i,j).
Definition mdd_types.h:80
MddLocalMatrix< T > build() const
Definition mdd_types.h:93
A network plus its refreshed NetworkStruct.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
Quasi-reduced ordered Multi-valued Decision Diagram.
The rate side of the decision-diagram domain: local matrices, events, the Kronecker descriptor,...
mam::Map< T > dist_to_map(const Distrib< T > &d)
The (D0,D1) pair that reaches sn.proc.
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
std::vector< T > mdd_entry_law(const std::vector< T > &given, const Matrix< T > &D1, std::size_t h, std::size_t i, const std::string &caller)
Entry law of a phase-type station, taken as given or derived from D1.
Definition mdd_types.h:268
@ Bk
Birman-Kogan saddle point with bottleneck detection.
Definition pfqn_nc.h:115
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
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
std::size_t order() const
Definition map_moment.h:57
Kronecker rate descriptor of a structured model, the input of mdd_mcd.
Definition mdd_types.h:165
std::size_t K
Number of levels, i.e.
Definition mdd_types.h:167
One event of the Kronecker rate descriptor.
Definition mdd_types.h:124
std::size_t a
Station (or transition node) the event departs from, 0-based.
Definition mdd_types.h:126
A local rate matrix W_k^e of the Kronecker descriptor, held row-compressed.
Definition mdd_types.h:46
Plain-array export of an MDD, the input contract of mdd_mcd.
Definition mdd.h:57
The parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
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< 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).
Everything the caller needs alongside the descriptor.
Definition spn_mdd.h:112
bool descriptor
Whether the Kronecker descriptor was built.
Definition spn_mdd.h:133
std::size_t nplacelevels
Definition spn_mdd.h:129
std::vector< SpnMode< T > > modes
Definition spn_mdd.h:122
std::vector< int > init
Definition spn_mdd.h:123
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 > levelname
Definition spn_mdd.h:121
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
std::vector< std::string > classnames
Definition spn_mdd.h:116
std::vector< std::size_t > phaseof
1-based phase level of each mode, 0 when the mode has one phase.
Definition spn_mdd.h:126
std::vector< bool > ever_disabled
Whether each mode was ever found disabled in a reachable marking.
Definition spn_mdd.h:128
std::vector< int > levelkind
1 for a place level, 2 for a phase level.
Definition spn_mdd.h:120
One (transition, mode) pair of the net, in level coordinates.
Definition spn_mdd.h:90
std::size_t trans
1-based node index of the transition.
Definition spn_mdd.h:92
std::vector< T > pie
Definition spn_mdd.h:103
std::vector< double > fire
Firing outcome per place level.
Definition spn_mdd.h:100
Matrix< T > D1
Definition spn_mdd.h:102
std::size_t nph
Definition spn_mdd.h:104
std::vector< double > enab
Enabling multiplicity per place level.
Definition spn_mdd.h:96
std::vector< double > inhib
Inhibition threshold per place level; infinite when absent.
Definition spn_mdd.h:98
Matrix< T > D0
Definition spn_mdd.h:101
std::function< T(const std::vector< T > &)> dep
Marking-dependent firing-rate multiplier; empty for the unit one.
Definition spn_mdd.h:107
std::size_t mode
Mode index within the transition, 0-based.
Definition spn_mdd.h:94
Options of the translation.
Definition spn_mdd.h:147
std::string phmemory
"exact" (default) or "resume".
Definition spn_mdd.h:153
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
std::vector< double > init
Initial marking per place level (P * R, place-major); empty reads the model.
Definition spn_mdd.h:151
Descriptor, diagram and metadata returned together.
Definition spn_mdd.h:140
mdd::MddDescriptor< T > desc
Definition spn_mdd.h:142
SpnInfo< T > info
Definition spn_mdd.h:143
mdd::MddStruct mdds
Definition spn_mdd.h:141