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 * f(c) the LEVEL of fluid place c (FSPN), one coordinate per fluid place, appended after the phase block.
39 * A fluid place holds one real level and no tokens; it moves only by the flows of the modes that run
40 * and by the impulses of the modes that fire.
41 *
42 * There is no Source coordinate: an exogenous arrival is a CONSTANT-propensity
43 * event depositing one token, as it is in the NRM SPN runner. There is no Sink
44 * coordinate either: a firing arc into a sink is mass leaving the net.
45 *
46 * THE EVENTS, one column of D each: kind 1 a firing of mode j out of phase h
47 * into phase h'; kind 2 an internal phase change; kind 3 an exogenous arrival;
48 * kind 4 a firing of an IMMEDIATE mode, at the algebraic flow phi_j; kind 5 the
49 * server latch of a multi-phase mode, at the free-sign unknown mu_j; kind 6 the FLUID FLOW of mode j into
50 * (or out of) fluid place c, a unit column on f(c) signed by the direction of the flow, at |r_jc| * theta_j(x).
51 * A flow is DETERMINISTIC: it enters the drift and its Jacobian but not the diffusion, as a server latch and an
52 * immediate flow do not. An impulse is not a column of its own -- it is the fluid entry of the mode's firing
53 * column, so it is a stochastic jump.
54 *
55 * Arithmetic: DOUBLE ONLY. The closures evaluate the normal CDF, which is not an
56 * element of the field generated by the inputs under any arithmetic, so the
57 * templated form this tree uses elsewhere would buy nothing.
58 */
59
60#include <algorithm>
61#include <cmath>
62#include <cstddef>
63#include <functional>
64#include <limits>
65#include <map>
66#include <string>
67#include <vector>
68
70#include "line/util/error.h"
71#include "line/util/matrix.h"
72
73namespace line {
74namespace fluid {
75namespace petri {
76
77/** GlobalConstants.FineTol, the reference's own "effectively zero". */
78inline double petri_fine_tol() { return 1e-8; }
79
80/** GlobalConstants.CoarseTol, also the relative width of the seed's smoothed inhibitor gate. */
81inline double petri_coarse_tol() { return 1e-3; }
82
83/** One firing mode of one transition, with its arcs and its firing process. */
84struct PetriMode {
85 std::size_t node = 0; ///< 1-based node index of the transition
86 std::size_t mode = 0; ///< mode index within it, 0-based
88 std::string label;
89
90 /** Input arcs, as state coordinates and their multiplicities. */
91 std::vector<std::size_t> arc_slot;
92 std::vector<double> arc_w;
93 /** Inhibitor arcs, as state coordinates and their thresholds. */
94 std::vector<std::size_t> inh_slot;
95 std::vector<double> inh_thr;
96
97 double c = 1.0; ///< servers of this mode; infinite for none
98 std::size_t nph = 1; ///< phases of the firing process; 0 for an immediate mode
100 std::vector<double> d1; ///< row sums of D1, the completion rate out of each phase
101 std::vector<double> pie; ///< entry distribution over the phases
102 /** Marking-dependent firing multiplier; empty for the unit one. */
103 std::function<double(const Matrix<double>&)> dep;
104 int prio = 1;
105 double weight = 1.0;
106 /** The mode's incidence column, over the whole state. */
107 std::vector<double> cvec;
108 /** The phase coordinates of a multi-phase mode; empty otherwise. */
109 std::vector<std::size_t> zblk;
110 /**
111 * Whether the enabling degree is worth closing over.
112 *
113 * A single input arc under an infinite server count makes min() the identity,
114 * so the closure would smooth nothing and the covariance entries it reads
115 * would be carried for no reason.
116 */
117 bool closable = false;
118
119 /** Fluid flows of the mode: level coordinates and signed rates per running server. */
120 std::vector<std::size_t> flow_slot;
121 std::vector<double> flow_rate;
122 /** Fluid guards: level coordinates, +1 for '>=' and -1 for '<=', and thresholds. */
123 std::vector<std::size_t> guard_slot;
124 std::vector<int> guard_sgn;
125 std::vector<double> guard_thr;
126 /** Fluid rate dependences: level coordinates and the multipliers h(level). */
127 std::vector<std::size_t> fdep_slot;
128 std::vector<std::function<double(double)>> fdep_fun;
129 /**
130 * Marking-dependent flow multipliers, aligned with `flow_slot` (an empty function is the unit one), each of the
131 * (I x K) marking restricted to the 1-based token places of `flow_dep_read` (empty: every coordinate).
132 */
133 std::vector<std::function<double(const Matrix<double>&)>> flow_dep_fun;
134 std::vector<std::vector<std::size_t>> flow_dep_read;
135};
136
137/** The assembled drift terms of a net. */
139 std::size_t I = 0; ///< nodes
140 std::size_t K = 0; ///< classes
141 std::size_t M = 0; ///< stations
142 std::vector<std::size_t> places, transitions; ///< 1-based node indices
143 std::vector<std::string> names_node; ///< 0-based, one per node
144
145 std::size_t nstate = 0; ///< marking coordinates plus phase coordinates plus fluid levels
146 std::size_t nm = 0; ///< marking coordinates alone
147
148 /** Fluid places (FSPN): 1-based nodes, their level coordinates, capacities and initial levels. */
149 std::vector<std::size_t> fluid_places;
150 std::size_t nf = 0;
151 std::vector<std::ptrdiff_t> fidx; ///< (I) level coordinate of a 0-based node, or -1
152 std::vector<std::size_t> fluid_slot;
153 std::vector<double> fluid_cap, fluid_init;
154 std::vector<std::ptrdiff_t> fluid_station; ///< 0-based station of each fluid place
155 std::vector<std::string> fluid_name;
156
157 /** pidx(p,k) is the state coordinate of (0-based node p, class k), or npos. */
158 std::vector<std::vector<std::ptrdiff_t>> pidx;
159 std::vector<std::size_t> coord_node, coord_class;
160 std::vector<std::ptrdiff_t> coord_station;
161
162 std::vector<PetriMode> modes;
163 std::vector<std::size_t> timed_idx, imm_idx;
164
165 Matrix<double> D; ///< (nstate x nev) incidence
166 std::vector<double> rate_base;
167 std::size_t nev = 0;
168 std::vector<int> ev_kind, ev_mode, ev_phase, ev_to;
169 std::vector<std::ptrdiff_t> ev_station, ev_class;
170 /** Kind 6: the flow's position in its mode's `flow_slot`; 0 for every other kind. */
171 std::vector<std::size_t> ev_flow;
172
173 std::vector<std::size_t> imm_col, latch_col, stoch_col, flow_col;
174 std::vector<std::size_t> latch_mode;
175
176 /** The Sigma entries the closure reads, and the map back to them. */
177 std::vector<std::pair<std::size_t, std::size_t>> cov_pairs;
178 std::size_t npair = 0;
179 std::vector<std::vector<std::ptrdiff_t>> pair_index;
180 std::vector<std::size_t> cov_idx;
181
182 /** (station*K + class) -> the events that take tokens out of / into it. */
183 std::map<std::size_t, std::vector<std::size_t>> consumers, producers;
184 std::map<std::size_t, std::vector<double>> consumer_w;
185
186 std::vector<double> x0;
187 std::vector<std::vector<double>> m0full;
188};
189
190namespace terms_detail {
191
192/** An (I x K) parameter matrix padded to full size with `fill`. */
193template <class T>
194inline std::vector<std::vector<double>> pad(const Matrix<T>& A, std::size_t I, std::size_t K,
195 double fill) {
196 std::vector<std::vector<double>> out(I, std::vector<double>(K, fill));
197 for (std::size_t i = 0; i < I && i < A.rows(); ++i)
198 for (std::size_t k = 0; k < K && k < A.cols(); ++k)
199 out[i][k] = num_traits<T>::to_double(A(i, k));
200 return out;
201}
202
203/** Record a covariance pair once, in ascending order. */
204inline void add_pair(std::map<std::pair<std::size_t, std::size_t>, std::size_t>& key,
205 std::vector<std::pair<std::size_t, std::size_t>>& pairs, std::size_t a,
206 std::size_t b) {
207 const std::pair<std::size_t, std::size_t> k(std::min(a, b), std::max(a, b));
208 if (key.find(k) != key.end()) return;
209 key[k] = pairs.size();
210 pairs.push_back(k);
211}
212
213/**
214 * One mode's arcs, incidence column and firing process.
215 *
216 * `nm` is the marking width at the time of the call; the column is GROWN once
217 * the phase coordinates are appended and the total state width is known.
218 */
219template <class T>
220inline PetriMode build_mode(const qn::NetworkStruct<T>& sn, const qn::TransitionParam<T>& tp,
221 std::size_t ind, std::size_t m, PetriTerms& t, std::size_t nm) {
222 PetriMode rec;
223 rec.node = ind;
224 rec.mode = m;
225 rec.timing = (m < tp.timing.size()) ? tp.timing[m] : lang::TimingStrategy::TIMED;
226 const std::string mname = (m < tp.modenames.size() && !tp.modenames[m].empty())
227 ? tp.modenames[m]
228 : ("Mode" + std::to_string(m + 1));
229 rec.label = t.names_node[ind - 1] + "." + mname;
230
231 const std::vector<std::vector<double>> en = pad(tp.enabling[m], t.I, t.K, 0.0);
232 const std::vector<std::vector<double>> fir = pad(tp.firing[m], t.I, t.K, 0.0);
233 const std::vector<std::vector<double>> inh =
234 pad(tp.inhibiting[m], t.I, t.K, std::numeric_limits<double>::infinity());
235
236 std::vector<double> cvec(nm, 0.0);
237 for (std::size_t p = 0; p < t.I; ++p)
238 for (std::size_t k = 0; k < t.K; ++k) {
239 const double w = en[p][k];
240 if (!(w > 0)) continue;
241 if (std::isinf(w))
242 throw InputError("fluid_petri_terms: mode " + rec.label +
243 " has a non-finite enabling arc weight at " + t.names_node[p] +
244 ". An arc that no marking can satisfy disables the mode; declare "
245 "a finite multiplicity");
246 if (t.pidx[p][k] < 0)
247 throw InputError("fluid_petri_terms: mode " + rec.label +
248 " takes an enabling arc from " + t.names_node[p] +
249 ", which is not a Place");
250 const std::size_t s = static_cast<std::size_t>(t.pidx[p][k]);
251 rec.arc_slot.push_back(s);
252 rec.arc_w.push_back(w);
253 cvec[s] -= w;
254 }
255 for (std::size_t p = 0; p < t.I; ++p)
256 for (std::size_t k = 0; k < t.K; ++k) {
257 // A NEGATIVE ENTRY MARKS AN INPUT PLACE, whose token the enabling arc
258 // already removed, and a firing arc into a Sink has no coordinate:
259 // that is mass leaving the net, not mass lost by accident.
260 if (fir[p][k] <= 0 || t.pidx[p][k] < 0) continue;
261 cvec[static_cast<std::size_t>(t.pidx[p][k])] += fir[p][k];
262 }
263 for (std::size_t p = 0; p < t.I; ++p)
264 for (std::size_t k = 0; k < t.K; ++k) {
265 const double th = inh[p][k];
266 // JMT writes a MISSING inhibitor arc as 0, as -1 or as Inf, never as
267 // a threshold, so none of those three is one.
268 if (std::isinf(th) || th <= 0 || t.pidx[p][k] < 0) continue;
269 rec.inh_slot.push_back(static_cast<std::size_t>(t.pidx[p][k]));
270 rec.inh_thr.push_back(th);
271 }
272 rec.cvec = cvec;
273
274 const double c = (m < tp.nmodeservers.size()) ? tp.nmodeservers[m] : 1.0;
275 rec.c = std::isnan(c) ? 1.0 : c;
276 rec.prio = (m < tp.firingprio.size()) ? static_cast<int>(tp.firingprio[m]) : 1;
277 rec.weight =
278 (m < tp.fireweight.size()) ? num_traits<T>::to_double(tp.fireweight[m]) : 1.0;
279 if (m < tp.firingdep.size() && tp.firingdep[m]) {
280 const std::function<T(const std::vector<T>&)> g = tp.firingdep[m];
281 const std::size_t I = t.I, K = t.K;
282 rec.dep = [g, I, K](const Matrix<double>& mm) -> double {
283 std::vector<T> flat(I * K, num_traits<T>::from_int(0));
284 for (std::size_t i = 0; i < I; ++i)
285 for (std::size_t k = 0; k < K; ++k)
286 flat[i * K + k] = num_traits<T>::from_double(mm(i, k));
287 return num_traits<T>::to_double(g(flat));
288 };
289 }
290
291 if (rec.timing == lang::TimingStrategy::IMMEDIATE) {
292 // An immediate mode carries no firing process: its flow is an algebraic
293 // unknown of the DAE, not a rate.
294 rec.nph = 0;
295 rec.d1.assign(1, 0.0);
296 rec.pie.assign(1, 1.0);
297 rec.closable = false;
298 return rec;
299 }
300
301 if (m >= tp.firingproc.size() || tp.firingproc[m].disabled)
302 throw InputError("fluid_petri_terms: mode " + rec.label +
303 " has no Markovian firing process. The renewal families are converted "
304 "to phase type before the solver runs, so this is a distribution the "
305 "fluid Petri route cannot time; use SolverCTMC or SolverLDES");
306 const lang::Distrib<T>& fp = tp.firingproc[m];
307 Matrix<double> D0, D1;
308 if (fp.D0.rows() > 0 && fp.D1.rows() > 0) {
309 D0 = Matrix<double>(fp.D0.rows(), fp.D0.cols(), 0.0);
310 for (std::size_t i = 0; i < D0.rows(); ++i)
311 for (std::size_t j = 0; j < D0.cols(); ++j)
312 D0(i, j) = num_traits<T>::to_double(fp.D0(i, j));
313 D1 = Matrix<double>(fp.D1.rows(), fp.D1.cols(), 0.0);
314 for (std::size_t i = 0; i < D1.rows(); ++i)
315 for (std::size_t j = 0; j < D1.cols(); ++j)
316 D1(i, j) = num_traits<T>::to_double(fp.D1(i, j));
317 } else {
318 // An exponential law carried by its mean alone: the one-phase (D0, D1).
319 const double mean = num_traits<T>::to_double(fp.mean);
320 if (!(mean > 0.0))
321 throw InputError("fluid_petri_terms: mode " + rec.label +
322 " has a non-positive mean firing time");
323 D0 = Matrix<double>(1, 1, -1.0 / mean);
324 D1 = Matrix<double>(1, 1, 1.0 / mean);
325 }
326 rec.nph = D0.rows();
327 rec.D0 = D0;
328 rec.D1 = D1;
329 rec.d1.assign(rec.nph, 0.0);
330 for (std::size_t h = 0; h < rec.nph; ++h) {
331 double acc = 0.0;
332 for (std::size_t hp = 0; hp < D1.cols(); ++hp) acc += D1(h, hp);
333 rec.d1[h] = acc;
334 }
335 rec.pie.assign(std::max<std::size_t>(rec.nph, 1), 0.0);
336 if (rec.nph > 1) {
337 double tot = 0.0;
338 for (std::size_t h = 0; h < rec.nph && h < fp.params.size(); ++h) {
339 rec.pie[h] = num_traits<T>::to_double(fp.params[h]);
340 tot += rec.pie[h];
341 }
342 if (!(tot > 0.0)) {
343 std::fill(rec.pie.begin(), rec.pie.end(), 0.0);
344 rec.pie[0] = 1.0;
345 } else {
346 for (std::size_t h = 0; h < rec.nph; ++h) rec.pie[h] /= tot;
347 }
348 } else {
349 rec.pie[0] = 1.0;
350 }
351 rec.closable = !(rec.arc_slot.size() <= 1 && std::isinf(rec.c));
352 return rec;
353}
354
355} // namespace terms_detail
356
357/**
358 * Assemble the drift terms of a net.
359 *
360 * @param sn a model whose nodes are Places, Transitions, Sources and Sinks only
361 */
362template <class T>
364 PetriTerms t;
365 t.I = sn.nodes.size();
366 t.K = sn.nclasses;
367 t.M = sn.nstations;
368 t.names_node.resize(t.I);
369 for (std::size_t i = 0; i < t.I; ++i) t.names_node[i] = sn.nodes[i].name;
370 for (std::size_t i = 1; i <= t.I; ++i) {
371 if (sn.nodes[i - 1].nodetype == lang::NodeType::Place) t.places.push_back(i);
372 else if (sn.nodes[i - 1].nodetype == lang::NodeType::Transition) t.transitions.push_back(i);
373 }
374 // Fluid places carry a level, not a token count per class, so they take no marking coordinate; their
375 // level coordinate is appended after the phase block.
376 {
377 std::vector<std::size_t> tok;
378 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
379 if (sn.is_fluid_node(t.places[pi])) t.fluid_places.push_back(t.places[pi]);
380 else tok.push_back(t.places[pi]);
381 }
382 t.places = tok;
383 }
384
385 // ---- the initial marking ------------------------------------------------
386 // A Place's DECLARED marking wins; absent one, each closed class loads its
387 // reference station, which is the rule spn_sinvariants and the exact SPN
388 // engines use. An infinite marking is refused rather than integrated.
389 t.m0full.assign(t.I, std::vector<double>(t.K, 0.0));
390 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
391 const std::size_t ind = t.places[pi];
392 const typename std::map<std::size_t, std::vector<T>>::const_iterator im =
393 sn.initmarking.find(ind);
394 if (im == sn.initmarking.end()) continue;
395 for (std::size_t k = 0; k < t.K && k < im->second.size(); ++k) {
396 const double v = num_traits<T>::to_double(im->second[k]);
397 if (std::isinf(v))
398 throw InputError("fluid_petri_terms: place " + t.names_node[ind - 1] +
399 " holds an infinite initial marking of class " +
400 std::to_string(k + 1));
401 t.m0full[ind - 1][k] = v;
402 }
403 }
404 bool any_declared = false;
405 for (std::size_t pi = 0; pi < t.places.size() && !any_declared; ++pi)
406 any_declared = (sn.initmarking.find(t.places[pi]) != sn.initmarking.end());
407 if (!any_declared) {
408 for (std::size_t r = 0; r < sn.classes.size(); ++r) {
409 const double njobs = num_traits<T>::to_double(sn.classes[r].population);
410 if (!(njobs > 0.0) || !std::isfinite(njobs)) continue;
411 if (sn.classes[r].refstat < 1 || sn.classes[r].refstat > sn.station_to_node.size())
412 continue;
413 const std::size_t ref_node = sn.station_to_node[sn.classes[r].refstat - 1];
414 if (ref_node >= 1 && ref_node <= t.I &&
415 sn.nodes[ref_node - 1].nodetype == lang::NodeType::Place)
416 t.m0full[ref_node - 1][r] += njobs;
417 }
418 }
419
420 // ---- which (place, class) pairs carry a coordinate ----------------------
421 std::vector<std::vector<bool>> touched(t.I, std::vector<bool>(t.K, false));
422 for (std::size_t ti = 0; ti < t.transitions.size(); ++ti) {
423 const std::size_t ind = t.transitions[ti];
424 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
425 sn.transparam.find(ind);
426 if (it == sn.transparam.end()) continue;
427 const qn::TransitionParam<T>& tp = it->second;
428 for (std::size_t m = 0; m < tp.nmodes; ++m) {
429 const std::vector<std::vector<double>> en =
430 terms_detail::pad(tp.enabling[m], t.I, t.K, 0.0);
431 const std::vector<std::vector<double>> fir =
432 terms_detail::pad(tp.firing[m], t.I, t.K, 0.0);
433 const std::vector<std::vector<double>> inh = terms_detail::pad(
434 tp.inhibiting[m], t.I, t.K, std::numeric_limits<double>::infinity());
435 for (std::size_t i = 0; i < t.I; ++i)
436 for (std::size_t k = 0; k < t.K; ++k)
437 if (en[i][k] > 0 || fir[i][k] != 0 ||
438 (!std::isinf(inh[i][k]) && inh[i][k] > 0))
439 touched[i][k] = true;
440 }
441 }
442
443 // An arrival makes its target a coordinate even when no arc mentions it.
444 struct SrcArr {
445 std::size_t snd, r, qnd, l;
446 };
447 std::vector<SrcArr> src_arr;
448 for (std::size_t ind = 1; ind <= t.I; ++ind) {
449 if (sn.nodes[ind - 1].nodetype != lang::NodeType::Source) continue;
450 const std::size_t ist = sn.nodes[ind - 1].station;
451 if (ist < 1) continue;
452 for (std::size_t r = 0; r < t.K; ++r) {
453 const double lambda = num_traits<T>::to_double(sn.rates(ist - 1, r));
454 if (std::isnan(lambda) || lambda <= 0) continue;
455 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
456 const std::size_t jnd = t.places[pi];
457 for (std::size_t s = 0; s < t.K; ++s) {
458 const double p = num_traits<T>::to_double(
459 sn.rtnodes((ind - 1) * t.K + r, (jnd - 1) * t.K + s));
460 if (p > 0) {
461 touched[jnd - 1][s] = true;
462 SrcArr a;
463 a.snd = ind;
464 a.r = r;
465 a.qnd = jnd;
466 a.l = s;
467 src_arr.push_back(a);
468 }
469 }
470 }
471 }
472 }
473
474 t.pidx.assign(t.I, std::vector<std::ptrdiff_t>(t.K, -1));
475 std::size_t nm = 0;
476 for (std::size_t pi = 0; pi < t.places.size(); ++pi) {
477 const std::size_t ind = t.places[pi];
478 for (std::size_t k = 0; k < t.K; ++k) {
479 if (!(touched[ind - 1][k] || t.m0full[ind - 1][k] > 0)) continue;
480 t.pidx[ind - 1][k] = static_cast<std::ptrdiff_t>(nm);
481 t.coord_node.push_back(ind - 1);
482 t.coord_class.push_back(k);
483 t.coord_station.push_back(static_cast<std::ptrdiff_t>(sn.nodes[ind - 1].station) - 1);
484 ++nm;
485 }
486 }
487 t.nm = nm;
488
489 // ---- the modes, and the phase coordinates of the multi-phase ones -------
490 std::size_t nstate = nm;
491 for (std::size_t ti = 0; ti < t.transitions.size(); ++ti) {
492 const std::size_t ind = t.transitions[ti];
493 const typename std::map<std::size_t, qn::TransitionParam<T>>::const_iterator it =
494 sn.transparam.find(ind);
495 if (it == sn.transparam.end()) continue;
496 for (std::size_t m = 0; m < it->second.nmodes; ++m) {
497 PetriMode rec = terms_detail::build_mode(sn, it->second, ind, m, t, nm);
498 if (rec.nph > 1) {
499 rec.zblk.resize(rec.nph);
500 for (std::size_t h = 0; h < rec.nph; ++h) rec.zblk[h] = nstate + h;
501 nstate += rec.nph;
502 }
503 t.modes.push_back(rec);
504 }
505 }
506 // ---- the fluid levels, one coordinate per fluid place after the phase block
507 t.nf = t.fluid_places.size();
508 t.fidx.assign(t.I, -1);
509 for (std::size_t q = 0; q < t.nf; ++q) {
510 const std::size_t ind = t.fluid_places[q];
511 const qn::Station<T>& st = sn.stations[sn.nodes[ind - 1].station - 1];
512 t.fidx[ind - 1] = static_cast<std::ptrdiff_t>(nstate + q);
513 t.fluid_slot.push_back(nstate + q);
514 t.fluid_cap.push_back(st.fluidcap);
515 t.fluid_init.push_back(st.fluidinit);
516 t.fluid_station.push_back(static_cast<std::ptrdiff_t>(sn.nodes[ind - 1].station) - 1);
517 t.fluid_name.push_back(t.names_node[ind - 1]);
518 }
519 nstate += t.nf;
520 t.nstate = nstate;
521 // The phase coordinates are appended after every marking coordinate, so a
522 // jump column built at nm width has to be GROWN once the total is known.
523 for (std::size_t j = 0; j < t.modes.size(); ++j)
524 if (t.modes[j].cvec.size() < nstate) t.modes[j].cvec.resize(nstate, 0.0);
525 // The fluid arcs of each mode are resolved to level coordinates: an impulse is the fluid entry of the
526 // firing column, a flow and a guard and a fluid rate dependence are read by index. `sn_fluid_refusal`
527 // has already refused an arc naming a place that is not fluid, so every fidx read here is a coordinate.
528 const auto level_of = [&](std::size_t node, const std::string& lbl) -> std::size_t {
529 if (node == 0 || node > t.I || t.fidx[node - 1] < 0)
530 throw InputError("fluid_petri_terms: mode " + lbl + " has a fluid arc on a node that is not a "
531 "fluid place");
532 return static_cast<std::size_t>(t.fidx[node - 1]);
533 };
534 for (std::size_t j = 0; j < t.modes.size(); ++j) {
535 PetriMode& md = t.modes[j];
536 const qn::TransitionParam<T>& tp = sn.transparam.find(md.node)->second;
537 const std::vector<typename qn::TransitionParam<T>::FluidArc>& imp = tp.fluid_impulses(md.mode);
538 for (std::size_t a = 0; a < imp.size(); ++a) md.cvec[level_of(imp[a].node, md.label)] += imp[a].value;
539 const std::vector<typename qn::TransitionParam<T>::FluidArc>& fr = tp.fluid_rates(md.mode);
540 const std::vector<typename qn::TransitionParam<T>::FluidFlowDep>& fld = tp.fluid_flow_deps(md.mode);
541 const std::size_t KK = t.K;
542 for (std::size_t a = 0; a < fr.size(); ++a) {
543 md.flow_slot.push_back(level_of(fr[a].node, md.label));
544 md.flow_rate.push_back(fr[a].value);
545 md.flow_dep_fun.push_back(std::function<double(const Matrix<double>&)>());
546 md.flow_dep_read.push_back(std::vector<std::size_t>());
547 for (std::size_t u = 0; u < fld.size(); ++u) {
548 if (fld[u].node != fr[a].node) continue;
549 // the setter's domain is the row-major flattening of the (I x K) marking
550 const std::function<double(const std::vector<double>&)> phi = fld[u].fun;
551 md.flow_dep_fun.back() = [phi, KK](const Matrix<double>& mm) -> double {
552 std::vector<double> flat(mm.rows() * KK, 0.0);
553 for (std::size_t i = 0; i < mm.rows(); ++i)
554 for (std::size_t k = 0; k < KK && k < mm.cols(); ++k) flat[i * KK + k] = mm(i, k);
555 return phi(flat);
556 };
557 md.flow_dep_read.back() = fld[u].read;
558 }
559 }
560 const std::vector<typename qn::TransitionParam<T>::FluidGuard>& gd = tp.fluid_guards(md.mode);
561 for (std::size_t a = 0; a < gd.size(); ++a) {
562 md.guard_slot.push_back(level_of(gd[a].node, md.label));
563 md.guard_sgn.push_back(gd[a].sgn);
564 md.guard_thr.push_back(gd[a].thr);
565 }
566 const std::vector<typename qn::TransitionParam<T>::FluidDep>& fd = tp.fluid_deps(md.mode);
567 for (std::size_t a = 0; a < fd.size(); ++a) {
568 md.fdep_slot.push_back(level_of(fd[a].node, md.label));
569 md.fdep_fun.push_back(fd[a].fun);
570 }
571 }
572 for (std::size_t j = 0; j < t.modes.size(); ++j) {
573 if (t.modes[j].timing == lang::TimingStrategy::TIMED) t.timed_idx.push_back(j);
574 else t.imm_idx.push_back(j);
575 }
576
577 // ---- the event columns --------------------------------------------------
578 std::vector<std::vector<double>> cols;
579 std::vector<double> rate_base;
580 std::vector<int> ev_kind, ev_mode, ev_phase, ev_to;
581 std::vector<std::ptrdiff_t> ev_station, ev_class;
582 std::vector<std::size_t> ev_flow;
583 const auto emit = [&](const std::vector<double>& col, double base, int kind, int mode,
584 int phase, int to, std::ptrdiff_t station, std::ptrdiff_t cls) {
585 cols.push_back(col);
586 rate_base.push_back(base);
587 ev_kind.push_back(kind);
588 ev_mode.push_back(mode);
589 ev_phase.push_back(phase);
590 ev_to.push_back(to);
591 ev_station.push_back(station);
592 ev_class.push_back(cls);
593 };
594
595 for (std::size_t q = 0; q < t.timed_idx.size(); ++q) {
596 const std::size_t j = t.timed_idx[q];
597 const PetriMode& md = t.modes[j];
598 if (md.nph == 1) {
599 emit(md.cvec, md.d1[0], 1, static_cast<int>(j), 0, 0, -1, -1);
600 } else {
601 for (std::size_t h = 0; h < md.nph; ++h)
602 for (std::size_t hp = 0; hp < md.nph; ++hp) {
603 const double w = md.D1(h, hp);
604 if (w <= 0) continue;
605 std::vector<double> col = md.cvec;
606 col[md.zblk[hp]] += 1.0;
607 col[md.zblk[h]] -= 1.0;
608 emit(col, w, 1, static_cast<int>(j), static_cast<int>(h),
609 static_cast<int>(hp), -1, -1);
610 }
611 for (std::size_t h = 0; h < md.nph; ++h)
612 for (std::size_t hp = 0; hp < md.nph; ++hp) {
613 if (hp == h) continue;
614 const double w = md.D0(h, hp);
615 if (w <= 0) continue;
616 std::vector<double> col(nstate, 0.0);
617 col[md.zblk[hp]] = 1.0;
618 col[md.zblk[h]] = -1.0;
619 emit(col, w, 2, static_cast<int>(j), static_cast<int>(h),
620 static_cast<int>(hp), -1, -1);
621 }
622 }
623 }
624 // One latch column per multi-phase mode: mu_j servers per unit time enter at
625 // the firing process's own entry distribution. The rate is FREE IN SIGN -- a
626 // mode whose enabling degree drops stops servers rather than starting them --
627 // and it is zero at any fixed point.
628 for (std::size_t q = 0; q < t.timed_idx.size(); ++q) {
629 const std::size_t j = t.timed_idx[q];
630 const PetriMode& md = t.modes[j];
631 if (md.nph <= 1) continue;
632 std::vector<double> col(nstate, 0.0);
633 for (std::size_t h = 0; h < md.nph; ++h) col[md.zblk[h]] = md.pie[h];
634 emit(col, 1.0, 5, static_cast<int>(j), 0, 0, -1, -1);
635 }
636 for (std::size_t q = 0; q < t.imm_idx.size(); ++q) {
637 const std::size_t j = t.imm_idx[q];
638 emit(t.modes[j].cvec, 1.0, 4, static_cast<int>(j), 0, 0, -1, -1);
639 }
640 for (std::size_t a = 0; a < src_arr.size(); ++a) {
641 const SrcArr& rec = src_arr[a];
642 const std::size_t ist = sn.nodes[rec.snd - 1].station;
643 if (ist >= 1 && ist <= sn.stations.size()) {
644 const lang::ProcessType pt = sn.procid(ist, rec.r + 1);
645 if (pt != lang::ProcessType::EXP)
646 throw UnsupportedError(ErrorKind{"NonExponential"},
647 "fluid_petri_terms: source " + t.names_node[rec.snd - 1] +
648 " has a non-exponential arrival for class " + std::to_string(rec.r + 1) +
649 ". The fluid Petri route models an arrival as a constant-propensity event, "
650 "which a renewal stream with memory is not; use SolverCTMC, SolverJMT or "
651 "SolverSSA");
652 }
653 std::vector<double> col(nstate, 0.0);
654 col[static_cast<std::size_t>(t.pidx[rec.qnd - 1][rec.l])] = 1.0;
655 const double base =
656 num_traits<T>::to_double(sn.rates(ist - 1, rec.r)) *
658 sn.rtnodes((rec.snd - 1) * t.K + rec.r, (rec.qnd - 1) * t.K + rec.l));
659 emit(col, base, 3, -1, 0, 0, static_cast<std::ptrdiff_t>(ist) - 1,
660 static_cast<std::ptrdiff_t>(rec.r));
661 }
662 // One flow column per (timed mode, fluid place): a unit jump on the level, signed by the direction of the
663 // flow, at |rate| per running server.
664 for (std::size_t q = 0; q < t.timed_idx.size(); ++q) {
665 const std::size_t j = t.timed_idx[q];
666 const PetriMode& md = t.modes[j];
667 for (std::size_t a = 0; a < md.flow_slot.size(); ++a) {
668 std::vector<double> col(nstate, 0.0);
669 col[md.flow_slot[a]] = md.flow_rate[a] > 0.0 ? 1.0 : (md.flow_rate[a] < 0.0 ? -1.0 : 0.0);
670 emit(col, std::fabs(md.flow_rate[a]), 6, static_cast<int>(j), 0, 0, -1, -1);
671 ev_flow.resize(rate_base.size(), 0);
672 ev_flow.back() = a;
673 }
674 }
675
676 t.nev = cols.size();
677 t.D = Matrix<double>(nstate, std::max<std::size_t>(t.nev, 1), 0.0);
678 for (std::size_t e = 0; e < t.nev; ++e)
679 for (std::size_t s = 0; s < nstate; ++s)
680 if (cols[e][s] != 0.0) t.D(s, e) = cols[e][s];
681 t.rate_base = rate_base;
682 t.ev_kind = ev_kind;
683 t.ev_mode = ev_mode;
684 t.ev_phase = ev_phase;
685 t.ev_to = ev_to;
686 t.ev_station = ev_station;
687 t.ev_class = ev_class;
688 ev_flow.resize(t.nev, 0);
689 t.ev_flow = ev_flow;
690
691 // ---- which Sigma entries the closure reads ------------------------------
692 std::map<std::pair<std::size_t, std::size_t>, std::size_t> pairkey;
693 for (std::size_t j = 0; j < t.modes.size(); ++j) {
694 const PetriMode& md = t.modes[j];
695 if (md.closable)
696 for (std::size_t a = 0; a < md.arc_slot.size(); ++a)
697 for (std::size_t b = a; b < md.arc_slot.size(); ++b)
698 terms_detail::add_pair(pairkey, t.cov_pairs, md.arc_slot[a], md.arc_slot[b]);
700 for (std::size_t b = 0; b < md.inh_slot.size(); ++b)
701 terms_detail::add_pair(pairkey, t.cov_pairs, md.inh_slot[b], md.inh_slot[b]);
702 // a fluid guard is smoothed by the marginal of its level, as an inhibitor gate is by the marginal
703 // of its place
704 for (std::size_t b = 0; b < md.guard_slot.size(); ++b)
705 terms_detail::add_pair(pairkey, t.cov_pairs, md.guard_slot[b], md.guard_slot[b]);
706 }
707 }
708 t.npair = t.cov_pairs.size();
709 const std::size_t pn = std::max<std::size_t>(nstate, 1);
710 t.pair_index.assign(pn, std::vector<std::ptrdiff_t>(pn, -1));
711 for (std::size_t i = 0; i < t.npair; ++i) {
712 t.pair_index[t.cov_pairs[i].first][t.cov_pairs[i].second] =
713 static_cast<std::ptrdiff_t>(i);
714 t.pair_index[t.cov_pairs[i].second][t.cov_pairs[i].first] =
715 static_cast<std::ptrdiff_t>(i);
716 }
717
718 for (std::size_t e = 0; e < t.nev; ++e) {
719 if (t.ev_kind[e] == 4) t.imm_col.push_back(e);
720 else if (t.ev_kind[e] == 5) t.latch_col.push_back(e);
721 // A fluid flow is deterministic given the marking, so it has no noise of its own either: the level
722 // inherits its fluctuation from the marking through the drift Jacobian, and from the impulses carried
723 // on the firing columns.
724 if (t.ev_kind[e] != 4 && t.ev_kind[e] != 5 && t.ev_kind[e] != 6) t.stoch_col.push_back(e);
725 if (t.ev_kind[e] == 6) t.flow_col.push_back(e);
726 }
727 for (std::size_t q = 0; q < t.timed_idx.size(); ++q)
728 if (t.modes[t.timed_idx[q]].nph > 1) t.latch_mode.push_back(t.timed_idx[q]);
729 t.cov_idx.resize(nstate);
730 for (std::size_t s = 0; s < nstate; ++s) t.cov_idx[s] = s;
731
732 // ---- per-place consumption and production, for the metric reader --------
733 // A Place's throughput is the rate at which TOKENS leave it, so each
734 // consuming mode contributes its firing rate times the multiplicity of the
735 // arc it takes them through. That is SolverCTMC's convention and the one
736 // Little's law needs; SolverSSA's NRM sums the UNWEIGHTED propensity, so the
737 // two disagree wherever an input arc has multiplicity above one.
738 for (std::size_t e = 0; e < t.nev; ++e) {
739 if (t.ev_kind[e] == 3) {
740 if (t.ev_station[e] < 0) continue;
741 const std::size_t kk =
742 static_cast<std::size_t>(t.ev_station[e]) * t.K +
743 static_cast<std::size_t>(t.ev_class[e]);
744 t.producers[kk].push_back(e);
745 continue;
746 }
747 if (t.ev_kind[e] != 1 && t.ev_kind[e] != 4) continue;
748 const PetriMode& md = t.modes[static_cast<std::size_t>(t.ev_mode[e])];
749 for (std::size_t a = 0; a < md.arc_slot.size(); ++a) {
750 const std::size_t s = md.arc_slot[a];
751 if (t.coord_station[s] < 0) continue;
752 const std::size_t kk =
753 static_cast<std::size_t>(t.coord_station[s]) * t.K + t.coord_class[s];
754 t.consumers[kk].push_back(e);
755 t.consumer_w[kk].push_back(md.arc_w[a]);
756 }
757 }
758
759 // ---- the initial state --------------------------------------------------
760 t.x0.assign(nstate, 0.0);
761 for (std::size_t s = 0; s < nm; ++s) t.x0[s] = t.m0full[t.coord_node[s]][t.coord_class[s]];
762 for (std::size_t q = 0; q < t.nf; ++q) t.x0[t.fluid_slot[q]] = t.fluid_init[q];
763 for (std::size_t j = 0; j < t.modes.size(); ++j) {
764 const PetriMode& md = t.modes[j];
765 if (md.nph <= 1) continue;
766 double e = std::numeric_limits<double>::infinity();
767 for (std::size_t a = 0; a < md.arc_slot.size(); ++a)
768 e = std::min(e, t.x0[md.arc_slot[a]] / md.arc_w[a]);
769 if (md.arc_slot.empty()) e = 1.0;
770 for (std::size_t h = 0; h < md.nph; ++h) t.x0[md.zblk[h]] = std::min(e, md.c) * md.pie[h];
771 }
772 return t;
773}
774
775} // namespace petri
776} // namespace fluid
777} // namespace line
778
779#endif // LINE_SOLVERS_FLUID_PETRI_TERMS_H
InputError(const std::string &what)
Definition error.h:105
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:119
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
double petri_coarse_tol()
GlobalConstants.CoarseTol, also the relative width of the seed's smoothed inhibitor gate.
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:368
@ IMMEDIATE
fires with zero delay, resolved by weight and priority
Definition lang_types.h:370
@ TIMED
fires after its firing distribution elapses
Definition lang_types.h:369
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:490
double tot(const std::vector< double > &pops)
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
One firing mode of one transition, with its arcs and its firing process.
std::vector< std::function< double(double)> > fdep_fun
std::vector< double > flow_rate
double c
servers of this mode; infinite for none
std::vector< double > guard_thr
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< std::size_t > flow_slot
Fluid flows of the mode: level coordinates and signed rates per running server.
std::vector< double > pie
entry distribution over the phases
std::vector< std::vector< std::size_t > > flow_dep_read
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< std::size_t > guard_slot
Fluid guards: level coordinates, +1 for '>=' and -1 for '<=', and thresholds.
std::vector< std::function< double(const Matrix< double > &)> > flow_dep_fun
Marking-dependent flow multipliers, aligned with flow_slot (an empty function is the unit one),...
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< std::size_t > fdep_slot
Fluid rate dependences: level coordinates and the multipliers h(level).
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::size_t > flow_col
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 plus fluid levels
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::ptrdiff_t > fluid_station
0-based station of each fluid place
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::vector< std::string > fluid_name
std::map< std::size_t, std::vector< std::size_t > > producers
std::vector< std::size_t > ev_flow
Kind 6: the flow's position in its mode's flow_slot; 0 for every other kind.
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< std::size_t > fluid_slot
std::vector< PetriMode > modes
std::vector< std::ptrdiff_t > fidx
(I) level coordinate of a 0-based node, or -1
std::vector< std::size_t > fluid_places
Fluid places (FSPN): 1-based nodes, their level coordinates, capacities and initial levels.
std::vector< std::ptrdiff_t > coord_station
Matrix< double > D
(nstate x nev) incidence
One station of the network.
The parameters of a Transition node: MATLAB sn.nodeparam{ind} for an SPN transition,...
const std::vector< FluidArc > & fluid_rates(std::size_t m) const
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
const std::vector< FluidArc > & fluid_impulses(std::size_t m) const
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.
const std::vector< FluidDep > & fluid_deps(std::size_t m) const
const std::vector< FluidFlowDep > & fluid_flow_deps(std::size_t m) const
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).
const std::vector< FluidGuard > & fluid_guards(std::size_t m) const