LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_petri_system.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_SYSTEM_H
6#define LINE_SOLVERS_FLUID_PETRI_SYSTEM_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The closures, the closed enabling term, the rate vector and the drift
12 * Jacobian of a stochastic Petri net's fluid limit.
13 *
14 * Port of `matlab/src/solvers/FLD/fluid_petri_theta.m`,
15 * `fluid_petri_rates.m` and `fluid_petri_jacobian.m`, cross-checked against
16 * `jar/src/main/java/jline/solvers/fluid/petri/PetriSystem.java` and
17 * `PetriClosures.java`.
18 */
19
20#include <algorithm>
21#include <cmath>
22#include <cstddef>
23#include <map>
24#include <vector>
25
27#include "line/util/error.h"
28#include "line/util/linalg.h"
29#include "line/util/matrix.h"
30
31namespace line {
32namespace fluid {
33namespace petri {
34
35/** The standard normal CDF, without a statistics dependency. */
36inline double norm_cdf(double z) { return 0.5 * std::erfc(-z / std::sqrt(2.0)); }
37
38/** The standard normal density. */
39inline double norm_pdf(double z) {
40 return std::exp(-0.5 * z * z) / std::sqrt(2.0 * 3.14159265358979323846);
41}
42
43/**
44 * E[min(X,Y)] and dE/dE[X] for jointly normal X, Y.
45 *
46 * @return the pair (expectation, derivative with respect to E[X])
47 */
48inline std::pair<double, double> min_closure(double n, double c, double s2, double vc,
49 double cov) {
50 if (std::isinf(c)) return std::make_pair(n, 1.0);
51 const double th2 = s2 - 2.0 * cov + vc;
52 if (th2 <= 0.0) {
53 // A degenerate pair: the min is the smaller of the two exactly. The band
54 // matches the other three codebases, so two of them stopping either side
55 // of the kink read the same indicator.
56 if (c - n > petri_fine_tol() * std::max(1.0, std::fabs(n)))
57 return std::make_pair(n, 1.0);
58 return std::make_pair(c, 0.0);
59 }
60 const double th = std::sqrt(th2);
61 const double al = (n - c) / th;
62 const double phi = norm_pdf(al);
63 const double p = 1.0 - norm_cdf(al);
64 const double h = n * p + c * (1.0 - p) - th * phi;
65 return std::make_pair(h, p);
66}
67
68/** The result of the many-argument closure. */
69struct MinMulti {
70 double h = 0.0; ///< E[min(X_1,...,X_A,c)]
71 std::vector<double> g; ///< dH/dMU(a): the probability that arc a binds
72 double v = 0.0; ///< Var[min] before the cap
73};
74
75/**
76 * Min-normal closure of E[min(X_1,...,X_A,c)] by Clark's (1961) recursion.
77 *
78 * The recursion is ORDER DEPENDENT, as Clark's approximation always is: only the
79 * first two moments of the running min are kept. The order is the caller's, i.e.
80 * increasing state coordinate, which the layout fixes.
81 *
82 * @param mu means of the arguments, already scaled by the arc weights
83 * @param S covariance of the arguments, symmetric positive semi-definite
84 * @param c deterministic cap (the mode's server count); infinite for none
85 */
86inline MinMulti minmulti_closure(const std::vector<double>& mu, const Matrix<double>& S,
87 double c) {
88 const std::size_t A = mu.size();
89 MinMulti out;
90 // A mode with no input arc is enabled at degree one, the convention the
91 // exact engines use (solver_ssa_nrm's spnEnDegree, after_global_event).
92 if (A == 0) {
93 out.h = std::min(1.0, c);
94 out.v = 0.0;
95 return out;
96 }
97 std::vector<std::vector<double>> s(A, std::vector<double>(A, 0.0));
98 if (S.rows() >= A && S.cols() >= A)
99 for (std::size_t i = 0; i < A; ++i)
100 for (std::size_t j = 0; j < A; ++j) s[i][j] = S(i, j);
101
102 double mz = mu[0];
103 double vz = s[0][0];
104 std::vector<double> covz(A, 0.0);
105 for (std::size_t i = 0; i < A; ++i) covz[i] = s[0][i];
106 std::vector<double> p(A, 1.0);
107
108 for (std::size_t k = 1; k < A; ++k) {
109 double th2 = vz + s[k][k] - 2.0 * covz[k];
110 // A covariance beyond the Cauchy-Schwarz bound is not admissible.
111 if (th2 < 0.0) th2 = 0.0;
112 const double th = std::sqrt(th2);
113 double pk, mw, vw;
114 std::vector<double> cw(A, 0.0);
115 if (th > 0.0) {
116 const double al = (mz - mu[k]) / th;
117 const double Phi = norm_cdf(al);
118 const double phi = norm_pdf(al);
119 pk = 1.0 - Phi;
120 mw = mz * pk + mu[k] * Phi - th * phi;
121 const double e2 = (mz * mz + vz) * pk + (mu[k] * mu[k] + s[k][k]) * Phi -
122 (mz + mu[k]) * th * phi;
123 vw = e2 - mw * mw;
124 // Clark's moment match can leave a negative variance where the two
125 // arguments are nearly identical; the min of two equal normals has
126 // the variance of either, which the clamp restores.
127 if (vw < 0.0) vw = 0.0;
128 for (std::size_t i = 0; i < A; ++i) cw[i] = covz[i] * pk + s[i][k] * Phi;
129 } else {
130 pk = (mu[k] - mz > petri_fine_tol() * std::max(1.0, std::fabs(mz))) ? 1.0 : 0.0;
131 mw = std::min(mz, mu[k]);
132 vw = pk * vz + (1.0 - pk) * s[k][k];
133 for (std::size_t i = 0; i < A; ++i) cw[i] = covz[i] * pk + s[i][k] * (1.0 - pk);
134 }
135 p[k] = pk;
136 mz = mw;
137 vz = vw;
138 covz = cw;
139 }
140
141 out.v = vz;
142 // The cap, by the two-argument closure itself: c is deterministic, so its
143 // variance and its covariance with the running min are both zero.
144 const std::pair<double, double> capped = min_closure(mz, c, vz, 0.0, 0.0);
145 out.h = capped.first;
146 out.g.assign(A, 0.0);
147 double tail = capped.second;
148 for (std::size_t a = A - 1; a >= 1; --a) {
149 out.g[a] = (1.0 - p[a]) * tail;
150 tail *= p[a];
151 }
152 out.g[0] = tail;
153 return out;
154}
155
156/** The closed enabling term of every mode and its derivative. */
158 std::vector<double> theta;
159 std::vector<std::vector<std::size_t>> dslot;
160 std::vector<std::vector<double>> dval;
161 std::vector<double> dep;
162 std::vector<std::vector<std::size_t>> depslot;
163 std::vector<std::vector<double>> depval;
164 /** Per mode, the marking multiplier of each flow (empty: every flow at 1), its gradient slots and values. */
165 std::vector<std::vector<double>> fmul;
166 std::vector<std::vector<std::vector<std::size_t>>> fmslot;
167 std::vector<std::vector<std::vector<double>>> fmval;
168};
169
170namespace system_detail {
171
172/** Sigma(a,b) as the closure reads it: zero where the drift never asks. */
173inline double sig(const PetriTerms& t, const std::vector<double>& s2, std::size_t a,
174 std::size_t b) {
175 if (a >= t.pair_index.size() || b >= t.pair_index.size()) return 0.0;
176 const std::ptrdiff_t i = t.pair_index[a][b];
177 return (i < 0 || static_cast<std::size_t>(i) >= s2.size()) ? 0.0
178 : s2[static_cast<std::size_t>(i)];
179}
180
181/**
182 * A user function of the (nnodes x nclasses) marking and its gradient (`i_markfun`).
183 *
184 * The function is evaluated at the MEAN marking -- a first-order closure, the same order at which SolverCTMC
185 * evaluates it per state and the only one available without the distribution of the marking. Its gradient has
186 * no analytic form, so it is taken by central differences. `read` (1-based nodes) restricts the matrix to those
187 * rows, the others held at 0; empty reads every token coordinate.
188 */
189inline double markfun_of(const PetriTerms& t, const std::function<double(const Matrix<double>&)>& fun,
190 const std::vector<double>& x, const std::vector<std::size_t>& read,
191 std::vector<std::size_t>& slots, std::vector<double>& vals) {
192 std::vector<std::size_t> coords;
193 for (std::size_t s = 0; s < t.nm; ++s) {
194 if (read.empty()) {
195 coords.push_back(s);
196 continue;
197 }
198 for (std::size_t nd : read)
199 if (t.coord_node[s] + 1 == nd) {
200 coords.push_back(s);
201 break;
202 }
203 }
204 Matrix<double> mm(t.I, t.K, 0.0);
205 for (std::size_t s : coords) mm(t.coord_node[s], t.coord_class[s]) = x[s];
206 const double g = fun(mm);
207 slots.clear();
208 vals.clear();
209 for (std::size_t s : coords) {
210 const double h = std::max(1e-6 * std::fabs(x[s]), 1e-6);
211 const std::size_t i = t.coord_node[s], k = t.coord_class[s];
212 Matrix<double> mp = mm, mn = mm;
213 mp(i, k) = mm(i, k) + h;
214 mn(i, k) = std::max(0.0, mm(i, k) - h);
215 const double hh = mp(i, k) - mn(i, k);
216 if (hh <= 0) continue;
217 const double d = (fun(mp) - fun(mn)) / hh;
218 if (d != 0) {
219 slots.push_back(s);
220 vals.push_back(d);
221 }
222 }
223 return g;
224}
225
226/** The marking-dependent firing multiplier and its gradient, over every token coordinate. */
227inline void dep_of(const PetriTerms& t, const PetriMode& md, const std::vector<double>& x,
228 PetriTheta& th, std::size_t j) {
229 th.dep[j] = markfun_of(t, md.dep, x, std::vector<std::size_t>(), th.depslot[j], th.depval[j]);
230}
231
232/**
233 * The marking-dependent multiplier of each flow of mode j (`setFluidFlowDependence`), read at the mean marking
234 * like the firing one, each restricted to the token places its flow reads.
235 */
236inline void flowdep_of(const PetriTerms& t, const PetriMode& md, const std::vector<double>& x, PetriTheta& th,
237 std::size_t j) {
238 bool any = false;
239 for (const auto& f : md.flow_dep_fun) any = any || static_cast<bool>(f);
240 if (!any) return;
241 const std::size_t nfl = md.flow_slot.size();
242 th.fmul[j].assign(nfl, 1.0);
243 th.fmslot[j].assign(nfl, std::vector<std::size_t>());
244 th.fmval[j].assign(nfl, std::vector<double>());
245 for (std::size_t a = 0; a < nfl; ++a) {
246 if (a >= md.flow_dep_fun.size() || !md.flow_dep_fun[a]) continue;
247 th.fmul[j][a] = markfun_of(t, md.flow_dep_fun[a], x, md.flow_dep_read[a], th.fmslot[j][a], th.fmval[j][a]);
248 }
249}
250
251/**
252 * The fluid-dependent firing multiplier prod_i h_i(f_i), times the marking one already in `th.dep[j]`, with the
253 * gradient of the product. Each h_i is a user function of one level, so its derivative is a central difference,
254 * one-sided at the empty level where the function is not defined below zero. Port of `i_fdep`.
255 */
256inline void fdep_of(const PetriMode& md, const std::vector<double>& x, PetriTheta& th, std::size_t j) {
257 const std::size_t nfd = md.fdep_slot.size();
258 std::vector<double> hv(nfd, 0.0), hd(nfd, 0.0);
259 for (std::size_t i = 0; i < nfd; ++i) {
260 const double f = std::max(0.0, x[md.fdep_slot[i]]);
261 hv[i] = md.fdep_fun[i](f);
262 const double step = std::max(1e-6 * std::fabs(f), 1e-6);
263 const double fp = f + step;
264 const double fn = std::max(0.0, f - step);
265 hd[i] = (md.fdep_fun[i](fp) - md.fdep_fun[i](fn)) / (fp - fn);
266 }
267 double H = 1.0;
268 for (std::size_t i = 0; i < nfd; ++i) H *= hv[i];
269 const double g0 = th.dep[j];
270 std::map<std::size_t, double> acc;
271 for (std::size_t q = 0; q < th.depslot[j].size(); ++q) acc[th.depslot[j][q]] += th.depval[j][q] * H;
272 for (std::size_t i = 0; i < nfd; ++i) {
273 double others = 1.0;
274 for (std::size_t q = 0; q < nfd; ++q)
275 if (q != i) others *= hv[q];
276 acc[md.fdep_slot[i]] += g0 * others * hd[i];
277 }
278 th.dep[j] = g0 * H;
279 th.depslot[j].clear();
280 th.depval[j].clear();
281 for (std::map<std::size_t, double>::const_iterator it = acc.begin(); it != acc.end(); ++it) {
282 th.depslot[j].push_back(it->first);
283 th.depval[j].push_back(it->second);
284 }
285}
286
287} // namespace system_detail
288
289/**
290 * The closed enabling term of every mode, and its derivative.
291 *
292 * theta_j = ( prod_b Phi((thr_b - m_b)/sd_b) ) * E[ min_a(m_a/w_a), c_j ]
293 *
294 * Both factors collapse to their first-order form at zero variance -- Phi becomes
295 * the hard indicator and the min closure becomes min() -- so the mean-field limit
296 * is one code path, not two.
297 *
298 * THE INHIBITOR GATE IS WHY A PETRI NET NEEDS A SMOOTHED CLOSURE AT ALL, quite
299 * apart from accuracy: the indicator is a step, and a Newton solver has no
300 * derivative to descend on a step.
301 *
302 * THE VARIANCES ARE UNKNOWNS, NOT FUNCTIONS OF X: s2 is pinned by its own
303 * consistency row in the DAE, so the derivative is with respect to the MEANS
304 * only.
305 *
306 * gate_width > 0 smooths the ZERO-VARIANCE gate to
307 * Phi((thr_b - m_b)/(gate_width*max(1,thr_b))). Only the first-order seed asks
308 * for it: its hard step makes a discontinuous drift that chatters at the
309 * threshold, and the stiff stepper then cannot get past it.
310 *
311 * A FLUID GUARD IS THE SAME GATE ON A LEVEL. 1{f_c >= tau} closes to Phi((f_c - tau)/sd_c) and 1{f_c <= tau}
312 * to Phi((tau - f_c)/sd_c), and joins the inhibitor product, so a mode whose guard is closed runs no servers:
313 * it neither fires nor pumps. `guard_width` (negative: the same as `gate_width`) smooths the zero-variance
314 * guards alone. A FLUID RATE DEPENDENCE h(f_c) multiplies the firing multiplier DEP alongside the
315 * marking-dependent one, evaluated at the mean level.
316 */
317inline PetriTheta petri_theta(const PetriTerms& t, const std::vector<double>& x,
318 const std::vector<double>& s2_in, double gate_width = 0.0,
319 double guard_width = -1.0) {
320 if (guard_width < 0.0) guard_width = gate_width;
321 const std::size_t nmod = t.modes.size();
322 PetriTheta th;
323 th.theta.assign(nmod, 0.0);
324 th.dslot.assign(nmod, std::vector<std::size_t>());
325 th.dval.assign(nmod, std::vector<double>());
326 th.dep.assign(nmod, 1.0);
327 th.depslot.assign(nmod, std::vector<std::size_t>());
328 th.depval.assign(nmod, std::vector<double>());
329 th.fmul.assign(nmod, std::vector<double>());
330 th.fmslot.assign(nmod, std::vector<std::vector<std::size_t>>());
331 th.fmval.assign(nmod, std::vector<std::vector<double>>());
332 for (std::size_t j = 0; j < nmod; ++j) system_detail::flowdep_of(t, t.modes[j], x, th, j);
333 const std::vector<double> zeros(std::max<std::size_t>(t.npair, 1), 0.0);
334 const std::vector<double>& s2 = s2_in.empty() ? zeros : s2_in;
335
336 for (std::size_t j = 0; j < nmod; ++j) {
337 const PetriMode& md = t.modes[j];
338 const std::size_t A = md.arc_slot.size();
339 std::vector<double> mu(A, 0.0);
340 for (std::size_t a = 0; a < A; ++a) mu[a] = x[md.arc_slot[a]] / md.arc_w[a];
341 Matrix<double> Sarg;
342 if (md.closable && A > 0) {
343 Sarg = Matrix<double>(A, A, 0.0);
344 for (std::size_t a = 0; a < A; ++a)
345 for (std::size_t b = 0; b < A; ++b)
346 Sarg(a, b) = system_detail::sig(t, s2, md.arc_slot[a], md.arc_slot[b]) /
347 (md.arc_w[a] * md.arc_w[b]);
348 }
349 const MinMulti mm = minmulti_closure(mu, Sarg, md.c);
350
351 const std::size_t nb = md.inh_slot.size();
352 std::vector<double> gate(nb, 0.0), dgate(nb, 0.0);
353 double ginh = 1.0;
354 for (std::size_t b = 0; b < nb; ++b) {
355 const double mb = x[md.inh_slot[b]];
356 const double thr = md.inh_thr[b];
357 const double vb = system_detail::sig(t, s2, md.inh_slot[b], md.inh_slot[b]);
358 if (vb > 0) {
359 const double sd = std::sqrt(vb);
360 const double zb = (thr - mb) / sd;
361 gate[b] = norm_cdf(zb);
362 dgate[b] = -norm_pdf(zb) / sd;
363 } else if (gate_width > 0) {
364 const double w = gate_width * std::max(1.0, thr);
365 const double zb = (thr - mb) / w;
366 gate[b] = norm_cdf(zb);
367 dgate[b] = -norm_pdf(zb) / w;
368 } else {
369 gate[b] = (mb < thr - petri_fine_tol() * std::max(1.0, thr)) ? 1.0 : 0.0;
370 dgate[b] = 0.0;
371 }
372 ginh *= gate[b];
373 }
374 // the fluid guards, closed by the marginal of the level
375 const std::size_t nfg = md.guard_slot.size();
376 std::vector<std::size_t> gslots(md.inh_slot);
377 for (std::size_t b = 0; b < nfg; ++b) {
378 const double fb = x[md.guard_slot[b]];
379 const double thr = md.guard_thr[b];
380 const double sgn = static_cast<double>(md.guard_sgn[b]);
381 const double vb = system_detail::sig(t, s2, md.guard_slot[b], md.guard_slot[b]);
382 double g = 0.0, dg = 0.0;
383 if (vb > 0) {
384 const double sd = std::sqrt(vb);
385 const double zb = sgn * (fb - thr) / sd;
386 g = norm_cdf(zb);
387 dg = sgn * norm_pdf(zb) / sd;
388 } else if (guard_width > 0) {
389 const double w = guard_width * std::max(1.0, thr);
390 const double zb = sgn * (fb - thr) / w;
391 g = norm_cdf(zb);
392 dg = sgn * norm_pdf(zb) / w;
393 } else {
394 const double tolg = petri_fine_tol() * std::max(1.0, thr);
395 g = (sgn > 0 ? fb >= thr - tolg : fb <= thr + tolg) ? 1.0 : 0.0;
396 dg = 0.0;
397 }
398 gate.push_back(g);
399 dgate.push_back(dg);
400 gslots.push_back(md.guard_slot[b]);
401 ginh *= g;
402 }
403 const std::size_t ng = nb + nfg;
404
405 // An arc and an inhibitor arc may share a coordinate, so accumulate.
406 std::map<std::size_t, double> acc;
407 for (std::size_t a = 0; a < A; ++a)
408 acc[md.arc_slot[a]] += ginh * mm.g[a] / md.arc_w[a];
409 for (std::size_t b = 0; b < ng; ++b) {
410 double others;
411 if (gate[b] != 0) {
412 others = ginh / gate[b];
413 } else {
414 others = 1.0;
415 for (std::size_t q = 0; q < ng; ++q)
416 if (q != b) others *= gate[q];
417 }
418 acc[gslots[b]] += mm.h * others * dgate[b];
419 }
420 for (std::map<std::size_t, double>::const_iterator it = acc.begin(); it != acc.end();
421 ++it) {
422 th.dslot[j].push_back(it->first);
423 th.dval[j].push_back(it->second);
424 }
425 th.theta[j] = ginh * mm.h;
426
427 if (md.dep) system_detail::dep_of(t, md, x, th, j);
428 if (!md.fdep_slot.empty()) system_detail::fdep_of(md, x, th, j);
429 }
430 return th;
431}
432
433/**
434 * The rate of every event column.
435 *
436 * kind 1 firing of mode j single phase: rateBase*theta*dep
437 * multi phase: rateBase*y(j,h)
438 * kind 2 internal phase change rateBase*y(j,h)
439 * kind 3 exogenous arrival a constant
440 * kind 4 firing of an IMMEDIATE mode phi_j, an algebraic unknown
441 * kind 5 the server latch mu_j, a free-sign unknown
442 * kind 6 fluid flow of mode j rateBase*theta*fmul, |rate| per running server times the
443 * flow's marking multiplier (setFluidFlowDependence); DEP does
444 * not scale it, since it times the firing and not the flow
445 */
446inline std::vector<double> petri_rates(const PetriTerms& t, const std::vector<double>& x,
447 const std::vector<double>& phi,
448 const std::vector<double>& mu, const PetriTheta& th) {
449 const std::size_t nmod = t.modes.size();
450 std::vector<std::ptrdiff_t> imm_pos(nmod, -1), latch_pos(nmod, -1);
451 for (std::size_t q = 0; q < t.imm_idx.size(); ++q)
452 imm_pos[t.imm_idx[q]] = static_cast<std::ptrdiff_t>(q);
453 for (std::size_t q = 0; q < t.latch_mode.size(); ++q)
454 latch_pos[t.latch_mode[q]] = static_cast<std::ptrdiff_t>(q);
455
456 std::vector<double> r(t.nev, 0.0);
457 for (std::size_t e = 0; e < t.nev; ++e) {
458 const int k = t.ev_kind[e];
459 if (k == 3) {
460 r[e] = t.rate_base[e];
461 } else if (k == 4) {
462 const std::ptrdiff_t at = imm_pos[static_cast<std::size_t>(t.ev_mode[e])];
463 r[e] = (phi.empty() || at < 0) ? 0.0 : phi[static_cast<std::size_t>(at)];
464 } else if (k == 5) {
465 const std::ptrdiff_t at = latch_pos[static_cast<std::size_t>(t.ev_mode[e])];
466 r[e] = (mu.empty() || at < 0) ? 0.0 : mu[static_cast<std::size_t>(at)];
467 } else if (k == 6) {
468 // a fluid flow per running server; THETA is the running-server count for a single-phase mode,
469 // and the latch pins the phase block of a multi-phase mode to it, so it is right for both
470 const std::size_t j = static_cast<std::size_t>(t.ev_mode[e]);
471 r[e] = t.rate_base[e] * th.theta[j];
472 if (!th.fmul[j].empty()) r[e] *= th.fmul[j][t.ev_flow[e]];
473 } else {
474 const std::size_t j = static_cast<std::size_t>(t.ev_mode[e]);
475 const PetriMode& md = t.modes[j];
476 if (md.nph == 1)
477 r[e] = t.rate_base[e] * th.theta[j] * th.dep[j];
478 else
479 r[e] = t.rate_base[e] * x[md.zblk[static_cast<std::size_t>(t.ev_phase[e])]];
480 }
481 }
482 return r;
483}
484
485/**
486 * Drift Jacobian A = D * dR/dX.
487 *
488 * This is what the Lyapunov equation of the linear noise approximation is
489 * written about, so it has to be the derivative of the SAME rate vector
490 * `petri_rates` returns: a covariance solved about an inconsistent Jacobian is
491 * not the covariance of anything. THE VARIANCES ARE HELD.
492 */
494 Matrix<double> Jr(std::max<std::size_t>(t.nev, 1), t.nstate, 0.0);
495 for (std::size_t e = 0; e < t.nev; ++e) {
496 const int k = t.ev_kind[e];
497 if (k == 3 || k == 4 || k == 5) continue;
498 const std::size_t j = static_cast<std::size_t>(t.ev_mode[e]);
499 const PetriMode& md = t.modes[j];
500 const double base = t.rate_base[e];
501 if (k == 6) {
502 double fm = 1.0;
503 if (!th.fmul[j].empty()) {
504 const std::size_t a = t.ev_flow[e];
505 fm = th.fmul[j][a];
506 for (std::size_t q = 0; q < th.fmslot[j][a].size(); ++q)
507 Jr(e, th.fmslot[j][a][q]) += base * th.theta[j] * th.fmval[j][a][q];
508 }
509 for (std::size_t q = 0; q < th.dslot[j].size(); ++q) Jr(e, th.dslot[j][q]) += base * fm * th.dval[j][q];
510 continue;
511 }
512 if (md.nph == 1) {
513 for (std::size_t q = 0; q < th.dslot[j].size(); ++q)
514 Jr(e, th.dslot[j][q]) += base * th.dep[j] * th.dval[j][q];
515 for (std::size_t q = 0; q < th.depslot[j].size(); ++q)
516 Jr(e, th.depslot[j][q]) += base * th.theta[j] * th.depval[j][q];
517 } else {
518 const std::size_t zc = md.zblk[static_cast<std::size_t>(t.ev_phase[e])];
519 Jr(e, zc) += base;
520 }
521 }
522 return matmul(t.D, Jr);
523}
524
525} // namespace petri
526} // namespace fluid
527} // namespace line
528
529#endif // LINE_SOLVERS_FLUID_PETRI_SYSTEM_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Event-based representation of the fluid marking process of a stochastic Petri net.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Dense matrix and non-owning view.
MinMulti minmulti_closure(const std::vector< double > &mu, const Matrix< double > &S, double c)
Min-normal closure of E[min(X_1,...,X_A,c)] by Clark's (1961) recursion.
PetriTheta petri_theta(const PetriTerms &t, const std::vector< double > &x, const std::vector< double > &s2_in, double gate_width=0.0, double guard_width=-1.0)
The closed enabling term of every mode, and its derivative.
double norm_cdf(double z)
The standard normal CDF, without a statistics dependency.
std::pair< double, double > min_closure(double n, double c, double s2, double vc, double cov)
E[min(X,Y)] and dE/dE[X] for jointly normal X, Y.
std::vector< double > petri_rates(const PetriTerms &t, const std::vector< double > &x, const std::vector< double > &phi, const std::vector< double > &mu, const PetriTheta &th)
The rate of every event column.
Matrix< double > petri_jacobian(const PetriTerms &t, const PetriTheta &th)
Drift Jacobian A = D * dR/dX.
double petri_fine_tol()
GlobalConstants.FineTol, the reference's own "effectively zero".
double norm_pdf(double z)
The standard normal density.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
The result of the many-argument closure.
std::vector< double > g
dH/dMU(a): the probability that arc a binds
double v
Var[min] before the cap.
double h
E[min(X_1,...,X_A,c)].
One firing mode of one transition, with its arcs and its firing process.
double c
servers of this mode; infinite for none
std::vector< double > guard_thr
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::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< double > inh_thr
std::function< double(const Matrix< double > &)> dep
Marking-dependent firing multiplier; empty for the unit one.
std::vector< std::size_t > fdep_slot
Fluid rate dependences: level coordinates and the multipliers h(level).
bool closable
Whether the enabling degree is worth closing over.
The assembled drift terms of a net.
std::size_t nstate
marking coordinates plus phase coordinates plus fluid levels
std::vector< std::vector< std::ptrdiff_t > > pair_index
std::vector< std::size_t > imm_idx
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::vector< std::size_t > latch_mode
std::vector< PetriMode > modes
Matrix< double > D
(nstate x nev) incidence
The closed enabling term of every mode and its derivative.
std::vector< std::vector< double > > dval
std::vector< std::vector< double > > depval
std::vector< std::vector< double > > fmul
Per mode, the marking multiplier of each flow (empty: every flow at 1), its gradient slots and values...
std::vector< std::vector< std::size_t > > dslot
std::vector< std::vector< std::vector< double > > > fmval
std::vector< std::vector< std::vector< std::size_t > > > fmslot
std::vector< std::vector< std::size_t > > depslot