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};
165
166namespace system_detail {
167
168/** Sigma(a,b) as the closure reads it: zero where the drift never asks. */
169inline double sig(const PetriTerms& t, const std::vector<double>& s2, std::size_t a,
170 std::size_t b) {
171 if (a >= t.pair_index.size() || b >= t.pair_index.size()) return 0.0;
172 const std::ptrdiff_t i = t.pair_index[a][b];
173 return (i < 0 || static_cast<std::size_t>(i) >= s2.size()) ? 0.0
174 : s2[static_cast<std::size_t>(i)];
175}
176
177/**
178 * The marking-dependent firing multiplier and its gradient.
179 *
180 * g is a user function of the (nnodes x nclasses) marking, so it is evaluated at
181 * the MEAN marking -- a first-order closure of g, the same order at which
182 * SolverCTMC evaluates it per state and the only one available without the
183 * distribution of the marking. Its gradient has no analytic form, so it is taken
184 * by central differences.
185 */
186inline void dep_of(const PetriTerms& t, const PetriMode& md, const std::vector<double>& x,
187 PetriTheta& th, std::size_t j) {
188 Matrix<double> mm(t.I, t.K, 0.0);
189 for (std::size_t s = 0; s < t.nm; ++s) mm(t.coord_node[s], t.coord_class[s]) = x[s];
190 th.dep[j] = md.dep(mm);
191 for (std::size_t s = 0; s < t.nm; ++s) {
192 const double h = std::max(1e-6 * std::fabs(x[s]), 1e-6);
193 const std::size_t i = t.coord_node[s], k = t.coord_class[s];
194 Matrix<double> mp = mm, mn = mm;
195 mp(i, k) = mm(i, k) + h;
196 mn(i, k) = std::max(0.0, mm(i, k) - h);
197 const double hh = mp(i, k) - mn(i, k);
198 if (hh <= 0) continue;
199 const double d = (md.dep(mp) - md.dep(mn)) / hh;
200 if (d != 0) {
201 th.depslot[j].push_back(s);
202 th.depval[j].push_back(d);
203 }
204 }
205}
206
207} // namespace system_detail
208
209/**
210 * The closed enabling term of every mode, and its derivative.
211 *
212 * theta_j = ( prod_b Phi((thr_b - m_b)/sd_b) ) * E[ min_a(m_a/w_a), c_j ]
213 *
214 * Both factors collapse to their first-order form at zero variance -- Phi becomes
215 * the hard indicator and the min closure becomes min() -- so the mean-field limit
216 * is one code path, not two.
217 *
218 * THE INHIBITOR GATE IS WHY A PETRI NET NEEDS A SMOOTHED CLOSURE AT ALL, quite
219 * apart from accuracy: the indicator is a step, and a Newton solver has no
220 * derivative to descend on a step.
221 *
222 * THE VARIANCES ARE UNKNOWNS, NOT FUNCTIONS OF X: s2 is pinned by its own
223 * consistency row in the DAE, so the derivative is with respect to the MEANS
224 * only.
225 */
226inline PetriTheta petri_theta(const PetriTerms& t, const std::vector<double>& x,
227 const std::vector<double>& s2_in) {
228 const std::size_t nmod = t.modes.size();
229 PetriTheta th;
230 th.theta.assign(nmod, 0.0);
231 th.dslot.assign(nmod, std::vector<std::size_t>());
232 th.dval.assign(nmod, std::vector<double>());
233 th.dep.assign(nmod, 1.0);
234 th.depslot.assign(nmod, std::vector<std::size_t>());
235 th.depval.assign(nmod, std::vector<double>());
236 const std::vector<double> zeros(std::max<std::size_t>(t.npair, 1), 0.0);
237 const std::vector<double>& s2 = s2_in.empty() ? zeros : s2_in;
238
239 for (std::size_t j = 0; j < nmod; ++j) {
240 const PetriMode& md = t.modes[j];
241 const std::size_t A = md.arc_slot.size();
242 std::vector<double> mu(A, 0.0);
243 for (std::size_t a = 0; a < A; ++a) mu[a] = x[md.arc_slot[a]] / md.arc_w[a];
244 Matrix<double> Sarg;
245 if (md.closable && A > 0) {
246 Sarg = Matrix<double>(A, A, 0.0);
247 for (std::size_t a = 0; a < A; ++a)
248 for (std::size_t b = 0; b < A; ++b)
249 Sarg(a, b) = system_detail::sig(t, s2, md.arc_slot[a], md.arc_slot[b]) /
250 (md.arc_w[a] * md.arc_w[b]);
251 }
252 const MinMulti mm = minmulti_closure(mu, Sarg, md.c);
253
254 const std::size_t nb = md.inh_slot.size();
255 std::vector<double> gate(nb, 0.0), dgate(nb, 0.0);
256 double ginh = 1.0;
257 for (std::size_t b = 0; b < nb; ++b) {
258 const double mb = x[md.inh_slot[b]];
259 const double thr = md.inh_thr[b];
260 const double vb = system_detail::sig(t, s2, md.inh_slot[b], md.inh_slot[b]);
261 if (vb > 0) {
262 const double sd = std::sqrt(vb);
263 const double zb = (thr - mb) / sd;
264 gate[b] = norm_cdf(zb);
265 dgate[b] = -norm_pdf(zb) / sd;
266 } else {
267 gate[b] = (mb < thr - petri_fine_tol() * std::max(1.0, thr)) ? 1.0 : 0.0;
268 dgate[b] = 0.0;
269 }
270 ginh *= gate[b];
271 }
272
273 // An arc and an inhibitor arc may share a coordinate, so accumulate.
274 std::map<std::size_t, double> acc;
275 for (std::size_t a = 0; a < A; ++a)
276 acc[md.arc_slot[a]] += ginh * mm.g[a] / md.arc_w[a];
277 for (std::size_t b = 0; b < nb; ++b) {
278 double others;
279 if (gate[b] != 0) {
280 others = ginh / gate[b];
281 } else {
282 others = 1.0;
283 for (std::size_t q = 0; q < nb; ++q)
284 if (q != b) others *= gate[q];
285 }
286 acc[md.inh_slot[b]] += mm.h * others * dgate[b];
287 }
288 for (std::map<std::size_t, double>::const_iterator it = acc.begin(); it != acc.end();
289 ++it) {
290 th.dslot[j].push_back(it->first);
291 th.dval[j].push_back(it->second);
292 }
293 th.theta[j] = ginh * mm.h;
294
295 if (md.dep) system_detail::dep_of(t, md, x, th, j);
296 }
297 return th;
298}
299
300/**
301 * The rate of every event column.
302 *
303 * kind 1 firing of mode j single phase: rateBase*theta*dep
304 * multi phase: rateBase*y(j,h)
305 * kind 2 internal phase change rateBase*y(j,h)
306 * kind 3 exogenous arrival a constant
307 * kind 4 firing of an IMMEDIATE mode phi_j, an algebraic unknown
308 * kind 5 the server latch mu_j, a free-sign unknown
309 */
310inline std::vector<double> petri_rates(const PetriTerms& t, const std::vector<double>& x,
311 const std::vector<double>& phi,
312 const std::vector<double>& mu, const PetriTheta& th) {
313 const std::size_t nmod = t.modes.size();
314 std::vector<std::ptrdiff_t> imm_pos(nmod, -1), latch_pos(nmod, -1);
315 for (std::size_t q = 0; q < t.imm_idx.size(); ++q)
316 imm_pos[t.imm_idx[q]] = static_cast<std::ptrdiff_t>(q);
317 for (std::size_t q = 0; q < t.latch_mode.size(); ++q)
318 latch_pos[t.latch_mode[q]] = static_cast<std::ptrdiff_t>(q);
319
320 std::vector<double> r(t.nev, 0.0);
321 for (std::size_t e = 0; e < t.nev; ++e) {
322 const int k = t.ev_kind[e];
323 if (k == 3) {
324 r[e] = t.rate_base[e];
325 } else if (k == 4) {
326 const std::ptrdiff_t at = imm_pos[static_cast<std::size_t>(t.ev_mode[e])];
327 r[e] = (phi.empty() || at < 0) ? 0.0 : phi[static_cast<std::size_t>(at)];
328 } else if (k == 5) {
329 const std::ptrdiff_t at = latch_pos[static_cast<std::size_t>(t.ev_mode[e])];
330 r[e] = (mu.empty() || at < 0) ? 0.0 : mu[static_cast<std::size_t>(at)];
331 } else {
332 const std::size_t j = static_cast<std::size_t>(t.ev_mode[e]);
333 const PetriMode& md = t.modes[j];
334 if (md.nph == 1)
335 r[e] = t.rate_base[e] * th.theta[j] * th.dep[j];
336 else
337 r[e] = t.rate_base[e] * x[md.zblk[static_cast<std::size_t>(t.ev_phase[e])]];
338 }
339 }
340 return r;
341}
342
343/**
344 * Drift Jacobian A = D * dR/dX.
345 *
346 * This is what the Lyapunov equation of the linear noise approximation is
347 * written about, so it has to be the derivative of the SAME rate vector
348 * `petri_rates` returns: a covariance solved about an inconsistent Jacobian is
349 * not the covariance of anything. THE VARIANCES ARE HELD.
350 */
352 Matrix<double> Jr(std::max<std::size_t>(t.nev, 1), t.nstate, 0.0);
353 for (std::size_t e = 0; e < t.nev; ++e) {
354 const int k = t.ev_kind[e];
355 if (k == 3 || k == 4 || k == 5) continue;
356 const std::size_t j = static_cast<std::size_t>(t.ev_mode[e]);
357 const PetriMode& md = t.modes[j];
358 const double base = t.rate_base[e];
359 if (md.nph == 1) {
360 for (std::size_t q = 0; q < th.dslot[j].size(); ++q)
361 Jr(e, th.dslot[j][q]) += base * th.dep[j] * th.dval[j][q];
362 for (std::size_t q = 0; q < th.depslot[j].size(); ++q)
363 Jr(e, th.depslot[j][q]) += base * th.theta[j] * th.depval[j][q];
364 } else {
365 const std::size_t zc = md.zblk[static_cast<std::size_t>(t.ev_phase[e])];
366 Jr(e, zc) += base;
367 }
368 }
369 return matmul(t.D, Jr);
370}
371
372} // namespace petri
373} // namespace fluid
374} // namespace line
375
376#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.
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.
PetriTheta petri_theta(const PetriTerms &t, const std::vector< double > &x, const std::vector< double > &s2_in)
The closed enabling term of every mode, and its derivative.
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< 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< double > inh_thr
std::function< double(const Matrix< double > &)> dep
Marking-dependent firing multiplier; empty for the unit one.
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
std::vector< std::vector< std::ptrdiff_t > > pair_index
std::vector< std::size_t > imm_idx
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< std::size_t > > dslot
std::vector< std::vector< std::size_t > > depslot