LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
spn_sinvariants.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_SINVARIANTS_H
6#define LINE_API_SPN_SPN_SINVARIANTS_H
7
8/**
9 * @file
10 * @ingroup api_spn
11 * Minimal-support S-invariants (P-invariants) of a stochastic Petri net, and
12 * the load vector V = S m0.
13 *
14 * An S-invariant is a non-negative left null vector of the incidence matrix,
15 * U' C = 0, so U' m is conserved by every firing. The minimal-support ones form
16 * a basis of all of them (S. Balsamo, A. Marin, I. Stojic, FGCS 111 (2020),
17 * Sec. 3.1) and are what the convolution algorithm `spn_conv` decomposes the
18 * reachability set along; `spn_mdd` uses a single positive invariant for a much
19 * weaker purpose, to bound each place a priori.
20 *
21 * FARKAS' ALGORITHM, on [C | I]: for each transition column in turn, keep the
22 * rows that already annihilate it and add, for every pair of rows of opposite
23 * sign in it, the positive combination that cancels it; then drop every row
24 * whose support strictly contains another's, which is what leaves the minimal
25 * supports. Rows are kept in integer arithmetic and divided by their gcd, so a
26 * multiplicity is never lost to rounding and two invariants that differ only by
27 * a positive scale are the same row.
28 *
29 * ARC MULTIPLICITIES MUST BE INTEGRAL. A fractional arc has no Petri-net
30 * meaning and would make the gcd normalisation and the ILP-free convolution
31 * both wrong, so it is refused rather than rounded.
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <cstddef>
37#include <map>
38#include <string>
39#include <vector>
40
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/matrix.h"
46
47namespace line {
48namespace spn {
49
50/** The invariant basis of a net, in place-level coordinates. */
52 /** 1-based node indices of the places, in level order. */
53 std::vector<std::size_t> places;
54 /** S[i][p]: weight of place p in minimal-support invariant i. */
55 std::vector<std::vector<long long>> S;
56 /** V = S m0, the load vector. */
57 std::vector<long long> V;
58 /** The initial marking the load vector was taken against. */
59 std::vector<long long> m0;
60};
61
62namespace detail {
63
64inline long long spn_gcd(long long a, long long b) {
65 a = a < 0 ? -a : a;
66 b = b < 0 ? -b : b;
67 while (b != 0) {
68 const long long t = a % b;
69 a = b;
70 b = t;
71 }
72 return a;
73}
74
75/** An arc multiplicity, refused unless integral. */
76inline long long spn_as_integer(double x, const char* what) {
77 const double r = std::floor(x + 0.5);
78 if (std::fabs(x - r) > 1e-9)
79 throw InputError(std::string("spn_sinvariants: ") + what +
80 " is not integral; a fractional arc multiplicity has no Petri-net "
81 "meaning and no invariant basis over the integers");
82 return static_cast<long long>(r);
83}
84
85/** True when the support of a is contained in the support of b. */
86inline bool spn_support_subset(const std::vector<long long>& a, const std::vector<long long>& b,
87 std::size_t off, std::size_t n) {
88 for (std::size_t k = 0; k < n; ++k)
89 if (a[off + k] != 0 && b[off + k] == 0) return false;
90 return true;
91}
92
93inline bool spn_support_equal(const std::vector<long long>& a, const std::vector<long long>& b,
94 std::size_t off, std::size_t n) {
95 for (std::size_t k = 0; k < n; ++k)
96 if ((a[off + k] != 0) != (b[off + k] != 0)) return false;
97 return true;
98}
99
100} // namespace detail
101
102/**
103 * Minimal-support S-invariants and the load vector of a net.
104 *
105 * @param sn a NetworkStruct holding Places and Transitions
106 * @param init initial marking per place level; empty takes it from the
107 * reference station of each closed class, as `spn_mdd` does
108 */
109template <class T>
111 const std::vector<double>& init = std::vector<double>()) {
112 std::vector<std::size_t> places, transitions;
113 for (std::size_t i = 1; i <= sn.nodes.size(); ++i) {
114 if (sn.nodes[i - 1].nodetype == lang::NodeType::Place) places.push_back(i);
115 else if (sn.nodes[i - 1].nodetype == lang::NodeType::Transition) transitions.push_back(i);
116 }
117 if (places.empty() || transitions.empty())
118 throw InputError("spn_sinvariants: the model holds no Place or no Transition node");
119 const std::size_t n = places.size();
120
121 // ---- incidence matrix C[p][mode] = post - pre, one column per (transition, mode)
122 std::vector<std::vector<long long>> C(n);
123 std::size_t ncols = 0;
124 for (std::size_t p = 0; p < n; ++p) C[p].clear();
125 for (std::size_t t = 0; t < transitions.size(); ++t) {
126 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
127 sn.transparam.find(transitions[t]);
128 if (it == sn.transparam.end()) continue;
129 const qn::TransitionParam<T>& tp = it->second;
130 for (std::size_t m = 0; m < tp.nmodes; ++m) {
131 // THE INCIDENCE IS OVER PLACES, so the arcs are summed over classes:
132 // an S-invariant of the class-summed net is a genuine invariant of
133 // the coloured one (every colour moves along the same arc), it is
134 // just not the finest one -- the per-(place, class) invariants
135 // refine it. Stated here rather than refused, because a weaker
136 // invariant is still an invariant.
137 const std::vector<T> en_t = qn::TransitionParam<T>::arc_total(tp.enabling, m);
138 const std::vector<T> fi_t = qn::TransitionParam<T>::arc_total(tp.firing, m);
139 for (std::size_t p = 0; p < n; ++p) {
140 const std::size_t q = places[p] - 1;
141 double pre = 0, post = 0;
142 if (q < en_t.size()) pre = num_traits<T>::to_double(en_t[q]);
143 if (q < fi_t.size()) post = num_traits<T>::to_double(fi_t[q]);
144 C[p].push_back(detail::spn_as_integer(post, "a firing arc") -
145 detail::spn_as_integer(pre, "an enabling arc"));
146 }
147 ++ncols;
148 }
149 }
150
151 // ---- Farkas on [C | I]: row p starts as (C[p], e_p)
152 std::vector<std::vector<long long>> rows(n);
153 for (std::size_t p = 0; p < n; ++p) {
154 rows[p].assign(ncols + n, 0);
155 for (std::size_t c = 0; c < ncols; ++c) rows[p][c] = C[p][c];
156 rows[p][ncols + p] = 1;
157 }
158 for (std::size_t c = 0; c < ncols; ++c) {
159 std::vector<std::vector<long long>> next;
160 for (std::size_t r = 0; r < rows.size(); ++r)
161 if (rows[r][c] == 0) next.push_back(rows[r]);
162 for (std::size_t a = 0; a < rows.size(); ++a) {
163 if (rows[a][c] <= 0) continue;
164 for (std::size_t b = 0; b < rows.size(); ++b) {
165 if (rows[b][c] >= 0) continue;
166 const long long pa = rows[a][c], nb = -rows[b][c];
167 const long long d = detail::spn_gcd(pa, nb);
168 const long long fa = nb / d, fb = pa / d;
169 std::vector<long long> combo(ncols + n, 0);
170 long long g = 0;
171 for (std::size_t k = 0; k < combo.size(); ++k) {
172 combo[k] = fa * rows[a][k] + fb * rows[b][k];
173 g = detail::spn_gcd(g, combo[k]);
174 }
175 if (g > 1)
176 for (std::size_t k = 0; k < combo.size(); ++k) combo[k] /= g;
177 bool nonzero = false;
178 for (std::size_t k = 0; k < n; ++k) nonzero = nonzero || combo[ncols + k] != 0;
179 if (nonzero) next.push_back(combo);
180 }
181 }
182 // support-minimality filter, applied at every step so the row set cannot
183 // grow combinatorially on the way to the answer
184 std::vector<std::vector<long long>> keep;
185 for (std::size_t r = 0; r < next.size(); ++r) {
186 bool dominated = false;
187 for (std::size_t s = 0; s < next.size() && !dominated; ++s) {
188 if (s == r) continue;
189 if (!detail::spn_support_subset(next[s], next[r], ncols, n)) continue;
190 const bool same = detail::spn_support_equal(next[s], next[r], ncols, n);
191 if (!same || s < r) dominated = true; // keep the first of equal supports
192 }
193 if (!dominated) keep.push_back(next[r]);
194 }
195 rows = keep;
196 }
197
198 SpnInvariants out;
199 out.places = places;
200 for (std::size_t r = 0; r < rows.size(); ++r) {
201 bool nonneg = true;
202 for (std::size_t k = 0; k < n; ++k) nonneg = nonneg && rows[r][ncols + k] >= 0;
203 if (!nonneg) continue; // an S-invariant is non-negative by definition
204 std::vector<long long> y(n, 0);
205 for (std::size_t k = 0; k < n; ++k) y[k] = rows[r][ncols + k];
206 out.S.push_back(y);
207 }
208
209 // ---- initial marking and the load vector V = S m0
210 out.m0.assign(n, 0);
211 if (!init.empty()) {
212 if (init.size() != n)
213 throw InputError("spn_sinvariants: init must hold one token count per place");
214 for (std::size_t p = 0; p < n; ++p)
215 out.m0[p] = detail::spn_as_integer(init[p], "an initial marking");
216 } else {
217 for (std::size_t r = 0; r < sn.classes.size(); ++r) {
218 const double njobs = sn.classes[r].population;
219 if (!std::isfinite(njobs))
220 throw UnsupportedError("spn_sinvariants: class " + std::to_string(r + 1) +
221 " is open, so the net has no finite load vector");
222 const std::size_t ref_node = sn.station_to_node[sn.classes[r].refstat - 1];
223 for (std::size_t p = 0; p < n; ++p)
224 if (places[p] == ref_node)
225 out.m0[p] += detail::spn_as_integer(njobs, "a class population");
226 }
227 }
228 out.V.assign(out.S.size(), 0);
229 for (std::size_t i = 0; i < out.S.size(); ++i)
230 for (std::size_t p = 0; p < n; ++p) out.V[i] += out.S[i][p] * out.m0[p];
231 return out;
232}
233
234} // namespace spn
235} // namespace line
236
237#endif // LINE_API_SPN_SPN_SINVARIANTS_H
InputError(const std::string &what)
Definition error.h:39
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
SpnInvariants spn_sinvariants(const qn::NetworkStruct< T > &sn, const std::vector< double > &init=std::vector< double >())
Minimal-support S-invariants and the load vector of a net.
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
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< 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.
The invariant basis of a net, in place-level coordinates.
std::vector< std::size_t > places
1-based node indices of the places, in level order.
std::vector< long long > m0
The initial marking the load vector was taken against.
std::vector< std::vector< long long > > S
S[i][p]: weight of place p in minimal-support invariant i.
std::vector< long long > V
V = S m0, the load vector.