LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_ispn.h
Go to the documentation of this file.
1/**
2 * @file solver_mam_ispn.h
3 * The 'ispn' method of SolverMAM: steady state of a stochastic Petri net with
4 * exactly one unbounded place, solved as a quasi-birth-death process by the
5 * matrix-geometric method (Haverkort 1998, ch. 17; spn_ispn).
6 *
7 * Port of matlab/src/solvers/MAM/solver_mam_ispn.m (Java twin
8 * jline.solvers.mam.analyzers.Solver_mam_ispn, Python twin
9 * line_solver.api.solvers.mam.ispn).
10 *
11 * The net is flattened to (place, class) levels, place-major as in spn_mdd, so
12 * level pp*R+k (0-based) is class k in place pp. An open class arriving from a
13 * Source to a Place becomes a timed transition with no input place, firing at
14 * the arrival rate times the routing probability; tokens a mode sends to a Sink
15 * are dropped. The unbounded level is MamOptions::ispn_place when set (a place
16 * name, or 'place.class' in a multiclass net), and otherwise the one level that
17 * no non-negative S-invariant covers.
18 *
19 * The measures are those SolverCTMC reports for a Petri net: Q(ist,k) is the
20 * mean number of class-k tokens in the place, U = Q (a Place is an INF
21 * station), Tp(ist,k) the token rate out of the place (the sum over the modes
22 * of their firing rate times the class-k input multiplicity), R = Q/Tp by
23 * Little's law. The Source row carries the arrival rate.
24 *
25 * spn_ispn works at double; another arithmetic is converted to double on the
26 * way in and back on the way out, since the matrix-geometric solve is a
27 * floating-point iteration (the R matrix has no closed form in general).
28 *
29 * Copyright (c) 2012-2026, Imperial College London
30 * All rights reserved.
31 */
32
33#ifndef LINE_SOLVERS_MAM_SOLVER_MAM_ISPN_H
34#define LINE_SOLVERS_MAM_SOLVER_MAM_ISPN_H
35
36#include <algorithm>
37#include <cmath>
38#include <limits>
39#include <map>
40#include <string>
41#include <vector>
42
47#include "line/util/error.h"
48
49namespace line {
50namespace mam {
51
52/** True when the model holds a Place node, i.e. it is a stochastic Petri net. */
53template <class T>
55 for (const qn::NodeDef& nd : L.nodes)
56 if (nd.nodetype == qn::NodeType::Place) return true;
57 return false;
58}
59
60/**
61 * Refuses the SolverMAM entry point ENTRY on a stochastic Petri net model with
62 * LINE:unsupported:Place. The ispn route returns steady-state averages only, so
63 * no state probability, response time distribution, transient or MAM result
64 * entry point can answer on a Place model. MATLAB solver_mam_ispn_refuse.m, Java
65 * SolverMAM.refusePlace and Python _refuse_place raise the same id and wording.
66 */
67template <class T>
68void ispn_refuse_entry(const qn::NetworkStruct<T>& L, const std::string& entry) {
69 if (ispn_has_place(L))
70 throw UnsupportedError(ErrorKind{"Place"},
71 "SolverMAM: " + entry + " has no stochastic Petri net analysis; the ispn method "
72 "reports steady-state averages only");
73}
74
75/**
76 * Why the iSPN route cannot take this model under METHOD, or "" when it can or
77 * when the model is not a net. 'ispn' needs a Place model; a Place model under
78 * 'default' or 'ispn' must be a pure net (Place, Transition, Source, Sink); any
79 * other method on a Place model is refused, since no queueing analyzer of
80 * SolverMAM reads a Place. Mirrors MATLAB SolverMAM.supportsModelMethod and
81 * Java SolverMAM.ispnRefusal.
82 */
83template <class T>
84std::string ispn_refusal(const qn::NetworkStruct<T>& L, const std::string& method) {
85 const bool hp = ispn_has_place(L);
86 if (method == "ispn" && !hp)
87 return "SolverMAM: the ispn method solves stochastic Petri nets and the model holds no "
88 "Place node. Use the 'default' method of SolverMAM for a queueing network.";
89 if (!hp) return std::string();
90 if (method != "default" && method != "ispn")
91 return "SolverMAM: the " + method + " method does not support Place nodes: on a "
92 "stochastic Petri net SolverMAM offers only the 'ispn' method, which 'default' "
93 "selects.";
94 for (const qn::NodeDef& nd : L.nodes)
95 if (nd.nodetype != qn::NodeType::Place && nd.nodetype != qn::NodeType::Transition &&
96 nd.nodetype != qn::NodeType::Source && nd.nodetype != qn::NodeType::Sink)
97 return "SolverMAM: node " + nd.name + " is neither a Place, a Transition, a Source "
98 "nor a Sink; the ispn method of SolverMAM solves pure stochastic Petri nets. "
99 "Use SolverCTMC for a mixed queueing and Petri net model.";
100 return std::string();
101}
102
103/** The analyzer. Returns the MvaSolution layout with actualmethod "ispn". */
104template <class T>
106 using Vec = std::vector<double>;
107 using Mat = std::vector<Vec>;
108 const double Inf = std::numeric_limits<double>::infinity();
109 auto D = [](const T& v) { return num_traits<T>::to_double(v); };
110 auto structure = [](const std::string& m) {
111 throw UnsupportedError(ErrorKind{"Structure"}, "solver_mam_ispn: " + m);
112 };
113
114 const std::size_t R = L.nclasses;
115 const std::size_t nn = L.nodes.size();
116 std::vector<std::size_t> places, transitions; // 1-based node indices
117 for (std::size_t i = 1; i <= nn; ++i) {
118 const qn::NodeType t = L.nodes[i - 1].nodetype;
119 if (t == qn::NodeType::Place) places.push_back(i);
120 else if (t == qn::NodeType::Transition) transitions.push_back(i);
121 else if (t != qn::NodeType::Source && t != qn::NodeType::Sink)
122 structure("node " + L.nodes[i - 1].name + " is neither a Place, a Transition, a "
123 "Source nor a Sink; the ispn method solves pure stochastic Petri nets");
124 }
125 if (places.empty())
126 throw InputError(ErrorKind{"Argument"},
127 "solver_mam_ispn: the ispn method needs a model holding Place nodes");
128 const std::size_t NP = places.size();
129 const std::size_t PL = NP * R;
130 auto lvl = [R](std::size_t pp, std::size_t k) { return pp * R + k; };
131 auto place_of = [&places](std::size_t node) -> long {
132 for (std::size_t pp = 0; pp < places.size(); ++pp)
133 if (places[pp] == node) return static_cast<long>(pp);
134 return -1;
135 };
136
137 // ---- modes of the transitions, one column each
138 Mat pre(PL), post(PL), inh(PL);
139 Vec rate, srv;
140 std::vector<bool> imm;
141 std::size_t ncol = 0;
142 auto add_col = [&]() {
143 for (std::size_t l = 0; l < PL; ++l) {
144 pre[l].push_back(0.0);
145 post[l].push_back(0.0);
146 inh[l].push_back(Inf);
147 }
148 return ncol++;
149 };
150 for (const std::size_t ind : transitions) {
151 const auto it = L.transparam.find(ind);
152 if (it == L.transparam.end()) continue;
153 const qn::TransitionParam<T>& tp = it->second;
154 const std::string& tname = L.nodes[ind - 1].name;
155 for (std::size_t m = 0; m < tp.nmodes; ++m) {
156 if (m < tp.firingdep.size() && tp.firingdep[m])
157 structure("mode " + std::to_string(m + 1) + " of transition " + tname +
158 " has a marking-dependent firing rate, which the ispn method does "
159 "not support");
160 const bool isimm = m < tp.timing.size() && tp.timing[m] == lang::TimingStrategy::IMMEDIATE;
161 double r = 1.0;
162 if (isimm) {
163 if (m < tp.fireweight.size()) r = D(tp.fireweight[m]);
164 } else {
165 bool expo = m < tp.firingproc.size() && !tp.firingproc[m].disabled;
166 if (expo) {
167 const mam::Map<T> proc = lang::dist_to_map(tp.firingproc[m]);
168 expo = proc.order() == 1;
169 if (expo) r = D(proc.D1(0, 0));
170 }
171 if (!expo)
172 throw UnsupportedError(ErrorKind{"NonExponential"},
173 "solver_mam_ispn: mode " + std::to_string(m + 1) + " of transition " +
174 tname + " has a non-exponential firing time; the ispn method needs "
175 "exponential or immediate transitions");
176 }
177 const Matrix<T>* en = m < tp.enabling.size() ? &tp.enabling[m] : nullptr;
178 const Matrix<T>* ih = m < tp.inhibiting.size() ? &tp.inhibiting[m] : nullptr;
179 const Matrix<T>* fi = m < tp.firing.size() ? &tp.firing[m] : nullptr;
180 const std::size_t c = add_col();
181 for (std::size_t pp = 0; pp < NP; ++pp) {
182 const std::size_t q = places[pp] - 1; // arcs are indexed by node
183 for (std::size_t k = 0; k < R; ++k) {
184 const std::size_t l = lvl(pp, k);
185 if (en && q < en->rows() && k < en->cols()) pre[l][c] = std::max(0.0, D((*en)(q, k)));
186 if (fi && q < fi->rows() && k < fi->cols()) post[l][c] = std::max(0.0, D((*fi)(q, k)));
187 if (ih && q < ih->rows() && k < ih->cols()) {
188 const double v = D((*ih)(q, k));
189 // a zero threshold is the "no arc" filler of older models
190 inh[l][c] = (v <= 0 || std::isnan(v)) ? Inf : v;
191 }
192 }
193 }
194 double ns = m < tp.nmodeservers.size() ? tp.nmodeservers[m] : 1.0;
195 if (ns >= 1e9) ns = Inf; // GlobalConstants.MaxInt stands for infinite servers
196 rate.push_back(r);
197 imm.push_back(isimm);
198 srv.push_back(ns);
199 }
200 }
201 const std::size_t nmodes = ncol;
202
203 // ---- arrivals from Sources, as transitions with no input place
204 Vec lambda(R, 0.0);
205 std::vector<std::size_t> srcst(R, 0); // 1-based station, 0 = none
206 for (std::size_t ind = 1; ind <= nn; ++ind) {
207 if (L.nodes[ind - 1].nodetype != qn::NodeType::Source) continue;
208 const std::size_t ist = L.nodes[ind - 1].station;
209 if (ist == 0) continue;
210 for (std::size_t k = 0; k < R; ++k) {
211 if (L.disabled[ist - 1][k]) continue;
212 const double lk = D(L.rates(ist - 1, k));
213 if (std::isnan(lk) || lk <= 0) continue;
214 if (L.procid(ist, k + 1) != lang::ProcessType::EXP)
215 throw UnsupportedError(ErrorKind{"NonExponential"},
216 "solver_mam_ispn: class " + L.classes[k].name + " arrives at the Source with "
217 "a non-exponential process; the ispn method needs Poisson arrivals");
218 lambda[k] += lk;
219 srcst[k] = ist;
220 for (std::size_t jnd = 1; jnd <= nn; ++jnd)
221 for (std::size_t r = 0; r < R; ++r) {
222 const std::size_t row = (ind - 1) * R + k, col = (jnd - 1) * R + r;
223 if (row >= L.rtnodes.rows() || col >= L.rtnodes.cols()) continue;
224 const double pr = D(L.rtnodes(row, col));
225 if (!(pr > 0)) continue;
226 const long pp = place_of(jnd);
227 if (pp < 0)
228 structure("the Source routes class " + L.classes[k].name + " to " +
229 L.nodes[jnd - 1].name + ", which is not a Place");
230 const std::size_t c = add_col();
231 post[lvl(static_cast<std::size_t>(pp), r)][c] = 1.0;
232 rate.push_back(lk * pr);
233 imm.push_back(false);
234 srv.push_back(1.0);
235 }
236 }
237 }
238
239 // ---- initial marking: the declared place state, else njobs at the reference station
240 Vec m0(PL, 0.0);
241 bool anyset = false;
242 for (std::size_t pp = 0; pp < NP; ++pp) {
243 const auto it = L.initmarking.find(places[pp]);
244 if (it == L.initmarking.end()) continue;
245 for (std::size_t k = 0; k < R && k < it->second.size(); ++k) {
246 const double v = D(it->second[k]);
247 m0[lvl(pp, k)] = v;
248 anyset = anyset || v != 0;
249 }
250 }
251 if (!anyset) {
252 std::fill(m0.begin(), m0.end(), 0.0);
253 for (std::size_t k = 0; k < R; ++k) {
254 const double nj = L.classes[k].population;
255 if (!std::isfinite(nj) || nj <= 0) continue;
256 const std::size_t rs = L.classes[k].refstat;
257 if (rs == 0 || rs > L.nstations) continue;
258 for (std::size_t pp = 0; pp < NP; ++pp)
259 if (L.nodes[places[pp] - 1].station == rs) {
260 m0[lvl(pp, k)] = nj;
261 break;
262 }
263 }
264 }
265
266 // ---- level names and the unbounded level
267 std::vector<std::string> lname(PL);
268 for (std::size_t pp = 0; pp < NP; ++pp)
269 for (std::size_t k = 0; k < R; ++k)
270 lname[lvl(pp, k)] = R == 1 ? L.nodes[places[pp] - 1].name
271 : L.nodes[places[pp] - 1].name + "." + L.classes[k].name;
272 auto join = [&lname](const std::vector<std::size_t>& ix) {
273 std::string s;
274 for (std::size_t i = 0; i < ix.size(); ++i) s += (i ? ", " : "") + lname[ix[i]];
275 return s;
276 };
277 auto used_level = [&](std::size_t l) {
278 for (std::size_t c = 0; c < ncol; ++c)
279 if (pre[l][c] != 0 || post[l][c] != 0) return true;
280 return false;
281 };
282 std::size_t p0 = PL;
283 if (!opt.ispn_place.empty()) {
284 const std::string& want = opt.ispn_place;
285 for (std::size_t l = 0; l < PL && p0 == PL; ++l)
286 if (lname[l] == want) p0 = l;
287 if (p0 == PL && R > 1) {
288 for (std::size_t pp = 0; pp < NP; ++pp) {
289 if (L.nodes[places[pp] - 1].name != want) continue;
290 std::vector<std::size_t> used;
291 for (std::size_t k = 0; k < R; ++k)
292 if (used_level(lvl(pp, k))) used.push_back(lvl(pp, k));
293 if (used.size() == 1) p0 = used[0];
294 break;
295 }
296 }
297 if (p0 == PL) {
298 std::vector<std::size_t> all(PL);
299 for (std::size_t l = 0; l < PL; ++l) all[l] = l;
300 throw InputError(ErrorKind{"Argument"},
301 "solver_mam_ispn: options.config.ispn_place = '" + want + "' names no place "
302 "level; the levels are " + join(all));
303 }
304 } else {
305 Mat C(PL, Vec(ncol, 0.0));
306 for (std::size_t l = 0; l < PL; ++l)
307 for (std::size_t c = 0; c < ncol; ++c) C[l][c] = post[l][c] - pre[l][c];
308 const std::vector<bool> covered = spn::spn_ispn_covered(C);
309 std::vector<std::size_t> cand;
310 for (std::size_t l = 0; l < PL; ++l)
311 if (!covered[l] && (used_level(l) || m0[l] > 0)) cand.push_back(l);
312 if (cand.empty())
313 structure("every place is covered by a non-negative S-invariant, so the net is "
314 "bounded and has a finite state space; solve it with SolverCTMC");
315 if (cand.size() > 1)
316 structure("the iSPN needs exactly one unbounded place, but " +
317 std::to_string(cand.size()) + " places are not covered by an S-invariant (" +
318 join(cand) + "); name the unbounded one with options.config.ispn_place");
319 p0 = cand[0];
320 }
321
322 // ---- solve
323 spn::IspnOptions iopt;
324 if (opt.ispn_maxlevel > 0) iopt.maxlevel = static_cast<int>(opt.ispn_maxlevel);
325 const spn::IspnResult res = spn::spn_ispn(pre, post, inh, rate, imm, srv, m0, p0, iopt);
326
327 // ---- pack the analyzer contract
328 const std::size_t M = L.nstations;
329 const T zero = num_traits<T>::from_int(0);
330 auto F = [](double v) { return num_traits<T>::from_double(v); };
331 MamSolution<T> out;
332 mva::MvaSolution<T>& s = out.sol;
333 s.Q = Matrix<T>(M, R, zero);
334 s.U = Matrix<T>(M, R, zero);
335 s.R = Matrix<T>(M, R, zero);
336 s.Tp = Matrix<T>(M, R, zero);
337 Matrix<double> Q(M, R, 0.0), Tp(M, R, 0.0);
338 for (std::size_t pp = 0; pp < NP; ++pp) {
339 const std::size_t ist = L.nodes[places[pp] - 1].station;
340 if (ist == 0) continue;
341 for (std::size_t k = 0; k < R; ++k) {
342 const std::size_t l = lvl(pp, k);
343 double tk = 0;
344 for (std::size_t c = 0; c < nmodes && c < res.tput.size(); ++c) tk += res.tput[c] * pre[l][c];
345 Q(ist - 1, k) = res.meanmarking[l];
346 Tp(ist - 1, k) = tk;
347 }
348 }
349 Vec X(R, 0.0), Cv(R, 0.0);
350 for (std::size_t k = 0; k < R; ++k) {
351 if (srcst[k] > 0) {
352 Tp(srcst[k] - 1, k) = lambda[k];
353 X[k] = lambda[k];
354 } else {
355 const std::size_t rs = L.classes[k].refstat;
356 if (rs >= 1 && rs <= M) X[k] = Tp(rs - 1, k);
357 }
358 double qs = 0;
359 for (std::size_t i = 0; i < M; ++i) qs += Q(i, k);
360 if (X[k] > 0) Cv[k] = qs / X[k];
361 }
362 for (std::size_t i = 0; i < M; ++i)
363 for (std::size_t k = 0; k < R; ++k) {
364 s.Q(i, k) = F(Q(i, k));
365 s.U(i, k) = F(Q(i, k));
366 s.Tp(i, k) = F(Tp(i, k));
367 if (Tp(i, k) > 0) s.R(i, k) = F(Q(i, k) / Tp(i, k));
368 }
369 s.X.assign(R, zero);
370 s.C.assign(R, zero);
371 for (std::size_t k = 0; k < R; ++k) {
372 s.X[k] = F(X[k]);
373 s.C[k] = F(Cv[k]);
374 }
375 s.iter = 1;
376 s.method = "ispn";
377 out.actualmethod = "ispn";
378 return out;
379}
380
381} // namespace mam
382} // namespace line
383
384#endif // LINE_SOLVERS_MAM_SOLVER_MAM_ISPN_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
std::map< std::size_t, TransitionParam< T > > transparam
Transition (SPN) parameters, keyed by 1-based node index.
std::vector< std::vector< bool > > disabled
Matrix< double > procid() const
sn.procid (M x K): the ProcessType id of each (station, class) pair, as refreshProcessTypes....
std::vector< JobClass > classes
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< NodeDef > nodes
every node, in creation order
std::map< std::size_t, std::vector< T > > initmarking
The DECLARED initial state of a stateful node, by 1-based node index.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
The option and result types SolverMAM shares with its analyzers.
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:378
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:339
void ispn_refuse_entry(const qn::NetworkStruct< T > &L, const std::string &entry)
Refuses the SolverMAM entry point ENTRY on a stochastic Petri net model with LINE:unsupported:Place.
bool ispn_has_place(const qn::NetworkStruct< T > &L)
True when the model holds a Place node, i.e.
std::string ispn_refusal(const qn::NetworkStruct< T > &L, const std::string &method)
Why the iSPN route cannot take this model under METHOD, or "" when it can or when the model is not a ...
MamSolution< T > solver_mam_ispn(const qn::NetworkStruct< T > &L, const MamOptions &opt)
The analyzer.
std::vector< bool > spn_ispn_covered(const std::vector< std::vector< double > > &C)
Places covered by a non-negative S-invariant (P-semiflow) y' C = 0, by Farkas' algorithm on [C | I] k...
Definition spn_ispn.h:579
IspnResult spn_ispn(const std::vector< std::vector< double > > &pre, const std::vector< std::vector< double > > &post, std::vector< std::vector< double > > inh, const std::vector< double > &rate, std::vector< bool > imm, std::vector< double > srv, const std::vector< double > &m0, std::size_t p0, const IspnOptions &opt=IspnOptions())
Steady state of an infinite-state SPN.
Definition spn_ispn.h:631
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Infinite-state stochastic Petri nets (iSPN) by the matrix-geometric method.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The options SolverMAM reads.
Definition mam_types.h:30
What the MAM dispatch returns: the metrics plus the algorithm that ran.
Definition mam_types.h:188
std::string actualmethod
The concrete algorithm, as the reference's actualmethod.
Definition mam_types.h:191
mva::MvaSolution< T > sol
Definition mam_types.h:189
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
std::size_t order() const
Definition map_moment.h:57
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:97
std::vector< T > X
Definition mva_types.h:99
std::vector< T > C
Definition mva_types.h:99
A node of the network.
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< T > fireweight
weight among simultaneously enabled modes
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).
Options of spn_ispn.
Definition spn_ispn.h:68
int maxlevel
largest explored window before the net is refused
Definition spn_ispn.h:70
Result of spn_ispn; fields as in the MATLAB help.
Definition spn_ispn.h:80
std::vector< double > meanmarking
Definition spn_ispn.h:88
std::vector< double > tput
Definition spn_ispn.h:93