LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_petri_terms.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_SOLVERS_FLUID_PETRI_TERMS_H
6#define LINE_SOLVERS_FLUID_PETRI_TERMS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Event-based representation of the fluid marking process of a stochastic Petri
12 * net. Port of `matlab/src/solvers/FLD/fluid_petri_terms.m`, cross-checked
13 * against `jar/src/main/java/jline/solvers/fluid/petri/PetriTerms.java`.
14 *
15 * A GSPN is already a density-dependent Markov population process, which is the
16 * object the moment-closure family of SolverFLD is built on: the marking is the
17 * population, a transition mode is a reaction, its incidence column is the jump,
18 * and the rate law `lambda*min(enabling degree, servers)` is the same min()
19 * non-linearity the min-normal closure exists to smooth. Nothing about the
20 * closure changes here; only where the drift comes from.
21 *
22 * dx/dt = D * r(x, Sigma, phi, mu)
23 *
24 * THE STATE, x = [ m ; y ].
25 *
26 * m(p,k) token mass of class k at place p. One coordinate per (place, class)
27 * pair some arc touches, the initial marking loads, or a Source feeds;
28 * a pair nothing reaches is dropped rather than carried as a null
29 * direction of the Newton system.
30 * y(j,h) the number of mode-j servers running in phase h, for a mode whose
31 * firing time has more than one phase. Their SUM is not free: the
32 * ENABLE synchronization latches it instantaneously to
33 * min(enabling degree, servers), so the latch is an ALGEBRAIC row with
34 * one free-sign unknown mu_j and the phase split evolves
35 * differentially. CARRYING THE DISTRIBUTION INSTEAD OF THE COUNT LOOKS
36 * TIDIER AND IS WRONG: the resulting equation is missing a term and
37 * agrees with the count form only AT a fixed point.
38 *
39 * There is no Source coordinate: an exogenous arrival is a CONSTANT-propensity
40 * event depositing one token, as it is in the NRM SPN runner. There is no Sink
41 * coordinate either: a firing arc into a sink is mass leaving the net.
42 *
43 * THE EVENTS, one column of D each: kind 1 a firing of mode j out of phase h
44 * into phase h'; kind 2 an internal phase change; kind 3 an exogenous arrival;
45 * kind 4 a firing of an IMMEDIATE mode, at the algebraic flow phi_j; kind 5 the
46 * server latch of a multi-phase mode, at the free-sign unknown mu_j.
47 *
48 * Arithmetic: DOUBLE ONLY. The closures evaluate the normal CDF, which is not an
49 * element of the field generated by the inputs under any arithmetic, so the
50 * templated form this tree uses elsewhere would buy nothing.
51 */
52
53#include <algorithm>
54#include <cmath>
55#include <cstddef>
56#include <functional>
57#include <limits>
58#include <map>
59#include <string>
60#include <vector>
61
63#include "line/util/error.h"
64#include "line/util/matrix.h"
65
66namespace line {
67namespace fluid {
68namespace petri {
69
70/** GlobalConstants.FineTol, the reference's own "effectively zero". */
71inline double petri_fine_tol() { return 1e-8; }
72
73/** One firing mode of one transition, with its arcs and its firing process. */
74struct PetriMode {
75 std::size_t node = 0; ///< 1-based node index of the transition
76 std::size_t mode = 0; ///< mode index within it, 0-based
78 std::string label;
79
80 /** Input arcs, as state coordinates and their multiplicities. */
81 std::vector<std::size_t> arc_slot;
82 std::vector<double> arc_w;
83 /** Inhibitor arcs, as state coordinates and their thresholds. */
84 std::vector<std::size_t> inh_slot;
85 std::vector<double> inh_thr;
86
87 double c = 1.0; ///< servers of this mode; infinite for none
88 std::size_t nph = 1; ///< phases of the firing process; 0 for an immediate mode
90 std::vector<double> d1; ///< row sums of D1, the completion rate out of each phase
91 std::vector<double> pie; ///< entry distribution over the phases
92 /** Marking-dependent firing multiplier; empty for the unit one. */
93 std::function<double(const Matrix<double>&)> dep;
94 int prio = 1;
95 double weight = 1.0;
96 /** The mode's incidence column, over the whole state. */
97 std::vector<double> cvec;
98 /** The phase coordinates of a multi-phase mode; empty otherwise. */
99 std::vector<std::size_t> zblk;
100 /**
101 * Whether the enabling degree is worth closing over.
102 *
103 * A single input arc under an infinite server count makes min() the identity,
104 * so the closure would smooth nothing and the covariance entries it reads
105 * would be carried for no reason.
106 */
107 bool closable = false;
108};
109
110/** The assembled drift terms of a net. */
112 std::size_t I = 0; ///< nodes
113 std::size_t K = 0; ///< classes
114 std::size_t M = 0; ///< stations
115 std::vector<std::size_t> places, transitions; ///< 1-based node indices
116 std::vector<std::string> names_node; ///< 0-based, one per node
117
118 std::size_t nstate = 0; ///< marking coordinates plus phase coordinates
119 std::size_t nm = 0; ///< marking coordinates alone
120
121 /** pidx(p,k) is the state coordinate of (0-based node p, class k), or npos. */
122 std::vector<std::vector<std::ptrdiff_t>> pidx;
123 std::vector<std::size_t> coord_node, coord_class;
124 std::vector<std::ptrdiff_t> coord_station;
125
126 std::vector<PetriMode> modes;
127 std::vector<std::size_t> timed_idx, imm_idx;
128
129 Matrix<double> D; ///< (nstate x nev) incidence
130 std::vector<double> rate_base;
131 std::size_t nev = 0;
132 std::vector<int> ev_kind, ev_mode, ev_phase, ev_to;
133 std::vector<std::ptrdiff_t> ev_station, ev_class;
134
135 std::vector<std::size_t> imm_col, latch_col, stoch_col;
136 std::vector<std::size_t> latch_mode;
137
138 /** The Sigma entries the closure reads, and the map back to them. */
139 std::vector<std::pair<std::size_t, std::size_t>> cov_pairs;
140 std::size_t npair = 0;
141 std::vector<std::vector<std::ptrdiff_t>> pair_index;
142 std::vector<std::size_t> cov_idx;
143
144 /** (station*K + class) -> the events that take tokens out of / into it. */
145 std::map<std::size_t, std::vector<std::size_t>> consumers, producers;
146 std::map<std::size_t, std::vector<double>> consumer_w;
147
148 std::vector<double> x0;
149 std::vector<std::vector<double>> m0full;
150};
151
152namespace terms_detail {
153
154/** An (I x K) parameter matrix padded to full size with `fill`. */
155template <class T>
156inline std::vector<std::vector<double>> pad(const Matrix<T>& A, std::size_t I, std::size_t K,
157 double fill) {
158 std::vector<std::vector<double>> out(I, std::vector<double>(K, fill));
159 for (std::size_t i = 0; i < I && i < A.rows(); ++i)
160 for (std::size_t k = 0; k < K && k < A.cols(); ++k)
161 out[i][k] = num_traits<T>::to_double(A(i, k));
162 return out;
163}
164
165/** Record a covariance pair once, in ascending order. */
166inline void add_pair(std::map<std::pair<std::size_t, std::size_t>, std::size_t>& key,
167 std::vector<std::pair<std::size_t, std::size_t>>& pairs, std::size_t a,
168 std::size_t b) {
169 const std::pair<std::size_t, std::size_t> k(std::min(a, b), std::max(a, b));
170 if (key.find(k) != key.end()) return;
171 key[k] = pairs.size();
172 pairs.push_back(k);
173}
174
175/**
176 * One mode's arcs, incidence column and firing process.
177 *
178 * `nm` is the marking width at the time of the call; the column is GROWN once
179 * the phase coordinates are appended and the total state width is known.
180 */
181template <class T>
182inline PetriMode build_mode(const qn::NetworkStruct<T>& sn, const qn::TransitionParam<T>& tp,
183 std::size_t ind, std::size_t m, PetriTerms& t, std::size_t nm) {
184 PetriMode rec;
185 rec.node = ind;
186 rec.mode = m;
187 rec.timing = (m < tp.timing.size()) ? tp.timing[m] : lang::TimingStrategy::TIMED;
188 const std::string mname = (m < tp.modenames.size() && !tp.modenames[m].empty())
189 ? tp.modenames[m]
190 : ("Mode" + std::to_string(m + 1));
191 rec.label = t.names_node[ind - 1] + "." + mname;
192
193 const std::vector<std::vector<double>> en = pad(tp.enabling[m], t.I, t.K, 0.0);
194 const std::vector<std::vector<double>> fir = pad(tp.firing[m], t.I, t.K, 0.0);
195 const std::vector<std::vector<double>> inh =
196 pad(tp.inhibiting[m], t.I, t.K, std::numeric_limits<double>::infinity());
197
198 std::vector<double> cvec(nm, 0.0);
199 for (std::size_t p = 0; p < t.I; ++p)
200 for (std::size_t k = 0; k < t.K; ++k) {
201 const double w = en[p][k];
202 if (!(w > 0)) continue;
203 if (std::isinf(w))
204 throw InputError("fluid_petri_terms: mode " + rec.label +
205 " has a non-finite enabling arc weight at " + t.names_node[p] +
206 ". An arc that no marking can satisfy disables the mode; declare "
207 "a finite multiplicity");
208 if (t.pidx[p][k] < 0)
209 throw InputError("fluid_petri_terms: mode " + rec.label +
210 " takes an enabling arc from " + t.names_node[p] +
211 ", which is not a Place");
212 const std::size_t s = static_cast<std::size_t>(t.pidx[p][k]);
213 rec.arc_slot.push_back(s);
214 rec.arc_w.push_back(w);
215 cvec[s] -= w;
216 }
217 for (std::size_t p = 0; p < t.I; ++p)
218 for (std::size_t k = 0; k < t.K; ++k) {
219 // A NEGATIVE ENTRY MARKS AN INPUT PLACE, whose token the enabling arc
220 // already removed, and a firing arc into a Sink has no coordinate:
221 // that is mass leaving the net, not mass lost by accident.
222 if (fir[p][k] <= 0 || t.pidx[p][k] < 0) continue;
223 cvec[static_cast<std::size_t>(t.pidx[p][k])] += fir[p][k];
224 }
225 for (std::size_t p = 0; p < t.I; ++p)
226 for (std::size_t k = 0; k < t.K; ++k) {
227 const double th = inh[p][k];
228 // JMT writes a MISSING inhibitor arc as 0, as -1 or as Inf, never as
229 // a threshold, so none of those three is one.
230 if (std::isinf(th) || th <= 0 || t.pidx[p][k] < 0) continue;
231 rec.inh_slot.push_back(static_cast<std::size_t>(t.pidx[p][k]));
232 rec.inh_thr.push_back(th);
233 }
234 rec.cvec = cvec;
235
236 const double c = (m < tp.nmodeservers.size()) ? tp.nmodeservers[m] : 1.0;
237 rec.c = std::isnan(c) ? 1.0 : c;
238 rec.prio = (m < tp.firingprio.size()) ? static_cast<int>(tp.firingprio[m]) : 1;
239 rec.weight =
240 (m < tp.fireweight.size()) ? num_traits<T>::to_double(tp.fireweight[m]) : 1.0;
241 if (m < tp.firingdep.size() && tp.firingdep[m]) {
242 const std::function<T(const std::vector<T>&)> g = tp.firingdep[m];
243 const std::size_t I = t.I, K = t.K;
244 rec.dep = [g, I, K](const Matrix<double>& mm) -> double {
245 std::vector<T> flat(I * K, num_traits<T>::from_int(0));
246 for (std::size_t i = 0; i < I; ++i)
247 for (std::size_t k = 0; k < K; ++k)
248 flat[i * K + k] = num_traits<T>::from_double(mm(i, k));
249 return num_traits<T>::to_double(g(flat));
250 };
251 }
252
253 if (rec.timing == lang::TimingStrategy::IMMEDIATE) {
254 // An immediate mode carries no firing process: its flow is an algebraic
255 // unknown of the DAE, not a rate.
256 rec.nph = 0;
257 rec.d1.assign(1, 0.0);
258 rec.pie.assign(1, 1.0);
259 rec.closable = false;
260 return rec;
261 }
262
263 if (m >= tp.firingproc.size() || tp.firingproc[m].disabled)
264 throw InputError("fluid_petri_terms: mode " + rec.label +
265 " has no Markovian firing process. The renewal families are converted "
266 "to phase type before the solver runs, so this is a distribution the "
267 "fluid Petri route cannot time; use SolverCTMC or SolverLDES");
268 const lang::Distrib<T>& fp = tp.firingproc[m];
269 Matrix<double> D0, D1;
270 if (fp.D0.rows() > 0 && fp.D1.rows() > 0) {
271 D0 = Matrix<double>(fp.D0.rows(), fp.D0.cols(), 0.0);
272 for (std::size_t i = 0; i < D0.rows(); ++i)
273 for (std::size_t j = 0; j < D0.cols(); ++j)
274 D0(i, j) = num_traits<T>::to_double(fp.D0(i, j));
275 D1 = Matrix<double>(fp.D1.rows(), fp.D1.cols(), 0.0);
276 for (std::size_t i = 0; i < D1.rows(); ++i)
277 for (std::size_t j = 0; j < D1.cols(); ++j)
278 D1(i, j) = num_traits<T>::to_double(fp.D1(i, j));
279 } else {
280 // An exponential law carried by its mean alone: the one-phase (D0, D1).
281 const double mean = num_traits<T>::to_double(fp.mean);
282 if (!(mean > 0.0))
283 throw InputError("fluid_petri_terms: mode " + rec.label +
284 " has a non-positive mean firing time");
285 D0 = Matrix<double>(1, 1, -1.0 / mean);
286 D1 = Matrix<double>(1, 1, 1.0 / mean);
287 }
288 rec.nph = D0.rows();
289 rec.D0 = D0;
290 rec.D1 = D1;
291 rec.d1.assign(rec.nph, 0.0);
292 for (std::size_t h = 0; h < rec.nph; ++h) {
293 double acc = 0.0;
294 for (std::size_t hp = 0; hp < D1.cols(); ++hp) acc += D1(h, hp);
295 rec.d1[h] = acc;
296 }
297 rec.pie.assign(std::max<std::size_t>(rec.nph, 1), 0.0);
298 if (rec.nph > 1) {
299 double tot = 0.0;
300 for (std::size_t h = 0; h < rec.nph && h < fp.params.size(); ++h) {
301 rec.pie[h] = num_traits<T>::to_double(fp.params[h]);
302 tot += rec.pie[h];
303 }
304 if (!(tot > 0.0)) {
305 std::fill(rec.pie.begin(), rec.pie.end(), 0.0);
306 rec.pie[0] = 1.0;
307 } else {
308 for (std::size_t h = 0; h < rec.nph; ++h) rec.pie[h] /= tot;
309 }
310 } else {
311 rec.pie[0] = 1.0;
312 }
313 rec.closable = !(rec.arc_slot.size() <= 1 && std::isinf(rec.c));
314 return rec;
315}
316
317} // namespace terms_detail
318
319/**
320 * Assemble the drift terms of a net.
321 *
322 * @param sn a model whose nodes are Places, Transitions, Sources and Sinks only
323 */
324template <class T>
326 PetriTerms t;
327 t.I = sn.nodes.size();
328 t.K = sn.nclasses;
329 t.M = sn.nstations;
330 t.names_node.resize(t.I);
331 for (std::size_t i = 0; i < t.I; ++i) t.names_node[i] = sn.nodes[i].name;
332 for (std::size_t i = 1; i <= t.I; ++i) {
333 if (sn.nodes[i - 1].nodetype == lang::NodeType::Place) t.places.push_back(i);
334 else if (sn.nodes[i - 1].nodetype == lang::NodeType::Transition) t.transitions.push_back(i);
335 }
336
337 // ---- the initial marking ------------------------------------------------
338 // A Place's DECLARED marking wins; absent one, each closed class loads its
339 // reference station, which is the rule spn_sinvariants and the exact SPN
340 // engines use. An infinite marking is refused rather than integrated.
341 t.m0full.assign(t.I, std::vector<double>(t.K, 0.0));
342 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
343 const std::size_t ind = t.places[pi];
344 const typename std::map<std::size_t, std::vector<T>>::const_iterator im =
345 sn.initmarking.find(ind);
346 if (im == sn.initmarking.end()) continue;
347 for (std::size_t k = 0; k < t.K && k < im->second.size(); ++k) {
348 const double v = num_traits<T>::to_double(im->second[k]);
349 if (std::isinf(v))
350 throw InputError("fluid_petri_terms: place " + t.names_node[ind - 1] +
351 " holds an infinite initial marking of class " +
352 std::to_string(k + 1));
353 t.m0full[ind - 1][k] = v;
354 }
355 }
356 bool any_declared = false;
357 for (std::size_t pi = 0; pi < t.places.size() && !any_declared; ++pi)
358 any_declared = (sn.initmarking.find(t.places[pi]) != sn.initmarking.end());
359 if (!any_declared) {
360 for (std::size_t r = 0; r < sn.classes.size(); ++r) {
361 const double njobs = num_traits<T>::to_double(sn.classes[r].population);
362 if (!(njobs > 0.0) || !std::isfinite(njobs)) continue;
363 if (sn.classes[r].refstat < 1 || sn.classes[r].refstat > sn.station_to_node.size())
364 continue;
365 const std::size_t ref_node = sn.station_to_node[sn.classes[r].refstat - 1];
366 if (ref_node >= 1 && ref_node <= t.I &&
367 sn.nodes[ref_node - 1].nodetype == lang::NodeType::Place)
368 t.m0full[ref_node - 1][r] += njobs;
369 }
370 }
371
372 // ---- which (place, class) pairs carry a coordinate ----------------------
373 std::vector<std::vector<bool>> touched(t.I, std::vector<bool>(t.K, false));
374 for (std::size_t ti = 0; ti < t.transitions.size(); ++ti) {
375 const std::size_t ind = t.transitions[ti];
376 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
377 sn.transparam.find(ind);
378 if (it == sn.transparam.end()) continue;
379 const qn::TransitionParam<T>& tp = it->second;
380 for (std::size_t m = 0; m < tp.nmodes; ++m) {
381 const std::vector<std::vector<double>> en =
382 terms_detail::pad(tp.enabling[m], t.I, t.K, 0.0);
383 const std::vector<std::vector<double>> fir =
384 terms_detail::pad(tp.firing[m], t.I, t.K, 0.0);
385 const std::vector<std::vector<double>> inh = terms_detail::pad(
386 tp.inhibiting[m], t.I, t.K, std::numeric_limits<double>::infinity());
387 for (std::size_t i = 0; i < t.I; ++i)
388 for (std::size_t k = 0; k < t.K; ++k)
389 if (en[i][k] > 0 || fir[i][k] != 0 ||
390 (!std::isinf(inh[i][k]) && inh[i][k] > 0))
391 touched[i][k] = true;
392 }
393 }
394
395 // An arrival makes its target a coordinate even when no arc mentions it.
396 struct SrcArr {
397 std::size_t snd, r, qnd, l;
398 };
399 std::vector<SrcArr> src_arr;
400 for (std::size_t ind = 1; ind <= t.I; ++ind) {
401 if (sn.nodes[ind - 1].nodetype != lang::NodeType::Source) continue;
402 const std::size_t ist = sn.nodes[ind - 1].station;
403 if (ist < 1) continue;
404 for (std::size_t r = 0; r < t.K; ++r) {
405 const double lambda = num_traits<T>::to_double(sn.rates(ist - 1, r));
406 if (std::isnan(lambda) || lambda <= 0) continue;
407 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
408 const std::size_t jnd = t.places[pi];
409 for (std::size_t s = 0; s < t.K; ++s) {
410 const double p = num_traits<T>::to_double(
411 sn.rtnodes((ind - 1) * t.K + r, (jnd - 1) * t.K + s));
412 if (p > 0) {
413 touched[jnd - 1][s] = true;
414 SrcArr a;
415 a.snd = ind;
416 a.r = r;
417 a.qnd = jnd;
418 a.l = s;
419 src_arr.push_back(a);
420 }
421 }
422 }
423 }
424 }
425
426 t.pidx.assign(t.I, std::vector<std::ptrdiff_t>(t.K, -1));
427 std::size_t nm = 0;
428 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
429 const std::size_t ind = t.places[pi];
430 for (std::size_t k = 0; k < t.K; ++k) {
431 if (!(touched[ind - 1][k] || t.m0full[ind - 1][k] > 0)) continue;
432 t.pidx[ind - 1][k] = static_cast<std::ptrdiff_t>(nm);
433 t.coord_node.push_back(ind - 1);
434 t.coord_class.push_back(k);
435 t.coord_station.push_back(static_cast<std::ptrdiff_t>(sn.nodes[ind - 1].station) - 1);
436 ++nm;
437 }
438 }
439 t.nm = nm;
440
441 // ---- the modes, and the phase coordinates of the multi-phase ones -------
442 std::size_t nstate = nm;
443 for (std::size_t ti = 0; ti < t.transitions.size(); ++ti) {
444 const std::size_t ind = t.transitions[ti];
445 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
446 sn.transparam.find(ind);
447 if (it == sn.transparam.end()) continue;
448 for (std::size_t m = 0; m < it->second.nmodes; ++m) {
449 PetriMode rec = terms_detail::build_mode(sn, it->second, ind, m, t, nm);
450 if (rec.nph > 1) {
451 rec.zblk.resize(rec.nph);
452 for (std::size_t h = 0; h < rec.nph; ++h) rec.zblk[h] = nstate + h;
453 nstate += rec.nph;
454 }
455 t.modes.push_back(rec);
456 }
457 }
458 t.nstate = nstate;
459 // The phase coordinates are appended after every marking coordinate, so a
460 // jump column built at nm width has to be GROWN once the total is known.
461 for (std::size_t j = 0; j < t.modes.size(); ++j)
462 if (t.modes[j].cvec.size() < nstate) t.modes[j].cvec.resize(nstate, 0.0);
463 for (std::size_t j = 0; j < t.modes.size(); ++j) {
464 if (t.modes[j].timing == lang::TimingStrategy::TIMED) t.timed_idx.push_back(j);
465 else t.imm_idx.push_back(j);
466 }
467
468 // ---- the event columns --------------------------------------------------
469 std::vector<std::vector<double>> cols;
470 std::vector<double> rate_base;
471 std::vector<int> ev_kind, ev_mode, ev_phase, ev_to;
472 std::vector<std::ptrdiff_t> ev_station, ev_class;
473 const auto emit = [&](const std::vector<double>& col, double base, int kind, int mode,
474 int phase, int to, std::ptrdiff_t station, std::ptrdiff_t cls) {
475 cols.push_back(col);
476 rate_base.push_back(base);
477 ev_kind.push_back(kind);
478 ev_mode.push_back(mode);
479 ev_phase.push_back(phase);
480 ev_to.push_back(to);
481 ev_station.push_back(station);
482 ev_class.push_back(cls);
483 };
484
485 for (std::size_t q = 0; q < t.timed_idx.size(); ++q) {
486 const std::size_t j = t.timed_idx[q];
487 const PetriMode& md = t.modes[j];
488 if (md.nph == 1) {
489 emit(md.cvec, md.d1[0], 1, static_cast<int>(j), 0, 0, -1, -1);
490 } else {
491 for (std::size_t h = 0; h < md.nph; ++h)
492 for (std::size_t hp = 0; hp < md.nph; ++hp) {
493 const double w = md.D1(h, hp);
494 if (w <= 0) continue;
495 std::vector<double> col = md.cvec;
496 col[md.zblk[hp]] += 1.0;
497 col[md.zblk[h]] -= 1.0;
498 emit(col, w, 1, static_cast<int>(j), static_cast<int>(h),
499 static_cast<int>(hp), -1, -1);
500 }
501 for (std::size_t h = 0; h < md.nph; ++h)
502 for (std::size_t hp = 0; hp < md.nph; ++hp) {
503 if (hp == h) continue;
504 const double w = md.D0(h, hp);
505 if (w <= 0) continue;
506 std::vector<double> col(nstate, 0.0);
507 col[md.zblk[hp]] = 1.0;
508 col[md.zblk[h]] = -1.0;
509 emit(col, w, 2, static_cast<int>(j), static_cast<int>(h),
510 static_cast<int>(hp), -1, -1);
511 }
512 }
513 }
514 // One latch column per multi-phase mode: mu_j servers per unit time enter at
515 // the firing process's own entry distribution. The rate is FREE IN SIGN -- a
516 // mode whose enabling degree drops stops servers rather than starting them --
517 // and it is zero at any fixed point.
518 for (std::size_t q = 0; q < t.timed_idx.size(); ++q) {
519 const std::size_t j = t.timed_idx[q];
520 const PetriMode& md = t.modes[j];
521 if (md.nph <= 1) continue;
522 std::vector<double> col(nstate, 0.0);
523 for (std::size_t h = 0; h < md.nph; ++h) col[md.zblk[h]] = md.pie[h];
524 emit(col, 1.0, 5, static_cast<int>(j), 0, 0, -1, -1);
525 }
526 for (std::size_t q = 0; q < t.imm_idx.size(); ++q) {
527 const std::size_t j = t.imm_idx[q];
528 emit(t.modes[j].cvec, 1.0, 4, static_cast<int>(j), 0, 0, -1, -1);
529 }
530 for (std::size_t a = 0; a < src_arr.size(); ++a) {
531 const SrcArr& rec = src_arr[a];
532 const std::size_t ist = sn.nodes[rec.snd - 1].station;
533 if (ist >= 1 && ist <= sn.stations.size()) {
534 const lang::ProcessType pt = sn.procid(ist, rec.r + 1);
535 if (pt != lang::ProcessType::EXP)
536 throw UnsupportedError(
537 "fluid_petri_terms: source " + t.names_node[rec.snd - 1] +
538 " has a non-exponential arrival for class " + std::to_string(rec.r + 1) +
539 ". The fluid Petri route models an arrival as a constant-propensity event, "
540 "which a renewal stream with memory is not; use SolverCTMC, SolverJMT or "
541 "SolverSSA");
542 }
543 std::vector<double> col(nstate, 0.0);
544 col[static_cast<std::size_t>(t.pidx[rec.qnd - 1][rec.l])] = 1.0;
545 const double base =
546 num_traits<T>::to_double(sn.rates(ist - 1, rec.r)) *
548 sn.rtnodes((rec.snd - 1) * t.K + rec.r, (rec.qnd - 1) * t.K + rec.l));
549 emit(col, base, 3, -1, 0, 0, static_cast<std::ptrdiff_t>(ist) - 1,
550 static_cast<std::ptrdiff_t>(rec.r));
551 }
552
553 t.nev = cols.size();
554 t.D = Matrix<double>(nstate, std::max<std::size_t>(t.nev, 1), 0.0);
555 for (std::size_t e = 0; e < t.nev; ++e)
556 for (std::size_t s = 0; s < nstate; ++s)
557 if (cols[e][s] != 0.0) t.D(s, e) = cols[e][s];
558 t.rate_base = rate_base;
559 t.ev_kind = ev_kind;
560 t.ev_mode = ev_mode;
561 t.ev_phase = ev_phase;
562 t.ev_to = ev_to;
563 t.ev_station = ev_station;
564 t.ev_class = ev_class;
565
566 // ---- which Sigma entries the closure reads ------------------------------
567 std::map<std::pair<std::size_t, std::size_t>, std::size_t> pairkey;
568 for (std::size_t j = 0; j < t.modes.size(); ++j) {
569 const PetriMode& md = t.modes[j];
570 if (md.closable)
571 for (std::size_t a = 0; a < md.arc_slot.size(); ++a)
572 for (std::size_t b = a; b < md.arc_slot.size(); ++b)
573 terms_detail::add_pair(pairkey, t.cov_pairs, md.arc_slot[a], md.arc_slot[b]);
575 for (std::size_t b = 0; b < md.inh_slot.size(); ++b)
576 terms_detail::add_pair(pairkey, t.cov_pairs, md.inh_slot[b], md.inh_slot[b]);
577 }
578 t.npair = t.cov_pairs.size();
579 const std::size_t pn = std::max<std::size_t>(nm, 1);
580 t.pair_index.assign(pn, std::vector<std::ptrdiff_t>(pn, -1));
581 for (std::size_t i = 0; i < t.npair; ++i) {
582 t.pair_index[t.cov_pairs[i].first][t.cov_pairs[i].second] =
583 static_cast<std::ptrdiff_t>(i);
584 t.pair_index[t.cov_pairs[i].second][t.cov_pairs[i].first] =
585 static_cast<std::ptrdiff_t>(i);
586 }
587
588 for (std::size_t e = 0; e < t.nev; ++e) {
589 if (t.ev_kind[e] == 4) t.imm_col.push_back(e);
590 else if (t.ev_kind[e] == 5) t.latch_col.push_back(e);
591 if (t.ev_kind[e] != 4 && t.ev_kind[e] != 5) t.stoch_col.push_back(e);
592 }
593 for (std::size_t q = 0; q < t.timed_idx.size(); ++q)
594 if (t.modes[t.timed_idx[q]].nph > 1) t.latch_mode.push_back(t.timed_idx[q]);
595 t.cov_idx.resize(nstate);
596 for (std::size_t s = 0; s < nstate; ++s) t.cov_idx[s] = s;
597
598 // ---- per-place consumption and production, for the metric reader --------
599 // A Place's throughput is the rate at which TOKENS leave it, so each
600 // consuming mode contributes its firing rate times the multiplicity of the
601 // arc it takes them through. That is SolverCTMC's convention and the one
602 // Little's law needs; SolverSSA's NRM sums the UNWEIGHTED propensity, so the
603 // two disagree wherever an input arc has multiplicity above one.
604 for (std::size_t e = 0; e < t.nev; ++e) {
605 if (t.ev_kind[e] == 3) {
606 if (t.ev_station[e] < 0) continue;
607 const std::size_t kk =
608 static_cast<std::size_t>(t.ev_station[e]) * t.K +
609 static_cast<std::size_t>(t.ev_class[e]);
610 t.producers[kk].push_back(e);
611 continue;
612 }
613 if (t.ev_kind[e] != 1 && t.ev_kind[e] != 4) continue;
614 const PetriMode& md = t.modes[static_cast<std::size_t>(t.ev_mode[e])];
615 for (std::size_t a = 0; a < md.arc_slot.size(); ++a) {
616 const std::size_t s = md.arc_slot[a];
617 if (t.coord_station[s] < 0) continue;
618 const std::size_t kk =
619 static_cast<std::size_t>(t.coord_station[s]) * t.K + t.coord_class[s];
620 t.consumers[kk].push_back(e);
621 t.consumer_w[kk].push_back(md.arc_w[a]);
622 }
623 }
624
625 // ---- the initial state --------------------------------------------------
626 t.x0.assign(nstate, 0.0);
627 for (std::size_t s = 0; s < nm; ++s) t.x0[s] = t.m0full[t.coord_node[s]][t.coord_class[s]];
628 for (std::size_t j = 0; j < t.modes.size(); ++j) {
629 const PetriMode& md = t.modes[j];
630 if (md.nph <= 1) continue;
631 double e = std::numeric_limits<double>::infinity();
632 for (std::size_t a = 0; a < md.arc_slot.size(); ++a)
633 e = std::min(e, t.x0[md.arc_slot[a]] / md.arc_w[a]);
634 if (md.arc_slot.empty()) e = 1.0;
635 for (std::size_t h = 0; h < md.nph; ++h) t.x0[md.zblk[h]] = std::min(e, md.c) * md.pie[h];
636 }
637 return t;
638}
639
640} // namespace petri
641} // namespace fluid
642} // namespace line
643
644#endif // LINE_SOLVERS_FLUID_PETRI_TERMS_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
PetriTerms petri_build_terms(const qn::NetworkStruct< T > &sn)
Assemble the drift terms of a net.
double petri_fine_tol()
GlobalConstants.FineTol, the reference's own "effectively zero".
TimingStrategy
SPN transition timing, with the values of MATLAB TimingStrategy.
Definition lang_types.h:361
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:363
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:362
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:483
A queueing network and its refreshed NetworkStruct.
One firing mode of one transition, with its arcs and its firing process.
double c
servers of this mode; infinite for none
std::size_t mode
mode index within it, 0-based
std::vector< double > d1
row sums of D1, the completion rate out of each phase
std::vector< std::size_t > inh_slot
Inhibitor arcs, as state coordinates and their thresholds.
std::vector< std::size_t > arc_slot
Input arcs, as state coordinates and their multiplicities.
std::vector< double > pie
entry distribution over the phases
std::size_t nph
phases of the firing process; 0 for an immediate mode
std::vector< std::size_t > zblk
The phase coordinates of a multi-phase mode; empty otherwise.
std::vector< double > inh_thr
std::function< double(const Matrix< double > &)> dep
Marking-dependent firing multiplier; empty for the unit one.
std::size_t node
1-based node index of the transition
std::vector< double > cvec
The mode's incidence column, over the whole state.
bool closable
Whether the enabling degree is worth closing over.
The assembled drift terms of a net.
std::vector< std::vector< double > > m0full
std::vector< std::size_t > stoch_col
std::vector< std::size_t > coord_class
std::vector< std::size_t > places
std::size_t nstate
marking coordinates plus phase coordinates
std::vector< std::size_t > imm_col
std::vector< std::vector< std::ptrdiff_t > > pidx
pidx(p,k) is the state coordinate of (0-based node p, class k), or npos.
std::vector< std::size_t > transitions
1-based node indices
std::vector< std::size_t > latch_col
std::vector< std::vector< std::ptrdiff_t > > pair_index
std::vector< std::size_t > imm_idx
std::vector< std::size_t > timed_idx
std::size_t nm
marking coordinates alone
std::vector< std::string > names_node
0-based, one per node
std::vector< std::size_t > coord_node
std::vector< std::pair< std::size_t, std::size_t > > cov_pairs
The Sigma entries the closure reads, and the map back to them.
std::map< std::size_t, std::vector< std::size_t > > producers
std::map< std::size_t, std::vector< std::size_t > > consumers
(station*K + class) -> the events that take tokens out of / into it.
std::vector< std::ptrdiff_t > ev_class
std::vector< std::size_t > cov_idx
std::map< std::size_t, std::vector< double > > consumer_w
std::vector< std::size_t > latch_mode
std::vector< std::ptrdiff_t > ev_station
std::vector< PetriMode > modes
std::vector< std::ptrdiff_t > coord_station
Matrix< double > D
(nstate x nev) incidence
The parameters of a Cache node, MATLAB's sn.nodeparam{ind} for a Cache.
std::vector< double > firingprio
firing priority per mode
std::vector< lang::TimingStrategy > timing
immediate or timed
std::vector< std::string > modenames
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).