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 * ONE LEVEL PER (PLACE, CLASS), place-major, as in the reference: the arcs
30 * are read per class off `TransitionParam::enabling / firing`, so a multiclass
31 * net gets P*R levels and the finest invariant basis, and `init` / `m0` / the
32 * columns of `S` are laid out as `(pp - 1) * R + k`. (This port used to
33 * class-sum the arcs onto P levels; a single-class net is unchanged.)
34 *
35 * ARC MULTIPLICITIES MUST BE INTEGRAL. A fractional arc has no Petri-net
36 * meaning and would make the gcd normalisation and the ILP-free convolution
37 * both wrong, so it is refused rather than rounded.
38 */
39
40#include <algorithm>
41#include <cmath>
42#include <cstddef>
43#include <map>
44#include <string>
45#include <vector>
46
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace spn {
55
56/**
57 * The invariant basis of a net, in PLACE-LEVEL coordinates: one level per
58 * (place, class) pair, place-major, level l = pp * nclasses + k for place pp
59 * and class k (both 0-based), exactly the `(pp - 1) * R + k` layout of the
60 * reference. A single-class net has one level per place.
61 */
63 /** 1-based node indices of the places, in level order. */
64 std::vector<std::size_t> places;
65 /** S[i][l]: weight of place level l in minimal-support invariant i. */
66 std::vector<std::vector<long long>> S;
67 /** V = S m0, the load vector. */
68 std::vector<long long> V;
69 /** The initial marking the load vector was taken against, per level. */
70 std::vector<long long> m0;
71 /** The class count R the levels are laid out with (a port-only helper). */
72 std::size_t nclasses = 1;
73};
74
75namespace detail {
76
77inline long long spn_gcd(long long a, long long b) {
78 a = a < 0 ? -a : a;
79 b = b < 0 ? -b : b;
80 while (b != 0) {
81 const long long t = a % b;
82 a = b;
83 b = t;
84 }
85 return a;
86}
87
88/** An arc multiplicity, refused unless integral. */
89inline long long spn_as_integer(double x, const char* what) {
90 const double r = std::floor(x + 0.5);
91 if (std::fabs(x - r) > 1e-9)
92 throw InputError(std::string("spn_sinvariants: ") + what +
93 " is not integral; a fractional arc multiplicity has no Petri-net "
94 "meaning and no invariant basis over the integers");
95 return static_cast<long long>(r);
96}
97
98/** True when the support of a is contained in the support of b. */
99inline bool spn_support_subset(const std::vector<long long>& a, const std::vector<long long>& b,
100 std::size_t off, std::size_t n) {
101 for (std::size_t k = 0; k < n; ++k)
102 if (a[off + k] != 0 && b[off + k] == 0) return false;
103 return true;
104}
105
106inline bool spn_support_equal(const std::vector<long long>& a, const std::vector<long long>& b,
107 std::size_t off, std::size_t n) {
108 for (std::size_t k = 0; k < n; ++k)
109 if ((a[off + k] != 0) != (b[off + k] != 0)) return false;
110 return true;
111}
112
113} // namespace detail
114
115/**
116 * Minimal-support S-invariants and the load vector of a net.
117 *
118 * @param sn a NetworkStruct holding Places and Transitions
119 * @param init initial marking per place level (P * R entries, place-major);
120 * empty takes it from the reference station of each closed class, as
121 * `spn_mdd` does
122 */
123template <class T>
125 const std::vector<double>& init = std::vector<double>()) {
126 std::vector<std::size_t> places, transitions;
127 for (std::size_t i = 1; i <= sn.nodes.size(); ++i) {
128 if (sn.nodes[i - 1].nodetype == lang::NodeType::Place) places.push_back(i);
129 else if (sn.nodes[i - 1].nodetype == lang::NodeType::Transition) transitions.push_back(i);
130 }
131 if (places.empty() || transitions.empty())
132 throw InputError("spn_sinvariants: the model holds no Place or no Transition node");
133 const std::size_t R = sn.nclasses;
134 const std::size_t P = places.size();
135 const std::size_t n = P * R; // one level per (place, class), place-major
136
137 // ---- incidence matrix C[l][mode] = post - pre, one column per (transition,
138 // mode) and one row per (place, class) level, read straight off the
139 // per-class arc matrices as the reference's reshape(param.enabling{m},
140 // nnodes, R) does
141 std::vector<std::vector<long long>> C(n);
142 std::size_t ncols = 0;
143 for (std::size_t t = 0; t < transitions.size(); ++t) {
144 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
145 sn.transparam.find(transitions[t]);
146 if (it == sn.transparam.end()) continue;
147 const qn::TransitionParam<T>& tp = it->second;
148 for (std::size_t m = 0; m < tp.nmodes; ++m) {
149 const Matrix<T>* en = m < tp.enabling.size() ? &tp.enabling[m] : nullptr;
150 const Matrix<T>* fi = m < tp.firing.size() ? &tp.firing[m] : nullptr;
151 for (std::size_t pp = 0; pp < P; ++pp) {
152 const std::size_t q = places[pp] - 1; // arcs are indexed by node
153 for (std::size_t k = 0; k < R; ++k) {
154 double pre = 0, post = 0;
155 if (en && q < en->rows() && k < en->cols())
156 pre = std::max(0.0, num_traits<T>::to_double((*en)(q, k)));
157 if (fi && q < fi->rows() && k < fi->cols())
158 post = std::max(0.0, num_traits<T>::to_double((*fi)(q, k)));
159 C[pp * R + k].push_back(detail::spn_as_integer(post, "a firing arc") -
160 detail::spn_as_integer(pre, "an enabling arc"));
161 }
162 }
163 ++ncols;
164 }
165 }
166
167 // ---- Farkas on [C | I]: row p starts as (C[p], e_p)
168 std::vector<std::vector<long long>> rows(n);
169 for (std::size_t p = 0; p < n; ++p) {
170 rows[p].assign(ncols + n, 0);
171 for (std::size_t c = 0; c < ncols; ++c) rows[p][c] = C[p][c];
172 rows[p][ncols + p] = 1;
173 }
174 for (std::size_t c = 0; c < ncols; ++c) {
175 std::vector<std::vector<long long>> next;
176 for (std::size_t r = 0; r < rows.size(); ++r)
177 if (rows[r][c] == 0) next.push_back(rows[r]);
178 for (std::size_t a = 0; a < rows.size(); ++a) {
179 if (rows[a][c] <= 0) continue;
180 for (std::size_t b = 0; b < rows.size(); ++b) {
181 if (rows[b][c] >= 0) continue;
182 const long long pa = rows[a][c], nb = -rows[b][c];
183 const long long d = detail::spn_gcd(pa, nb);
184 const long long fa = nb / d, fb = pa / d;
185 std::vector<long long> combo(ncols + n, 0);
186 long long g = 0;
187 for (std::size_t k = 0; k < combo.size(); ++k) {
188 combo[k] = fa * rows[a][k] + fb * rows[b][k];
189 g = detail::spn_gcd(g, combo[k]);
190 }
191 if (g > 1)
192 for (std::size_t k = 0; k < combo.size(); ++k) combo[k] /= g;
193 bool nonzero = false;
194 for (std::size_t k = 0; k < n; ++k) nonzero = nonzero || combo[ncols + k] != 0;
195 if (nonzero) next.push_back(combo);
196 }
197 }
198 // support-minimality filter, applied at every step so the row set cannot
199 // grow combinatorially on the way to the answer
200 std::vector<std::vector<long long>> keep;
201 for (std::size_t r = 0; r < next.size(); ++r) {
202 bool dominated = false;
203 for (std::size_t s = 0; s < next.size() && !dominated; ++s) {
204 if (s == r) continue;
205 if (!detail::spn_support_subset(next[s], next[r], ncols, n)) continue;
206 const bool same = detail::spn_support_equal(next[s], next[r], ncols, n);
207 if (!same || s < r) dominated = true; // keep the first of equal supports
208 }
209 if (!dominated) keep.push_back(next[r]);
210 }
211 rows = keep;
212 }
213
214 SpnInvariants out;
215 out.places = places;
216 for (std::size_t r = 0; r < rows.size(); ++r) {
217 bool nonneg = true;
218 for (std::size_t k = 0; k < n; ++k) nonneg = nonneg && rows[r][ncols + k] >= 0;
219 if (!nonneg) continue; // an S-invariant is non-negative by definition
220 std::vector<long long> y(n, 0);
221 for (std::size_t k = 0; k < n; ++k) y[k] = rows[r][ncols + k];
222 out.S.push_back(y);
223 }
224
225 // ---- initial marking and the load vector V = S m0
226 out.nclasses = R;
227 out.m0.assign(n, 0);
228 if (!init.empty()) {
229 if (init.size() != n)
230 throw InputError("spn_sinvariants: init must hold one token count per place level");
231 for (std::size_t l = 0; l < n; ++l)
232 out.m0[l] = detail::spn_as_integer(init[l], "an initial marking");
233 } else {
234 for (std::size_t k = 0; k < R; ++k) {
235 const double njobs = sn.classes[k].population;
236 if (!std::isfinite(njobs))
237 throw UnsupportedError(ErrorKind{"OpenClass"}, "spn_sinvariants: class " + std::to_string(k + 1) +
238 " is open, so the net has no finite load vector");
239 const std::size_t ref_node = sn.station_to_node[sn.classes[k].refstat - 1];
240 for (std::size_t pp = 0; pp < P; ++pp)
241 if (places[pp] == ref_node)
242 out.m0[pp * R + k] += detail::spn_as_integer(njobs, "a class population");
243 }
244 }
245 out.V.assign(out.S.size(), 0);
246 for (std::size_t i = 0; i < out.S.size(); ++i)
247 for (std::size_t p = 0; p < n; ++p) out.V[i] += out.S[i][p] * out.m0[p];
248 return out;
249}
250
251} // namespace spn
252} // namespace line
253
254#endif // LINE_API_SPN_SPN_SINVARIANTS_H
InputError(const std::string &what)
Definition error.h:105
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.
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 parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
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: one level per (place, class) pair,...
std::vector< std::size_t > places
1-based node indices of the places, in level order.
std::size_t nclasses
The class count R the levels are laid out with (a port-only helper).
std::vector< long long > m0
The initial marking the load vector was taken against, per level.
std::vector< std::vector< long long > > S
S[i][l]: weight of place level l in minimal-support invariant i.
std::vector< long long > V
V = S m0, the load vector.