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